interop.rs (342726B)
1 use std::{fs, path::Path}; 2 3 use radroots_app_view::{ 4 FarmId, FarmOrderMethod, FarmReadiness, FarmSetupDraft, FarmSetupProjection, FarmSummary, 5 FulfillmentWindowId, OrderId, PickupLocationId, ProductId, ProductStatus, 6 TradeProvenanceProjection, TradeRevisionStatus, TradeValidationReceiptProofSystem, 7 TradeValidationReceiptResult, TradeValidationReceiptType, TradeWorkflowProjection, 8 TradeWorkflowSource, order_status_from_active_order_projection, 9 }; 10 use radroots_events::{ 11 RadrootsNostrEvent, 12 ids::{RadrootsEventId, RadrootsOrderId, RadrootsPublicKey}, 13 kinds::{ 14 KIND_FARM as RADROOTS_KIND_FARM, KIND_LISTING as RADROOTS_KIND_LISTING, 15 KIND_LISTING_DRAFT as RADROOTS_KIND_LISTING_DRAFT, 16 KIND_ORDER_CANCELLATION as RADROOTS_KIND_ORDER_CANCELLATION, 17 KIND_ORDER_DECISION as RADROOTS_KIND_ORDER_DECISION, 18 KIND_ORDER_REQUEST as RADROOTS_KIND_ORDER_REQUEST, 19 KIND_ORDER_REVISION_DECISION as RADROOTS_KIND_ORDER_REVISION_DECISION, 20 KIND_ORDER_REVISION_PROPOSAL as RADROOTS_KIND_ORDER_REVISION_PROPOSAL, 21 KIND_TRADE_VALIDATION_RECEIPT, 22 }, 23 order::{ 24 RadrootsOrderEconomics, RadrootsOrderItem, RadrootsOrderRequest, 25 RadrootsOrderRevisionOutcome, 26 }, 27 }; 28 use radroots_events_codec::order::{ 29 order_cancellation_from_event, order_decision_from_event, order_event_context_from_tags, 30 order_request_from_event, order_revision_decision_from_event, 31 order_revision_proposal_from_event, 32 }; 33 use radroots_local_events::{ 34 LocalEventRecord, LocalEventsStore, LocalRecordFamily, LocalRecordStatus, PublishOutboxStatus, 35 RelayDeliveryEvidence, RelayDeliveryState, SourceRuntime, 36 }; 37 use radroots_sql_core::{SqlExecutor, SqliteExecutor}; 38 use radroots_trade::order::{ 39 RadrootsOrderCancellationRecord, RadrootsOrderDecisionRecord, RadrootsOrderProjection, 40 RadrootsOrderReductionInputs, RadrootsOrderRequestRecord, RadrootsOrderRevisionDecisionRecord, 41 RadrootsOrderRevisionProposalRecord, reduce_order_events, 42 }; 43 use radroots_trade::validation_receipt::{ 44 RadrootsTradeValidationReceipt, RadrootsValidationReceiptTags, validation_receipt_from_event, 45 }; 46 use rusqlite::{Connection, OptionalExtension, params}; 47 use serde_json::Value; 48 use uuid::Uuid; 49 50 use crate::repo::farm_setup::AppFarmSetupRepository; 51 use crate::{AppSqliteError, AppSqliteStore}; 52 53 const LOCAL_EVENTS_BATCH_LIMIT: u32 = 500; 54 const APP_LOCAL_INTEROP_CURSOR_ID: &str = "radroots_app_sqlite_projection_v1"; 55 const KIND_FARM: i64 = RADROOTS_KIND_FARM as i64; 56 const KIND_LISTING: i64 = RADROOTS_KIND_LISTING as i64; 57 const KIND_LISTING_DRAFT: i64 = RADROOTS_KIND_LISTING_DRAFT as i64; 58 const KIND_ORDER_REQUEST: i64 = RADROOTS_KIND_ORDER_REQUEST as i64; 59 const KIND_ORDER_DECISION: i64 = RADROOTS_KIND_ORDER_DECISION as i64; 60 const KIND_ORDER_REVISION: i64 = RADROOTS_KIND_ORDER_REVISION_PROPOSAL as i64; 61 const KIND_ORDER_REVISION_DECISION: i64 = RADROOTS_KIND_ORDER_REVISION_DECISION as i64; 62 const KIND_ORDER_CANCEL: i64 = RADROOTS_KIND_ORDER_CANCELLATION as i64; 63 const KIND_VALIDATION_RECEIPT: i64 = KIND_TRADE_VALIDATION_RECEIPT as i64; 64 const ACTIVE_ORDER_EVENT_KINDS: [i64; 5] = [ 65 KIND_ORDER_REQUEST, 66 KIND_ORDER_DECISION, 67 KIND_ORDER_REVISION, 68 KIND_ORDER_REVISION_DECISION, 69 KIND_ORDER_CANCEL, 70 ]; 71 72 #[derive(Clone, Debug, Default, Eq, PartialEq)] 73 pub struct AppLocalInteropImportReport { 74 pub scanned_records: u32, 75 pub imported_records: u32, 76 pub skipped_records: u32, 77 pub self_observed_records: u32, 78 pub last_change_seq: Option<i64>, 79 } 80 81 #[derive(Clone, Debug, Eq, PartialEq)] 82 pub struct StoredLocalInteropRecord { 83 pub record_id: String, 84 pub local_seq: i64, 85 pub record_family: String, 86 pub local_status: String, 87 pub source_runtime: String, 88 pub owner_account_id: Option<String>, 89 pub owner_pubkey: Option<String>, 90 pub farm_key: Option<String>, 91 pub listing_addr: Option<String>, 92 pub projected_kind: String, 93 pub projected_id: Option<String>, 94 pub event_id: Option<String>, 95 pub event_kind: Option<i64>, 96 pub outbox_status: String, 97 pub relay_delivery_json: Option<String>, 98 } 99 100 pub struct AppLocalInteropRepository<'a> { 101 connection: &'a Connection, 102 } 103 104 impl<'a> AppLocalInteropRepository<'a> { 105 pub const fn new(connection: &'a Connection) -> Self { 106 Self { connection } 107 } 108 109 pub fn import_from_path( 110 &self, 111 shared_database_path: &Path, 112 ) -> Result<AppLocalInteropImportReport, AppSqliteError> { 113 if let Some(parent) = shared_database_path.parent() { 114 fs::create_dir_all(parent).map_err(|source| AppSqliteError::CreateParentDirectory { 115 path: parent.to_path_buf(), 116 source, 117 })?; 118 } 119 let executor = SqliteExecutor::open(shared_database_path).map_err(|source| { 120 AppSqliteError::LocalEventsSql { 121 operation: "open shared local events database", 122 source, 123 } 124 })?; 125 let store = LocalEventsStore::new(executor); 126 store 127 .migrate_up() 128 .map_err(|source| AppSqliteError::LocalEventsSql { 129 operation: "migrate shared local events database", 130 source, 131 })?; 132 self.import_from_store(&store) 133 } 134 135 pub fn import_from_store<E>( 136 &self, 137 store: &LocalEventsStore<E>, 138 ) -> Result<AppLocalInteropImportReport, AppSqliteError> 139 where 140 E: SqlExecutor, 141 { 142 let mut report = AppLocalInteropImportReport::default(); 143 let mut after_change_seq = self.last_imported_change_seq()?; 144 loop { 145 let records = store 146 .list_records_changed_after(after_change_seq, LOCAL_EVENTS_BATCH_LIMIT) 147 .map_err(|source| AppSqliteError::LocalEvents { 148 operation: "list changed shared local event records", 149 source, 150 })?; 151 let batch_len = records.len(); 152 for record in records { 153 after_change_seq = record.change_seq; 154 report.scanned_records += 1; 155 report.last_change_seq = Some(record.change_seq); 156 match self.import_record(&record)? { 157 ImportOutcome::Imported => report.imported_records += 1, 158 ImportOutcome::Skipped => report.skipped_records += 1, 159 } 160 } 161 if batch_len < LOCAL_EVENTS_BATCH_LIMIT as usize { 162 break; 163 } 164 } 165 if let Some(last_change_seq) = report.last_change_seq { 166 self.advance_import_cursor(last_change_seq)?; 167 } 168 Ok(report) 169 } 170 171 pub fn import_records( 172 &self, 173 records: &[LocalEventRecord], 174 ) -> Result<AppLocalInteropImportReport, AppSqliteError> { 175 let mut report = AppLocalInteropImportReport::default(); 176 for record in records { 177 report.scanned_records += 1; 178 report.last_change_seq = Some(record.change_seq); 179 match self.import_record(record)? { 180 ImportOutcome::Imported => report.imported_records += 1, 181 ImportOutcome::Skipped => report.skipped_records += 1, 182 } 183 } 184 Ok(report) 185 } 186 187 pub fn load_records(&self) -> Result<Vec<StoredLocalInteropRecord>, AppSqliteError> { 188 let mut statement = self 189 .connection 190 .prepare( 191 "SELECT 192 record_id, 193 local_seq, 194 record_family, 195 local_status, 196 source_runtime, 197 owner_account_id, 198 owner_pubkey, 199 farm_key, 200 listing_addr, 201 projected_kind, 202 projected_id, 203 event_id, 204 event_kind, 205 outbox_status, 206 relay_delivery_json 207 FROM local_interop_imports 208 ORDER BY local_seq ASC, record_id ASC", 209 ) 210 .map_err(|source| AppSqliteError::Query { 211 operation: "prepare local interop import query", 212 source, 213 })?; 214 let rows = statement 215 .query_map([], |row| { 216 Ok(StoredLocalInteropRecord { 217 record_id: row.get(0)?, 218 local_seq: row.get(1)?, 219 record_family: row.get(2)?, 220 local_status: row.get(3)?, 221 source_runtime: row.get(4)?, 222 owner_account_id: row.get(5)?, 223 owner_pubkey: row.get(6)?, 224 farm_key: row.get(7)?, 225 listing_addr: row.get(8)?, 226 projected_kind: row.get(9)?, 227 projected_id: row.get(10)?, 228 event_id: row.get(11)?, 229 event_kind: row.get(12)?, 230 outbox_status: row.get(13)?, 231 relay_delivery_json: row.get(14)?, 232 }) 233 }) 234 .map_err(|source| AppSqliteError::Query { 235 operation: "query local interop imports", 236 source, 237 })?; 238 rows.map(|row| { 239 row.map_err(|source| AppSqliteError::Query { 240 operation: "read local interop import row", 241 source, 242 }) 243 }) 244 .collect() 245 } 246 247 pub fn load_signed_events_by_kind( 248 &self, 249 event_kind: i64, 250 ) -> Result<Vec<RadrootsNostrEvent>, AppSqliteError> { 251 let mut statement = self 252 .connection 253 .prepare( 254 "SELECT 255 event_id, 256 event_kind, 257 local_status, 258 outbox_status, 259 relay_delivery_json, 260 event_pubkey, 261 event_created_at, 262 event_tags_json, 263 event_content, 264 event_sig 265 FROM local_interop_imports 266 WHERE record_family = 'signed_event' 267 AND local_status = 'published' 268 AND event_kind = ?1 269 ORDER BY local_seq ASC, record_id ASC", 270 ) 271 .map_err(|source| AppSqliteError::Query { 272 operation: "prepare local interop signed event evidence query", 273 source, 274 })?; 275 let rows = statement 276 .query_map(params![event_kind], |row| { 277 Ok(StoredLocalInteropSignedEventEvidence { 278 event_id: row.get(0)?, 279 event_kind: row.get(1)?, 280 local_status: row.get(2)?, 281 outbox_status: row.get(3)?, 282 relay_delivery_json: row.get(4)?, 283 event_pubkey: row.get(5)?, 284 event_created_at: row.get(6)?, 285 event_tags_json: row.get(7)?, 286 event_content: row.get(8)?, 287 event_sig: row.get(9)?, 288 }) 289 }) 290 .map_err(|source| AppSqliteError::Query { 291 operation: "query local interop signed event evidence", 292 source, 293 })?; 294 let mut events = Vec::new(); 295 for row in rows { 296 let evidence = row.map_err(|source| AppSqliteError::Query { 297 operation: "read local interop signed event evidence row", 298 source, 299 })?; 300 if !signed_event_local_interop_evidence_is_usable(&evidence) { 301 continue; 302 } 303 if let Some(event) = signed_event_from_local_interop_evidence(&evidence)? { 304 events.push(event); 305 } 306 } 307 Ok(events) 308 } 309 310 fn load_signed_event_by_event_id( 311 &self, 312 event_id: &str, 313 ) -> Result<Option<RadrootsNostrEvent>, AppSqliteError> { 314 let mut statement = self 315 .connection 316 .prepare( 317 "SELECT 318 event_id, 319 event_kind, 320 local_status, 321 outbox_status, 322 relay_delivery_json, 323 event_pubkey, 324 event_created_at, 325 event_tags_json, 326 event_content, 327 event_sig 328 FROM local_interop_imports 329 WHERE record_family = 'signed_event' 330 AND event_id = ?1 331 ORDER BY local_seq DESC, record_id DESC", 332 ) 333 .map_err(|source| AppSqliteError::Query { 334 operation: "prepare local interop signed event id evidence query", 335 source, 336 })?; 337 let rows = statement 338 .query_map(params![event_id], |row| { 339 Ok(StoredLocalInteropSignedEventEvidence { 340 event_id: row.get(0)?, 341 event_kind: row.get(1)?, 342 local_status: row.get(2)?, 343 outbox_status: row.get(3)?, 344 relay_delivery_json: row.get(4)?, 345 event_pubkey: row.get(5)?, 346 event_created_at: row.get(6)?, 347 event_tags_json: row.get(7)?, 348 event_content: row.get(8)?, 349 event_sig: row.get(9)?, 350 }) 351 }) 352 .map_err(|source| AppSqliteError::Query { 353 operation: "query local interop signed event id evidence", 354 source, 355 })?; 356 357 for row in rows { 358 let evidence = row.map_err(|source| AppSqliteError::Query { 359 operation: "read local interop signed event id evidence row", 360 source, 361 })?; 362 if !signed_event_local_interop_evidence_is_usable(&evidence) { 363 continue; 364 } 365 if let Some(event) = signed_event_from_local_interop_evidence(&evidence)? { 366 return Ok(Some(event)); 367 } 368 } 369 370 Ok(None) 371 } 372 373 fn last_imported_change_seq(&self) -> Result<i64, AppSqliteError> { 374 match self.connection.query_row( 375 "SELECT last_change_seq 376 FROM local_interop_projection_cursor 377 WHERE consumer_id = ?1 378 LIMIT 1", 379 [APP_LOCAL_INTEROP_CURSOR_ID], 380 |row| row.get::<_, i64>(0), 381 ) { 382 Ok(last_change_seq) => Ok(last_change_seq), 383 Err(rusqlite::Error::QueryReturnedNoRows) => Ok(0), 384 Err(source) => Err(AppSqliteError::Query { 385 operation: "read app local interop projection cursor", 386 source, 387 }), 388 } 389 } 390 391 fn advance_import_cursor(&self, last_change_seq: i64) -> Result<(), AppSqliteError> { 392 self.connection 393 .execute( 394 "INSERT INTO local_interop_projection_cursor ( 395 consumer_id, 396 last_change_seq, 397 updated_at 398 ) VALUES (?1, ?2, strftime('%Y-%m-%dT%H:%M:%SZ', 'now')) 399 ON CONFLICT(consumer_id) DO UPDATE SET 400 last_change_seq = max( 401 local_interop_projection_cursor.last_change_seq, 402 excluded.last_change_seq 403 ), 404 updated_at = excluded.updated_at", 405 params![APP_LOCAL_INTEROP_CURSOR_ID, last_change_seq], 406 ) 407 .map_err(|source| AppSqliteError::Query { 408 operation: "advance app local interop projection cursor", 409 source, 410 })?; 411 Ok(()) 412 } 413 414 fn import_record(&self, record: &LocalEventRecord) -> Result<ImportOutcome, AppSqliteError> { 415 self.begin_import_record_savepoint()?; 416 match self.import_record_inner(record) { 417 Ok(outcome) => { 418 self.release_import_record_savepoint()?; 419 Ok(outcome) 420 } 421 Err(error) => { 422 let _ = self.rollback_import_record_savepoint(); 423 let _ = self.release_import_record_savepoint(); 424 Err(error) 425 } 426 } 427 } 428 429 fn import_record_inner( 430 &self, 431 record: &LocalEventRecord, 432 ) -> Result<ImportOutcome, AppSqliteError> { 433 let superseded_listing_ids = match self.duplicate_signed_event_action(record)? { 434 DuplicateSignedEventAction::Import => Vec::new(), 435 DuplicateSignedEventAction::ReplaceExisting(event_id) => self 436 .delete_duplicate_signed_event_imports( 437 event_id.as_str(), 438 record.record_id.as_str(), 439 )?, 440 DuplicateSignedEventAction::Skip => return Ok(ImportOutcome::Skipped), 441 }; 442 let projection = match record.family { 443 LocalRecordFamily::LocalWork => self.import_local_work(record)?, 444 LocalRecordFamily::SignedEvent => self.import_signed_event(record)?, 445 }; 446 match projection { 447 Some(projection) => { 448 let projected_kind = projection.kind; 449 let projected_id = projection.projected_id; 450 self.record_import(record, projected_kind, projected_id.clone())?; 451 if projected_kind == "listing" { 452 if let Some(projected_id) = projected_id.as_deref() { 453 self.finish_duplicate_listing_replacement( 454 &superseded_listing_ids, 455 projected_id, 456 )?; 457 } 458 } 459 Ok(ImportOutcome::Imported) 460 } 461 None => { 462 self.record_import(record, "unsupported", None)?; 463 Ok(ImportOutcome::Skipped) 464 } 465 } 466 } 467 468 fn begin_import_record_savepoint(&self) -> Result<(), AppSqliteError> { 469 self.connection 470 .execute_batch("SAVEPOINT app_local_interop_import_record") 471 .map_err(|source| AppSqliteError::Query { 472 operation: "begin local interop import record transaction", 473 source, 474 }) 475 } 476 477 fn rollback_import_record_savepoint(&self) -> Result<(), AppSqliteError> { 478 self.connection 479 .execute_batch("ROLLBACK TO app_local_interop_import_record") 480 .map_err(|source| AppSqliteError::Query { 481 operation: "rollback local interop import record transaction", 482 source, 483 }) 484 } 485 486 fn release_import_record_savepoint(&self) -> Result<(), AppSqliteError> { 487 self.connection 488 .execute_batch("RELEASE app_local_interop_import_record") 489 .map_err(|source| AppSqliteError::Query { 490 operation: "release local interop import record transaction", 491 source, 492 }) 493 } 494 495 fn duplicate_signed_event_action( 496 &self, 497 record: &LocalEventRecord, 498 ) -> Result<DuplicateSignedEventAction, AppSqliteError> { 499 if record.family != LocalRecordFamily::SignedEvent { 500 return Ok(DuplicateSignedEventAction::Import); 501 } 502 let Some(event_id) = record 503 .event_id 504 .as_deref() 505 .map(str::trim) 506 .filter(|event_id| !event_id.is_empty()) 507 else { 508 return Ok(DuplicateSignedEventAction::Import); 509 }; 510 let mut statement = self 511 .connection 512 .prepare( 513 "SELECT source_runtime, owner_account_id, local_status, outbox_status 514 FROM local_interop_imports 515 WHERE event_id = ?1 516 AND record_id <> ?2 517 AND record_family = 'signed_event'", 518 ) 519 .map_err(|source| AppSqliteError::Query { 520 operation: "prepare duplicate local interop signed event query", 521 source, 522 })?; 523 let rows = statement 524 .query_map(params![event_id, record.record_id.as_str()], |row| { 525 Ok(StoredSignedEventDuplicate { 526 source_runtime: row.get(0)?, 527 owner_account_id: row.get(1)?, 528 local_status: row.get(2)?, 529 outbox_status: row.get(3)?, 530 }) 531 }) 532 .map_err(|source| AppSqliteError::Query { 533 operation: "query duplicate local interop signed events", 534 source, 535 })?; 536 let mut existing_precedence = None; 537 for row in rows { 538 let duplicate = row.map_err(|source| AppSqliteError::Query { 539 operation: "read duplicate local interop signed event", 540 source, 541 })?; 542 existing_precedence = Some(existing_precedence.unwrap_or(0).max( 543 signed_event_evidence_precedence( 544 duplicate.source_runtime.as_str(), 545 duplicate.owner_account_id.as_deref(), 546 duplicate.local_status.as_str(), 547 duplicate.outbox_status.as_str(), 548 ), 549 )); 550 } 551 let Some(existing_precedence) = existing_precedence else { 552 return Ok(DuplicateSignedEventAction::Import); 553 }; 554 let incoming_precedence = signed_event_evidence_precedence( 555 record.source_runtime.as_str(), 556 record.owner_account_id.as_deref(), 557 record.status.as_str(), 558 record.outbox_status.as_str(), 559 ); 560 if incoming_precedence > existing_precedence { 561 Ok(DuplicateSignedEventAction::ReplaceExisting( 562 event_id.to_owned(), 563 )) 564 } else { 565 Ok(DuplicateSignedEventAction::Skip) 566 } 567 } 568 569 fn delete_duplicate_signed_event_imports( 570 &self, 571 event_id: &str, 572 record_id: &str, 573 ) -> Result<Vec<String>, AppSqliteError> { 574 let superseded_listing_ids = 575 self.superseded_duplicate_listing_projection_ids(event_id, record_id)?; 576 self.connection 577 .execute( 578 "DELETE FROM local_interop_imports 579 WHERE event_id = ?1 580 AND record_id <> ?2 581 AND record_family = 'signed_event'", 582 params![event_id, record_id], 583 ) 584 .map_err(|source| AppSqliteError::Query { 585 operation: "delete superseded duplicate local interop signed event", 586 source, 587 })?; 588 Ok(superseded_listing_ids) 589 } 590 591 fn finish_duplicate_listing_replacement( 592 &self, 593 superseded_listing_ids: &[String], 594 canonical_listing_product_id: &str, 595 ) -> Result<(), AppSqliteError> { 596 self.migrate_duplicate_buyer_cart_lines( 597 superseded_listing_ids, 598 canonical_listing_product_id, 599 )?; 600 self.delete_unreferenced_listing_products(superseded_listing_ids)?; 601 Ok(()) 602 } 603 604 fn superseded_duplicate_listing_projection_ids( 605 &self, 606 event_id: &str, 607 record_id: &str, 608 ) -> Result<Vec<String>, AppSqliteError> { 609 let mut statement = self 610 .connection 611 .prepare( 612 "SELECT projected_id 613 FROM local_interop_imports 614 WHERE event_id = ?1 615 AND record_id <> ?2 616 AND record_family = 'signed_event' 617 AND projected_kind = 'listing' 618 AND projected_id IS NOT NULL", 619 ) 620 .map_err(|source| AppSqliteError::Query { 621 operation: "prepare superseded duplicate listing projection query", 622 source, 623 })?; 624 let rows = statement 625 .query_map(params![event_id, record_id], |row| row.get::<_, String>(0)) 626 .map_err(|source| AppSqliteError::Query { 627 operation: "query superseded duplicate listing projections", 628 source, 629 })?; 630 rows.map(|row| { 631 row.map_err(|source| AppSqliteError::Query { 632 operation: "read superseded duplicate listing projection", 633 source, 634 }) 635 }) 636 .collect() 637 } 638 639 fn delete_unreferenced_listing_products( 640 &self, 641 product_ids: &[String], 642 ) -> Result<(), AppSqliteError> { 643 for product_id in product_ids { 644 self.connection 645 .execute( 646 "DELETE FROM products 647 WHERE id = ?1 648 AND NOT EXISTS ( 649 SELECT 1 650 FROM local_interop_imports 651 WHERE projected_kind = 'listing' 652 AND projected_id = ?1 653 )", 654 params![product_id], 655 ) 656 .map_err(|source| AppSqliteError::Query { 657 operation: "delete unreferenced superseded listing product", 658 source, 659 })?; 660 } 661 Ok(()) 662 } 663 664 fn migrate_duplicate_buyer_cart_lines( 665 &self, 666 product_ids: &[String], 667 canonical_product_id: &str, 668 ) -> Result<(), AppSqliteError> { 669 for product_id in product_ids { 670 if product_id == canonical_product_id { 671 continue; 672 } 673 self.connection 674 .execute( 675 "INSERT INTO buyer_cart_lines ( 676 buyer_context_key, 677 product_id, 678 quantity, 679 listing_bin_id, 680 quantity_unit_label, 681 unit_price_minor_units, 682 price_currency, 683 farm_key, 684 listing_addr, 685 listing_event_id, 686 seller_pubkey, 687 listing_relays_json, 688 updated_at 689 ) 690 SELECT 691 buyer_context_key, 692 ?2, 693 quantity, 694 listing_bin_id, 695 quantity_unit_label, 696 unit_price_minor_units, 697 price_currency, 698 farm_key, 699 listing_addr, 700 listing_event_id, 701 seller_pubkey, 702 listing_relays_json, 703 strftime('%Y-%m-%dT%H:%M:%SZ', 'now') 704 FROM buyer_cart_lines 705 WHERE product_id = ?1 706 ON CONFLICT(buyer_context_key, product_id) DO UPDATE SET 707 quantity = buyer_cart_lines.quantity + excluded.quantity, 708 listing_bin_id = coalesce(nullif(buyer_cart_lines.listing_bin_id, ''), excluded.listing_bin_id), 709 quantity_unit_label = coalesce(nullif(buyer_cart_lines.quantity_unit_label, ''), excluded.quantity_unit_label), 710 unit_price_minor_units = coalesce(buyer_cart_lines.unit_price_minor_units, excluded.unit_price_minor_units), 711 price_currency = coalesce(nullif(buyer_cart_lines.price_currency, ''), excluded.price_currency), 712 farm_key = coalesce(nullif(buyer_cart_lines.farm_key, ''), excluded.farm_key), 713 listing_addr = coalesce(nullif(buyer_cart_lines.listing_addr, ''), excluded.listing_addr), 714 listing_event_id = coalesce(nullif(buyer_cart_lines.listing_event_id, ''), excluded.listing_event_id), 715 seller_pubkey = coalesce(nullif(buyer_cart_lines.seller_pubkey, ''), excluded.seller_pubkey), 716 listing_relays_json = coalesce(nullif(buyer_cart_lines.listing_relays_json, ''), excluded.listing_relays_json), 717 updated_at = excluded.updated_at", 718 params![product_id, canonical_product_id], 719 ) 720 .map_err(|source| AppSqliteError::Query { 721 operation: "migrate duplicate listing buyer cart lines", 722 source, 723 })?; 724 self.connection 725 .execute( 726 "DELETE FROM buyer_cart_lines 727 WHERE product_id = ?1", 728 params![product_id], 729 ) 730 .map_err(|source| AppSqliteError::Query { 731 operation: "delete migrated duplicate listing buyer cart lines", 732 source, 733 })?; 734 } 735 Ok(()) 736 } 737 738 fn import_local_work( 739 &self, 740 record: &LocalEventRecord, 741 ) -> Result<Option<ProjectionRecord>, AppSqliteError> { 742 let Some(payload) = record.local_work_json.as_ref() else { 743 return Ok(None); 744 }; 745 match string_at(payload, &["record_kind"]).as_deref() { 746 Some("farm_config_v1") => self.import_farm_config(record, payload), 747 Some("listing_draft_v1") => self.import_listing_draft(record, payload), 748 _ => Ok(None), 749 } 750 } 751 752 fn import_signed_event( 753 &self, 754 record: &LocalEventRecord, 755 ) -> Result<Option<ProjectionRecord>, AppSqliteError> { 756 match record.event_kind { 757 Some(KIND_FARM) => self.import_signed_farm(record), 758 Some(KIND_LISTING | KIND_LISTING_DRAFT) => self.import_signed_listing(record), 759 Some(KIND_VALIDATION_RECEIPT) => self.import_signed_validation_receipt(record), 760 Some(kind) if active_order_event_kind(kind) => self.import_signed_active_order(record), 761 _ => Ok(Some(ProjectionRecord { 762 kind: "signed_event", 763 projected_id: record.event_id.clone(), 764 })), 765 } 766 } 767 768 fn import_farm_config( 769 &self, 770 record: &LocalEventRecord, 771 payload: &Value, 772 ) -> Result<Option<ProjectionRecord>, AppSqliteError> { 773 let Some(document) = payload.get("document") else { 774 return Ok(None); 775 }; 776 let Some(farm_key) = record 777 .farm_id 778 .clone() 779 .or_else(|| string_at(document, &["selection", "farm_d_tag"])) 780 .or_else(|| string_at(document, &["farm", "d_tag"])) 781 else { 782 return Ok(None); 783 }; 784 let owner_pubkey = record.owner_pubkey.clone(); 785 let Some(farm_id) = projected_farm_id( 786 record.source_runtime, 787 owner_pubkey.as_deref(), 788 farm_key.as_str(), 789 ) else { 790 return Ok(None); 791 }; 792 let display_name = string_at(document, &["profile", "display_name"]) 793 .or_else(|| string_at(document, &["profile", "name"])) 794 .or_else(|| string_at(document, &["farm", "name"])) 795 .unwrap_or_else(|| "Local farm".to_owned()); 796 let location = string_at(document, &["farm", "location", "primary"]) 797 .or_else(|| string_at(document, &["listing_defaults", "location", "primary"])) 798 .unwrap_or_default(); 799 let methods = string_at(document, &["listing_defaults", "delivery_method"]) 800 .and_then(|method| farm_order_method(method.as_str())) 801 .into_iter() 802 .collect::<Vec<_>>(); 803 let saved_farm = FarmSummary { 804 farm_id, 805 display_name: display_name.clone(), 806 readiness: FarmReadiness::Incomplete, 807 }; 808 self.upsert_local_work_farm_summary(&saved_farm)?; 809 let owner_account_id = record 810 .owner_account_id 811 .clone() 812 .or_else(|| string_at(document, &["selection", "account"])); 813 if let Some(owner_account_id) = owner_account_id.as_deref() { 814 let projection = FarmSetupProjection::new( 815 FarmSetupDraft::new(display_name, location, methods), 816 Some(saved_farm), 817 ); 818 AppFarmSetupRepository::new(self.connection) 819 .save_farm_setup(owner_account_id, &projection)?; 820 } 821 Ok(Some(ProjectionRecord { 822 kind: "farm", 823 projected_id: Some(farm_id.to_string()), 824 })) 825 } 826 827 fn import_listing_draft( 828 &self, 829 record: &LocalEventRecord, 830 payload: &Value, 831 ) -> Result<Option<ProjectionRecord>, AppSqliteError> { 832 let Some(document) = payload.get("document") else { 833 return Ok(None); 834 }; 835 let Some(listing_key) = 836 string_at(document, &["listing", "d_tag"]).or_else(|| listing_id(record)) 837 else { 838 return Ok(None); 839 }; 840 let owner_pubkey = record 841 .owner_pubkey 842 .clone() 843 .or_else(|| string_at(document, &["seller_actor", "pubkey"])); 844 let farm_key = record 845 .farm_id 846 .clone() 847 .or_else(|| string_at(document, &["listing", "farm_d_tag"])); 848 let Some(farm_key) = farm_key else { 849 return Ok(None); 850 }; 851 let Some(farm_id) = projected_farm_id( 852 record.source_runtime, 853 owner_pubkey.as_deref(), 854 farm_key.as_str(), 855 ) else { 856 return Ok(None); 857 }; 858 self.ensure_farm_exists(farm_id)?; 859 let Some(product_id) = projected_product_id( 860 record.source_runtime, 861 owner_pubkey.as_deref(), 862 listing_key.as_str(), 863 ) else { 864 return Ok(None); 865 }; 866 let title = string_at(document, &["product", "title"]) 867 .or_else(|| string_at(document, &["product", "key"])) 868 .unwrap_or_else(|| "Local product".to_owned()); 869 let subtitle = string_at(document, &["product", "summary"]).unwrap_or_default(); 870 let unit_label = string_at(document, &["primary_bin", "quantity_unit"]) 871 .or_else(|| string_at(document, &["primary_bin", "price_per_unit"])) 872 .unwrap_or_default(); 873 let listing_bin_id = string_at(document, &["primary_bin", "bin_id"]); 874 let price_minor_units = string_at(document, &["primary_bin", "price_amount"]) 875 .and_then(|price| parse_decimal_minor_units(price.as_str())); 876 let price_currency = string_at(document, &["primary_bin", "price_currency"]) 877 .unwrap_or_else(|| "USD".to_owned()); 878 let stock_count = string_at(document, &["inventory", "available"]) 879 .and_then(|quantity| parse_u32_quantity(quantity.as_str())); 880 self.upsert_product(ProductProjection { 881 product_id, 882 farm_id, 883 title, 884 subtitle, 885 status: ProductStatus::Draft, 886 unit_label, 887 price_minor_units, 888 price_currency, 889 stock_count, 890 availability_window_id: None, 891 listing_bin_id, 892 })?; 893 Ok(Some(ProjectionRecord { 894 kind: "listing", 895 projected_id: Some(product_id.to_string()), 896 })) 897 } 898 899 fn import_signed_farm( 900 &self, 901 record: &LocalEventRecord, 902 ) -> Result<Option<ProjectionRecord>, AppSqliteError> { 903 let Some(content) = record.event_content.as_deref() else { 904 return Ok(None); 905 }; 906 let content = parse_json_value(content)?; 907 let tags = record.event_tags_json.as_ref(); 908 let Some(farm_key) = tag_index_value(tags, "d", 1) 909 .or_else(|| string_at(&content, &["d_tag"])) 910 .or_else(|| record.farm_id.clone()) 911 else { 912 return Ok(None); 913 }; 914 let owner_pubkey = record 915 .event_pubkey 916 .as_deref() 917 .or(record.owner_pubkey.as_deref()); 918 let Some(farm_id) = 919 projected_farm_id(record.source_runtime, owner_pubkey, farm_key.as_str()) 920 else { 921 return Ok(None); 922 }; 923 let display_name = 924 string_at(&content, &["name"]).unwrap_or_else(|| "Local farm".to_owned()); 925 let readiness = match signed_farm_readiness(&content, tags) { 926 Some(readiness) => readiness, 927 None => self 928 .load_farm_readiness(farm_id)? 929 .unwrap_or(FarmReadiness::Incomplete), 930 }; 931 self.upsert_farm_summary(&FarmSummary { 932 farm_id, 933 display_name, 934 readiness, 935 })?; 936 Ok(Some(ProjectionRecord { 937 kind: "farm", 938 projected_id: Some(farm_id.to_string()), 939 })) 940 } 941 942 fn import_signed_listing( 943 &self, 944 record: &LocalEventRecord, 945 ) -> Result<Option<ProjectionRecord>, AppSqliteError> { 946 let content = record 947 .event_content 948 .as_deref() 949 .and_then(parse_json_value_opt); 950 let tags = record.event_tags_json.as_ref(); 951 let listing_key = content 952 .as_ref() 953 .and_then(|content| string_at(content, &["d_tag"])) 954 .or_else(|| tag_index_value(tags, "d", 1)) 955 .or_else(|| listing_id(record)); 956 let Some(listing_key) = listing_key else { 957 return Ok(None); 958 }; 959 let farm_key = content 960 .as_ref() 961 .and_then(|content| string_at(content, &["farm", "d_tag"])) 962 .or_else(|| tag_index_value(tags, "a", 1).and_then(|addr| address_d_tag(&addr))) 963 .or_else(|| record.farm_id.clone()); 964 let Some(farm_key) = farm_key else { 965 return Ok(None); 966 }; 967 let signed_farm_pubkey = content 968 .as_ref() 969 .and_then(|content| string_at(content, &["farm", "pubkey"])) 970 .or_else(|| tag_index_value(tags, "a", 1).and_then(|addr| address_pubkey(&addr))); 971 let farm_pubkey = signed_farm_pubkey 972 .as_deref() 973 .or(record.event_pubkey.as_deref()) 974 .or(record.owner_pubkey.as_deref()); 975 let listing_pubkey = record 976 .event_pubkey 977 .as_deref() 978 .or(signed_farm_pubkey.as_deref()) 979 .or(record.owner_pubkey.as_deref()); 980 let app_shaped_network_listing = record.source_runtime == SourceRuntime::Network 981 && parse_app_d_tag_uuid(farm_key.as_str()).is_some() 982 && parse_app_d_tag_uuid(listing_key.as_str()).is_some(); 983 let mut existing_projection = if app_shaped_network_listing { 984 None 985 } else { 986 self.existing_listing_projection(record.listing_addr.as_deref())? 987 }; 988 if existing_projection.is_none() { 989 existing_projection = self.existing_app_origin_listing_projection( 990 record, 991 farm_key.as_str(), 992 listing_key.as_str(), 993 listing_pubkey, 994 tags, 995 )?; 996 } 997 let (farm_id, product_id) = if let Some(existing_projection) = existing_projection { 998 (existing_projection.farm_id, existing_projection.product_id) 999 } else { 1000 let Some(farm_id) = 1001 projected_farm_id(record.source_runtime, farm_pubkey, farm_key.as_str()) 1002 else { 1003 return Ok(None); 1004 }; 1005 let Some(product_id) = 1006 projected_product_id(record.source_runtime, listing_pubkey, listing_key.as_str()) 1007 else { 1008 return Ok(None); 1009 }; 1010 (farm_id, product_id) 1011 }; 1012 let projection_record = ProjectionRecord { 1013 kind: "listing", 1014 projected_id: Some(product_id.to_string()), 1015 }; 1016 if !self.signed_listing_is_current(record, listing_key.as_str())? { 1017 return Ok(Some(projection_record)); 1018 } 1019 self.ensure_farm_exists(farm_id)?; 1020 let title = content 1021 .as_ref() 1022 .and_then(|content| string_at(content, &["product", "title"])) 1023 .or_else(|| tag_index_value(tags, "title", 1)) 1024 .or_else(|| { 1025 content 1026 .as_ref() 1027 .and_then(|content| string_at(content, &["product", "key"])) 1028 }) 1029 .or_else(|| tag_index_value(tags, "key", 1)) 1030 .unwrap_or_else(|| "Local product".to_owned()); 1031 let subtitle = content 1032 .as_ref() 1033 .and_then(|content| string_at(content, &["product", "summary"])) 1034 .or_else(|| tag_index_value(tags, "summary", 1)) 1035 .unwrap_or_default(); 1036 let bin = content.as_ref().and_then(primary_bin); 1037 let listing_bin_id = bin 1038 .and_then(|value| string_at(value, &["bin_id"])) 1039 .or_else(|| tag_index_value(tags, "radroots:bin", 1)); 1040 let unit_label = bin 1041 .and_then(|value| { 1042 string_at(value, &["quantity", "unit"]) 1043 .or_else(|| string_at(value, &["display_unit"])) 1044 .or_else(|| string_at(value, &["display_price_unit"])) 1045 }) 1046 .or_else(|| tag_index_value(tags, "radroots:bin", 3)) 1047 .unwrap_or_default(); 1048 let price_minor_units = bin 1049 .and_then(|value| { 1050 string_at(value, &["price_per_canonical_unit", "amount", "amount"]) 1051 .or_else(|| string_at(value, &["display_price", "amount"])) 1052 .and_then(|price| parse_decimal_minor_units(price.as_str())) 1053 }) 1054 .or_else(|| { 1055 tag_index_value(tags, "radroots:price", 2) 1056 .or_else(|| tag_index_value(tags, "price", 1)) 1057 .and_then(|price| parse_decimal_minor_units(price.as_str())) 1058 }); 1059 let price_currency = bin 1060 .and_then(|value| { 1061 string_at(value, &["price_per_canonical_unit", "amount", "currency"]) 1062 .or_else(|| string_at(value, &["display_price", "currency"])) 1063 }) 1064 .or_else(|| tag_index_value(tags, "radroots:price", 3)) 1065 .or_else(|| tag_index_value(tags, "price", 2)) 1066 .unwrap_or_else(|| "USD".to_owned()); 1067 let stock_count = content 1068 .as_ref() 1069 .and_then(|content| string_at(content, &["inventory_available"])) 1070 .or_else(|| tag_index_value(tags, "inventory", 1)) 1071 .and_then(|quantity| parse_u32_quantity(quantity.as_str())); 1072 let Some(status) = signed_listing_product_status(record, content.as_ref(), tags) else { 1073 return Ok(None); 1074 }; 1075 let fulfillment_method = signed_listing_fulfillment_method(content.as_ref(), tags); 1076 let availability_window_id = if status == ProductStatus::Published { 1077 match fulfillment_method { 1078 Some(method) => self.ensure_signed_listing_availability_window( 1079 farm_id, 1080 listing_key.as_str(), 1081 content.as_ref(), 1082 tags, 1083 method, 1084 )?, 1085 None => None, 1086 } 1087 } else { 1088 None 1089 }; 1090 if availability_window_id.is_some() 1091 && let Some(method) = fulfillment_method 1092 { 1093 self.mark_farm_buyer_visible(farm_id, record, method)?; 1094 } 1095 self.upsert_product(ProductProjection { 1096 product_id, 1097 farm_id, 1098 title, 1099 subtitle, 1100 status, 1101 unit_label, 1102 price_minor_units, 1103 price_currency, 1104 stock_count, 1105 availability_window_id, 1106 listing_bin_id, 1107 })?; 1108 Ok(Some(projection_record)) 1109 } 1110 1111 fn import_signed_active_order( 1112 &self, 1113 record: &LocalEventRecord, 1114 ) -> Result<Option<ProjectionRecord>, AppSqliteError> { 1115 if !signed_event_record_is_usable(record) { 1116 return Ok(Some(signed_event_projection(record))); 1117 } 1118 let Some(event) = signed_event_from_record(record)? else { 1119 return Ok(Some(signed_event_projection(record))); 1120 }; 1121 let Some(current_evidence) = active_order_evidence_from_event(&event) else { 1122 return Ok(Some(signed_event_projection(record))); 1123 }; 1124 self.project_active_order(record, current_evidence)?; 1125 Ok(Some(signed_event_projection(record))) 1126 } 1127 1128 fn import_signed_validation_receipt( 1129 &self, 1130 record: &LocalEventRecord, 1131 ) -> Result<Option<ProjectionRecord>, AppSqliteError> { 1132 if !signed_event_record_is_usable(record) { 1133 return Ok(Some(signed_event_projection(record))); 1134 } 1135 let Some(event) = signed_event_from_record(record)? else { 1136 return Ok(Some(signed_event_projection(record))); 1137 }; 1138 let Ok(verified) = validation_receipt_from_event(&event) else { 1139 return Ok(Some(signed_event_projection(record))); 1140 }; 1141 self.upsert_validation_receipt_projection(&event, &verified.receipt, &verified.tags)?; 1142 Ok(Some(ProjectionRecord { 1143 kind: "validation_receipt", 1144 projected_id: Some(event.id), 1145 })) 1146 } 1147 1148 fn project_active_order( 1149 &self, 1150 record: &LocalEventRecord, 1151 current_evidence: ActiveOrderEvidence, 1152 ) -> Result<(), AppSqliteError> { 1153 if let ActiveOrderEvidence::Request(request) = ¤t_evidence { 1154 let order_id = self.upsert_order_request(record, &request.payload)?; 1155 self.attach_validation_receipts_for_request( 1156 request.event_id.as_str(), 1157 request.payload.order_id.as_str(), 1158 order_id, 1159 )?; 1160 } 1161 let mut evidence = self.load_active_order_evidence(current_evidence.order_id())?; 1162 evidence.push(current_evidence); 1163 dedupe_active_order_evidence(&mut evidence); 1164 let Some((raw_order_id, buyer_pubkey)) = evidence 1165 .first() 1166 .map(ActiveOrderEvidence::order_projection_identity) 1167 else { 1168 return Ok(()); 1169 }; 1170 let raw_order_id = raw_order_id.to_owned(); 1171 let buyer_pubkey = buyer_pubkey.to_owned(); 1172 let order_id = projected_order_id(raw_order_id.as_str(), buyer_pubkey.as_str()); 1173 let buckets = ActiveOrderEvidenceBuckets::from_evidence(evidence); 1174 let requests = buckets.requests.clone(); 1175 let revision_proposals = buckets.revision_proposals.clone(); 1176 let revision_decisions = buckets.revision_decisions.clone(); 1177 let reducer_order_id = 1178 raw_order_id 1179 .parse::<RadrootsOrderId>() 1180 .map_err(|_| AppSqliteError::DecodeId { 1181 field: "order_id", 1182 value: raw_order_id.clone(), 1183 })?; 1184 let projection = reduce_order_events( 1185 &reducer_order_id, 1186 RadrootsOrderReductionInputs { 1187 requests: buckets.requests, 1188 decisions: buckets.decisions, 1189 revision_proposals: buckets.revision_proposals, 1190 revision_decisions: buckets.revision_decisions, 1191 cancellations: buckets.cancellations, 1192 }, 1193 ); 1194 let request_payload = projection.request_event_id.as_deref().and_then(|event_id| { 1195 requests 1196 .iter() 1197 .find(|request| request.event_id == event_id) 1198 .map(|request| &request.payload) 1199 }); 1200 let revision = 1201 active_order_revision_status(&projection, &revision_proposals, &revision_decisions); 1202 let agreement_source = request_payload.map(|request| { 1203 active_order_agreement_source( 1204 request, 1205 &projection, 1206 &revision_proposals, 1207 &revision_decisions, 1208 ) 1209 }); 1210 self.apply_active_order_projection( 1211 order_id, 1212 &projection, 1213 revision, 1214 agreement_source.as_ref(), 1215 ) 1216 } 1217 1218 fn upsert_order_request( 1219 &self, 1220 record: &LocalEventRecord, 1221 payload: &RadrootsOrderRequest, 1222 ) -> Result<OrderId, AppSqliteError> { 1223 let existing_listing = 1224 self.existing_listing_projection(Some(payload.listing_addr.as_str()))?; 1225 let farm_id = if let Some(existing_listing) = existing_listing.as_ref() { 1226 existing_listing.farm_id 1227 } else { 1228 deterministic_farm_id( 1229 Some(payload.seller_pubkey.as_str()), 1230 payload.listing_addr.as_str(), 1231 ) 1232 }; 1233 self.ensure_farm_exists(farm_id)?; 1234 let order_id = projected_order_id(payload.order_id.as_str(), payload.buyer_pubkey.as_str()); 1235 let order_number = existing_order_number(self.connection, order_id)? 1236 .unwrap_or_else(|| deterministic_order_number(payload.order_id.as_str())); 1237 self.connection 1238 .execute( 1239 "INSERT INTO orders ( 1240 id, 1241 farm_id, 1242 fulfillment_window_id, 1243 order_number, 1244 customer_display_name, 1245 status, 1246 updated_at, 1247 buyer_context_key, 1248 buyer_email, 1249 buyer_phone, 1250 buyer_order_note 1251 ) VALUES (?1, ?2, null, ?3, ?4, 'needs_action', strftime('%Y-%m-%dT%H:%M:%SZ', 'now'), ?5, '', '', '') 1252 ON CONFLICT(id) DO UPDATE SET 1253 farm_id = excluded.farm_id, 1254 order_number = excluded.order_number, 1255 customer_display_name = excluded.customer_display_name, 1256 status = excluded.status, 1257 buyer_context_key = coalesce(orders.buyer_context_key, excluded.buyer_context_key), 1258 updated_at = excluded.updated_at", 1259 params![ 1260 order_id.to_string(), 1261 farm_id.to_string(), 1262 order_number.as_str(), 1263 order_customer_display_name(payload.buyer_pubkey.as_str()), 1264 order_buyer_context_key(record, payload.buyer_pubkey.as_str()), 1265 ], 1266 ) 1267 .map_err(|source| AppSqliteError::Query { 1268 operation: "upsert local interop order request", 1269 source, 1270 })?; 1271 self.replace_order_request_lines(order_id, payload, existing_listing.as_ref(), record)?; 1272 Ok(order_id) 1273 } 1274 1275 fn apply_active_order_projection( 1276 &self, 1277 order_id: OrderId, 1278 projection: &RadrootsOrderProjection, 1279 revision: TradeRevisionStatus, 1280 agreement_source: Option<&ActiveOrderAgreementSource>, 1281 ) -> Result<(), AppSqliteError> { 1282 let workflow = TradeWorkflowProjection::from_active_order_projection( 1283 order_id, 1284 projection, 1285 revision, 1286 TradeProvenanceProjection::from_primary_source(TradeWorkflowSource::LocalEvents), 1287 ); 1288 let Some(status) = order_status_from_active_order_projection(projection) else { 1289 return Ok(()); 1290 }; 1291 self.connection 1292 .execute( 1293 "UPDATE orders 1294 SET status = ?2, 1295 workflow_revision = ?3, 1296 workflow_agreement = ?4, 1297 workflow_inventory = ?5, 1298 workflow_provenance_source = ?6, 1299 workflow_provenance_last_event_id = ?7, 1300 updated_at = strftime('%Y-%m-%dT%H:%M:%SZ', 'now') 1301 WHERE id = ?1", 1302 params![ 1303 workflow.order_id.to_string(), 1304 status.storage_key(), 1305 workflow.revision.storage_key(), 1306 workflow.agreement.storage_key(), 1307 workflow.inventory.storage_key(), 1308 workflow.provenance.primary_source.storage_key(), 1309 workflow.provenance.last_event_id.as_deref() 1310 ], 1311 ) 1312 .map_err(|source| AppSqliteError::Query { 1313 operation: "apply local interop active order projection", 1314 source, 1315 })?; 1316 if projection.economics.is_some() 1317 && let Some(agreement_source) = agreement_source 1318 { 1319 self.replace_active_order_agreement_lines(workflow.order_id, agreement_source)?; 1320 } 1321 Ok(()) 1322 } 1323 1324 fn upsert_validation_receipt_projection( 1325 &self, 1326 event: &RadrootsNostrEvent, 1327 receipt: &RadrootsTradeValidationReceipt, 1328 tags: &RadrootsValidationReceiptTags, 1329 ) -> Result<(), AppSqliteError> { 1330 let order_id = match self.validation_receipt_order_attachment(tags)? { 1331 ValidationReceiptOrderAttachment::Pending => None, 1332 ValidationReceiptOrderAttachment::Attached(order_id) => Some(order_id), 1333 ValidationReceiptOrderAttachment::Rejected => return Ok(()), 1334 }; 1335 let result = TradeValidationReceiptResult::from_validation_receipt_result(receipt.result); 1336 let receipt_type = 1337 TradeValidationReceiptType::from_validation_receipt_type(receipt.receipt_type); 1338 let proof_system = TradeValidationReceiptProofSystem::from_validation_receipt_proof_system( 1339 receipt.proof.system, 1340 ); 1341 1342 self.connection 1343 .execute( 1344 "INSERT INTO order_validation_receipts ( 1345 event_id, 1346 order_id, 1347 raw_order_id, 1348 root_event_id, 1349 listing_event_id, 1350 target_event_id, 1351 receipt_type, 1352 result, 1353 proof_system, 1354 event_set_root, 1355 reducer_output_root, 1356 public_values_hash, 1357 event_created_at 1358 ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13) 1359 ON CONFLICT(event_id) DO UPDATE SET 1360 order_id = excluded.order_id, 1361 raw_order_id = excluded.raw_order_id, 1362 root_event_id = excluded.root_event_id, 1363 listing_event_id = excluded.listing_event_id, 1364 target_event_id = excluded.target_event_id, 1365 receipt_type = excluded.receipt_type, 1366 result = excluded.result, 1367 proof_system = excluded.proof_system, 1368 event_set_root = excluded.event_set_root, 1369 reducer_output_root = excluded.reducer_output_root, 1370 public_values_hash = excluded.public_values_hash, 1371 event_created_at = excluded.event_created_at", 1372 params![ 1373 event.id.as_str(), 1374 order_id.map(|order_id| order_id.to_string()), 1375 tags.order_id.as_str(), 1376 tags.root_event_id.as_str(), 1377 tags.listing_event_id.as_str(), 1378 tags.target_event_id.as_str(), 1379 receipt_type.storage_key(), 1380 result.storage_key(), 1381 proof_system.storage_key(), 1382 tags.event_set_root.as_str(), 1383 tags.reducer_output_root.as_str(), 1384 tags.public_values_hash.as_str(), 1385 i64::from(event.created_at), 1386 ], 1387 ) 1388 .map_err(|source| AppSqliteError::Query { 1389 operation: "upsert local interop validation receipt projection", 1390 source, 1391 })?; 1392 Ok(()) 1393 } 1394 1395 fn validation_receipt_order_attachment( 1396 &self, 1397 tags: &RadrootsValidationReceiptTags, 1398 ) -> Result<ValidationReceiptOrderAttachment, AppSqliteError> { 1399 let Some(root_event) = self.load_signed_event_by_event_id(tags.root_event_id.as_str())? 1400 else { 1401 return Ok(ValidationReceiptOrderAttachment::Pending); 1402 }; 1403 let Ok(envelope) = order_request_from_event(&root_event) else { 1404 return Ok(ValidationReceiptOrderAttachment::Rejected); 1405 }; 1406 if envelope.payload.order_id != tags.order_id { 1407 return Ok(ValidationReceiptOrderAttachment::Rejected); 1408 } 1409 1410 Ok(ValidationReceiptOrderAttachment::Attached( 1411 projected_order_id( 1412 envelope.payload.order_id.as_str(), 1413 envelope.payload.buyer_pubkey.as_str(), 1414 ), 1415 )) 1416 } 1417 1418 fn attach_validation_receipts_for_request( 1419 &self, 1420 root_event_id: &str, 1421 raw_order_id: &str, 1422 order_id: OrderId, 1423 ) -> Result<(), AppSqliteError> { 1424 self.connection 1425 .execute( 1426 "UPDATE order_validation_receipts 1427 SET order_id = ?3 1428 WHERE root_event_id = ?1 1429 AND raw_order_id = ?2 1430 AND order_id IS NULL", 1431 params![root_event_id, raw_order_id, order_id.to_string()], 1432 ) 1433 .map_err(|source| AppSqliteError::Query { 1434 operation: "attach local interop validation receipts to request", 1435 source, 1436 })?; 1437 Ok(()) 1438 } 1439 1440 fn load_active_order_evidence( 1441 &self, 1442 order_id: &str, 1443 ) -> Result<Vec<ActiveOrderEvidence>, AppSqliteError> { 1444 let mut evidence = Vec::new(); 1445 for kind in ACTIVE_ORDER_EVENT_KINDS { 1446 for event in self.load_signed_events_by_kind(kind)? { 1447 let Some(record) = active_order_evidence_from_event(&event) else { 1448 continue; 1449 }; 1450 if record.order_id() == order_id { 1451 evidence.push(record); 1452 } 1453 } 1454 } 1455 Ok(evidence) 1456 } 1457 1458 fn replace_order_request_lines( 1459 &self, 1460 order_id: OrderId, 1461 payload: &RadrootsOrderRequest, 1462 existing_listing: Option<&ExistingListingProjection>, 1463 record: &LocalEventRecord, 1464 ) -> Result<(), AppSqliteError> { 1465 self.connection 1466 .execute( 1467 "DELETE FROM order_lines WHERE order_id = ?1", 1468 params![order_id.to_string()], 1469 ) 1470 .map_err(|source| AppSqliteError::Query { 1471 operation: "replace local interop order lines", 1472 source, 1473 })?; 1474 for (index, item) in payload.items.iter().enumerate() { 1475 let economics_item = payload 1476 .economics 1477 .items 1478 .iter() 1479 .find(|candidate| candidate.bin_id == item.bin_id); 1480 let unit_label = economics_item 1481 .map(|item| item.quantity_unit.to_string()) 1482 .or_else(|| existing_listing.map(|listing| listing.unit_label.clone())) 1483 .unwrap_or_else(|| "item".to_owned()); 1484 let unit_price_minor_units = economics_item.and_then(|item| { 1485 parse_decimal_minor_units(item.unit_price_amount.to_string().as_str()) 1486 }); 1487 let price_currency = economics_item 1488 .map(|item| item.unit_price_currency.to_string()) 1489 .unwrap_or_else(|| payload.economics.currency.to_string()); 1490 let title = existing_listing 1491 .map(|listing| listing.title.clone()) 1492 .unwrap_or_else(|| item.bin_id.to_string()); 1493 self.connection 1494 .execute( 1495 "INSERT INTO order_lines ( 1496 id, 1497 order_id, 1498 title, 1499 quantity_value, 1500 quantity_unit_label, 1501 quantity_display, 1502 listing_bin_id, 1503 unit_price_minor_units, 1504 price_currency, 1505 farm_key, 1506 listing_addr, 1507 listing_event_id, 1508 listing_relays_json, 1509 seller_pubkey, 1510 sort_index 1511 ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, null, ?13, ?14)", 1512 params![ 1513 format!( 1514 "{}:{}", 1515 order_id, 1516 order_line_product_id(payload, existing_listing, item) 1517 ), 1518 order_id.to_string(), 1519 title.as_str(), 1520 i64::from(item.bin_count), 1521 unit_label.as_str(), 1522 format_quantity_display(item.bin_count, unit_label.as_str()), 1523 item.bin_id.as_str(), 1524 unit_price_minor_units, 1525 price_currency.as_str(), 1526 existing_listing.and_then(|listing| listing.farm_key.as_deref()), 1527 payload.listing_addr.as_str(), 1528 listing_event_id_from_order_record(record).as_deref(), 1529 payload.seller_pubkey.as_str(), 1530 index as i64, 1531 ], 1532 ) 1533 .map_err(|source| AppSqliteError::Query { 1534 operation: "insert local interop order line", 1535 source, 1536 })?; 1537 } 1538 Ok(()) 1539 } 1540 1541 fn replace_active_order_agreement_lines( 1542 &self, 1543 order_id: OrderId, 1544 source: &ActiveOrderAgreementSource, 1545 ) -> Result<(), AppSqliteError> { 1546 let existing_listing = 1547 self.existing_listing_projection(Some(source.listing_addr.as_str()))?; 1548 let metadata = self.existing_order_line_metadata(order_id)?; 1549 self.connection 1550 .execute( 1551 "DELETE FROM order_lines WHERE order_id = ?1", 1552 params![order_id.to_string()], 1553 ) 1554 .map_err(|source| AppSqliteError::Query { 1555 operation: "replace local interop active order agreement lines", 1556 source, 1557 })?; 1558 for (index, item) in source.items.iter().enumerate() { 1559 let economics_item = source 1560 .economics 1561 .items 1562 .iter() 1563 .find(|candidate| candidate.bin_id == item.bin_id); 1564 let unit_label = economics_item 1565 .map(|item| item.quantity_unit.to_string()) 1566 .or_else(|| { 1567 existing_listing 1568 .as_ref() 1569 .map(|listing| listing.unit_label.clone()) 1570 }) 1571 .unwrap_or_else(|| "item".to_owned()); 1572 let unit_price_minor_units = economics_item.and_then(|item| { 1573 parse_decimal_minor_units(item.unit_price_amount.to_string().as_str()) 1574 }); 1575 let price_currency = economics_item 1576 .map(|item| item.unit_price_currency.to_string()) 1577 .unwrap_or_else(|| source.economics.currency.to_string()); 1578 let title = existing_listing 1579 .as_ref() 1580 .filter(|listing| { 1581 listing 1582 .listing_bin_id 1583 .as_deref() 1584 .is_none_or(|listing_bin_id| listing_bin_id == item.bin_id) 1585 }) 1586 .map(|listing| listing.title.clone()) 1587 .unwrap_or_else(|| item.bin_id.to_string()); 1588 self.connection 1589 .execute( 1590 "INSERT INTO order_lines ( 1591 id, 1592 order_id, 1593 title, 1594 quantity_value, 1595 quantity_unit_label, 1596 quantity_display, 1597 listing_bin_id, 1598 unit_price_minor_units, 1599 price_currency, 1600 farm_key, 1601 listing_addr, 1602 listing_event_id, 1603 listing_relays_json, 1604 seller_pubkey, 1605 sort_index 1606 ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15)", 1607 params![ 1608 format!( 1609 "{}:{}", 1610 order_id, 1611 order_agreement_line_product_id( 1612 source.listing_addr.as_str(), 1613 source.seller_pubkey.as_str(), 1614 existing_listing.as_ref(), 1615 item, 1616 ) 1617 ), 1618 order_id.to_string(), 1619 title.as_str(), 1620 i64::from(item.bin_count), 1621 unit_label.as_str(), 1622 format_quantity_display(item.bin_count, unit_label.as_str()), 1623 item.bin_id.as_str(), 1624 unit_price_minor_units, 1625 price_currency.as_str(), 1626 existing_listing 1627 .as_ref() 1628 .and_then(|listing| listing.farm_key.as_deref()), 1629 source.listing_addr.as_str(), 1630 metadata 1631 .as_ref() 1632 .and_then(|metadata| metadata.listing_event_id.as_deref()), 1633 metadata 1634 .as_ref() 1635 .and_then(|metadata| metadata.listing_relays_json.as_deref()), 1636 source.seller_pubkey.as_str(), 1637 index as i64, 1638 ], 1639 ) 1640 .map_err(|source| AppSqliteError::Query { 1641 operation: "insert local interop active order agreement line", 1642 source, 1643 })?; 1644 } 1645 Ok(()) 1646 } 1647 1648 fn existing_order_line_metadata( 1649 &self, 1650 order_id: OrderId, 1651 ) -> Result<Option<ExistingOrderLineMetadata>, AppSqliteError> { 1652 self.connection 1653 .query_row( 1654 "SELECT listing_event_id, listing_relays_json 1655 FROM order_lines 1656 WHERE order_id = ?1 1657 ORDER BY sort_index ASC, id ASC 1658 LIMIT 1", 1659 params![order_id.to_string()], 1660 |row| { 1661 Ok(ExistingOrderLineMetadata { 1662 listing_event_id: row.get::<_, Option<String>>(0)?, 1663 listing_relays_json: row.get::<_, Option<String>>(1)?, 1664 }) 1665 }, 1666 ) 1667 .optional() 1668 .map_err(|source| AppSqliteError::Query { 1669 operation: "load existing local interop order line metadata", 1670 source, 1671 }) 1672 } 1673 1674 fn upsert_farm_summary(&self, farm: &FarmSummary) -> Result<(), AppSqliteError> { 1675 self.connection 1676 .execute( 1677 "INSERT INTO farms (id, display_name, readiness, created_at, updated_at) 1678 VALUES (?1, ?2, ?3, strftime('%Y-%m-%dT%H:%M:%SZ', 'now'), strftime('%Y-%m-%dT%H:%M:%SZ', 'now')) 1679 ON CONFLICT(id) DO UPDATE SET 1680 display_name = excluded.display_name, 1681 readiness = excluded.readiness, 1682 updated_at = excluded.updated_at", 1683 params![ 1684 farm.farm_id.to_string(), 1685 farm.display_name.as_str(), 1686 farm_readiness_storage_key(farm.readiness), 1687 ], 1688 ) 1689 .map_err(|source| AppSqliteError::Query { 1690 operation: "upsert local interop farm summary", 1691 source, 1692 })?; 1693 Ok(()) 1694 } 1695 1696 fn upsert_local_work_farm_summary(&self, farm: &FarmSummary) -> Result<(), AppSqliteError> { 1697 self.connection 1698 .execute( 1699 "INSERT INTO farms (id, display_name, readiness, created_at, updated_at) 1700 VALUES (?1, ?2, ?3, strftime('%Y-%m-%dT%H:%M:%SZ', 'now'), strftime('%Y-%m-%dT%H:%M:%SZ', 'now')) 1701 ON CONFLICT(id) DO UPDATE SET 1702 display_name = excluded.display_name, 1703 readiness = CASE 1704 WHEN farms.readiness = 'ready' AND excluded.readiness = 'incomplete' 1705 THEN farms.readiness 1706 ELSE excluded.readiness 1707 END, 1708 updated_at = excluded.updated_at", 1709 params![ 1710 farm.farm_id.to_string(), 1711 farm.display_name.as_str(), 1712 farm_readiness_storage_key(farm.readiness), 1713 ], 1714 ) 1715 .map_err(|source| AppSqliteError::Query { 1716 operation: "upsert local interop local work farm summary", 1717 source, 1718 })?; 1719 Ok(()) 1720 } 1721 1722 fn mark_farm_buyer_visible( 1723 &self, 1724 farm_id: FarmId, 1725 record: &LocalEventRecord, 1726 method: FarmOrderMethod, 1727 ) -> Result<(), AppSqliteError> { 1728 self.connection 1729 .execute( 1730 "UPDATE farms 1731 SET readiness = 'ready', 1732 updated_at = strftime('%Y-%m-%dT%H:%M:%SZ', 'now') 1733 WHERE id = ?1", 1734 [farm_id.to_string()], 1735 ) 1736 .map_err(|source| AppSqliteError::Query { 1737 operation: "mark local interop farm buyer visible", 1738 source, 1739 })?; 1740 let Some(account_id) = record 1741 .owner_account_id 1742 .as_deref() 1743 .map(str::trim) 1744 .filter(|value| !value.is_empty()) 1745 else { 1746 return Ok(()); 1747 }; 1748 let display_name = self 1749 .load_farm_display_name(farm_id)? 1750 .unwrap_or_else(|| "Local farm".to_owned()); 1751 self.connection 1752 .execute( 1753 "INSERT INTO account_farm_setups ( 1754 account_id, 1755 farm_name, 1756 location_or_service_area, 1757 pickup_enabled, 1758 delivery_enabled, 1759 shipping_enabled, 1760 saved_farm_id, 1761 saved_farm_display_name, 1762 saved_farm_readiness, 1763 updated_at 1764 ) VALUES (?1, ?2, '', ?3, ?4, ?5, ?6, ?2, 'ready', strftime('%Y-%m-%dT%H:%M:%SZ', 'now')) 1765 ON CONFLICT(account_id) DO UPDATE SET 1766 farm_name = CASE 1767 WHEN trim(account_farm_setups.farm_name) = '' THEN excluded.farm_name 1768 ELSE account_farm_setups.farm_name 1769 END, 1770 pickup_enabled = max(account_farm_setups.pickup_enabled, excluded.pickup_enabled), 1771 delivery_enabled = max(account_farm_setups.delivery_enabled, excluded.delivery_enabled), 1772 shipping_enabled = max(account_farm_setups.shipping_enabled, excluded.shipping_enabled), 1773 saved_farm_id = excluded.saved_farm_id, 1774 saved_farm_display_name = excluded.saved_farm_display_name, 1775 saved_farm_readiness = excluded.saved_farm_readiness, 1776 updated_at = excluded.updated_at", 1777 params![ 1778 account_id, 1779 display_name.as_str(), 1780 i64::from(method == FarmOrderMethod::Pickup), 1781 i64::from(method == FarmOrderMethod::Delivery), 1782 i64::from(method == FarmOrderMethod::Shipping), 1783 farm_id.to_string(), 1784 ], 1785 ) 1786 .map_err(|source| AppSqliteError::Query { 1787 operation: "upsert local interop buyer fulfillment method", 1788 source, 1789 })?; 1790 Ok(()) 1791 } 1792 1793 fn ensure_farm_exists(&self, farm_id: FarmId) -> Result<(), AppSqliteError> { 1794 let exists = self 1795 .connection 1796 .query_row( 1797 "SELECT EXISTS(SELECT 1 FROM farms WHERE id = ?1)", 1798 [farm_id.to_string()], 1799 |row| row.get::<_, bool>(0), 1800 ) 1801 .map_err(|source| AppSqliteError::Query { 1802 operation: "check local interop farm existence", 1803 source, 1804 })?; 1805 if !exists { 1806 self.upsert_farm_summary(&FarmSummary { 1807 farm_id, 1808 display_name: "Local farm".to_owned(), 1809 readiness: FarmReadiness::Incomplete, 1810 })?; 1811 } 1812 Ok(()) 1813 } 1814 1815 fn load_farm_display_name(&self, farm_id: FarmId) -> Result<Option<String>, AppSqliteError> { 1816 self.connection 1817 .query_row( 1818 "SELECT display_name FROM farms WHERE id = ?1 LIMIT 1", 1819 [farm_id.to_string()], 1820 |row| row.get::<_, String>(0), 1821 ) 1822 .optional() 1823 .map_err(|source| AppSqliteError::Query { 1824 operation: "load local interop farm display name", 1825 source, 1826 }) 1827 } 1828 1829 fn load_farm_readiness( 1830 &self, 1831 farm_id: FarmId, 1832 ) -> Result<Option<FarmReadiness>, AppSqliteError> { 1833 self.connection 1834 .query_row( 1835 "SELECT readiness FROM farms WHERE id = ?1 LIMIT 1", 1836 [farm_id.to_string()], 1837 |row| row.get::<_, String>(0), 1838 ) 1839 .optional() 1840 .map_err(|source| AppSqliteError::Query { 1841 operation: "load local interop farm readiness", 1842 source, 1843 })? 1844 .map(|readiness| farm_readiness_from_storage_key(readiness.as_str())) 1845 .transpose() 1846 } 1847 1848 fn ensure_signed_listing_availability_window( 1849 &self, 1850 farm_id: FarmId, 1851 listing_key: &str, 1852 content: Option<&Value>, 1853 tags: Option<&Value>, 1854 method: FarmOrderMethod, 1855 ) -> Result<Option<FulfillmentWindowId>, AppSqliteError> { 1856 let Some(window) = signed_listing_availability_window(content, tags) else { 1857 return Ok(None); 1858 }; 1859 let starts_at = 1860 self.unix_epoch_to_utc_timestamp(window.start, "format listing availability start")?; 1861 let ends_at = 1862 self.unix_epoch_to_utc_timestamp(window.end, "format listing availability end")?; 1863 if ends_at <= starts_at { 1864 return Ok(None); 1865 } 1866 let pickup_location_id = if method == FarmOrderMethod::Pickup { 1867 let Some(location_primary) = signed_listing_location_primary(content, tags) else { 1868 return Ok(None); 1869 }; 1870 Some(self.upsert_signed_listing_pickup_location(farm_id, location_primary.as_str())?) 1871 } else { 1872 None 1873 }; 1874 let farm_id_string = farm_id.to_string(); 1875 let fulfillment_window_id = FulfillmentWindowId::from(deterministic_uuid( 1876 "radroots-app-local-interop-fulfillment-window", 1877 Some(farm_id_string.as_str()), 1878 listing_key, 1879 )); 1880 self.connection 1881 .execute( 1882 "INSERT INTO fulfillment_windows ( 1883 id, 1884 farm_id, 1885 starts_at, 1886 ends_at, 1887 capacity_limit, 1888 created_at, 1889 updated_at, 1890 pickup_location_id, 1891 label, 1892 order_cutoff_at 1893 ) VALUES (?1, ?2, ?3, ?4, null, strftime('%Y-%m-%dT%H:%M:%SZ', 'now'), strftime('%Y-%m-%dT%H:%M:%SZ', 'now'), ?5, '', ?3) 1894 ON CONFLICT(id) DO UPDATE SET 1895 farm_id = excluded.farm_id, 1896 starts_at = excluded.starts_at, 1897 ends_at = excluded.ends_at, 1898 pickup_location_id = excluded.pickup_location_id, 1899 order_cutoff_at = excluded.order_cutoff_at, 1900 updated_at = excluded.updated_at", 1901 params![ 1902 fulfillment_window_id.to_string(), 1903 farm_id_string.as_str(), 1904 starts_at.as_str(), 1905 ends_at.as_str(), 1906 pickup_location_id.map(|id| id.to_string()), 1907 ], 1908 ) 1909 .map_err(|source| AppSqliteError::Query { 1910 operation: "upsert local interop listing fulfillment window", 1911 source, 1912 })?; 1913 Ok(Some(fulfillment_window_id)) 1914 } 1915 1916 fn upsert_signed_listing_pickup_location( 1917 &self, 1918 farm_id: FarmId, 1919 location_primary: &str, 1920 ) -> Result<PickupLocationId, AppSqliteError> { 1921 let farm_id_string = farm_id.to_string(); 1922 let pickup_location_id = PickupLocationId::from(deterministic_uuid( 1923 "radroots-app-local-interop-pickup-location", 1924 Some(farm_id_string.as_str()), 1925 location_primary, 1926 )); 1927 self.connection 1928 .execute( 1929 "INSERT INTO pickup_locations ( 1930 id, 1931 farm_id, 1932 label, 1933 address_line, 1934 directions, 1935 is_default, 1936 created_at, 1937 updated_at 1938 ) VALUES (?1, ?2, ?3, ?3, null, 0, strftime('%Y-%m-%dT%H:%M:%SZ', 'now'), strftime('%Y-%m-%dT%H:%M:%SZ', 'now')) 1939 ON CONFLICT(id) DO UPDATE SET 1940 farm_id = excluded.farm_id, 1941 label = excluded.label, 1942 address_line = excluded.address_line, 1943 updated_at = excluded.updated_at", 1944 params![ 1945 pickup_location_id.to_string(), 1946 farm_id_string.as_str(), 1947 location_primary, 1948 ], 1949 ) 1950 .map_err(|source| AppSqliteError::Query { 1951 operation: "upsert local interop listing pickup location", 1952 source, 1953 })?; 1954 Ok(pickup_location_id) 1955 } 1956 1957 fn unix_epoch_to_utc_timestamp( 1958 &self, 1959 seconds: u64, 1960 operation: &'static str, 1961 ) -> Result<String, AppSqliteError> { 1962 let seconds = i64::try_from(seconds).map_err(|_| AppSqliteError::InvalidProjection { 1963 reason: "listing availability timestamp is out of range", 1964 })?; 1965 let timestamp = self 1966 .connection 1967 .query_row( 1968 "SELECT strftime('%Y-%m-%dT%H:%M:%SZ', ?1, 'unixepoch')", 1969 [seconds], 1970 |row| row.get::<_, Option<String>>(0), 1971 ) 1972 .map_err(|source| AppSqliteError::Query { operation, source })?; 1973 timestamp.ok_or(AppSqliteError::InvalidProjection { 1974 reason: "listing availability timestamp is invalid", 1975 }) 1976 } 1977 1978 fn upsert_product(&self, projection: ProductProjection) -> Result<(), AppSqliteError> { 1979 self.connection 1980 .execute( 1981 "INSERT INTO products ( 1982 id, 1983 farm_id, 1984 title, 1985 subtitle, 1986 status, 1987 unit_label, 1988 price_minor_units, 1989 price_currency, 1990 stock_count, 1991 availability_window_id, 1992 listing_bin_id, 1993 updated_at 1994 ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, strftime('%Y-%m-%dT%H:%M:%SZ', 'now')) 1995 ON CONFLICT(id) DO UPDATE SET 1996 farm_id = excluded.farm_id, 1997 title = excluded.title, 1998 subtitle = excluded.subtitle, 1999 status = CASE 2000 WHEN excluded.status = 'draft' 2001 AND products.status IN ('published', 'paused', 'archived') 2002 THEN products.status 2003 ELSE excluded.status 2004 END, 2005 unit_label = excluded.unit_label, 2006 price_minor_units = excluded.price_minor_units, 2007 price_currency = excluded.price_currency, 2008 stock_count = excluded.stock_count, 2009 availability_window_id = CASE 2010 WHEN excluded.status = 'draft' 2011 AND products.status IN ('published', 'paused', 'archived') 2012 THEN products.availability_window_id 2013 ELSE excluded.availability_window_id 2014 END, 2015 listing_bin_id = coalesce(excluded.listing_bin_id, products.listing_bin_id), 2016 updated_at = excluded.updated_at", 2017 params![ 2018 projection.product_id.to_string(), 2019 projection.farm_id.to_string(), 2020 projection.title.as_str(), 2021 projection.subtitle.as_str(), 2022 projection.status.storage_key(), 2023 projection.unit_label.as_str(), 2024 projection.price_minor_units, 2025 projection.price_currency.as_str(), 2026 projection.stock_count, 2027 projection.availability_window_id.map(|id| id.to_string()), 2028 projection.listing_bin_id.as_deref(), 2029 ], 2030 ) 2031 .map_err(|source| AppSqliteError::Query { 2032 operation: "upsert local interop product", 2033 source, 2034 })?; 2035 Ok(()) 2036 } 2037 2038 fn existing_listing_projection( 2039 &self, 2040 listing_addr: Option<&str>, 2041 ) -> Result<Option<ExistingListingProjection>, AppSqliteError> { 2042 let Some(listing_addr) = listing_addr 2043 .map(str::trim) 2044 .filter(|listing_addr| !listing_addr.is_empty()) 2045 else { 2046 return Ok(None); 2047 }; 2048 let Some((product_id, farm_id, title, unit_label, listing_bin_id, farm_key)) = self 2049 .connection 2050 .query_row( 2051 "SELECT 2052 products.id, 2053 products.farm_id, 2054 products.title, 2055 products.unit_label, 2056 products.listing_bin_id, 2057 local_interop_imports.farm_key 2058 FROM local_interop_imports 2059 JOIN products ON products.id = local_interop_imports.projected_id 2060 WHERE local_interop_imports.projected_kind = 'listing' 2061 AND local_interop_imports.projected_id IS NOT NULL 2062 AND local_interop_imports.listing_addr = ?1 2063 ORDER BY local_interop_imports.local_seq DESC 2064 LIMIT 1", 2065 [listing_addr], 2066 |row| { 2067 Ok(( 2068 row.get::<_, String>(0)?, 2069 row.get::<_, String>(1)?, 2070 row.get::<_, String>(2)?, 2071 row.get::<_, String>(3)?, 2072 row.get::<_, Option<String>>(4)?, 2073 row.get::<_, Option<String>>(5)?, 2074 )) 2075 }, 2076 ) 2077 .optional() 2078 .map_err(|source| AppSqliteError::Query { 2079 operation: "load existing local interop listing projection", 2080 source, 2081 })? 2082 else { 2083 return Ok(None); 2084 }; 2085 Ok(Some(ExistingListingProjection { 2086 product_id: product_id 2087 .parse() 2088 .map_err(|_| AppSqliteError::InvalidProjection { 2089 reason: "existing listing projection product id must parse", 2090 })?, 2091 farm_id: farm_id 2092 .parse() 2093 .map_err(|_| AppSqliteError::InvalidProjection { 2094 reason: "existing listing projection farm id must parse", 2095 })?, 2096 title, 2097 unit_label, 2098 listing_bin_id, 2099 farm_key, 2100 })) 2101 } 2102 2103 fn existing_app_origin_listing_projection( 2104 &self, 2105 record: &LocalEventRecord, 2106 farm_key: &str, 2107 listing_key: &str, 2108 listing_pubkey: Option<&str>, 2109 tags: Option<&Value>, 2110 ) -> Result<Option<ExistingListingProjection>, AppSqliteError> { 2111 if record.source_runtime != SourceRuntime::Network { 2112 return Ok(None); 2113 } 2114 let Some(farm_id) = parse_app_d_tag_uuid(farm_key).map(FarmId::from) else { 2115 return Ok(None); 2116 }; 2117 let Some(product_id) = parse_app_d_tag_uuid(listing_key).map(ProductId::from) else { 2118 return Ok(None); 2119 }; 2120 let Some(listing_addr) = record 2121 .listing_addr 2122 .as_deref() 2123 .map(str::trim) 2124 .filter(|listing_addr| !listing_addr.is_empty()) 2125 else { 2126 return Ok(None); 2127 }; 2128 let Some(listing_addr_parts) = listing_address_parts(listing_addr) else { 2129 return Ok(None); 2130 }; 2131 let Some(event_pubkey) = record 2132 .event_pubkey 2133 .as_deref() 2134 .map(str::trim) 2135 .filter(|event_pubkey| !event_pubkey.is_empty()) 2136 else { 2137 return Ok(None); 2138 }; 2139 if listing_addr_parts.kind != KIND_LISTING 2140 || listing_addr_parts.pubkey != event_pubkey 2141 || listing_addr_parts.d_tag != listing_key 2142 || listing_pubkey.map(str::trim) != Some(event_pubkey) 2143 || !signed_farm_address_matches(tags, farm_key, event_pubkey) 2144 { 2145 return Ok(None); 2146 } 2147 let Some((product_id, farm_id, title, unit_label, listing_bin_id, evidence_farm_key)) = 2148 self.connection 2149 .query_row( 2150 "SELECT 2151 products.id, 2152 products.farm_id, 2153 products.title, 2154 products.unit_label, 2155 products.listing_bin_id, 2156 local_interop_imports.farm_key 2157 FROM local_interop_imports 2158 JOIN products ON products.id = local_interop_imports.projected_id 2159 WHERE local_interop_imports.projected_kind = 'listing' 2160 AND local_interop_imports.projected_id = ?1 2161 AND local_interop_imports.source_runtime = 'app' 2162 AND local_interop_imports.farm_key = ?2 2163 AND local_interop_imports.listing_addr = ?3 2164 AND local_interop_imports.owner_pubkey = ?4 2165 AND products.id = ?1 2166 AND products.farm_id = ?5 2167 LIMIT 1", 2168 params![ 2169 product_id.to_string(), 2170 farm_key, 2171 listing_addr, 2172 event_pubkey, 2173 farm_id.to_string(), 2174 ], 2175 |row| { 2176 Ok(( 2177 row.get::<_, String>(0)?, 2178 row.get::<_, String>(1)?, 2179 row.get::<_, String>(2)?, 2180 row.get::<_, String>(3)?, 2181 row.get::<_, Option<String>>(4)?, 2182 row.get::<_, Option<String>>(5)?, 2183 )) 2184 }, 2185 ) 2186 .optional() 2187 .map_err(|source| AppSqliteError::Query { 2188 operation: "load existing app-origin listing projection", 2189 source, 2190 })? 2191 else { 2192 return Ok(None); 2193 }; 2194 Ok(Some(ExistingListingProjection { 2195 product_id: product_id 2196 .parse() 2197 .map_err(|_| AppSqliteError::InvalidProjection { 2198 reason: "existing app-origin listing projection product id must parse", 2199 })?, 2200 farm_id: farm_id 2201 .parse() 2202 .map_err(|_| AppSqliteError::InvalidProjection { 2203 reason: "existing app-origin listing projection farm id must parse", 2204 })?, 2205 title, 2206 unit_label, 2207 listing_bin_id, 2208 farm_key: evidence_farm_key, 2209 })) 2210 } 2211 2212 fn signed_listing_is_current( 2213 &self, 2214 record: &LocalEventRecord, 2215 listing_key: &str, 2216 ) -> Result<bool, AppSqliteError> { 2217 if !signed_listing_has_public_evidence(record) { 2218 return Ok(true); 2219 } 2220 let Some(incoming_key) = listing_currentness_key( 2221 record.event_created_at, 2222 record.event_id.as_deref(), 2223 signed_event_evidence_precedence( 2224 record.source_runtime.as_str(), 2225 record.owner_account_id.as_deref(), 2226 record.status.as_str(), 2227 record.outbox_status.as_str(), 2228 ), 2229 ) else { 2230 return Ok(true); 2231 }; 2232 let Some(identity) = ListingCurrentnessIdentity::from_record(record, listing_key) else { 2233 return Ok(true); 2234 }; 2235 let Some(current_key) = self.current_listing_key(&identity)? else { 2236 return Ok(true); 2237 }; 2238 Ok(incoming_key >= current_key) 2239 } 2240 2241 fn current_listing_key( 2242 &self, 2243 identity: &ListingCurrentnessIdentity, 2244 ) -> Result<Option<ListingCurrentnessKey>, AppSqliteError> { 2245 let mut keys = Vec::new(); 2246 match identity { 2247 ListingCurrentnessIdentity::ListingAddress(listing_addr) => { 2248 let mut statement = self 2249 .connection 2250 .prepare( 2251 "SELECT 2252 event_id, 2253 event_created_at, 2254 source_runtime, 2255 owner_account_id, 2256 local_status, 2257 outbox_status, 2258 relay_delivery_json 2259 FROM local_interop_imports 2260 WHERE record_family = 'signed_event' 2261 AND projected_kind = 'listing' 2262 AND listing_addr = ?1", 2263 ) 2264 .map_err(|source| AppSqliteError::Query { 2265 operation: "prepare current listing-address evidence query", 2266 source, 2267 })?; 2268 let rows = statement 2269 .query_map(params![listing_addr.as_str()], listing_currentness_row) 2270 .map_err(|source| AppSqliteError::Query { 2271 operation: "query current listing-address evidence", 2272 source, 2273 })?; 2274 for row in rows { 2275 let evidence = row.map_err(|source| AppSqliteError::Query { 2276 operation: "read current listing-address evidence", 2277 source, 2278 })?; 2279 if let Some(key) = evidence.into_currentness_key() { 2280 keys.push(key); 2281 } 2282 } 2283 } 2284 ListingCurrentnessIdentity::KindPubkeyDTag { 2285 event_kind, 2286 event_pubkey, 2287 listing_key, 2288 } => { 2289 let mut statement = self 2290 .connection 2291 .prepare( 2292 "SELECT 2293 event_id, 2294 event_created_at, 2295 source_runtime, 2296 owner_account_id, 2297 local_status, 2298 outbox_status, 2299 relay_delivery_json, 2300 event_tags_json, 2301 event_content, 2302 listing_addr 2303 FROM local_interop_imports 2304 WHERE record_family = 'signed_event' 2305 AND projected_kind = 'listing' 2306 AND event_kind = ?1 2307 AND event_pubkey = ?2", 2308 ) 2309 .map_err(|source| AppSqliteError::Query { 2310 operation: "prepare current listing identity evidence query", 2311 source, 2312 })?; 2313 let rows = statement 2314 .query_map( 2315 params![event_kind, event_pubkey.as_str()], 2316 listing_currentness_identity_row, 2317 ) 2318 .map_err(|source| AppSqliteError::Query { 2319 operation: "query current listing identity evidence", 2320 source, 2321 })?; 2322 for row in rows { 2323 let evidence = row.map_err(|source| AppSqliteError::Query { 2324 operation: "read current listing identity evidence", 2325 source, 2326 })?; 2327 if evidence.listing_key().as_deref() == Some(listing_key.as_str()) 2328 && let Some(key) = evidence.currentness.into_currentness_key() 2329 { 2330 keys.push(key); 2331 } 2332 } 2333 } 2334 } 2335 Ok(keys.into_iter().max()) 2336 } 2337 2338 fn record_import( 2339 &self, 2340 record: &LocalEventRecord, 2341 projected_kind: &str, 2342 projected_id: Option<String>, 2343 ) -> Result<(), AppSqliteError> { 2344 let event_tags_json = record 2345 .event_tags_json 2346 .as_ref() 2347 .map(serde_json::to_string) 2348 .transpose() 2349 .map_err(|_| AppSqliteError::InvalidProjection { 2350 reason: "local interop event tags json must encode", 2351 })?; 2352 let raw_event_json = record 2353 .raw_event_json 2354 .as_ref() 2355 .map(serde_json::to_string) 2356 .transpose() 2357 .map_err(|_| AppSqliteError::InvalidProjection { 2358 reason: "local interop raw event json must encode", 2359 })?; 2360 let relay_delivery_json = record 2361 .relay_delivery_json 2362 .as_ref() 2363 .map(serde_json::to_string) 2364 .transpose() 2365 .map_err(|_| AppSqliteError::InvalidProjection { 2366 reason: "local interop relay delivery json must encode", 2367 })?; 2368 self.connection 2369 .execute( 2370 "INSERT INTO local_interop_imports ( 2371 record_id, 2372 local_seq, 2373 record_family, 2374 local_status, 2375 source_runtime, 2376 owner_account_id, 2377 owner_pubkey, 2378 farm_key, 2379 listing_addr, 2380 projected_kind, 2381 projected_id, 2382 event_id, 2383 event_kind, 2384 event_pubkey, 2385 event_created_at, 2386 event_tags_json, 2387 event_content, 2388 event_sig, 2389 raw_event_json, 2390 outbox_status, 2391 relay_delivery_json, 2392 imported_at 2393 ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16, ?17, ?18, ?19, ?20, ?21, strftime('%Y-%m-%dT%H:%M:%SZ', 'now')) 2394 ON CONFLICT(record_id) DO UPDATE SET 2395 local_seq = excluded.local_seq, 2396 record_family = excluded.record_family, 2397 local_status = excluded.local_status, 2398 source_runtime = excluded.source_runtime, 2399 owner_account_id = excluded.owner_account_id, 2400 owner_pubkey = excluded.owner_pubkey, 2401 farm_key = excluded.farm_key, 2402 listing_addr = excluded.listing_addr, 2403 projected_kind = excluded.projected_kind, 2404 projected_id = excluded.projected_id, 2405 event_id = excluded.event_id, 2406 event_kind = excluded.event_kind, 2407 event_pubkey = excluded.event_pubkey, 2408 event_created_at = excluded.event_created_at, 2409 event_tags_json = excluded.event_tags_json, 2410 event_content = excluded.event_content, 2411 event_sig = excluded.event_sig, 2412 raw_event_json = excluded.raw_event_json, 2413 outbox_status = excluded.outbox_status, 2414 relay_delivery_json = excluded.relay_delivery_json, 2415 imported_at = excluded.imported_at", 2416 params![ 2417 record.record_id.as_str(), 2418 record.seq, 2419 record.family.as_str(), 2420 record.status.as_str(), 2421 record.source_runtime.as_str(), 2422 record.owner_account_id.as_deref(), 2423 record.owner_pubkey.as_deref(), 2424 record.farm_id.as_deref(), 2425 record.listing_addr.as_deref(), 2426 projected_kind, 2427 projected_id.as_deref(), 2428 record.event_id.as_deref(), 2429 record.event_kind, 2430 record.event_pubkey.as_deref(), 2431 record.event_created_at, 2432 event_tags_json.as_deref(), 2433 record.event_content.as_deref(), 2434 record.event_sig.as_deref(), 2435 raw_event_json.as_deref(), 2436 record.outbox_status.as_str(), 2437 relay_delivery_json.as_deref(), 2438 ], 2439 ) 2440 .map_err(|source| AppSqliteError::Query { 2441 operation: "record local interop import", 2442 source, 2443 })?; 2444 Ok(()) 2445 } 2446 } 2447 2448 impl AppSqliteStore { 2449 pub fn local_interop_repository(&self) -> AppLocalInteropRepository<'_> { 2450 AppLocalInteropRepository::new(&self.connection) 2451 } 2452 2453 pub fn import_shared_local_events_from_path( 2454 &self, 2455 shared_database_path: &Path, 2456 ) -> Result<AppLocalInteropImportReport, AppSqliteError> { 2457 self.local_interop_repository() 2458 .import_from_path(shared_database_path) 2459 } 2460 2461 pub fn import_shared_local_events_from_store<E>( 2462 &self, 2463 store: &LocalEventsStore<E>, 2464 ) -> Result<AppLocalInteropImportReport, AppSqliteError> 2465 where 2466 E: SqlExecutor, 2467 { 2468 self.local_interop_repository().import_from_store(store) 2469 } 2470 2471 pub fn import_local_event_records( 2472 &self, 2473 records: &[LocalEventRecord], 2474 ) -> Result<AppLocalInteropImportReport, AppSqliteError> { 2475 self.local_interop_repository().import_records(records) 2476 } 2477 2478 pub fn load_local_interop_records( 2479 &self, 2480 ) -> Result<Vec<StoredLocalInteropRecord>, AppSqliteError> { 2481 self.local_interop_repository().load_records() 2482 } 2483 2484 pub fn load_local_interop_signed_events_by_kind( 2485 &self, 2486 event_kind: i64, 2487 ) -> Result<Vec<RadrootsNostrEvent>, AppSqliteError> { 2488 self.local_interop_repository() 2489 .load_signed_events_by_kind(event_kind) 2490 } 2491 } 2492 2493 #[derive(Clone, Copy, Debug, Eq, PartialEq)] 2494 enum ImportOutcome { 2495 Imported, 2496 Skipped, 2497 } 2498 2499 #[derive(Clone, Debug, Eq, PartialEq)] 2500 enum DuplicateSignedEventAction { 2501 Import, 2502 ReplaceExisting(String), 2503 Skip, 2504 } 2505 2506 #[derive(Clone, Copy, Debug, Eq, PartialEq)] 2507 enum ValidationReceiptOrderAttachment { 2508 Pending, 2509 Attached(OrderId), 2510 Rejected, 2511 } 2512 2513 #[derive(Clone, Debug, Eq, PartialEq)] 2514 struct ProjectionRecord { 2515 kind: &'static str, 2516 projected_id: Option<String>, 2517 } 2518 2519 #[derive(Clone, Debug, Eq, PartialEq)] 2520 struct StoredSignedEventDuplicate { 2521 source_runtime: String, 2522 owner_account_id: Option<String>, 2523 local_status: String, 2524 outbox_status: String, 2525 } 2526 2527 #[derive(Clone, Debug, Eq, PartialEq)] 2528 struct StoredLocalInteropSignedEventEvidence { 2529 event_id: Option<String>, 2530 event_kind: Option<i64>, 2531 local_status: String, 2532 outbox_status: String, 2533 relay_delivery_json: Option<String>, 2534 event_pubkey: Option<String>, 2535 event_created_at: Option<i64>, 2536 event_tags_json: Option<String>, 2537 event_content: Option<String>, 2538 event_sig: Option<String>, 2539 } 2540 2541 #[derive(Clone, Debug, Eq, PartialEq)] 2542 struct StoredListingCurrentnessEvidence { 2543 event_id: Option<String>, 2544 event_created_at: Option<i64>, 2545 source_runtime: String, 2546 owner_account_id: Option<String>, 2547 local_status: String, 2548 outbox_status: String, 2549 relay_delivery_json: Option<String>, 2550 } 2551 2552 impl StoredListingCurrentnessEvidence { 2553 fn into_currentness_key(self) -> Option<ListingCurrentnessKey> { 2554 if !signed_event_import_has_public_evidence( 2555 self.local_status.as_str(), 2556 self.outbox_status.as_str(), 2557 self.relay_delivery_json.as_deref(), 2558 ) { 2559 return None; 2560 } 2561 listing_currentness_key( 2562 self.event_created_at, 2563 self.event_id.as_deref(), 2564 signed_event_evidence_precedence( 2565 self.source_runtime.as_str(), 2566 self.owner_account_id.as_deref(), 2567 self.local_status.as_str(), 2568 self.outbox_status.as_str(), 2569 ), 2570 ) 2571 } 2572 } 2573 2574 #[derive(Clone, Debug, Eq, PartialEq)] 2575 struct StoredListingCurrentnessIdentityEvidence { 2576 currentness: StoredListingCurrentnessEvidence, 2577 event_tags_json: Option<String>, 2578 event_content: Option<String>, 2579 listing_addr: Option<String>, 2580 } 2581 2582 impl StoredListingCurrentnessIdentityEvidence { 2583 fn listing_key(&self) -> Option<String> { 2584 self.event_content 2585 .as_deref() 2586 .and_then(parse_json_value_opt) 2587 .and_then(|content| string_at(&content, &["d_tag"])) 2588 .or_else(|| { 2589 self.event_tags_json 2590 .as_deref() 2591 .and_then(|raw| serde_json::from_str::<Value>(raw).ok()) 2592 .and_then(|tags| tag_index_value(Some(&tags), "d", 1)) 2593 }) 2594 .or_else(|| self.listing_addr.as_deref().and_then(address_d_tag)) 2595 } 2596 } 2597 2598 #[derive(Clone, Debug, Eq, PartialEq)] 2599 enum ListingCurrentnessIdentity { 2600 ListingAddress(String), 2601 KindPubkeyDTag { 2602 event_kind: i64, 2603 event_pubkey: String, 2604 listing_key: String, 2605 }, 2606 } 2607 2608 impl ListingCurrentnessIdentity { 2609 fn from_record(record: &LocalEventRecord, listing_key: &str) -> Option<Self> { 2610 if let Some(listing_addr) = record 2611 .listing_addr 2612 .as_deref() 2613 .map(str::trim) 2614 .filter(|listing_addr| !listing_addr.is_empty()) 2615 { 2616 return Some(Self::ListingAddress(listing_addr.to_owned())); 2617 } 2618 let event_kind = record.event_kind?; 2619 let event_pubkey = record 2620 .event_pubkey 2621 .as_deref() 2622 .map(str::trim) 2623 .filter(|event_pubkey| !event_pubkey.is_empty())?; 2624 Some(Self::KindPubkeyDTag { 2625 event_kind, 2626 event_pubkey: event_pubkey.to_owned(), 2627 listing_key: listing_key.to_owned(), 2628 }) 2629 } 2630 } 2631 2632 #[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)] 2633 struct ListingCurrentnessKey { 2634 event_created_at: i64, 2635 evidence_precedence: u8, 2636 event_id: String, 2637 } 2638 2639 #[derive(Clone, Debug, Eq, PartialEq)] 2640 struct ProductProjection { 2641 product_id: ProductId, 2642 farm_id: FarmId, 2643 title: String, 2644 subtitle: String, 2645 status: ProductStatus, 2646 unit_label: String, 2647 price_minor_units: Option<u32>, 2648 price_currency: String, 2649 stock_count: Option<u32>, 2650 availability_window_id: Option<FulfillmentWindowId>, 2651 listing_bin_id: Option<String>, 2652 } 2653 2654 #[derive(Clone, Debug, Eq, PartialEq)] 2655 struct ExistingListingProjection { 2656 product_id: ProductId, 2657 farm_id: FarmId, 2658 title: String, 2659 unit_label: String, 2660 listing_bin_id: Option<String>, 2661 farm_key: Option<String>, 2662 } 2663 2664 #[derive(Clone, Debug, Eq, PartialEq)] 2665 struct ActiveOrderAgreementSource { 2666 listing_addr: String, 2667 seller_pubkey: String, 2668 items: Vec<RadrootsOrderItem>, 2669 economics: RadrootsOrderEconomics, 2670 } 2671 2672 #[derive(Clone, Debug, Eq, PartialEq)] 2673 struct ExistingOrderLineMetadata { 2674 listing_event_id: Option<String>, 2675 listing_relays_json: Option<String>, 2676 } 2677 2678 #[derive(Clone, Debug, Eq, PartialEq)] 2679 enum ActiveOrderEvidence { 2680 Request(RadrootsOrderRequestRecord), 2681 Decision(RadrootsOrderDecisionRecord), 2682 RevisionProposal(RadrootsOrderRevisionProposalRecord), 2683 RevisionDecision(RadrootsOrderRevisionDecisionRecord), 2684 Cancellation(RadrootsOrderCancellationRecord), 2685 } 2686 2687 impl ActiveOrderEvidence { 2688 fn event_id(&self) -> &str { 2689 match self { 2690 Self::Request(record) => record.event_id.as_str(), 2691 Self::Decision(record) => record.event_id.as_str(), 2692 Self::RevisionProposal(record) => record.event_id.as_str(), 2693 Self::RevisionDecision(record) => record.event_id.as_str(), 2694 Self::Cancellation(record) => record.event_id.as_str(), 2695 } 2696 } 2697 2698 fn order_id(&self) -> &str { 2699 match self { 2700 Self::Request(record) => record.payload.order_id.as_str(), 2701 Self::Decision(record) => record.payload.order_id.as_str(), 2702 Self::RevisionProposal(record) => record.payload.order_id.as_str(), 2703 Self::RevisionDecision(record) => record.payload.order_id.as_str(), 2704 Self::Cancellation(record) => record.payload.order_id.as_str(), 2705 } 2706 } 2707 2708 fn order_projection_identity(&self) -> (&str, &str) { 2709 match self { 2710 Self::Request(record) => ( 2711 record.payload.order_id.as_str(), 2712 record.payload.buyer_pubkey.as_str(), 2713 ), 2714 Self::Decision(record) => ( 2715 record.payload.order_id.as_str(), 2716 record.payload.buyer_pubkey.as_str(), 2717 ), 2718 Self::RevisionProposal(record) => ( 2719 record.payload.order_id.as_str(), 2720 record.payload.buyer_pubkey.as_str(), 2721 ), 2722 Self::RevisionDecision(record) => ( 2723 record.payload.order_id.as_str(), 2724 record.payload.buyer_pubkey.as_str(), 2725 ), 2726 Self::Cancellation(record) => ( 2727 record.payload.order_id.as_str(), 2728 record.payload.buyer_pubkey.as_str(), 2729 ), 2730 } 2731 } 2732 } 2733 2734 #[derive(Clone, Debug, Default, Eq, PartialEq)] 2735 struct ActiveOrderEvidenceBuckets { 2736 requests: Vec<RadrootsOrderRequestRecord>, 2737 decisions: Vec<RadrootsOrderDecisionRecord>, 2738 revision_proposals: Vec<RadrootsOrderRevisionProposalRecord>, 2739 revision_decisions: Vec<RadrootsOrderRevisionDecisionRecord>, 2740 cancellations: Vec<RadrootsOrderCancellationRecord>, 2741 } 2742 2743 impl ActiveOrderEvidenceBuckets { 2744 fn from_evidence(evidence: Vec<ActiveOrderEvidence>) -> Self { 2745 let mut buckets = Self::default(); 2746 for record in evidence { 2747 match record { 2748 ActiveOrderEvidence::Request(record) => buckets.requests.push(record), 2749 ActiveOrderEvidence::Decision(record) => buckets.decisions.push(record), 2750 ActiveOrderEvidence::RevisionProposal(record) => { 2751 buckets.revision_proposals.push(record); 2752 } 2753 ActiveOrderEvidence::RevisionDecision(record) => { 2754 buckets.revision_decisions.push(record); 2755 } 2756 ActiveOrderEvidence::Cancellation(record) => buckets.cancellations.push(record), 2757 } 2758 } 2759 buckets 2760 } 2761 } 2762 2763 fn listing_currentness_row( 2764 row: &rusqlite::Row<'_>, 2765 ) -> rusqlite::Result<StoredListingCurrentnessEvidence> { 2766 Ok(StoredListingCurrentnessEvidence { 2767 event_id: row.get(0)?, 2768 event_created_at: row.get(1)?, 2769 source_runtime: row.get(2)?, 2770 owner_account_id: row.get(3)?, 2771 local_status: row.get(4)?, 2772 outbox_status: row.get(5)?, 2773 relay_delivery_json: row.get(6)?, 2774 }) 2775 } 2776 2777 fn listing_currentness_identity_row( 2778 row: &rusqlite::Row<'_>, 2779 ) -> rusqlite::Result<StoredListingCurrentnessIdentityEvidence> { 2780 Ok(StoredListingCurrentnessIdentityEvidence { 2781 currentness: StoredListingCurrentnessEvidence { 2782 event_id: row.get(0)?, 2783 event_created_at: row.get(1)?, 2784 source_runtime: row.get(2)?, 2785 owner_account_id: row.get(3)?, 2786 local_status: row.get(4)?, 2787 outbox_status: row.get(5)?, 2788 relay_delivery_json: row.get(6)?, 2789 }, 2790 event_tags_json: row.get(7)?, 2791 event_content: row.get(8)?, 2792 listing_addr: row.get(9)?, 2793 }) 2794 } 2795 2796 fn listing_currentness_key( 2797 event_created_at: Option<i64>, 2798 event_id: Option<&str>, 2799 evidence_precedence: u8, 2800 ) -> Option<ListingCurrentnessKey> { 2801 Some(ListingCurrentnessKey { 2802 event_created_at: event_created_at?, 2803 evidence_precedence, 2804 event_id: event_id 2805 .map(str::trim) 2806 .filter(|event_id| !event_id.is_empty())? 2807 .to_owned(), 2808 }) 2809 } 2810 2811 fn signed_event_evidence_precedence( 2812 source_runtime: &str, 2813 owner_account_id: Option<&str>, 2814 local_status: &str, 2815 outbox_status: &str, 2816 ) -> u8 { 2817 let mut precedence = 0; 2818 if local_status == LocalRecordStatus::Published.as_str() { 2819 precedence += 1; 2820 } 2821 if outbox_status == PublishOutboxStatus::Acknowledged.as_str() { 2822 precedence += 2; 2823 } 2824 if owner_account_id 2825 .map(str::trim) 2826 .is_some_and(|owner_account_id| !owner_account_id.is_empty()) 2827 { 2828 precedence += 4; 2829 } 2830 if source_runtime == SourceRuntime::App.as_str() { 2831 precedence += 8; 2832 } 2833 precedence 2834 } 2835 2836 fn deterministic_farm_id(owner_pubkey: Option<&str>, farm_key: &str) -> FarmId { 2837 FarmId::from(deterministic_uuid( 2838 "radroots-cli-farm", 2839 owner_pubkey, 2840 farm_key, 2841 )) 2842 } 2843 2844 fn deterministic_product_id(owner_pubkey: Option<&str>, listing_key: &str) -> ProductId { 2845 ProductId::from(deterministic_uuid( 2846 "radroots-cli-listing", 2847 owner_pubkey, 2848 listing_key, 2849 )) 2850 } 2851 2852 fn projected_farm_id( 2853 source_runtime: SourceRuntime, 2854 owner_pubkey: Option<&str>, 2855 farm_key: &str, 2856 ) -> Option<FarmId> { 2857 match source_runtime { 2858 SourceRuntime::App => parse_app_d_tag_uuid(farm_key).map(FarmId::from), 2859 _ => Some(deterministic_farm_id(owner_pubkey, farm_key)), 2860 } 2861 } 2862 2863 fn projected_product_id( 2864 source_runtime: SourceRuntime, 2865 owner_pubkey: Option<&str>, 2866 listing_key: &str, 2867 ) -> Option<ProductId> { 2868 match source_runtime { 2869 SourceRuntime::App => parse_app_d_tag_uuid(listing_key).map(ProductId::from), 2870 _ => Some(deterministic_product_id(owner_pubkey, listing_key)), 2871 } 2872 } 2873 2874 fn deterministic_uuid(scope: &str, owner_pubkey: Option<&str>, key: &str) -> Uuid { 2875 let seed = format!( 2876 "{scope}:{}:{}", 2877 owner_pubkey.unwrap_or("unknown-owner"), 2878 key.trim() 2879 ); 2880 Uuid::new_v5(&Uuid::NAMESPACE_URL, seed.as_bytes()) 2881 } 2882 2883 fn parse_app_d_tag_uuid(value: &str) -> Option<Uuid> { 2884 let mut decoded = Vec::with_capacity(16); 2885 let mut buffer = 0u32; 2886 let mut bits = 0u8; 2887 for byte in value.trim().bytes() { 2888 let digit = base64_url_digit(byte)?; 2889 buffer = (buffer << 6) | u32::from(digit); 2890 bits += 6; 2891 while bits >= 8 { 2892 bits -= 8; 2893 decoded.push(((buffer >> bits) & 0xff) as u8); 2894 buffer &= (1u32 << bits) - 1; 2895 } 2896 } 2897 if bits > 0 && buffer != 0 { 2898 return None; 2899 } 2900 if decoded.len() == 16 { 2901 Uuid::from_slice(decoded.as_slice()).ok() 2902 } else { 2903 None 2904 } 2905 } 2906 2907 fn active_order_event_kind(kind: i64) -> bool { 2908 ACTIVE_ORDER_EVENT_KINDS.contains(&kind) 2909 } 2910 2911 fn active_event_id(event: &RadrootsNostrEvent) -> Option<RadrootsEventId> { 2912 event.id.parse().ok() 2913 } 2914 2915 fn active_author_pubkey(event: &RadrootsNostrEvent) -> Option<RadrootsPublicKey> { 2916 event.author.parse().ok() 2917 } 2918 2919 fn active_order_evidence_from_event(event: &RadrootsNostrEvent) -> Option<ActiveOrderEvidence> { 2920 match i64::from(event.kind) { 2921 KIND_ORDER_REQUEST => { 2922 let envelope = order_request_from_event(event).ok()?; 2923 Some(ActiveOrderEvidence::Request(RadrootsOrderRequestRecord { 2924 event_id: active_event_id(event)?, 2925 author_pubkey: active_author_pubkey(event)?, 2926 payload: envelope.payload, 2927 })) 2928 } 2929 KIND_ORDER_DECISION => { 2930 let envelope = order_decision_from_event(event).ok()?; 2931 let context = order_event_context_from_tags(envelope.message_type, &event.tags).ok()?; 2932 Some(ActiveOrderEvidence::Decision(RadrootsOrderDecisionRecord { 2933 event_id: active_event_id(event)?, 2934 author_pubkey: active_author_pubkey(event)?, 2935 counterparty_pubkey: context.counterparty_pubkey, 2936 root_event_id: context.root_event_id?, 2937 prev_event_id: context.prev_event_id?, 2938 payload: envelope.payload, 2939 })) 2940 } 2941 KIND_ORDER_REVISION => { 2942 let envelope = order_revision_proposal_from_event(event).ok()?; 2943 let context = order_event_context_from_tags(envelope.message_type, &event.tags).ok()?; 2944 Some(ActiveOrderEvidence::RevisionProposal( 2945 RadrootsOrderRevisionProposalRecord { 2946 event_id: active_event_id(event)?, 2947 author_pubkey: active_author_pubkey(event)?, 2948 counterparty_pubkey: context.counterparty_pubkey, 2949 root_event_id: context.root_event_id?, 2950 prev_event_id: context.prev_event_id?, 2951 payload: envelope.payload, 2952 }, 2953 )) 2954 } 2955 KIND_ORDER_REVISION_DECISION => { 2956 let envelope = order_revision_decision_from_event(event).ok()?; 2957 let context = order_event_context_from_tags(envelope.message_type, &event.tags).ok()?; 2958 Some(ActiveOrderEvidence::RevisionDecision( 2959 RadrootsOrderRevisionDecisionRecord { 2960 event_id: active_event_id(event)?, 2961 author_pubkey: active_author_pubkey(event)?, 2962 counterparty_pubkey: context.counterparty_pubkey, 2963 root_event_id: context.root_event_id?, 2964 prev_event_id: context.prev_event_id?, 2965 payload: envelope.payload, 2966 }, 2967 )) 2968 } 2969 KIND_ORDER_CANCEL => { 2970 let envelope = order_cancellation_from_event(event).ok()?; 2971 let context = order_event_context_from_tags(envelope.message_type, &event.tags).ok()?; 2972 Some(ActiveOrderEvidence::Cancellation( 2973 RadrootsOrderCancellationRecord { 2974 event_id: active_event_id(event)?, 2975 author_pubkey: active_author_pubkey(event)?, 2976 counterparty_pubkey: context.counterparty_pubkey, 2977 root_event_id: context.root_event_id?, 2978 prev_event_id: context.prev_event_id?, 2979 payload: envelope.payload, 2980 }, 2981 )) 2982 } 2983 _ => None, 2984 } 2985 } 2986 2987 fn dedupe_active_order_evidence(evidence: &mut Vec<ActiveOrderEvidence>) { 2988 evidence.sort_by(|left, right| left.event_id().cmp(right.event_id())); 2989 evidence.dedup_by(|left, right| left.event_id() == right.event_id()); 2990 } 2991 2992 fn signed_event_projection(record: &LocalEventRecord) -> ProjectionRecord { 2993 ProjectionRecord { 2994 kind: "signed_event", 2995 projected_id: record.event_id.clone(), 2996 } 2997 } 2998 2999 fn signed_event_from_record( 3000 record: &LocalEventRecord, 3001 ) -> Result<Option<RadrootsNostrEvent>, AppSqliteError> { 3002 let Some(id) = record 3003 .event_id 3004 .as_deref() 3005 .map(str::trim) 3006 .filter(|value| !value.is_empty()) 3007 else { 3008 return Ok(None); 3009 }; 3010 let Some(author) = record 3011 .event_pubkey 3012 .as_deref() 3013 .map(str::trim) 3014 .filter(|value| !value.is_empty()) 3015 else { 3016 return Ok(None); 3017 }; 3018 let Some(kind) = record.event_kind.and_then(|kind| u32::try_from(kind).ok()) else { 3019 return Ok(None); 3020 }; 3021 let Some(created_at) = record 3022 .event_created_at 3023 .and_then(|created_at| u32::try_from(created_at).ok()) 3024 else { 3025 return Ok(None); 3026 }; 3027 let Some(sig) = record 3028 .event_sig 3029 .as_deref() 3030 .map(str::trim) 3031 .filter(|value| !value.is_empty()) 3032 else { 3033 return Ok(None); 3034 }; 3035 let Some(tags) = record.event_tags_json.as_ref().and_then(tags_from_json) else { 3036 return Ok(None); 3037 }; 3038 Ok(Some(RadrootsNostrEvent { 3039 id: id.to_owned(), 3040 author: author.to_owned(), 3041 created_at, 3042 kind, 3043 tags, 3044 content: record.event_content.clone().unwrap_or_default(), 3045 sig: sig.to_owned(), 3046 })) 3047 } 3048 3049 fn signed_event_record_is_usable(record: &LocalEventRecord) -> bool { 3050 if record.status != LocalRecordStatus::Published 3051 || matches!( 3052 record.outbox_status, 3053 PublishOutboxStatus::Pending | PublishOutboxStatus::Failed 3054 ) 3055 { 3056 return false; 3057 } 3058 let Some(relay_delivery_json) = record.relay_delivery_json.as_ref() else { 3059 return false; 3060 }; 3061 let Ok(relay_delivery) = RelayDeliveryEvidence::from_json_value(relay_delivery_json) else { 3062 return false; 3063 }; 3064 matches!( 3065 relay_delivery.state, 3066 RelayDeliveryState::Acknowledged | RelayDeliveryState::Observed 3067 ) 3068 } 3069 3070 fn signed_event_local_interop_evidence_is_usable( 3071 evidence: &StoredLocalInteropSignedEventEvidence, 3072 ) -> bool { 3073 if evidence.local_status != LocalRecordStatus::Published.as_str() 3074 || matches!(evidence.outbox_status.as_str(), "pending" | "failed") 3075 { 3076 return false; 3077 } 3078 let Some(relay_delivery_json) = evidence.relay_delivery_json.as_deref() else { 3079 return false; 3080 }; 3081 let Ok(relay_delivery_value) = serde_json::from_str::<Value>(relay_delivery_json) else { 3082 return false; 3083 }; 3084 let Ok(relay_delivery) = RelayDeliveryEvidence::from_json_value(&relay_delivery_value) else { 3085 return false; 3086 }; 3087 matches!( 3088 relay_delivery.state, 3089 RelayDeliveryState::Acknowledged | RelayDeliveryState::Observed 3090 ) 3091 } 3092 3093 fn signed_event_from_local_interop_evidence( 3094 evidence: &StoredLocalInteropSignedEventEvidence, 3095 ) -> Result<Option<RadrootsNostrEvent>, AppSqliteError> { 3096 let Some(id) = evidence 3097 .event_id 3098 .as_deref() 3099 .map(str::trim) 3100 .filter(|value| !value.is_empty()) 3101 else { 3102 return Ok(None); 3103 }; 3104 let Some(author) = evidence 3105 .event_pubkey 3106 .as_deref() 3107 .map(str::trim) 3108 .filter(|value| !value.is_empty()) 3109 else { 3110 return Ok(None); 3111 }; 3112 let Some(kind) = evidence 3113 .event_kind 3114 .and_then(|kind| u32::try_from(kind).ok()) 3115 else { 3116 return Ok(None); 3117 }; 3118 let Some(created_at) = evidence 3119 .event_created_at 3120 .and_then(|created_at| u32::try_from(created_at).ok()) 3121 else { 3122 return Ok(None); 3123 }; 3124 let Some(sig) = evidence 3125 .event_sig 3126 .as_deref() 3127 .map(str::trim) 3128 .filter(|value| !value.is_empty()) 3129 else { 3130 return Ok(None); 3131 }; 3132 let Some(tags_json) = evidence.event_tags_json.as_deref() else { 3133 return Ok(None); 3134 }; 3135 let Ok(tags_value) = serde_json::from_str::<Value>(tags_json) else { 3136 return Ok(None); 3137 }; 3138 let Some(tags) = tags_from_json(&tags_value) else { 3139 return Ok(None); 3140 }; 3141 Ok(Some(RadrootsNostrEvent { 3142 id: id.to_owned(), 3143 author: author.to_owned(), 3144 created_at, 3145 kind, 3146 tags, 3147 content: evidence.event_content.clone().unwrap_or_default(), 3148 sig: sig.to_owned(), 3149 })) 3150 } 3151 3152 fn tags_from_json(value: &Value) -> Option<Vec<Vec<String>>> { 3153 value.as_array().map(|tags| { 3154 tags.iter() 3155 .filter_map(|tag| { 3156 tag.as_array().map(|values| { 3157 values 3158 .iter() 3159 .filter_map(|value| value.as_str().map(str::to_owned)) 3160 .collect::<Vec<_>>() 3161 }) 3162 }) 3163 .collect::<Vec<_>>() 3164 }) 3165 } 3166 3167 pub fn projected_order_id_from_trade_request(order_id: &str, buyer_pubkey: &str) -> OrderId { 3168 order_id.parse().unwrap_or_else(|_| { 3169 OrderId::from(deterministic_uuid( 3170 "radroots-cli-order", 3171 Some(buyer_pubkey), 3172 order_id, 3173 )) 3174 }) 3175 } 3176 3177 fn projected_order_id(order_id: &str, buyer_pubkey: &str) -> OrderId { 3178 projected_order_id_from_trade_request(order_id, buyer_pubkey) 3179 } 3180 3181 fn active_order_revision_status( 3182 projection: &RadrootsOrderProjection, 3183 revision_proposals: &[RadrootsOrderRevisionProposalRecord], 3184 revision_decisions: &[RadrootsOrderRevisionDecisionRecord], 3185 ) -> TradeRevisionStatus { 3186 let Some(mut parent_event_id) = projection 3187 .decision_event_id 3188 .clone() 3189 .or_else(|| projection.request_event_id.clone()) 3190 else { 3191 return TradeRevisionStatus::None; 3192 }; 3193 let mut status = TradeRevisionStatus::None; 3194 loop { 3195 let matching_proposals = revision_proposals 3196 .iter() 3197 .filter(|proposal| proposal.prev_event_id == parent_event_id) 3198 .collect::<Vec<_>>(); 3199 let proposal = match matching_proposals.as_slice() { 3200 [] => return status, 3201 [proposal] => *proposal, 3202 _ => return TradeRevisionStatus::None, 3203 }; 3204 let matching_decisions = revision_decisions 3205 .iter() 3206 .filter(|decision| decision.prev_event_id == proposal.event_id) 3207 .collect::<Vec<_>>(); 3208 let decision = match matching_decisions.as_slice() { 3209 [] => return TradeRevisionStatus::ChangeProposed, 3210 [decision] => *decision, 3211 _ => return TradeRevisionStatus::None, 3212 }; 3213 if decision.payload.revision_id != proposal.payload.revision_id { 3214 return TradeRevisionStatus::None; 3215 } 3216 status = match &decision.payload.decision { 3217 RadrootsOrderRevisionOutcome::Accepted => TradeRevisionStatus::Updated, 3218 RadrootsOrderRevisionOutcome::Declined { .. } => TradeRevisionStatus::KeptAsPlaced, 3219 }; 3220 parent_event_id.clone_from(&decision.event_id); 3221 } 3222 } 3223 3224 fn active_order_agreement_source( 3225 request: &RadrootsOrderRequest, 3226 projection: &RadrootsOrderProjection, 3227 revision_proposals: &[RadrootsOrderRevisionProposalRecord], 3228 revision_decisions: &[RadrootsOrderRevisionDecisionRecord], 3229 ) -> ActiveOrderAgreementSource { 3230 if let Some(agreement_event_id) = projection.agreement_event_id.as_deref() 3231 && projection.decision_event_id.as_deref() != Some(agreement_event_id) 3232 && let Some(revision_decision) = revision_decisions.iter().find(|decision| { 3233 decision.event_id == agreement_event_id 3234 && matches!( 3235 &decision.payload.decision, 3236 RadrootsOrderRevisionOutcome::Accepted 3237 ) 3238 }) 3239 && let Some(revision_proposal) = revision_proposals.iter().find(|proposal| { 3240 proposal.event_id == revision_decision.prev_event_id 3241 && proposal.payload.revision_id == revision_decision.payload.revision_id 3242 }) 3243 { 3244 return ActiveOrderAgreementSource { 3245 listing_addr: revision_proposal.payload.listing_addr.to_string(), 3246 seller_pubkey: revision_proposal.payload.seller_pubkey.to_string(), 3247 items: revision_proposal.payload.items.clone(), 3248 economics: revision_proposal.payload.economics.clone(), 3249 }; 3250 } 3251 ActiveOrderAgreementSource { 3252 listing_addr: request.listing_addr.to_string(), 3253 seller_pubkey: request.seller_pubkey.to_string(), 3254 items: request.items.clone(), 3255 economics: request.economics.clone(), 3256 } 3257 } 3258 3259 fn order_line_product_id( 3260 payload: &RadrootsOrderRequest, 3261 existing_listing: Option<&ExistingListingProjection>, 3262 item: &radroots_events::order::RadrootsOrderItem, 3263 ) -> ProductId { 3264 order_agreement_line_product_id( 3265 payload.listing_addr.as_str(), 3266 payload.seller_pubkey.as_str(), 3267 existing_listing, 3268 item, 3269 ) 3270 } 3271 3272 fn order_agreement_line_product_id( 3273 listing_addr: &str, 3274 seller_pubkey: &str, 3275 existing_listing: Option<&ExistingListingProjection>, 3276 item: &RadrootsOrderItem, 3277 ) -> ProductId { 3278 if let Some(existing_listing) = existing_listing 3279 && existing_listing 3280 .listing_bin_id 3281 .as_deref() 3282 .is_none_or(|listing_bin_id| listing_bin_id == item.bin_id) 3283 { 3284 return existing_listing.product_id; 3285 } 3286 let product_key = format!("{listing_addr}:{}", item.bin_id); 3287 deterministic_product_id(Some(seller_pubkey), product_key.as_str()) 3288 } 3289 3290 fn deterministic_order_number(order_id: &str) -> String { 3291 let trimmed = order_id.trim(); 3292 let suffix = trimmed 3293 .chars() 3294 .filter(|ch| ch.is_ascii_alphanumeric()) 3295 .take(8) 3296 .collect::<String>(); 3297 if suffix.is_empty() { 3298 "R-RELAY".to_owned() 3299 } else { 3300 format!("R-{suffix}") 3301 } 3302 } 3303 3304 fn existing_order_number( 3305 connection: &Connection, 3306 order_id: OrderId, 3307 ) -> Result<Option<String>, AppSqliteError> { 3308 connection 3309 .query_row( 3310 "SELECT order_number FROM orders WHERE id = ?1 LIMIT 1", 3311 params![order_id.to_string()], 3312 |row| row.get::<_, String>(0), 3313 ) 3314 .optional() 3315 .map_err(|source| AppSqliteError::Query { 3316 operation: "load existing local interop order number", 3317 source, 3318 }) 3319 } 3320 3321 fn order_customer_display_name(buyer_pubkey: &str) -> String { 3322 let prefix = buyer_pubkey.trim().chars().take(12).collect::<String>(); 3323 if prefix.is_empty() { 3324 "Relay buyer".to_owned() 3325 } else { 3326 format!("Relay buyer {prefix}") 3327 } 3328 } 3329 3330 fn order_buyer_context_key(record: &LocalEventRecord, buyer_pubkey: &str) -> String { 3331 if record.source_runtime == SourceRuntime::App 3332 && record 3333 .event_pubkey 3334 .as_deref() 3335 .map(str::trim) 3336 .is_some_and(|event_pubkey| event_pubkey == buyer_pubkey.trim()) 3337 && let Some(owner_account_id) = record 3338 .owner_account_id 3339 .as_deref() 3340 .map(str::trim) 3341 .filter(|owner_account_id| !owner_account_id.is_empty()) 3342 { 3343 return format!("account:{owner_account_id}"); 3344 } 3345 format!("nostr:{}", buyer_pubkey.trim()) 3346 } 3347 3348 fn format_quantity_display(quantity: u32, unit_label: &str) -> String { 3349 let unit_label = unit_label.trim(); 3350 if unit_label.is_empty() { 3351 quantity.to_string() 3352 } else { 3353 format!("{quantity} {unit_label}") 3354 } 3355 } 3356 3357 fn listing_event_id_from_order_record(record: &LocalEventRecord) -> Option<String> { 3358 record 3359 .event_tags_json 3360 .as_ref() 3361 .and_then(|tags| tag_index_value(Some(tags), "listing_event", 1)) 3362 } 3363 3364 fn base64_url_digit(byte: u8) -> Option<u8> { 3365 match byte { 3366 b'A'..=b'Z' => Some(byte - b'A'), 3367 b'a'..=b'z' => Some(byte - b'a' + 26), 3368 b'0'..=b'9' => Some(byte - b'0' + 52), 3369 b'-' => Some(62), 3370 b'_' => Some(63), 3371 _ => None, 3372 } 3373 } 3374 3375 fn string_at(value: &Value, path: &[&str]) -> Option<String> { 3376 let mut cursor = value; 3377 for segment in path { 3378 cursor = cursor.get(*segment)?; 3379 } 3380 match cursor { 3381 Value::String(value) => { 3382 let trimmed = value.trim(); 3383 (!trimmed.is_empty()).then(|| trimmed.to_owned()) 3384 } 3385 Value::Number(number) => Some(number.to_string()), 3386 _ => None, 3387 } 3388 } 3389 3390 fn listing_id(record: &LocalEventRecord) -> Option<String> { 3391 record 3392 .listing_addr 3393 .as_deref() 3394 .and_then(|addr| addr.rsplit(':').next()) 3395 .map(str::trim) 3396 .filter(|value| !value.is_empty()) 3397 .map(str::to_owned) 3398 } 3399 3400 fn farm_order_method(value: &str) -> Option<FarmOrderMethod> { 3401 match value.trim() { 3402 "pickup" => Some(FarmOrderMethod::Pickup), 3403 "delivery" | "local_delivery" => Some(FarmOrderMethod::Delivery), 3404 "shipping" => Some(FarmOrderMethod::Shipping), 3405 _ => None, 3406 } 3407 } 3408 3409 fn parse_decimal_minor_units(value: &str) -> Option<u32> { 3410 let value = value.trim(); 3411 if value.is_empty() || value.starts_with('-') { 3412 return None; 3413 } 3414 let (whole, fraction) = value.split_once('.').unwrap_or((value, "")); 3415 let whole_units = whole.parse::<u32>().ok()?; 3416 let cents = match fraction.len() { 3417 0 => 0, 3418 1 => fraction.parse::<u32>().ok()? * 10, 3419 _ => fraction.get(0..2)?.parse::<u32>().ok()?, 3420 }; 3421 whole_units.checked_mul(100)?.checked_add(cents) 3422 } 3423 3424 fn parse_u32_quantity(value: &str) -> Option<u32> { 3425 let value = value.trim(); 3426 if value.is_empty() || value.starts_with('-') { 3427 return None; 3428 } 3429 let whole = value.split_once('.').map_or(value, |(whole, _)| whole); 3430 whole.parse::<u32>().ok() 3431 } 3432 3433 fn parse_u64_quantity(value: &str) -> Option<u64> { 3434 let value = value.trim(); 3435 if value.is_empty() || value.starts_with('-') { 3436 return None; 3437 } 3438 let whole = value.split_once('.').map_or(value, |(whole, _)| whole); 3439 whole.parse::<u64>().ok() 3440 } 3441 3442 fn signed_listing_product_status( 3443 record: &LocalEventRecord, 3444 content: Option<&Value>, 3445 tags: Option<&Value>, 3446 ) -> Option<ProductStatus> { 3447 if !signed_listing_has_public_evidence(record) { 3448 return Some(ProductStatus::Draft); 3449 } 3450 match signed_listing_lifecycle(content, tags)? { 3451 SignedListingLifecycle::Active | SignedListingLifecycle::Window => { 3452 Some(ProductStatus::Published) 3453 } 3454 SignedListingLifecycle::Archived => Some(ProductStatus::Archived), 3455 SignedListingLifecycle::Sold => Some(ProductStatus::Paused), 3456 } 3457 } 3458 3459 fn signed_listing_has_public_evidence(record: &LocalEventRecord) -> bool { 3460 if record.status != LocalRecordStatus::Published { 3461 return false; 3462 } 3463 if record.outbox_status == PublishOutboxStatus::Acknowledged { 3464 return true; 3465 } 3466 record 3467 .relay_delivery_json 3468 .as_ref() 3469 .and_then(|delivery| RelayDeliveryEvidence::from_json_value(delivery).ok()) 3470 .is_some_and(|delivery| delivery.state == RelayDeliveryState::Observed) 3471 } 3472 3473 fn signed_event_import_has_public_evidence( 3474 local_status: &str, 3475 outbox_status: &str, 3476 relay_delivery_json: Option<&str>, 3477 ) -> bool { 3478 if local_status != LocalRecordStatus::Published.as_str() { 3479 return false; 3480 } 3481 if outbox_status == PublishOutboxStatus::Acknowledged.as_str() { 3482 return true; 3483 } 3484 relay_delivery_json 3485 .and_then(|delivery| serde_json::from_str::<Value>(delivery).ok()) 3486 .and_then(|delivery| RelayDeliveryEvidence::from_json_value(&delivery).ok()) 3487 .is_some_and(|delivery| delivery.state == RelayDeliveryState::Observed) 3488 } 3489 3490 fn signed_farm_readiness(content: &Value, tags: Option<&Value>) -> Option<FarmReadiness> { 3491 string_at(content, &["readiness"]) 3492 .or_else(|| { 3493 content 3494 .get("tags")? 3495 .as_array()? 3496 .iter() 3497 .filter_map(Value::as_str) 3498 .find_map(readiness_tag_value) 3499 }) 3500 .or_else(|| { 3501 tags?.as_array()?.iter().find_map(|tag| { 3502 let values = tag.as_array()?; 3503 (values.first()?.as_str()? == "t") 3504 .then(|| values.get(1).and_then(Value::as_str)) 3505 .flatten() 3506 .and_then(readiness_tag_value) 3507 }) 3508 }) 3509 .and_then(|value| match value.as_str() { 3510 "ready" => Some(FarmReadiness::Ready), 3511 "incomplete" => Some(FarmReadiness::Incomplete), 3512 _ => None, 3513 }) 3514 } 3515 3516 fn readiness_tag_value(value: &str) -> Option<String> { 3517 value 3518 .strip_prefix("radroots:readiness:") 3519 .map(str::trim) 3520 .filter(|value| !value.is_empty()) 3521 .map(str::to_owned) 3522 } 3523 3524 fn signed_listing_fulfillment_method( 3525 content: Option<&Value>, 3526 tags: Option<&Value>, 3527 ) -> Option<FarmOrderMethod> { 3528 content.and_then(delivery_method_from_content).or_else(|| { 3529 tag_index_value(tags, "delivery", 1).and_then(|method| farm_order_method(&method)) 3530 }) 3531 } 3532 3533 fn delivery_method_from_content(content: &Value) -> Option<FarmOrderMethod> { 3534 string_at(content, &["delivery_method", "kind"]) 3535 .or_else(|| string_at(content, &["delivery", "method"])) 3536 .or_else(|| string_at(content, &["delivery_method"])) 3537 .and_then(|method| farm_order_method(method.as_str())) 3538 } 3539 3540 fn signed_listing_availability_window( 3541 content: Option<&Value>, 3542 tags: Option<&Value>, 3543 ) -> Option<ListingAvailabilityWindow> { 3544 let start = content 3545 .and_then(|content| string_at(content, &["availability", "amount", "start"])) 3546 .or_else(|| content.and_then(|content| string_at(content, &["availability", "start"]))) 3547 .or_else(|| tag_index_value(tags, "radroots:availability_start", 1)) 3548 .and_then(|value| parse_u64_quantity(value.as_str())); 3549 let end = content 3550 .and_then(|content| string_at(content, &["availability", "amount", "end"])) 3551 .or_else(|| content.and_then(|content| string_at(content, &["availability", "end"]))) 3552 .or_else(|| tag_index_value(tags, "expires_at", 1)) 3553 .and_then(|value| parse_u64_quantity(value.as_str())); 3554 3555 match (start, end) { 3556 (Some(start), Some(end)) if end > start => Some(ListingAvailabilityWindow { start, end }), 3557 _ => None, 3558 } 3559 } 3560 3561 fn signed_listing_location_primary( 3562 content: Option<&Value>, 3563 tags: Option<&Value>, 3564 ) -> Option<String> { 3565 content 3566 .and_then(|content| string_at(content, &["location", "primary"])) 3567 .or_else(|| tag_index_value(tags, "location", 1)) 3568 } 3569 3570 #[derive(Clone, Copy, Debug, Eq, PartialEq)] 3571 struct ListingAvailabilityWindow { 3572 start: u64, 3573 end: u64, 3574 } 3575 3576 fn signed_listing_lifecycle( 3577 content: Option<&Value>, 3578 tags: Option<&Value>, 3579 ) -> Option<SignedListingLifecycle> { 3580 content 3581 .and_then(lifecycle_from_content) 3582 .or_else(|| lifecycle_from_tags(tags)) 3583 } 3584 3585 #[derive(Clone, Copy, Debug, Eq, PartialEq)] 3586 enum SignedListingLifecycle { 3587 Active, 3588 Window, 3589 Archived, 3590 Sold, 3591 } 3592 3593 fn lifecycle_from_content(content: &Value) -> Option<SignedListingLifecycle> { 3594 string_at(content, &["status"]) 3595 .or_else(|| string_at(content, &["availability", "status"])) 3596 .or_else(|| string_at(content, &["availability", "amount", "status"])) 3597 .or_else(|| string_at(content, &["availability", "amount", "kind"])) 3598 .or_else(|| string_at(content, &["availability", "amount", "value"])) 3599 .and_then(|status| parse_listing_lifecycle(status.as_str())) 3600 .or_else(|| { 3601 matches!( 3602 string_at(content, &["availability", "kind"]).as_deref(), 3603 Some("window") 3604 ) 3605 .then_some(SignedListingLifecycle::Window) 3606 }) 3607 } 3608 3609 fn lifecycle_from_tags(tags: Option<&Value>) -> Option<SignedListingLifecycle> { 3610 tag_index_value(tags, "status", 1) 3611 .and_then(|status| parse_listing_lifecycle(status.as_str())) 3612 .or_else(|| { 3613 tag_index_value(tags, "radroots:availability_start", 1) 3614 .or_else(|| tag_index_value(tags, "expires_at", 1)) 3615 .map(|_| SignedListingLifecycle::Window) 3616 }) 3617 } 3618 3619 fn parse_listing_lifecycle(value: &str) -> Option<SignedListingLifecycle> { 3620 match value.trim().to_ascii_lowercase().as_str() { 3621 "active" | "available" | "published" => Some(SignedListingLifecycle::Active), 3622 "window" => Some(SignedListingLifecycle::Window), 3623 "archived" => Some(SignedListingLifecycle::Archived), 3624 "sold" => Some(SignedListingLifecycle::Sold), 3625 _ => None, 3626 } 3627 } 3628 3629 fn primary_bin(content: &Value) -> Option<&Value> { 3630 let bins = content.get("bins")?.as_array()?; 3631 let primary_bin_id = string_at(content, &["primary_bin_id"]); 3632 primary_bin_id 3633 .as_deref() 3634 .and_then(|primary_bin_id| { 3635 bins.iter() 3636 .find(|bin| string_at(bin, &["bin_id"]).as_deref() == Some(primary_bin_id)) 3637 }) 3638 .or_else(|| bins.first()) 3639 } 3640 3641 fn parse_json_value(raw: &str) -> Result<Value, AppSqliteError> { 3642 serde_json::from_str(raw).map_err(|_| AppSqliteError::InvalidProjection { 3643 reason: "shared local signed event content must be json", 3644 }) 3645 } 3646 3647 fn parse_json_value_opt(raw: &str) -> Option<Value> { 3648 serde_json::from_str(raw).ok() 3649 } 3650 3651 fn tag_index_value(tags: Option<&Value>, tag_name: &str, index: usize) -> Option<String> { 3652 tags?.as_array()?.iter().find_map(|tag| { 3653 let values = tag.as_array()?; 3654 (values.first()?.as_str()? == tag_name) 3655 .then(|| values.get(index).and_then(Value::as_str)) 3656 .flatten() 3657 .map(str::trim) 3658 .filter(|value| !value.is_empty()) 3659 .map(str::to_owned) 3660 }) 3661 } 3662 3663 fn signed_farm_address_matches(tags: Option<&Value>, farm_key: &str, seller_pubkey: &str) -> bool { 3664 let Some(address) = tag_index_value(tags, "a", 1) else { 3665 return false; 3666 }; 3667 address_d_tag(address.as_str()).as_deref() == Some(farm_key) 3668 && address_pubkey(address.as_str()).as_deref() == Some(seller_pubkey) 3669 } 3670 3671 #[derive(Clone, Debug, Eq, PartialEq)] 3672 struct ListingAddressParts<'a> { 3673 kind: i64, 3674 pubkey: &'a str, 3675 d_tag: &'a str, 3676 } 3677 3678 fn listing_address_parts(address: &str) -> Option<ListingAddressParts<'_>> { 3679 let mut parts = address.trim().split(':'); 3680 let kind = parts.next()?.parse::<i64>().ok()?; 3681 let pubkey = parts.next()?.trim(); 3682 let d_tag = parts.next()?.trim(); 3683 if parts.next().is_some() || pubkey.is_empty() || d_tag.is_empty() { 3684 return None; 3685 } 3686 Some(ListingAddressParts { 3687 kind, 3688 pubkey, 3689 d_tag, 3690 }) 3691 } 3692 3693 fn address_d_tag(address: &str) -> Option<String> { 3694 address 3695 .rsplit(':') 3696 .next() 3697 .map(str::trim) 3698 .filter(|value| !value.is_empty()) 3699 .map(str::to_owned) 3700 } 3701 3702 fn address_pubkey(address: &str) -> Option<String> { 3703 let mut parts = address.split(':'); 3704 let _kind = parts.next()?; 3705 parts 3706 .next() 3707 .map(str::trim) 3708 .filter(|value| !value.is_empty()) 3709 .map(str::to_owned) 3710 } 3711 3712 fn farm_readiness_storage_key(readiness: FarmReadiness) -> &'static str { 3713 match readiness { 3714 FarmReadiness::Incomplete => "incomplete", 3715 FarmReadiness::Ready => "ready", 3716 } 3717 } 3718 3719 fn farm_readiness_from_storage_key(readiness: &str) -> Result<FarmReadiness, AppSqliteError> { 3720 match readiness { 3721 "incomplete" => Ok(FarmReadiness::Incomplete), 3722 "ready" => Ok(FarmReadiness::Ready), 3723 _ => Err(AppSqliteError::InvalidProjection { 3724 reason: "farm readiness storage key is invalid", 3725 }), 3726 } 3727 } 3728 3729 #[cfg(test)] 3730 mod tests { 3731 use std::collections::BTreeSet; 3732 3733 use radroots_app_view::{ 3734 BuyerContext, BuyerOrderStatus, FarmId, FarmOrderMethod, OrderId, OrderStatus, 3735 OrdersFilter, OrdersScreenQueryState, ProductAvailabilityState, ProductId, 3736 TradeAgreementStatus, TradeInventoryStatus, TradeRevisionStatus, 3737 TradeValidationReceiptProofSystem, TradeValidationReceiptResult, 3738 TradeValidationReceiptType, TradeWorkflowSource, 3739 }; 3740 use radroots_core::{ 3741 RadrootsCoreCurrency, RadrootsCoreDecimal, RadrootsCoreMoney, RadrootsCoreUnit, 3742 }; 3743 use radroots_events::{ 3744 RadrootsNostrEvent, RadrootsNostrEventPtr, 3745 ids::{ 3746 RadrootsEventId, RadrootsInventoryBinId, RadrootsListingAddress, RadrootsOrderId, 3747 RadrootsOrderQuoteId, RadrootsOrderRevisionId, RadrootsPublicKey, 3748 }, 3749 order::{ 3750 RadrootsOrderCancellation, RadrootsOrderDecision, RadrootsOrderDecisionOutcome, 3751 RadrootsOrderEconomicItem, RadrootsOrderEconomicLine, RadrootsOrderEconomics, 3752 RadrootsOrderInventoryCommitment, RadrootsOrderItem, RadrootsOrderPricingBasis, 3753 RadrootsOrderRequest, RadrootsOrderRevisionDecision, RadrootsOrderRevisionOutcome, 3754 RadrootsOrderRevisionProposal, 3755 }, 3756 }; 3757 use radroots_events_codec::{ 3758 order::{ 3759 order_cancellation_event_build, order_decision_event_build, order_request_event_build, 3760 order_revision_decision_event_build, order_revision_proposal_event_build, 3761 }, 3762 wire::WireEventParts, 3763 }; 3764 use radroots_local_events::{ 3765 LocalEventRecordInput, LocalEventRecordUpdate, LocalEventsStore, LocalRecordFamily, 3766 LocalRecordStatus, PublishOutboxStatus, RelayDeliveryEvidence, SourceRuntime, 3767 }; 3768 use radroots_sql_core::SqliteExecutor; 3769 use radroots_trade::validation_receipt::{ 3770 RadrootsTradeValidationReceipt, RadrootsValidationReceiptProof, 3771 RadrootsValidationReceiptProofSystem, RadrootsValidationReceiptResult, 3772 RadrootsValidationReceiptStatement, RadrootsValidationReceiptType, 3773 VALIDATION_RECEIPT_DOMAIN, VALIDATION_RECEIPT_VERSION, validation_receipt_event_build, 3774 }; 3775 use rusqlite::params; 3776 use serde_json::json; 3777 use uuid::Uuid; 3778 3779 use super::{ 3780 KIND_FARM, KIND_LISTING, KIND_ORDER_REQUEST, KIND_VALIDATION_RECEIPT, 3781 deterministic_farm_id, deterministic_product_id, projected_farm_id, projected_order_id, 3782 projected_product_id, 3783 }; 3784 use crate::{AppSqliteStore, BuyerRepeatDemandApplyOutcome, DatabaseTarget}; 3785 3786 fn local_events_store() -> LocalEventsStore<SqliteExecutor> { 3787 let executor = SqliteExecutor::open_memory().expect("open local events memory db"); 3788 let store = LocalEventsStore::new(executor); 3789 store.migrate_up().expect("migrate local events store"); 3790 store 3791 } 3792 3793 fn local_work_record( 3794 record_id: &str, 3795 farm_key: &str, 3796 payload: serde_json::Value, 3797 ) -> LocalEventRecordInput { 3798 LocalEventRecordInput { 3799 record_id: record_id.to_owned(), 3800 family: LocalRecordFamily::LocalWork, 3801 status: LocalRecordStatus::LocalSaved, 3802 source_runtime: SourceRuntime::Cli, 3803 created_at_ms: 1000, 3804 inserted_at_ms: 1001, 3805 owner_account_id: Some("seller-account".to_owned()), 3806 owner_pubkey: Some("seller-pubkey".to_owned()), 3807 farm_id: Some(farm_key.to_owned()), 3808 listing_addr: None, 3809 local_work_json: Some(payload), 3810 event_id: None, 3811 event_kind: None, 3812 event_pubkey: None, 3813 event_created_at: None, 3814 event_tags_json: None, 3815 event_content: None, 3816 event_sig: None, 3817 raw_event_json: None, 3818 outbox_status: PublishOutboxStatus::None, 3819 relay_set_fingerprint: None, 3820 relay_delivery_json: None, 3821 } 3822 } 3823 3824 fn signed_farm_record( 3825 record_id: &str, 3826 event_id: &str, 3827 source_runtime: SourceRuntime, 3828 owner_pubkey: &str, 3829 farm_key: &str, 3830 readiness: &str, 3831 display_name: &str, 3832 ) -> LocalEventRecordInput { 3833 LocalEventRecordInput { 3834 record_id: record_id.to_owned(), 3835 family: LocalRecordFamily::SignedEvent, 3836 status: LocalRecordStatus::Published, 3837 source_runtime, 3838 created_at_ms: 1100, 3839 inserted_at_ms: 1101, 3840 owner_account_id: Some("seller-account".to_owned()), 3841 owner_pubkey: Some(owner_pubkey.to_owned()), 3842 farm_id: Some(farm_key.to_owned()), 3843 listing_addr: None, 3844 local_work_json: None, 3845 event_id: Some(event_id.to_owned()), 3846 event_kind: Some(KIND_FARM), 3847 event_pubkey: Some(owner_pubkey.to_owned()), 3848 event_created_at: Some(1100), 3849 event_tags_json: Some(json!([ 3850 ["d", farm_key], 3851 ["t", format!("radroots:readiness:{readiness}")] 3852 ])), 3853 event_content: Some( 3854 json!({ 3855 "d_tag": farm_key, 3856 "name": display_name, 3857 "tags": [format!("radroots:readiness:{readiness}")] 3858 }) 3859 .to_string(), 3860 ), 3861 event_sig: Some("signature".to_owned()), 3862 raw_event_json: Some(json!({ 3863 "id": event_id, 3864 "kind": KIND_FARM, 3865 "pubkey": owner_pubkey, 3866 })), 3867 outbox_status: PublishOutboxStatus::Acknowledged, 3868 relay_set_fingerprint: Some("relay-set".to_owned()), 3869 relay_delivery_json: Some(json!({ 3870 "state": "acknowledged", 3871 "target_relays": ["ws://127.0.0.1:1234"], 3872 "connected_relays": ["ws://127.0.0.1:1234"], 3873 "acknowledged_relays": ["ws://127.0.0.1:1234"] 3874 })), 3875 } 3876 } 3877 3878 fn signed_listing_record( 3879 record_id: &str, 3880 farm_key: &str, 3881 listing_key: &str, 3882 status_tag: &str, 3883 ) -> LocalEventRecordInput { 3884 signed_listing_record_with_publish_state( 3885 record_id, 3886 farm_key, 3887 listing_key, 3888 status_tag, 3889 LocalRecordStatus::Published, 3890 PublishOutboxStatus::Acknowledged, 3891 ) 3892 } 3893 3894 fn signed_listing_record_with_publish_state( 3895 record_id: &str, 3896 farm_key: &str, 3897 listing_key: &str, 3898 status_tag: &str, 3899 record_status: LocalRecordStatus, 3900 outbox_status: PublishOutboxStatus, 3901 ) -> LocalEventRecordInput { 3902 let relay_delivery_json = match outbox_status { 3903 PublishOutboxStatus::Acknowledged => Some(json!({ 3904 "state": "acknowledged", 3905 "acknowledged_relays": ["ws://127.0.0.1:1234/"] 3906 })), 3907 PublishOutboxStatus::Failed => Some(json!({ 3908 "state": "failed", 3909 "failed_relays": ["ws://127.0.0.1:1234/"] 3910 })), 3911 PublishOutboxStatus::Pending | PublishOutboxStatus::None => None, 3912 }; 3913 LocalEventRecordInput { 3914 record_id: record_id.to_owned(), 3915 family: LocalRecordFamily::SignedEvent, 3916 status: record_status, 3917 source_runtime: SourceRuntime::Cli, 3918 created_at_ms: 1100, 3919 inserted_at_ms: 1101, 3920 owner_account_id: Some("seller-account".to_owned()), 3921 owner_pubkey: Some("seller-pubkey".to_owned()), 3922 farm_id: Some(farm_key.to_owned()), 3923 listing_addr: Some(format!("30402:seller-pubkey:{listing_key}")), 3924 local_work_json: None, 3925 event_id: Some(format!("event-{record_id}")), 3926 event_kind: Some(KIND_LISTING), 3927 event_pubkey: Some("seller-pubkey".to_owned()), 3928 event_created_at: Some(1100), 3929 event_tags_json: Some(json!([ 3930 ["d", listing_key], 3931 ["a", format!("30340:seller-pubkey:{farm_key}")], 3932 ["key", "eggs"], 3933 ["title", "Relay Eggs"], 3934 ["summary", "Published eggs"], 3935 ["radroots:bin", "bin-1", "1", "each"], 3936 ["radroots:price", "bin-1", "8", "USD", "1", "each"], 3937 ["inventory", "9"], 3938 ["status", status_tag] 3939 ])), 3940 event_content: Some("# Relay Eggs\n\nPublished eggs".to_owned()), 3941 event_sig: Some("signature".to_owned()), 3942 raw_event_json: Some(json!({ 3943 "id": format!("event-{record_id}"), 3944 "kind": KIND_LISTING, 3945 "pubkey": "seller-pubkey", 3946 "content": "# Relay Eggs\n\nPublished eggs" 3947 })), 3948 outbox_status, 3949 relay_set_fingerprint: Some("relay-set".to_owned()), 3950 relay_delivery_json, 3951 } 3952 } 3953 3954 fn signed_market_listing_record( 3955 record_id: &str, 3956 owner_pubkey: &str, 3957 farm_key: &str, 3958 listing_key: &str, 3959 title: &str, 3960 inventory_available: &str, 3961 status_tag: &str, 3962 delivery_method: &str, 3963 location_primary: &str, 3964 availability_start: u64, 3965 availability_end: u64, 3966 record_status: LocalRecordStatus, 3967 outbox_status: PublishOutboxStatus, 3968 ) -> LocalEventRecordInput { 3969 let relay_delivery_json = match outbox_status { 3970 PublishOutboxStatus::Acknowledged => Some(json!({ 3971 "state": "acknowledged", 3972 "acknowledged_relays": ["ws://127.0.0.1:1234/"] 3973 })), 3974 PublishOutboxStatus::Failed => Some(json!({ 3975 "state": "failed", 3976 "failed_relays": ["ws://127.0.0.1:1234/"] 3977 })), 3978 PublishOutboxStatus::Pending | PublishOutboxStatus::None => None, 3979 }; 3980 let content = json!({ 3981 "d_tag": listing_key, 3982 "status": status_tag, 3983 "farm": { 3984 "pubkey": owner_pubkey, 3985 "d_tag": farm_key, 3986 }, 3987 "product": { 3988 "key": listing_key, 3989 "title": title, 3990 "summary": "Published local listing", 3991 }, 3992 "availability": { 3993 "kind": "window", 3994 "amount": { 3995 "start": availability_start, 3996 "end": availability_end, 3997 }, 3998 }, 3999 "delivery_method": { 4000 "kind": delivery_method, 4001 }, 4002 "location": { 4003 "primary": location_primary, 4004 }, 4005 }); 4006 4007 LocalEventRecordInput { 4008 record_id: record_id.to_owned(), 4009 family: LocalRecordFamily::SignedEvent, 4010 status: record_status, 4011 source_runtime: SourceRuntime::Cli, 4012 created_at_ms: 1100, 4013 inserted_at_ms: 1101, 4014 owner_account_id: Some("seller-account".to_owned()), 4015 owner_pubkey: Some(owner_pubkey.to_owned()), 4016 farm_id: Some(farm_key.to_owned()), 4017 listing_addr: Some(format!("30402:{owner_pubkey}:{listing_key}")), 4018 local_work_json: None, 4019 event_id: Some(format!("event-{record_id}")), 4020 event_kind: Some(KIND_LISTING), 4021 event_pubkey: Some(owner_pubkey.to_owned()), 4022 event_created_at: Some(1100), 4023 event_tags_json: Some(json!([ 4024 ["d", listing_key], 4025 ["a", format!("30340:{owner_pubkey}:{farm_key}")], 4026 ["key", listing_key], 4027 ["title", title], 4028 ["summary", "Published local listing"], 4029 ["radroots:bin", "bin-1", "1", "each"], 4030 ["radroots:price", "bin-1", "8", "USD", "1", "each"], 4031 ["inventory", inventory_available], 4032 ["status", status_tag], 4033 [ 4034 "radroots:availability_start", 4035 availability_start.to_string() 4036 ], 4037 ["expires_at", availability_end.to_string()], 4038 ["delivery", delivery_method], 4039 ["location", location_primary], 4040 ])), 4041 event_content: Some(content.to_string()), 4042 event_sig: Some("signature".to_owned()), 4043 raw_event_json: Some(json!({ 4044 "id": format!("event-{record_id}"), 4045 "kind": KIND_LISTING, 4046 "pubkey": owner_pubkey, 4047 "content": content.to_string(), 4048 })), 4049 outbox_status, 4050 relay_set_fingerprint: Some("relay-set".to_owned()), 4051 relay_delivery_json, 4052 } 4053 } 4054 4055 fn set_listing_event_version( 4056 record: &mut LocalEventRecordInput, 4057 event_id: &str, 4058 created_at: i64, 4059 title: &str, 4060 inventory_available: &str, 4061 ) { 4062 record.event_id = Some(event_id.to_owned()); 4063 record.event_created_at = Some(created_at); 4064 record.created_at_ms = created_at * 1_000; 4065 record.inserted_at_ms = created_at * 1_000 + 1; 4066 if let Some(content) = record.event_content.as_deref() { 4067 let mut content: serde_json::Value = 4068 serde_json::from_str(content).expect("listing content should parse"); 4069 content["product"]["title"] = json!(title); 4070 content["inventory_available"] = json!(inventory_available); 4071 record.event_content = Some(content.to_string()); 4072 } 4073 if let Some(serde_json::Value::Array(tags)) = record.event_tags_json.as_mut() { 4074 for tag in tags { 4075 let Some(values) = tag.as_array_mut() else { 4076 continue; 4077 }; 4078 match values.first().and_then(serde_json::Value::as_str) { 4079 Some("title") => { 4080 values[1] = json!(title); 4081 } 4082 Some("inventory") => { 4083 values[1] = json!(inventory_available); 4084 } 4085 _ => {} 4086 } 4087 } 4088 } 4089 record.raw_event_json = Some(json!({ 4090 "id": event_id, 4091 "kind": record.event_kind, 4092 "pubkey": record.event_pubkey, 4093 "content": record.event_content, 4094 })); 4095 } 4096 4097 fn buyer_listing_titles(app_store: &AppSqliteStore) -> Vec<String> { 4098 app_store 4099 .load_buyer_listings("", &BTreeSet::new()) 4100 .expect("buyer listings should load") 4101 .rows 4102 .into_iter() 4103 .map(|row| row.title) 4104 .collect() 4105 } 4106 4107 fn app_d_tag_from_uuid(uuid: Uuid) -> String { 4108 const ALPHABET: &[u8; 64] = 4109 b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"; 4110 let bytes = uuid.as_bytes(); 4111 let mut output = String::with_capacity((bytes.len() * 4).div_ceil(3)); 4112 let mut chunks = bytes.chunks_exact(3); 4113 for chunk in &mut chunks { 4114 output.push(ALPHABET[(chunk[0] >> 2) as usize] as char); 4115 output.push( 4116 ALPHABET[(((chunk[0] & 0b0000_0011) << 4) | (chunk[1] >> 4)) as usize] as char, 4117 ); 4118 output.push( 4119 ALPHABET[(((chunk[1] & 0b0000_1111) << 2) | (chunk[2] >> 6)) as usize] as char, 4120 ); 4121 output.push(ALPHABET[(chunk[2] & 0b0011_1111) as usize] as char); 4122 } 4123 match chunks.remainder() { 4124 [one] => { 4125 output.push(ALPHABET[(one >> 2) as usize] as char); 4126 output.push(ALPHABET[((one & 0b0000_0011) << 4) as usize] as char); 4127 } 4128 [one, two] => { 4129 output.push(ALPHABET[(one >> 2) as usize] as char); 4130 output.push(ALPHABET[(((one & 0b0000_0011) << 4) | (two >> 4)) as usize] as char); 4131 output.push(ALPHABET[((two & 0b0000_1111) << 2) as usize] as char); 4132 } 4133 [] => {} 4134 _ => unreachable!(), 4135 } 4136 output 4137 } 4138 4139 #[test] 4140 fn app_shaped_keys_use_uuid_projection_only_for_app_runtime() { 4141 let owner_pubkey = "projection-owner-pubkey"; 4142 let farm_uuid = Uuid::from_u128(0x11111111111141118111111111111111); 4143 let product_uuid = Uuid::from_u128(0x22222222222242228222222222222222); 4144 let farm_key = app_d_tag_from_uuid(farm_uuid); 4145 let listing_key = app_d_tag_from_uuid(product_uuid); 4146 4147 assert_eq!( 4148 projected_farm_id(SourceRuntime::App, Some(owner_pubkey), farm_key.as_str()), 4149 Some(FarmId::from(farm_uuid)) 4150 ); 4151 assert_eq!( 4152 projected_product_id(SourceRuntime::App, Some(owner_pubkey), listing_key.as_str()), 4153 Some(ProductId::from(product_uuid)) 4154 ); 4155 assert_eq!( 4156 projected_farm_id( 4157 SourceRuntime::Network, 4158 Some(owner_pubkey), 4159 farm_key.as_str() 4160 ), 4161 Some(deterministic_farm_id(Some(owner_pubkey), farm_key.as_str())) 4162 ); 4163 assert_eq!( 4164 projected_product_id( 4165 SourceRuntime::Network, 4166 Some(owner_pubkey), 4167 listing_key.as_str() 4168 ), 4169 Some(deterministic_product_id( 4170 Some(owner_pubkey), 4171 listing_key.as_str() 4172 )) 4173 ); 4174 } 4175 4176 fn app_local_work_record( 4177 record_id: &str, 4178 farm_key: &str, 4179 payload: serde_json::Value, 4180 ) -> LocalEventRecordInput { 4181 let mut record = local_work_record(record_id, farm_key, payload); 4182 record.source_runtime = SourceRuntime::App; 4183 record.owner_pubkey = Some("app-seller-pubkey".to_owned()); 4184 record 4185 } 4186 4187 fn seed_app_projection(app_store: &AppSqliteStore, farm_id: Uuid, product_id: Uuid) { 4188 app_store 4189 .connection() 4190 .execute( 4191 "INSERT INTO farms (id, display_name, readiness, created_at, updated_at) 4192 VALUES (?1, 'Origin Farm', 'ready', '2026-01-01T00:00:00Z', '2026-01-01T00:00:00Z')", 4193 params![farm_id.to_string()], 4194 ) 4195 .expect("seed origin farm"); 4196 app_store 4197 .connection() 4198 .execute( 4199 "INSERT INTO products ( 4200 id, 4201 farm_id, 4202 title, 4203 subtitle, 4204 status, 4205 unit_label, 4206 price_minor_units, 4207 price_currency, 4208 stock_count, 4209 availability_window_id, 4210 updated_at 4211 ) VALUES ( 4212 ?1, 4213 ?2, 4214 'Origin Eggs', 4215 'Seeded product', 4216 'draft', 4217 'each', 4218 400, 4219 'USD', 4220 3, 4221 NULL, 4222 '2026-01-01T00:00:00Z' 4223 )", 4224 params![product_id.to_string(), farm_id.to_string()], 4225 ) 4226 .expect("seed origin product"); 4227 } 4228 4229 fn decimal(raw: &str) -> RadrootsCoreDecimal { 4230 raw.parse().expect("valid decimal") 4231 } 4232 4233 fn usd(raw: &str) -> RadrootsCoreMoney { 4234 RadrootsCoreMoney::new(decimal(raw), RadrootsCoreCurrency::USD) 4235 } 4236 4237 fn test_pubkey(seed: &str) -> String { 4238 let left = Uuid::new_v5(&Uuid::NAMESPACE_URL, seed.as_bytes()); 4239 let right_seed = format!("{seed}:right"); 4240 let right = Uuid::new_v5(&Uuid::NAMESPACE_URL, right_seed.as_bytes()); 4241 format!("{}{}", left.simple(), right.simple()) 4242 } 4243 4244 fn test_event_id_seed(seed: &str) -> String { 4245 test_pubkey(seed) 4246 } 4247 4248 fn typed_order_id(raw: &str) -> RadrootsOrderId { 4249 raw.parse().expect("valid order id") 4250 } 4251 4252 fn typed_revision_id(raw: &str) -> RadrootsOrderRevisionId { 4253 raw.parse().expect("valid revision id") 4254 } 4255 4256 fn typed_quote_id(raw: &str) -> RadrootsOrderQuoteId { 4257 raw.parse().expect("valid quote id") 4258 } 4259 4260 fn typed_bin_id(raw: &str) -> RadrootsInventoryBinId { 4261 raw.parse().expect("valid bin id") 4262 } 4263 4264 fn typed_event_id(raw: &str) -> RadrootsEventId { 4265 raw.parse().expect("valid event id") 4266 } 4267 4268 fn typed_pubkey(raw: &str) -> RadrootsPublicKey { 4269 raw.parse().expect("valid pubkey") 4270 } 4271 4272 fn typed_listing_addr(raw: &str) -> RadrootsListingAddress { 4273 raw.parse().expect("valid listing address") 4274 } 4275 4276 fn listing_event_ptr(event_id: &str) -> RadrootsNostrEventPtr { 4277 RadrootsNostrEventPtr { 4278 id: test_event_id_seed(event_id), 4279 relays: Some("ws://127.0.0.1:1234/".to_owned()), 4280 } 4281 } 4282 4283 fn order_request_payload( 4284 order_id: &str, 4285 listing_addr: &str, 4286 buyer_pubkey: &str, 4287 seller_pubkey: &str, 4288 ) -> RadrootsOrderRequest { 4289 RadrootsOrderRequest { 4290 order_id: typed_order_id(order_id), 4291 listing_addr: typed_listing_addr(listing_addr), 4292 buyer_pubkey: typed_pubkey(buyer_pubkey), 4293 seller_pubkey: typed_pubkey(seller_pubkey), 4294 items: vec![RadrootsOrderItem { 4295 bin_id: typed_bin_id("bin-1"), 4296 bin_count: 2, 4297 }], 4298 economics: RadrootsOrderEconomics { 4299 quote_id: typed_quote_id(format!("quote-{order_id}").as_str()), 4300 quote_version: 1, 4301 pricing_basis: RadrootsOrderPricingBasis::ListingEvent, 4302 currency: RadrootsCoreCurrency::USD, 4303 items: vec![RadrootsOrderEconomicItem { 4304 bin_id: typed_bin_id("bin-1"), 4305 bin_count: 2, 4306 quantity_amount: decimal("1"), 4307 quantity_unit: RadrootsCoreUnit::Each, 4308 unit_price_amount: decimal("8"), 4309 unit_price_currency: RadrootsCoreCurrency::USD, 4310 line_subtotal: usd("16"), 4311 }], 4312 discounts: Vec::<RadrootsOrderEconomicLine>::new(), 4313 adjustments: Vec::<RadrootsOrderEconomicLine>::new(), 4314 subtotal: usd("16"), 4315 discount_total: usd("0"), 4316 adjustment_total: usd("0"), 4317 total: usd("16"), 4318 }, 4319 } 4320 } 4321 4322 fn accepted_order_decision_payload( 4323 order_id: &str, 4324 listing_addr: &str, 4325 buyer_pubkey: &str, 4326 seller_pubkey: &str, 4327 ) -> RadrootsOrderDecision { 4328 RadrootsOrderDecision { 4329 order_id: typed_order_id(order_id), 4330 listing_addr: typed_listing_addr(listing_addr), 4331 buyer_pubkey: typed_pubkey(buyer_pubkey), 4332 seller_pubkey: typed_pubkey(seller_pubkey), 4333 decision: RadrootsOrderDecisionOutcome::Accepted { 4334 inventory_commitments: vec![RadrootsOrderInventoryCommitment { 4335 bin_id: typed_bin_id("bin-1"), 4336 bin_count: 2, 4337 }], 4338 }, 4339 } 4340 } 4341 4342 fn declined_order_decision_payload( 4343 order_id: &str, 4344 listing_addr: &str, 4345 buyer_pubkey: &str, 4346 seller_pubkey: &str, 4347 ) -> RadrootsOrderDecision { 4348 RadrootsOrderDecision { 4349 order_id: typed_order_id(order_id), 4350 listing_addr: typed_listing_addr(listing_addr), 4351 buyer_pubkey: typed_pubkey(buyer_pubkey), 4352 seller_pubkey: typed_pubkey(seller_pubkey), 4353 decision: RadrootsOrderDecisionOutcome::Declined { 4354 reason: "not available for this pickup".to_owned(), 4355 }, 4356 } 4357 } 4358 4359 fn revision_proposal_payload( 4360 revision_id: &str, 4361 order_id: &str, 4362 listing_addr: &str, 4363 buyer_pubkey: &str, 4364 seller_pubkey: &str, 4365 root_event_id: &str, 4366 prev_event_id: &str, 4367 ) -> RadrootsOrderRevisionProposal { 4368 let mut request = 4369 order_request_payload(order_id, listing_addr, buyer_pubkey, seller_pubkey); 4370 request.items[0].bin_count = 3; 4371 request.economics.quote_id = 4372 typed_quote_id(format!("quote-{order_id}-{revision_id}").as_str()); 4373 request.economics.quote_version = 2; 4374 request.economics.items[0].bin_count = 3; 4375 request.economics.items[0].line_subtotal = usd("24"); 4376 request.economics.subtotal = usd("24"); 4377 request.economics.total = usd("24"); 4378 RadrootsOrderRevisionProposal { 4379 revision_id: typed_revision_id(revision_id), 4380 order_id: typed_order_id(order_id), 4381 listing_addr: typed_listing_addr(listing_addr), 4382 buyer_pubkey: typed_pubkey(buyer_pubkey), 4383 seller_pubkey: typed_pubkey(seller_pubkey), 4384 root_event_id: typed_event_id(root_event_id), 4385 prev_event_id: typed_event_id(prev_event_id), 4386 items: request.items, 4387 economics: request.economics, 4388 reason: "seller confirmed updated pickup details".to_owned(), 4389 } 4390 } 4391 4392 fn revision_decision_payload( 4393 revision_id: &str, 4394 order_id: &str, 4395 listing_addr: &str, 4396 buyer_pubkey: &str, 4397 seller_pubkey: &str, 4398 root_event_id: &str, 4399 prev_event_id: &str, 4400 decision: RadrootsOrderRevisionOutcome, 4401 ) -> RadrootsOrderRevisionDecision { 4402 RadrootsOrderRevisionDecision { 4403 revision_id: typed_revision_id(revision_id), 4404 order_id: typed_order_id(order_id), 4405 listing_addr: typed_listing_addr(listing_addr), 4406 buyer_pubkey: typed_pubkey(buyer_pubkey), 4407 seller_pubkey: typed_pubkey(seller_pubkey), 4408 root_event_id: typed_event_id(root_event_id), 4409 prev_event_id: typed_event_id(prev_event_id), 4410 decision, 4411 } 4412 } 4413 4414 fn order_cancel_payload( 4415 order_id: &str, 4416 listing_addr: &str, 4417 buyer_pubkey: &str, 4418 seller_pubkey: &str, 4419 ) -> RadrootsOrderCancellation { 4420 RadrootsOrderCancellation { 4421 order_id: typed_order_id(order_id), 4422 listing_addr: typed_listing_addr(listing_addr), 4423 buyer_pubkey: typed_pubkey(buyer_pubkey), 4424 seller_pubkey: typed_pubkey(seller_pubkey), 4425 reason: "buyer changed pickup plan".to_owned(), 4426 } 4427 } 4428 4429 struct ValidationReceiptOrderFixture { 4430 app_store: AppSqliteStore, 4431 events: LocalEventsStore<SqliteExecutor>, 4432 buyer_context: BuyerContext, 4433 seller_farm_id: FarmId, 4434 order_id: OrderId, 4435 order_id_raw: String, 4436 listing_addr: String, 4437 buyer_pubkey: String, 4438 seller_pubkey: String, 4439 listing_event_id: String, 4440 request_event_id: String, 4441 decision_event_id: String, 4442 } 4443 4444 fn validation_receipt_order_fixture(label: &str) -> ValidationReceiptOrderFixture { 4445 let app_store = 4446 AppSqliteStore::open(DatabaseTarget::InMemory).expect("open app sqlite store"); 4447 let events = local_events_store(); 4448 let farm_key = "DDDDDDDDDDDDDDDDDDDDDD"; 4449 let listing_key = "AAAAAAAAAAAAAAAAAAAAAw"; 4450 let seller_pubkey = test_pubkey(format!("{label}-seller").as_str()); 4451 let buyer_pubkey = test_pubkey(format!("{label}-buyer").as_str()); 4452 let order_id_raw = format!("{label}-order"); 4453 let listing_addr = format!("30402:{seller_pubkey}:{listing_key}"); 4454 let listing_event_id = hex_event_id(21); 4455 let request_event_id = hex_event_id(22); 4456 let decision_event_id = hex_event_id(23); 4457 events 4458 .append_record(&signed_market_listing_record( 4459 format!("{label}-listing-record").as_str(), 4460 seller_pubkey.as_str(), 4461 farm_key, 4462 listing_key, 4463 "Validation Eggs", 4464 "9", 4465 "active", 4466 "pickup", 4467 "North barn pickup", 4468 4_102_444_800, 4469 4_102_531_200, 4470 LocalRecordStatus::Published, 4471 PublishOutboxStatus::Acknowledged, 4472 )) 4473 .expect("append signed listing"); 4474 app_store 4475 .import_shared_local_events_from_store(&events) 4476 .expect("import signed listing"); 4477 4478 let request_payload = order_request_payload( 4479 order_id_raw.as_str(), 4480 listing_addr.as_str(), 4481 buyer_pubkey.as_str(), 4482 seller_pubkey.as_str(), 4483 ); 4484 let request_parts = 4485 order_request_event_build(&listing_event_ptr(&listing_event_id), &request_payload) 4486 .expect("build validation order request"); 4487 let request_event = event_from_parts_at( 4488 request_event_id.as_str(), 4489 buyer_pubkey.as_str(), 4490 request_parts, 4491 1_777_665_601, 4492 ); 4493 events 4494 .append_record(&signed_order_event_record( 4495 format!("app:signed_event:{label}:request").as_str(), 4496 &request_event, 4497 listing_addr.as_str(), 4498 SourceRuntime::App, 4499 Some("acct_validation"), 4500 )) 4501 .expect("append validation order request"); 4502 app_store 4503 .import_shared_local_events_from_store(&events) 4504 .expect("import validation order request"); 4505 4506 let decision_payload = accepted_order_decision_payload( 4507 order_id_raw.as_str(), 4508 listing_addr.as_str(), 4509 buyer_pubkey.as_str(), 4510 seller_pubkey.as_str(), 4511 ); 4512 let decision_parts = order_decision_event_build( 4513 &typed_event_id(request_event_id.as_str()), 4514 &typed_event_id(request_event_id.as_str()), 4515 &decision_payload, 4516 ) 4517 .expect("build validation order decision"); 4518 let decision_event = event_from_parts_at( 4519 decision_event_id.as_str(), 4520 seller_pubkey.as_str(), 4521 decision_parts, 4522 1_777_665_602, 4523 ); 4524 events 4525 .append_record(&signed_order_event_record( 4526 format!("cli:signed_event:{label}:decision").as_str(), 4527 &decision_event, 4528 listing_addr.as_str(), 4529 SourceRuntime::Cli, 4530 None, 4531 )) 4532 .expect("append validation order decision"); 4533 app_store 4534 .import_shared_local_events_from_store(&events) 4535 .expect("import validation order decision"); 4536 4537 ValidationReceiptOrderFixture { 4538 app_store, 4539 events, 4540 buyer_context: BuyerContext::account("acct_validation"), 4541 seller_farm_id: deterministic_farm_id(Some(seller_pubkey.as_str()), farm_key), 4542 order_id: projected_order_id(order_id_raw.as_str(), buyer_pubkey.as_str()), 4543 order_id_raw, 4544 listing_addr, 4545 buyer_pubkey, 4546 seller_pubkey, 4547 listing_event_id, 4548 request_event_id, 4549 decision_event_id, 4550 } 4551 } 4552 4553 fn event_from_parts(event_id: &str, author: &str, parts: WireEventParts) -> RadrootsNostrEvent { 4554 let event_id = event_id 4555 .parse::<RadrootsEventId>() 4556 .map(|event_id| event_id.to_string()) 4557 .unwrap_or_else(|_| test_event_id_seed(event_id)); 4558 RadrootsNostrEvent { 4559 sig: format!("sig-{event_id}"), 4560 id: event_id, 4561 author: author.to_owned(), 4562 created_at: 1_777_665_600, 4563 kind: parts.kind, 4564 tags: parts.tags, 4565 content: parts.content, 4566 } 4567 } 4568 4569 fn event_from_parts_at( 4570 event_id: &str, 4571 author: &str, 4572 parts: WireEventParts, 4573 created_at: u32, 4574 ) -> RadrootsNostrEvent { 4575 let mut event = event_from_parts(event_id, author, parts); 4576 event.created_at = created_at; 4577 event 4578 } 4579 4580 fn hex_event_id(seed: u8) -> String { 4581 format!("{seed:064x}") 4582 } 4583 4584 fn hash32(seed: u8) -> String { 4585 format!("0x{seed:064x}") 4586 } 4587 4588 fn validation_error_bitmap(result: RadrootsValidationReceiptResult) -> String { 4589 match result { 4590 RadrootsValidationReceiptResult::Valid => format!("0x{:032x}", 0), 4591 RadrootsValidationReceiptResult::Invalid => format!("0x{:032x}", 1), 4592 } 4593 } 4594 4595 fn validation_receipt_payload( 4596 listing_event_id: &str, 4597 root_event_id: &str, 4598 target_event_id: &str, 4599 result: RadrootsValidationReceiptResult, 4600 ) -> RadrootsTradeValidationReceipt { 4601 RadrootsTradeValidationReceipt { 4602 changed_records_root: hash32(41), 4603 domain: VALIDATION_RECEIPT_DOMAIN.to_owned(), 4604 error_bitmap: validation_error_bitmap(result), 4605 event_set_root: hash32(42), 4606 new_state_root: hash32(43), 4607 previous_state_root: hash32(44), 4608 proof: RadrootsValidationReceiptProof { 4609 inline_proof_base64: None, 4610 mode: None, 4611 program_hash: None, 4612 proof_reference: None, 4613 system: RadrootsValidationReceiptProofSystem::None, 4614 verifying_key_hash: None, 4615 }, 4616 public_values_hash: hash32(45), 4617 receipt_type: RadrootsValidationReceiptType::TradeTransition, 4618 result, 4619 statement: RadrootsValidationReceiptStatement { 4620 listing_event_id: listing_event_id.to_owned(), 4621 root_event_id: root_event_id.to_owned(), 4622 target_event_id: target_event_id.to_owned(), 4623 statement_type: RadrootsValidationReceiptType::TradeTransition, 4624 }, 4625 version: VALIDATION_RECEIPT_VERSION, 4626 } 4627 } 4628 4629 fn validation_receipt_event( 4630 event_id: &str, 4631 author: &str, 4632 order_id: &str, 4633 listing_event_id: &str, 4634 root_event_id: &str, 4635 target_event_id: &str, 4636 result: RadrootsValidationReceiptResult, 4637 created_at: u32, 4638 ) -> RadrootsNostrEvent { 4639 let receipt = 4640 validation_receipt_payload(listing_event_id, root_event_id, target_event_id, result); 4641 let parts = 4642 validation_receipt_event_build(order_id, &receipt).expect("validation receipt parts"); 4643 event_from_parts_at(event_id, author, parts, created_at) 4644 } 4645 4646 fn signed_order_event_record( 4647 record_id: &str, 4648 event: &RadrootsNostrEvent, 4649 listing_addr: &str, 4650 source_runtime: SourceRuntime, 4651 owner_account_id: Option<&str>, 4652 ) -> LocalEventRecordInput { 4653 let relay_delivery_json = RelayDeliveryEvidence::acknowledged( 4654 ["ws://127.0.0.1:1234"], 4655 ["ws://127.0.0.1:1234"], 4656 ["ws://127.0.0.1:1234"], 4657 Vec::new(), 4658 ) 4659 .expect("acknowledged relay evidence") 4660 .to_json_value() 4661 .expect("acknowledged relay evidence json"); 4662 LocalEventRecordInput { 4663 record_id: record_id.to_owned(), 4664 family: LocalRecordFamily::SignedEvent, 4665 status: LocalRecordStatus::Published, 4666 source_runtime, 4667 created_at_ms: i64::from(event.created_at) * 1_000, 4668 inserted_at_ms: i64::from(event.created_at) * 1_000 + 1, 4669 owner_account_id: owner_account_id.map(str::to_owned), 4670 owner_pubkey: Some(event.author.clone()), 4671 farm_id: None, 4672 listing_addr: Some(listing_addr.to_owned()), 4673 local_work_json: None, 4674 event_id: Some(event.id.clone()), 4675 event_kind: Some(i64::from(event.kind)), 4676 event_pubkey: Some(event.author.clone()), 4677 event_created_at: Some(i64::from(event.created_at)), 4678 event_tags_json: Some(json!(event.tags)), 4679 event_content: Some(event.content.clone()), 4680 event_sig: Some(event.sig.clone()), 4681 raw_event_json: Some(json!({ 4682 "id": event.id, 4683 "kind": event.kind, 4684 "pubkey": event.author, 4685 "created_at": event.created_at, 4686 "tags": event.tags, 4687 "content": event.content, 4688 "sig": event.sig, 4689 })), 4690 outbox_status: PublishOutboxStatus::Acknowledged, 4691 relay_set_fingerprint: Some("relay-set".to_owned()), 4692 relay_delivery_json: Some(relay_delivery_json), 4693 } 4694 } 4695 4696 #[test] 4697 fn imports_signed_order_request_into_seller_order_projection() { 4698 let app_store = 4699 AppSqliteStore::open(DatabaseTarget::InMemory).expect("open app sqlite store"); 4700 let events = local_events_store(); 4701 let farm_key = "AAAAAAAAAAAAAAAAAAAAAA"; 4702 let listing_key = "AAAAAAAAAAAAAAAAAAAAAg"; 4703 let seller_pubkey = test_pubkey("seller-pubkey"); 4704 let seller_pubkey = seller_pubkey.as_str(); 4705 let buyer_pubkey = test_pubkey("buyer-pubkey"); 4706 let buyer_pubkey = buyer_pubkey.as_str(); 4707 let order_id_raw = "relay-order-1"; 4708 let listing_addr = format!("30402:{seller_pubkey}:{listing_key}"); 4709 events 4710 .append_record(&signed_market_listing_record( 4711 "order-visible-listing", 4712 seller_pubkey, 4713 farm_key, 4714 listing_key, 4715 "Order Visible Eggs", 4716 "9", 4717 "active", 4718 "pickup", 4719 "North barn pickup", 4720 4_102_444_800, 4721 4_102_531_200, 4722 LocalRecordStatus::Published, 4723 PublishOutboxStatus::Acknowledged, 4724 )) 4725 .expect("append signed listing"); 4726 app_store 4727 .import_shared_local_events_from_store(&events) 4728 .expect("import signed listing"); 4729 let payload = order_request_payload( 4730 order_id_raw, 4731 listing_addr.as_str(), 4732 buyer_pubkey, 4733 seller_pubkey, 4734 ); 4735 let parts = order_request_event_build(&listing_event_ptr("listing-event-1"), &payload) 4736 .expect("build order request event"); 4737 let event = event_from_parts("order-request-event-1", buyer_pubkey, parts); 4738 events 4739 .append_record(&signed_order_event_record( 4740 "cli:signed_event:order-request:1", 4741 &event, 4742 listing_addr.as_str(), 4743 SourceRuntime::Cli, 4744 None, 4745 )) 4746 .expect("append order request"); 4747 4748 let report = app_store 4749 .import_shared_local_events_from_store(&events) 4750 .expect("import signed order request"); 4751 let farm_id = deterministic_farm_id(Some(seller_pubkey), farm_key); 4752 let order_id = projected_order_id(order_id_raw, buyer_pubkey); 4753 let orders = app_store 4754 .load_orders_list( 4755 farm_id, 4756 &OrdersScreenQueryState { 4757 filter: OrdersFilter::All, 4758 fulfillment_window_id: None, 4759 }, 4760 ) 4761 .expect("load seller orders"); 4762 let detail = app_store 4763 .load_order_detail(farm_id, order_id) 4764 .expect("load order detail") 4765 .expect("order detail"); 4766 let imported = app_store 4767 .load_local_interop_records() 4768 .expect("load imported records"); 4769 let signed_evidence = app_store 4770 .load_local_interop_signed_events_by_kind(KIND_ORDER_REQUEST) 4771 .expect("load signed event evidence"); 4772 let buyer_context_key: String = app_store 4773 .connection() 4774 .query_row( 4775 "SELECT buyer_context_key FROM orders WHERE id = ?1", 4776 [order_id.to_string()], 4777 |row| row.get(0), 4778 ) 4779 .expect("load buyer context key"); 4780 4781 assert_eq!(report.imported_records, 1); 4782 assert!( 4783 imported 4784 .iter() 4785 .any(|record| record.projected_kind == "signed_event" 4786 && record.event_kind == Some(KIND_ORDER_REQUEST) 4787 && record.event_id.as_deref() == Some(event.id.as_str())) 4788 ); 4789 assert_eq!(signed_evidence, vec![event.clone()]); 4790 assert_eq!(orders.rows.len(), 1); 4791 assert_eq!(orders.rows[0].order_id, order_id); 4792 assert_eq!(orders.rows[0].status, OrderStatus::NeedsAction); 4793 assert_eq!( 4794 orders.rows[0].customer_display_name, 4795 format!("Relay buyer {}", &buyer_pubkey[..12]) 4796 ); 4797 assert_eq!(detail.items.len(), 1); 4798 assert_eq!(detail.items[0].title, "Order Visible Eggs"); 4799 assert_eq!(detail.items[0].quantity_display, "2 each"); 4800 assert_eq!(buyer_context_key, format!("nostr:{buyer_pubkey}")); 4801 } 4802 4803 #[test] 4804 fn local_interop_order_request_evidence_requires_usable_delivery_state() { 4805 let app_store = 4806 AppSqliteStore::open(DatabaseTarget::InMemory).expect("open app sqlite store"); 4807 let events = local_events_store(); 4808 let buyer_pubkey = test_pubkey("buyer-pubkey"); 4809 let buyer_pubkey = buyer_pubkey.as_str(); 4810 let seller_pubkey = test_pubkey("seller-pubkey"); 4811 let seller_pubkey = seller_pubkey.as_str(); 4812 let listing_addr = format!("30402:{seller_pubkey}:AAAAAAAAAAAAAAAAAAAAAg"); 4813 let relay_url = "ws://127.0.0.1:1234"; 4814 let build_event = |event_id: &str, order_id_raw: &str| { 4815 let payload = order_request_payload( 4816 order_id_raw, 4817 listing_addr.as_str(), 4818 buyer_pubkey, 4819 seller_pubkey, 4820 ); 4821 let parts = order_request_event_build(&listing_event_ptr("listing-event-1"), &payload) 4822 .expect("build order request event"); 4823 event_from_parts(event_id, buyer_pubkey, parts) 4824 }; 4825 let acknowledged_event = build_event("order-request-evidence-ack", "usable-ack"); 4826 events 4827 .append_record(&signed_order_event_record( 4828 "cli:signed_event:order-request:evidence-ack", 4829 &acknowledged_event, 4830 listing_addr.as_str(), 4831 SourceRuntime::Cli, 4832 None, 4833 )) 4834 .expect("append acknowledged order request evidence"); 4835 4836 let observed_event = build_event("order-request-evidence-observed", "usable-observed"); 4837 let mut observed_record = signed_order_event_record( 4838 "cli:signed_event:order-request:evidence-observed", 4839 &observed_event, 4840 listing_addr.as_str(), 4841 SourceRuntime::Cli, 4842 None, 4843 ); 4844 observed_record.outbox_status = PublishOutboxStatus::None; 4845 observed_record.relay_delivery_json = Some( 4846 RelayDeliveryEvidence::observed([relay_url], [relay_url], [relay_url], Vec::new()) 4847 .expect("observed relay evidence") 4848 .to_json_value() 4849 .expect("observed relay evidence json"), 4850 ); 4851 events 4852 .append_record(&observed_record) 4853 .expect("append observed order request evidence"); 4854 4855 let pending_event = build_event("order-request-evidence-pending", "pending"); 4856 let mut pending_record = signed_order_event_record( 4857 "cli:signed_event:order-request:evidence-pending", 4858 &pending_event, 4859 listing_addr.as_str(), 4860 SourceRuntime::Cli, 4861 None, 4862 ); 4863 pending_record.status = LocalRecordStatus::PendingPublish; 4864 pending_record.outbox_status = PublishOutboxStatus::Pending; 4865 pending_record.relay_delivery_json = Some( 4866 RelayDeliveryEvidence::pending([relay_url]) 4867 .expect("pending relay evidence") 4868 .to_json_value() 4869 .expect("pending relay evidence json"), 4870 ); 4871 events 4872 .append_record(&pending_record) 4873 .expect("append pending order request evidence"); 4874 4875 let failed_event = build_event("order-request-evidence-failed", "failed"); 4876 let mut failed_record = signed_order_event_record( 4877 "cli:signed_event:order-request:evidence-failed", 4878 &failed_event, 4879 listing_addr.as_str(), 4880 SourceRuntime::Cli, 4881 None, 4882 ); 4883 failed_record.outbox_status = PublishOutboxStatus::Failed; 4884 failed_record.relay_delivery_json = Some(json!({ 4885 "state": "failed", 4886 "target_relays": [relay_url], 4887 "connected_relays": [relay_url], 4888 "acknowledged_relays": [], 4889 "failed_relays": [{"relay_url": relay_url, "error": "relay rejected event"}] 4890 })); 4891 events 4892 .append_record(&failed_record) 4893 .expect("append failed order request evidence"); 4894 4895 let local_only_event = build_event("order-request-evidence-local-only", "local-only"); 4896 let mut local_only_record = signed_order_event_record( 4897 "cli:signed_event:order-request:evidence-local-only", 4898 &local_only_event, 4899 listing_addr.as_str(), 4900 SourceRuntime::Cli, 4901 None, 4902 ); 4903 local_only_record.outbox_status = PublishOutboxStatus::None; 4904 local_only_record.relay_set_fingerprint = None; 4905 local_only_record.relay_delivery_json = None; 4906 events 4907 .append_record(&local_only_record) 4908 .expect("append local-only order request evidence"); 4909 4910 let malformed_delivery_event = build_event( 4911 "order-request-evidence-malformed-delivery", 4912 "malformed-delivery", 4913 ); 4914 let mut malformed_delivery_record = signed_order_event_record( 4915 "cli:signed_event:order-request:evidence-malformed-delivery", 4916 &malformed_delivery_event, 4917 listing_addr.as_str(), 4918 SourceRuntime::Cli, 4919 None, 4920 ); 4921 malformed_delivery_record.relay_delivery_json = Some(json!({ 4922 "state": "acknowledged" 4923 })); 4924 events 4925 .append_record(&malformed_delivery_record) 4926 .expect("append malformed delivery order request evidence"); 4927 4928 let malformed_event = 4929 build_event("order-request-evidence-malformed-event", "malformed-event"); 4930 let mut malformed_record = signed_order_event_record( 4931 "cli:signed_event:order-request:evidence-malformed-event", 4932 &malformed_event, 4933 listing_addr.as_str(), 4934 SourceRuntime::Cli, 4935 None, 4936 ); 4937 malformed_record.event_tags_json = Some(json!({"invalid": "tags"})); 4938 events 4939 .append_record(&malformed_record) 4940 .expect("append malformed order request evidence"); 4941 4942 app_store 4943 .import_shared_local_events_from_store(&events) 4944 .expect("import signed evidence records"); 4945 let signed_evidence = app_store 4946 .load_local_interop_signed_events_by_kind(KIND_ORDER_REQUEST) 4947 .expect("load filtered signed event evidence"); 4948 4949 assert_eq!(signed_evidence, vec![acknowledged_event, observed_event]); 4950 } 4951 4952 #[test] 4953 fn app_origin_signed_order_request_and_decision_project_to_buyer_orders() { 4954 let app_store = 4955 AppSqliteStore::open(DatabaseTarget::InMemory).expect("open app sqlite store"); 4956 let events = local_events_store(); 4957 let farm_key = "CCCCCCCCCCCCCCCCCCCCCC"; 4958 let listing_key = "AAAAAAAAAAAAAAAAAAAAAg"; 4959 let seller_pubkey = test_pubkey("seller-pubkey"); 4960 let seller_pubkey = seller_pubkey.as_str(); 4961 let buyer_pubkey = test_pubkey("app-buyer-pubkey"); 4962 let buyer_pubkey = buyer_pubkey.as_str(); 4963 let order_id_raw = "app-relay-order-1"; 4964 let listing_addr = format!("30402:{seller_pubkey}:{listing_key}"); 4965 events 4966 .append_record(&signed_market_listing_record( 4967 "buyer-order-listing", 4968 seller_pubkey, 4969 farm_key, 4970 listing_key, 4971 "Buyer Order Eggs", 4972 "9", 4973 "active", 4974 "pickup", 4975 "North barn pickup", 4976 4_102_444_800, 4977 4_102_531_200, 4978 LocalRecordStatus::Published, 4979 PublishOutboxStatus::Acknowledged, 4980 )) 4981 .expect("append signed listing"); 4982 app_store 4983 .import_shared_local_events_from_store(&events) 4984 .expect("import signed listing"); 4985 let request_payload = order_request_payload( 4986 order_id_raw, 4987 listing_addr.as_str(), 4988 buyer_pubkey, 4989 seller_pubkey, 4990 ); 4991 let request_parts = order_request_event_build( 4992 &listing_event_ptr("buyer-order-listing-event"), 4993 &request_payload, 4994 ) 4995 .expect("build order request event"); 4996 let request_event = 4997 event_from_parts("buyer-order-request-event", buyer_pubkey, request_parts); 4998 events 4999 .append_record(&signed_order_event_record( 5000 "app:signed_event:order-request:buyer", 5001 &request_event, 5002 listing_addr.as_str(), 5003 SourceRuntime::App, 5004 Some("acct_buyer"), 5005 )) 5006 .expect("append app order request"); 5007 5008 let request_report = app_store 5009 .import_shared_local_events_from_store(&events) 5010 .expect("import app order request"); 5011 let buyer_context = BuyerContext::account("acct_buyer"); 5012 let order_id = projected_order_id(order_id_raw, buyer_pubkey); 5013 let farm_id = deterministic_farm_id(Some(seller_pubkey), farm_key); 5014 let buyer_orders = app_store 5015 .load_buyer_orders(&buyer_context) 5016 .expect("load buyer orders after request"); 5017 5018 assert_eq!(request_report.imported_records, 1); 5019 assert_eq!(buyer_orders.rows.len(), 1); 5020 assert_eq!(buyer_orders.rows[0].order_id, order_id); 5021 assert_eq!(buyer_orders.rows[0].status, BuyerOrderStatus::Placed); 5022 5023 let decision_payload = accepted_order_decision_payload( 5024 order_id_raw, 5025 listing_addr.as_str(), 5026 buyer_pubkey, 5027 seller_pubkey, 5028 ); 5029 let decision_parts = order_decision_event_build( 5030 &typed_event_id(request_event.id.as_str()), 5031 &typed_event_id(request_event.id.as_str()), 5032 &decision_payload, 5033 ) 5034 .expect("build order decision event"); 5035 let decision_event = 5036 event_from_parts("buyer-order-decision-event", seller_pubkey, decision_parts); 5037 events 5038 .append_record(&signed_order_event_record( 5039 "cli:signed_event:order-decision:buyer", 5040 &decision_event, 5041 listing_addr.as_str(), 5042 SourceRuntime::Cli, 5043 None, 5044 )) 5045 .expect("append order decision"); 5046 5047 let decision_report = app_store 5048 .import_shared_local_events_from_store(&events) 5049 .expect("import order decision"); 5050 let buyer_orders = app_store 5051 .load_buyer_orders(&buyer_context) 5052 .expect("load buyer orders after decision"); 5053 let buyer_detail = app_store 5054 .load_buyer_order_detail(&buyer_context, order_id) 5055 .expect("load buyer order detail") 5056 .expect("buyer order detail"); 5057 let seller_orders = app_store 5058 .load_orders_list( 5059 farm_id, 5060 &OrdersScreenQueryState { 5061 filter: OrdersFilter::All, 5062 fulfillment_window_id: None, 5063 }, 5064 ) 5065 .expect("load seller orders after decision"); 5066 5067 assert_eq!(decision_report.imported_records, 1); 5068 assert_eq!(buyer_orders.rows.len(), 1); 5069 assert_eq!(buyer_orders.rows[0].status, BuyerOrderStatus::Scheduled); 5070 assert_eq!(buyer_detail.status, BuyerOrderStatus::Scheduled); 5071 assert_eq!(seller_orders.rows[0].status, OrderStatus::Scheduled); 5072 assert_eq!(buyer_detail.workflow, buyer_orders.rows[0].workflow); 5073 assert_eq!( 5074 seller_orders.rows[0].workflow, 5075 buyer_orders.rows[0].workflow 5076 ); 5077 assert_eq!( 5078 buyer_orders.rows[0].workflow.inventory, 5079 TradeInventoryStatus::Reserved 5080 ); 5081 assert_eq!( 5082 buyer_orders.rows[0].workflow.provenance.primary_source, 5083 TradeWorkflowSource::LocalEvents 5084 ); 5085 assert_eq!( 5086 buyer_orders.rows[0] 5087 .workflow 5088 .provenance 5089 .last_event_id 5090 .as_deref(), 5091 Some(decision_event.id.as_str()) 5092 ); 5093 assert_eq!( 5094 buyer_orders.rows[0].workflow.economics.total_minor_units, 5095 Some(1600) 5096 ); 5097 } 5098 5099 #[test] 5100 fn app_origin_signed_order_request_and_decline_project_to_buyer_orders() { 5101 let app_store = 5102 AppSqliteStore::open(DatabaseTarget::InMemory).expect("open app sqlite store"); 5103 let events = local_events_store(); 5104 let farm_key = "CCCCCCCCCCCCCCCCCCCCCC"; 5105 let listing_key = "AAAAAAAAAAAAAAAAAAAAAg"; 5106 let seller_pubkey = test_pubkey("seller-pubkey"); 5107 let seller_pubkey = seller_pubkey.as_str(); 5108 let buyer_pubkey = test_pubkey("app-buyer-pubkey"); 5109 let buyer_pubkey = buyer_pubkey.as_str(); 5110 let order_id_raw = "app-relay-order-declined-1"; 5111 let listing_addr = format!("30402:{seller_pubkey}:{listing_key}"); 5112 events 5113 .append_record(&signed_market_listing_record( 5114 "buyer-order-decline-listing", 5115 seller_pubkey, 5116 farm_key, 5117 listing_key, 5118 "Buyer Order Eggs", 5119 "9", 5120 "active", 5121 "pickup", 5122 "North barn pickup", 5123 4_102_444_800, 5124 4_102_531_200, 5125 LocalRecordStatus::Published, 5126 PublishOutboxStatus::Acknowledged, 5127 )) 5128 .expect("append signed listing"); 5129 app_store 5130 .import_shared_local_events_from_store(&events) 5131 .expect("import signed listing"); 5132 let request_payload = order_request_payload( 5133 order_id_raw, 5134 listing_addr.as_str(), 5135 buyer_pubkey, 5136 seller_pubkey, 5137 ); 5138 let request_parts = order_request_event_build( 5139 &listing_event_ptr("buyer-order-decline-listing-event"), 5140 &request_payload, 5141 ) 5142 .expect("build order request event"); 5143 let request_event = event_from_parts( 5144 "buyer-order-decline-request-event", 5145 buyer_pubkey, 5146 request_parts, 5147 ); 5148 events 5149 .append_record(&signed_order_event_record( 5150 "app:signed_event:order-request:buyer-declined", 5151 &request_event, 5152 listing_addr.as_str(), 5153 SourceRuntime::App, 5154 Some("acct_buyer"), 5155 )) 5156 .expect("append app order request"); 5157 5158 let request_report = app_store 5159 .import_shared_local_events_from_store(&events) 5160 .expect("import app order request"); 5161 let buyer_context = BuyerContext::account("acct_buyer"); 5162 let order_id = projected_order_id(order_id_raw, buyer_pubkey); 5163 let buyer_orders = app_store 5164 .load_buyer_orders(&buyer_context) 5165 .expect("load buyer orders after request"); 5166 5167 assert_eq!(request_report.imported_records, 1); 5168 assert_eq!(buyer_orders.rows.len(), 1); 5169 assert_eq!(buyer_orders.rows[0].order_id, order_id); 5170 assert_eq!(buyer_orders.rows[0].status, BuyerOrderStatus::Placed); 5171 5172 let decision_payload = declined_order_decision_payload( 5173 order_id_raw, 5174 listing_addr.as_str(), 5175 buyer_pubkey, 5176 seller_pubkey, 5177 ); 5178 let decision_parts = order_decision_event_build( 5179 &typed_event_id(request_event.id.as_str()), 5180 &typed_event_id(request_event.id.as_str()), 5181 &decision_payload, 5182 ) 5183 .expect("build declined order decision event"); 5184 let decision_event = event_from_parts( 5185 "buyer-order-decline-decision-event", 5186 seller_pubkey, 5187 decision_parts, 5188 ); 5189 events 5190 .append_record(&signed_order_event_record( 5191 "cli:signed_event:order-decision:buyer-declined", 5192 &decision_event, 5193 listing_addr.as_str(), 5194 SourceRuntime::Cli, 5195 None, 5196 )) 5197 .expect("append declined order decision"); 5198 5199 let decision_report = app_store 5200 .import_shared_local_events_from_store(&events) 5201 .expect("import declined order decision"); 5202 let buyer_orders = app_store 5203 .load_buyer_orders(&buyer_context) 5204 .expect("load buyer orders after declined decision"); 5205 let buyer_detail = app_store 5206 .load_buyer_order_detail(&buyer_context, order_id) 5207 .expect("load buyer order detail") 5208 .expect("buyer order detail"); 5209 let seller_orders = app_store 5210 .load_orders_list( 5211 deterministic_farm_id(Some(seller_pubkey), farm_key), 5212 &OrdersScreenQueryState { 5213 filter: OrdersFilter::All, 5214 fulfillment_window_id: None, 5215 }, 5216 ) 5217 .expect("load seller orders after declined decision"); 5218 5219 assert_eq!(decision_report.imported_records, 1); 5220 assert_eq!(buyer_orders.rows.len(), 1); 5221 assert_eq!(buyer_orders.rows[0].status, BuyerOrderStatus::Declined); 5222 assert_eq!(buyer_detail.status, BuyerOrderStatus::Declined); 5223 assert_eq!(seller_orders.rows[0].status, OrderStatus::Declined); 5224 assert_eq!(seller_orders.summary.needs_action_orders, 0); 5225 assert_eq!(seller_orders.summary.scheduled_orders, 0); 5226 assert_eq!(seller_orders.summary.packed_orders, 0); 5227 assert!(seller_orders.rows[0].primary_action.is_none()); 5228 } 5229 5230 #[test] 5231 fn active_order_decision_projects_agreement_state_through_cli_reducer() { 5232 let app_store = 5233 AppSqliteStore::open(DatabaseTarget::InMemory).expect("open app sqlite store"); 5234 let events = local_events_store(); 5235 let farm_key = "DDDDDDDDDDDDDDDDDDDDDD"; 5236 let listing_key = "AAAAAAAAAAAAAAAAAAAAAw"; 5237 let seller_pubkey = test_pubkey("seller-pubkey"); 5238 let seller_pubkey = seller_pubkey.as_str(); 5239 let buyer_pubkey = test_pubkey("app-buyer-pubkey"); 5240 let buyer_pubkey = buyer_pubkey.as_str(); 5241 let order_id_raw = "active-lifecycle-order-1"; 5242 let listing_addr = format!("30402:{seller_pubkey}:{listing_key}"); 5243 events 5244 .append_record(&signed_market_listing_record( 5245 "active-lifecycle-listing", 5246 seller_pubkey, 5247 farm_key, 5248 listing_key, 5249 "Lifecycle Eggs", 5250 "9", 5251 "active", 5252 "pickup", 5253 "North barn pickup", 5254 4_102_444_800, 5255 4_102_531_200, 5256 LocalRecordStatus::Published, 5257 PublishOutboxStatus::Acknowledged, 5258 )) 5259 .expect("append signed listing"); 5260 app_store 5261 .import_shared_local_events_from_store(&events) 5262 .expect("import signed listing"); 5263 5264 let request_payload = order_request_payload( 5265 order_id_raw, 5266 listing_addr.as_str(), 5267 buyer_pubkey, 5268 seller_pubkey, 5269 ); 5270 let request_parts = order_request_event_build( 5271 &listing_event_ptr("active-lifecycle-listing-event"), 5272 &request_payload, 5273 ) 5274 .expect("build lifecycle order request"); 5275 let request_event = event_from_parts( 5276 "active-lifecycle-request-event", 5277 buyer_pubkey, 5278 request_parts, 5279 ); 5280 events 5281 .append_record(&signed_order_event_record( 5282 "app:signed_event:active-lifecycle:request", 5283 &request_event, 5284 listing_addr.as_str(), 5285 SourceRuntime::App, 5286 Some("acct_lifecycle"), 5287 )) 5288 .expect("append lifecycle order request"); 5289 app_store 5290 .import_shared_local_events_from_store(&events) 5291 .expect("import lifecycle order request"); 5292 5293 let seller_farm_id = deterministic_farm_id(Some(seller_pubkey), farm_key); 5294 let decision_payload = accepted_order_decision_payload( 5295 order_id_raw, 5296 listing_addr.as_str(), 5297 buyer_pubkey, 5298 seller_pubkey, 5299 ); 5300 let decision_parts = order_decision_event_build( 5301 &typed_event_id(request_event.id.as_str()), 5302 &typed_event_id(request_event.id.as_str()), 5303 &decision_payload, 5304 ) 5305 .expect("build lifecycle order decision"); 5306 let decision_event = event_from_parts( 5307 "active-lifecycle-decision-event", 5308 seller_pubkey, 5309 decision_parts, 5310 ); 5311 events 5312 .append_record(&signed_order_event_record( 5313 "cli:signed_event:active-lifecycle:decision", 5314 &decision_event, 5315 listing_addr.as_str(), 5316 SourceRuntime::Cli, 5317 None, 5318 )) 5319 .expect("append lifecycle order decision"); 5320 app_store 5321 .import_shared_local_events_from_store(&events) 5322 .expect("import lifecycle order decision"); 5323 let seller_orders = app_store 5324 .load_orders_list( 5325 seller_farm_id, 5326 &OrdersScreenQueryState { 5327 filter: OrdersFilter::All, 5328 fulfillment_window_id: None, 5329 }, 5330 ) 5331 .expect("load lifecycle seller orders after decision"); 5332 assert_eq!(seller_orders.rows[0].status, OrderStatus::Scheduled); 5333 assert_eq!( 5334 seller_orders.rows[0].workflow.inventory, 5335 TradeInventoryStatus::Reserved 5336 ); 5337 assert_eq!(seller_orders.rows[0].primary_action, None); 5338 } 5339 5340 #[test] 5341 fn validation_receipts_project_passively_on_buyer_and_seller_order_details() { 5342 let fixture = validation_receipt_order_fixture("validation-receipt-passive"); 5343 let valid_event = validation_receipt_event( 5344 hex_event_id(31).as_str(), 5345 fixture.seller_pubkey.as_str(), 5346 fixture.order_id_raw.as_str(), 5347 fixture.listing_event_id.as_str(), 5348 fixture.request_event_id.as_str(), 5349 fixture.decision_event_id.as_str(), 5350 RadrootsValidationReceiptResult::Valid, 5351 1_777_665_603, 5352 ); 5353 let invalid_event = validation_receipt_event( 5354 hex_event_id(32).as_str(), 5355 fixture.seller_pubkey.as_str(), 5356 fixture.order_id_raw.as_str(), 5357 fixture.listing_event_id.as_str(), 5358 fixture.request_event_id.as_str(), 5359 fixture.decision_event_id.as_str(), 5360 RadrootsValidationReceiptResult::Invalid, 5361 1_777_665_604, 5362 ); 5363 let duplicate_valid_event = valid_event.clone(); 5364 fixture 5365 .events 5366 .append_record(&signed_order_event_record( 5367 "cli:signed_event:validation-receipt:valid", 5368 &valid_event, 5369 fixture.listing_addr.as_str(), 5370 SourceRuntime::Cli, 5371 None, 5372 )) 5373 .expect("append valid validation receipt"); 5374 fixture 5375 .events 5376 .append_record(&signed_order_event_record( 5377 "cli:signed_event:validation-receipt:invalid", 5378 &invalid_event, 5379 fixture.listing_addr.as_str(), 5380 SourceRuntime::Cli, 5381 None, 5382 )) 5383 .expect("append invalid validation receipt"); 5384 fixture 5385 .events 5386 .append_record(&signed_order_event_record( 5387 "cli:signed_event:validation-receipt:valid-duplicate", 5388 &duplicate_valid_event, 5389 fixture.listing_addr.as_str(), 5390 SourceRuntime::Cli, 5391 None, 5392 )) 5393 .expect("append duplicate validation receipt"); 5394 fixture 5395 .app_store 5396 .import_shared_local_events_from_store(&fixture.events) 5397 .expect("import validation receipts"); 5398 5399 let buyer_detail = fixture 5400 .app_store 5401 .load_buyer_order_detail(&fixture.buyer_context, fixture.order_id) 5402 .expect("load buyer validation receipt detail") 5403 .expect("buyer validation receipt detail"); 5404 let seller_detail = fixture 5405 .app_store 5406 .load_order_detail(fixture.seller_farm_id, fixture.order_id) 5407 .expect("load seller validation receipt detail") 5408 .expect("seller validation receipt detail"); 5409 let imports = fixture 5410 .app_store 5411 .load_local_interop_records() 5412 .expect("load validation receipt imports"); 5413 5414 assert_eq!(buyer_detail.status, BuyerOrderStatus::Scheduled); 5415 assert_eq!(seller_detail.status, OrderStatus::Scheduled); 5416 assert_eq!(seller_detail.primary_action, None); 5417 assert_eq!(buyer_detail.validation_receipts.len(), 2); 5418 assert_eq!( 5419 buyer_detail.validation_receipts, 5420 seller_detail.validation_receipts 5421 ); 5422 assert_eq!( 5423 buyer_detail.validation_receipts[0].event_id, 5424 invalid_event.id 5425 ); 5426 assert_eq!( 5427 buyer_detail.validation_receipts[0].result, 5428 TradeValidationReceiptResult::NeedsReview 5429 ); 5430 assert_eq!( 5431 buyer_detail.validation_receipts[0].receipt_type, 5432 TradeValidationReceiptType::TradeTransition 5433 ); 5434 assert_eq!( 5435 buyer_detail.validation_receipts[0].proof_system, 5436 TradeValidationReceiptProofSystem::None 5437 ); 5438 assert_eq!( 5439 buyer_detail.validation_receipts[0].target_event_id, 5440 fixture.decision_event_id 5441 ); 5442 assert_eq!( 5443 buyer_detail.validation_receipts[0].event_set_root, 5444 hash32(42) 5445 ); 5446 assert_eq!( 5447 buyer_detail.validation_receipts[0].reducer_output_root, 5448 hash32(43) 5449 ); 5450 assert_eq!( 5451 buyer_detail.validation_receipts[0].public_values_hash, 5452 hash32(45) 5453 ); 5454 assert_eq!( 5455 buyer_detail.validation_receipts[0].recorded_at, 5456 1_777_665_604 5457 ); 5458 assert_eq!(buyer_detail.validation_receipts[1].event_id, valid_event.id); 5459 assert_eq!( 5460 buyer_detail.validation_receipts[1].result, 5461 TradeValidationReceiptResult::Valid 5462 ); 5463 assert!( 5464 imports 5465 .iter() 5466 .any(|record| record.projected_kind == "validation_receipt" 5467 && record.event_kind == Some(KIND_VALIDATION_RECEIPT) 5468 && record.event_id.as_deref() == Some(invalid_event.id.as_str())) 5469 ); 5470 } 5471 5472 #[test] 5473 fn validation_receipt_import_before_order_request_attaches_when_request_arrives() { 5474 let app_store = 5475 AppSqliteStore::open(DatabaseTarget::InMemory).expect("open app sqlite store"); 5476 let events = local_events_store(); 5477 let farm_key = "DDDDDDDDDDDDDDDDDDDDDD"; 5478 let listing_key = "AAAAAAAAAAAAAAAAAAAAAw"; 5479 let seller_pubkey = test_pubkey("validation-out-of-order-seller"); 5480 let seller_pubkey = seller_pubkey.as_str(); 5481 let buyer_pubkey = test_pubkey("validation-out-of-order-buyer"); 5482 let buyer_pubkey = buyer_pubkey.as_str(); 5483 let order_id_raw = "validation-out-of-order"; 5484 let listing_addr = format!("30402:{seller_pubkey}:{listing_key}"); 5485 let listing_event_id = hex_event_id(51); 5486 let request_event_id = hex_event_id(52); 5487 let target_event_id = hex_event_id(53); 5488 events 5489 .append_record(&signed_market_listing_record( 5490 "validation-out-of-order-listing", 5491 seller_pubkey, 5492 farm_key, 5493 listing_key, 5494 "Validation Eggs", 5495 "9", 5496 "active", 5497 "pickup", 5498 "North barn pickup", 5499 4_102_444_800, 5500 4_102_531_200, 5501 LocalRecordStatus::Published, 5502 PublishOutboxStatus::Acknowledged, 5503 )) 5504 .expect("append out-of-order listing"); 5505 app_store 5506 .import_shared_local_events_from_store(&events) 5507 .expect("import out-of-order listing"); 5508 5509 let receipt_event = validation_receipt_event( 5510 hex_event_id(54).as_str(), 5511 seller_pubkey, 5512 order_id_raw, 5513 listing_event_id.as_str(), 5514 request_event_id.as_str(), 5515 target_event_id.as_str(), 5516 RadrootsValidationReceiptResult::Valid, 5517 1_777_665_603, 5518 ); 5519 events 5520 .append_record(&signed_order_event_record( 5521 "cli:signed_event:validation-receipt:before-request", 5522 &receipt_event, 5523 listing_addr.as_str(), 5524 SourceRuntime::Cli, 5525 None, 5526 )) 5527 .expect("append receipt before request"); 5528 app_store 5529 .import_shared_local_events_from_store(&events) 5530 .expect("import receipt before request"); 5531 5532 let pending_count: i64 = app_store 5533 .connection() 5534 .query_row( 5535 "SELECT count(*) FROM order_validation_receipts WHERE order_id IS NULL", 5536 [], 5537 |row| row.get(0), 5538 ) 5539 .expect("count pending validation receipts"); 5540 assert_eq!(pending_count, 1); 5541 5542 let request_payload = order_request_payload( 5543 order_id_raw, 5544 listing_addr.as_str(), 5545 buyer_pubkey, 5546 seller_pubkey, 5547 ); 5548 let request_parts = order_request_event_build( 5549 &listing_event_ptr(listing_event_id.as_str()), 5550 &request_payload, 5551 ) 5552 .expect("build request after validation receipt"); 5553 let request_event = event_from_parts_at( 5554 request_event_id.as_str(), 5555 buyer_pubkey, 5556 request_parts, 5557 1_777_665_604, 5558 ); 5559 events 5560 .append_record(&signed_order_event_record( 5561 "app:signed_event:validation-receipt:request-after", 5562 &request_event, 5563 listing_addr.as_str(), 5564 SourceRuntime::App, 5565 Some("acct_validation_out_of_order"), 5566 )) 5567 .expect("append request after receipt"); 5568 app_store 5569 .import_shared_local_events_from_store(&events) 5570 .expect("import request after receipt"); 5571 5572 let order_id = projected_order_id(order_id_raw, buyer_pubkey); 5573 let buyer_context = BuyerContext::account("acct_validation_out_of_order"); 5574 let buyer_detail = app_store 5575 .load_buyer_order_detail(&buyer_context, order_id) 5576 .expect("load attached buyer validation receipt detail") 5577 .expect("attached buyer validation receipt detail"); 5578 let seller_detail = app_store 5579 .load_order_detail( 5580 deterministic_farm_id(Some(seller_pubkey), farm_key), 5581 order_id, 5582 ) 5583 .expect("load attached seller validation receipt detail") 5584 .expect("attached seller validation receipt detail"); 5585 5586 assert_eq!(buyer_detail.validation_receipts.len(), 1); 5587 assert_eq!( 5588 buyer_detail.validation_receipts, 5589 seller_detail.validation_receipts 5590 ); 5591 assert_eq!( 5592 buyer_detail.validation_receipts[0].event_id, 5593 receipt_event.id 5594 ); 5595 assert_eq!(buyer_detail.status, BuyerOrderStatus::Placed); 5596 assert_eq!(seller_detail.status, OrderStatus::NeedsAction); 5597 } 5598 5599 #[test] 5600 fn validation_receipt_invalid_candidates_do_not_surface_as_order_evidence() { 5601 let fixture = validation_receipt_order_fixture("validation-receipt-invalid-candidates"); 5602 let mut mismatched_tag_event = validation_receipt_event( 5603 hex_event_id(61).as_str(), 5604 fixture.seller_pubkey.as_str(), 5605 fixture.order_id_raw.as_str(), 5606 fixture.listing_event_id.as_str(), 5607 fixture.request_event_id.as_str(), 5608 fixture.decision_event_id.as_str(), 5609 RadrootsValidationReceiptResult::Valid, 5610 1_777_665_603, 5611 ); 5612 if let Some(tag) = mismatched_tag_event 5613 .tags 5614 .iter_mut() 5615 .find(|tag| tag.first().map(String::as_str) == Some("event_set_root")) 5616 { 5617 tag[1] = hash32(99); 5618 } 5619 let wrong_order_event = validation_receipt_event( 5620 hex_event_id(62).as_str(), 5621 fixture.seller_pubkey.as_str(), 5622 "wrong-order-id", 5623 fixture.listing_event_id.as_str(), 5624 fixture.request_event_id.as_str(), 5625 fixture.decision_event_id.as_str(), 5626 RadrootsValidationReceiptResult::Valid, 5627 1_777_665_604, 5628 ); 5629 let mut buyer_kind_candidate = validation_receipt_event( 5630 hex_event_id(63).as_str(), 5631 fixture.buyer_pubkey.as_str(), 5632 fixture.order_id_raw.as_str(), 5633 fixture.listing_event_id.as_str(), 5634 fixture.request_event_id.as_str(), 5635 fixture.decision_event_id.as_str(), 5636 RadrootsValidationReceiptResult::Valid, 5637 1_777_665_605, 5638 ); 5639 buyer_kind_candidate.kind = KIND_ORDER_REQUEST as u32; 5640 5641 for (record_id, event) in [ 5642 ( 5643 "cli:signed_event:validation-receipt:mismatched-tag", 5644 &mismatched_tag_event, 5645 ), 5646 ( 5647 "cli:signed_event:validation-receipt:wrong-order", 5648 &wrong_order_event, 5649 ), 5650 ( 5651 "cli:signed_event:validation-receipt:buyer-kind", 5652 &buyer_kind_candidate, 5653 ), 5654 ] { 5655 fixture 5656 .events 5657 .append_record(&signed_order_event_record( 5658 record_id, 5659 event, 5660 fixture.listing_addr.as_str(), 5661 SourceRuntime::Cli, 5662 None, 5663 )) 5664 .expect("append invalid validation receipt candidate"); 5665 } 5666 fixture 5667 .app_store 5668 .import_shared_local_events_from_store(&fixture.events) 5669 .expect("import invalid validation receipt candidates"); 5670 5671 let buyer_detail = fixture 5672 .app_store 5673 .load_buyer_order_detail(&fixture.buyer_context, fixture.order_id) 5674 .expect("load buyer detail after invalid validation receipt candidates") 5675 .expect("buyer detail after invalid validation receipt candidates"); 5676 let seller_detail = fixture 5677 .app_store 5678 .load_order_detail(fixture.seller_farm_id, fixture.order_id) 5679 .expect("load seller detail after invalid validation receipt candidates") 5680 .expect("seller detail after invalid validation receipt candidates"); 5681 5682 assert!(buyer_detail.validation_receipts.is_empty()); 5683 assert!(seller_detail.validation_receipts.is_empty()); 5684 assert_eq!(buyer_detail.status, BuyerOrderStatus::Scheduled); 5685 assert_eq!(seller_detail.status, OrderStatus::Scheduled); 5686 assert_eq!(seller_detail.primary_action, None); 5687 } 5688 5689 #[test] 5690 fn active_order_revision_projects_through_cli_reducer_state() { 5691 let app_store = 5692 AppSqliteStore::open(DatabaseTarget::InMemory).expect("open app sqlite store"); 5693 let events = local_events_store(); 5694 let farm_key = "EEEEEEEEEEEEEEEEEEEEEE"; 5695 let listing_key = "AAAAAAAAAAAAAAAAAAAAAw"; 5696 let seller_pubkey = test_pubkey("seller-pubkey"); 5697 let seller_pubkey = seller_pubkey.as_str(); 5698 let buyer_pubkey = test_pubkey("app-buyer-pubkey"); 5699 let buyer_pubkey = buyer_pubkey.as_str(); 5700 let order_id_raw = "active-revision-order-1"; 5701 let listing_addr = format!("30402:{seller_pubkey}:{listing_key}"); 5702 events 5703 .append_record(&signed_market_listing_record( 5704 "active-revision-listing", 5705 seller_pubkey, 5706 farm_key, 5707 listing_key, 5708 "Revision Eggs", 5709 "9", 5710 "active", 5711 "pickup", 5712 "North barn pickup", 5713 4_102_444_800, 5714 4_102_531_200, 5715 LocalRecordStatus::Published, 5716 PublishOutboxStatus::Acknowledged, 5717 )) 5718 .expect("append revision listing"); 5719 app_store 5720 .import_shared_local_events_from_store(&events) 5721 .expect("import revision listing"); 5722 5723 let request_payload = order_request_payload( 5724 order_id_raw, 5725 listing_addr.as_str(), 5726 buyer_pubkey, 5727 seller_pubkey, 5728 ); 5729 let request_parts = order_request_event_build( 5730 &listing_event_ptr("active-revision-listing-event"), 5731 &request_payload, 5732 ) 5733 .expect("build revision order request"); 5734 let request_event = 5735 event_from_parts("active-revision-request-event", buyer_pubkey, request_parts); 5736 events 5737 .append_record(&signed_order_event_record( 5738 "app:signed_event:active-revision:request", 5739 &request_event, 5740 listing_addr.as_str(), 5741 SourceRuntime::App, 5742 Some("acct_revision"), 5743 )) 5744 .expect("append revision order request"); 5745 app_store 5746 .import_shared_local_events_from_store(&events) 5747 .expect("import revision order request"); 5748 5749 let proposal_payload = revision_proposal_payload( 5750 "revision-1", 5751 order_id_raw, 5752 listing_addr.as_str(), 5753 buyer_pubkey, 5754 seller_pubkey, 5755 request_event.id.as_str(), 5756 request_event.id.as_str(), 5757 ); 5758 let proposal_parts = order_revision_proposal_event_build( 5759 &typed_event_id(request_event.id.as_str()), 5760 &typed_event_id(request_event.id.as_str()), 5761 &proposal_payload, 5762 ) 5763 .expect("build revision proposal"); 5764 let proposal_event = event_from_parts( 5765 "active-revision-proposal-event", 5766 seller_pubkey, 5767 proposal_parts, 5768 ); 5769 events 5770 .append_record(&signed_order_event_record( 5771 "cli:signed_event:active-revision:proposal", 5772 &proposal_event, 5773 listing_addr.as_str(), 5774 SourceRuntime::Cli, 5775 None, 5776 )) 5777 .expect("append revision proposal"); 5778 app_store 5779 .import_shared_local_events_from_store(&events) 5780 .expect("import revision proposal"); 5781 5782 let seller_farm_id = deterministic_farm_id(Some(seller_pubkey), farm_key); 5783 let order_id = projected_order_id(order_id_raw, buyer_pubkey); 5784 let buyer_context = BuyerContext::account("acct_revision"); 5785 let seller_orders = app_store 5786 .load_orders_list( 5787 seller_farm_id, 5788 &OrdersScreenQueryState { 5789 filter: OrdersFilter::All, 5790 fulfillment_window_id: None, 5791 }, 5792 ) 5793 .expect("load revision seller orders after proposal"); 5794 assert_eq!(seller_orders.rows[0].status, OrderStatus::NeedsAction); 5795 assert_eq!( 5796 seller_orders.rows[0].workflow.revision, 5797 TradeRevisionStatus::ChangeProposed 5798 ); 5799 let buyer_detail = app_store 5800 .load_buyer_order_detail(&buyer_context, order_id) 5801 .expect("load revision buyer detail after proposal") 5802 .expect("revision buyer detail after proposal"); 5803 assert_eq!( 5804 buyer_detail.workflow.revision, 5805 TradeRevisionStatus::ChangeProposed 5806 ); 5807 assert_eq!(buyer_detail.economics.total_minor_units, Some(1600)); 5808 5809 let revision_decision_payload = revision_decision_payload( 5810 "revision-1", 5811 order_id_raw, 5812 listing_addr.as_str(), 5813 buyer_pubkey, 5814 seller_pubkey, 5815 request_event.id.as_str(), 5816 proposal_event.id.as_str(), 5817 RadrootsOrderRevisionOutcome::Accepted, 5818 ); 5819 let revision_decision_parts = order_revision_decision_event_build( 5820 &typed_event_id(request_event.id.as_str()), 5821 &typed_event_id(proposal_event.id.as_str()), 5822 &revision_decision_payload, 5823 ) 5824 .expect("build revision decision"); 5825 let revision_decision_event = event_from_parts( 5826 "active-revision-decision-response-event", 5827 buyer_pubkey, 5828 revision_decision_parts, 5829 ); 5830 events 5831 .append_record(&signed_order_event_record( 5832 "app:signed_event:active-revision:decision-response", 5833 &revision_decision_event, 5834 listing_addr.as_str(), 5835 SourceRuntime::App, 5836 Some("acct_revision"), 5837 )) 5838 .expect("append revision decision"); 5839 app_store 5840 .import_shared_local_events_from_store(&events) 5841 .expect("import revision decision"); 5842 let seller_orders = app_store 5843 .load_orders_list( 5844 seller_farm_id, 5845 &OrdersScreenQueryState { 5846 filter: OrdersFilter::All, 5847 fulfillment_window_id: None, 5848 }, 5849 ) 5850 .expect("load revision seller orders after decision"); 5851 assert_eq!(seller_orders.rows[0].status, OrderStatus::Scheduled); 5852 assert_eq!( 5853 seller_orders.rows[0].workflow.revision, 5854 TradeRevisionStatus::Updated 5855 ); 5856 let seller_detail = app_store 5857 .load_order_detail(seller_farm_id, order_id) 5858 .expect("load revision seller detail after decision") 5859 .expect("revision seller detail after decision"); 5860 let buyer_detail = app_store 5861 .load_buyer_order_detail(&buyer_context, order_id) 5862 .expect("load revision buyer detail after decision") 5863 .expect("revision buyer detail after decision"); 5864 assert_eq!( 5865 seller_detail.workflow.revision, 5866 TradeRevisionStatus::Updated 5867 ); 5868 assert_eq!(seller_detail.economics.total_minor_units, Some(2400)); 5869 assert_eq!(buyer_detail.workflow.revision, TradeRevisionStatus::Updated); 5870 assert_eq!(buyer_detail.economics.total_minor_units, Some(2400)); 5871 } 5872 5873 #[test] 5874 fn active_order_pre_agreement_cancellation_projects_through_cli_reducer_state() { 5875 let app_store = 5876 AppSqliteStore::open(DatabaseTarget::InMemory).expect("open app sqlite store"); 5877 let events = local_events_store(); 5878 let farm_key = "EEEEEEEEEEEEEEEEEEEEEE"; 5879 let listing_key = "AAAAAAAAAAAAAAAAAAAAAx"; 5880 let seller_pubkey = test_pubkey("seller-pubkey"); 5881 let seller_pubkey = seller_pubkey.as_str(); 5882 let buyer_pubkey = test_pubkey("app-buyer-pubkey"); 5883 let buyer_pubkey = buyer_pubkey.as_str(); 5884 let order_id_raw = "active-cancel-order-1"; 5885 let listing_addr = format!("30402:{seller_pubkey}:{listing_key}"); 5886 events 5887 .append_record(&signed_market_listing_record( 5888 "active-cancel-listing", 5889 seller_pubkey, 5890 farm_key, 5891 listing_key, 5892 "Cancellation Eggs", 5893 "9", 5894 "active", 5895 "pickup", 5896 "North barn pickup", 5897 4_102_444_800, 5898 4_102_531_200, 5899 LocalRecordStatus::Published, 5900 PublishOutboxStatus::Acknowledged, 5901 )) 5902 .expect("append cancellation listing"); 5903 app_store 5904 .import_shared_local_events_from_store(&events) 5905 .expect("import cancellation listing"); 5906 5907 let request_payload = order_request_payload( 5908 order_id_raw, 5909 listing_addr.as_str(), 5910 buyer_pubkey, 5911 seller_pubkey, 5912 ); 5913 let request_parts = order_request_event_build( 5914 &listing_event_ptr("active-cancel-listing-event"), 5915 &request_payload, 5916 ) 5917 .expect("build cancellation order request"); 5918 let request_event = 5919 event_from_parts("active-cancel-request-event", buyer_pubkey, request_parts); 5920 events 5921 .append_record(&signed_order_event_record( 5922 "app:signed_event:active-cancel:request", 5923 &request_event, 5924 listing_addr.as_str(), 5925 SourceRuntime::App, 5926 Some("acct_cancel"), 5927 )) 5928 .expect("append cancellation order request"); 5929 app_store 5930 .import_shared_local_events_from_store(&events) 5931 .expect("import cancellation order request"); 5932 5933 let cancel_payload = order_cancel_payload( 5934 order_id_raw, 5935 listing_addr.as_str(), 5936 buyer_pubkey, 5937 seller_pubkey, 5938 ); 5939 let cancel_parts = order_cancellation_event_build( 5940 &typed_event_id(request_event.id.as_str()), 5941 &typed_event_id(request_event.id.as_str()), 5942 &cancel_payload, 5943 ) 5944 .expect("build cancellation"); 5945 let cancel_event = event_from_parts("active-cancel-event", buyer_pubkey, cancel_parts); 5946 events 5947 .append_record(&signed_order_event_record( 5948 "app:signed_event:active-cancel:cancel", 5949 &cancel_event, 5950 listing_addr.as_str(), 5951 SourceRuntime::App, 5952 Some("acct_cancel"), 5953 )) 5954 .expect("append cancellation"); 5955 let cancel_report = app_store 5956 .import_shared_local_events_from_store(&events) 5957 .expect("import cancellation"); 5958 5959 let seller_farm_id = deterministic_farm_id(Some(seller_pubkey), farm_key); 5960 let order_id = projected_order_id(order_id_raw, buyer_pubkey); 5961 let buyer_context = BuyerContext::account("acct_cancel"); 5962 let buyer_detail = app_store 5963 .load_buyer_order_detail(&buyer_context, order_id) 5964 .expect("load cancellation buyer detail") 5965 .expect("cancellation buyer detail"); 5966 let seller_orders = app_store 5967 .load_orders_list( 5968 seller_farm_id, 5969 &OrdersScreenQueryState { 5970 filter: OrdersFilter::All, 5971 fulfillment_window_id: None, 5972 }, 5973 ) 5974 .expect("load cancellation seller orders"); 5975 5976 assert_eq!(cancel_report.imported_records, 1); 5977 assert_eq!(buyer_detail.status, BuyerOrderStatus::Declined); 5978 assert_eq!( 5979 buyer_detail.workflow.agreement, 5980 TradeAgreementStatus::Cancelled 5981 ); 5982 assert_eq!(seller_orders.rows[0].status, OrderStatus::Declined); 5983 assert_eq!( 5984 seller_orders.rows[0].workflow.agreement, 5985 TradeAgreementStatus::Cancelled 5986 ); 5987 assert_eq!(seller_orders.rows[0].primary_action, None); 5988 } 5989 5990 #[test] 5991 fn conflicting_order_decisions_project_to_needs_action_deterministically() { 5992 let run_case = |accepted_first: bool| { 5993 let app_store = 5994 AppSqliteStore::open(DatabaseTarget::InMemory).expect("open app sqlite store"); 5995 let events = local_events_store(); 5996 let farm_key = "FFFFFFFFFFFFFFFFFFFFFF"; 5997 let listing_key = "AAAAAAAAAAAAAAAAAAAAAw"; 5998 let seller_pubkey = test_pubkey("seller-pubkey"); 5999 let seller_pubkey = seller_pubkey.as_str(); 6000 let buyer_pubkey = test_pubkey("app-buyer-pubkey"); 6001 let buyer_pubkey = buyer_pubkey.as_str(); 6002 let order_id_raw = if accepted_first { 6003 "active-conflict-order-accepted-first" 6004 } else { 6005 "active-conflict-order-declined-first" 6006 }; 6007 let listing_addr = format!("30402:{seller_pubkey}:{listing_key}"); 6008 events 6009 .append_record(&signed_market_listing_record( 6010 "active-conflict-listing", 6011 seller_pubkey, 6012 farm_key, 6013 listing_key, 6014 "Conflict Eggs", 6015 "9", 6016 "active", 6017 "pickup", 6018 "North barn pickup", 6019 4_102_444_800, 6020 4_102_531_200, 6021 LocalRecordStatus::Published, 6022 PublishOutboxStatus::Acknowledged, 6023 )) 6024 .expect("append conflict listing"); 6025 let request_payload = order_request_payload( 6026 order_id_raw, 6027 listing_addr.as_str(), 6028 buyer_pubkey, 6029 seller_pubkey, 6030 ); 6031 let request_parts = order_request_event_build( 6032 &listing_event_ptr("active-conflict-listing-event"), 6033 &request_payload, 6034 ) 6035 .expect("build conflict request"); 6036 let request_event = 6037 event_from_parts("active-conflict-request-event", buyer_pubkey, request_parts); 6038 events 6039 .append_record(&signed_order_event_record( 6040 "app:signed_event:active-conflict:request", 6041 &request_event, 6042 listing_addr.as_str(), 6043 SourceRuntime::App, 6044 Some("acct_conflict"), 6045 )) 6046 .expect("append conflict request"); 6047 let accepted_payload = accepted_order_decision_payload( 6048 order_id_raw, 6049 listing_addr.as_str(), 6050 buyer_pubkey, 6051 seller_pubkey, 6052 ); 6053 let accepted_parts = order_decision_event_build( 6054 &typed_event_id(request_event.id.as_str()), 6055 &typed_event_id(request_event.id.as_str()), 6056 &accepted_payload, 6057 ) 6058 .expect("build accepted conflict decision"); 6059 let accepted_event = event_from_parts( 6060 "active-conflict-accepted-event", 6061 seller_pubkey, 6062 accepted_parts, 6063 ); 6064 let declined_payload = declined_order_decision_payload( 6065 order_id_raw, 6066 listing_addr.as_str(), 6067 buyer_pubkey, 6068 seller_pubkey, 6069 ); 6070 let declined_parts = order_decision_event_build( 6071 &typed_event_id(request_event.id.as_str()), 6072 &typed_event_id(request_event.id.as_str()), 6073 &declined_payload, 6074 ) 6075 .expect("build declined conflict decision"); 6076 let declined_event = event_from_parts( 6077 "active-conflict-declined-event", 6078 seller_pubkey, 6079 declined_parts, 6080 ); 6081 let ordered_events = if accepted_first { 6082 [accepted_event, declined_event] 6083 } else { 6084 [declined_event, accepted_event] 6085 }; 6086 for (index, event) in ordered_events.iter().enumerate() { 6087 events 6088 .append_record(&signed_order_event_record( 6089 &format!("cli:signed_event:active-conflict:decision:{index}"), 6090 event, 6091 listing_addr.as_str(), 6092 SourceRuntime::Cli, 6093 None, 6094 )) 6095 .expect("append conflict decision"); 6096 } 6097 6098 app_store 6099 .import_shared_local_events_from_store(&events) 6100 .expect("import conflicting decisions"); 6101 let order_id = projected_order_id(order_id_raw, buyer_pubkey); 6102 let detail = app_store 6103 .load_order_detail( 6104 deterministic_farm_id(Some(seller_pubkey), farm_key), 6105 order_id, 6106 ) 6107 .expect("load conflict order detail") 6108 .expect("conflict order detail"); 6109 detail.status 6110 }; 6111 6112 assert_eq!(run_case(true), OrderStatus::NeedsAction); 6113 assert_eq!(run_case(false), OrderStatus::NeedsAction); 6114 } 6115 6116 #[test] 6117 fn malformed_order_event_remains_signed_event_evidence_without_projection() { 6118 let app_store = 6119 AppSqliteStore::open(DatabaseTarget::InMemory).expect("open app sqlite store"); 6120 let events = local_events_store(); 6121 events 6122 .append_record(&LocalEventRecordInput { 6123 record_id: "cli:signed_event:order-request:malformed".to_owned(), 6124 family: LocalRecordFamily::SignedEvent, 6125 status: LocalRecordStatus::Published, 6126 source_runtime: SourceRuntime::Cli, 6127 created_at_ms: 1100, 6128 inserted_at_ms: 1101, 6129 owner_account_id: None, 6130 owner_pubkey: Some("buyer-pubkey".to_owned()), 6131 farm_id: None, 6132 listing_addr: Some("30402:seller-pubkey:listing-key".to_owned()), 6133 local_work_json: None, 6134 event_id: Some("malformed-order-event".to_owned()), 6135 event_kind: Some(KIND_ORDER_REQUEST), 6136 event_pubkey: Some("buyer-pubkey".to_owned()), 6137 event_created_at: Some(1100), 6138 event_tags_json: Some(json!([["d", "bad-order"]])), 6139 event_content: Some("not-json".to_owned()), 6140 event_sig: Some("signature".to_owned()), 6141 raw_event_json: Some(json!({ 6142 "id": "malformed-order-event", 6143 "kind": KIND_ORDER_REQUEST, 6144 "pubkey": "buyer-pubkey", 6145 "content": "not-json" 6146 })), 6147 outbox_status: PublishOutboxStatus::Acknowledged, 6148 relay_set_fingerprint: Some("relay-set".to_owned()), 6149 relay_delivery_json: Some(json!({ 6150 "state": "acknowledged", 6151 "acknowledged_relays": ["ws://127.0.0.1:1234/"] 6152 })), 6153 }) 6154 .expect("append malformed order event"); 6155 6156 let report = app_store 6157 .import_shared_local_events_from_store(&events) 6158 .expect("import malformed order event"); 6159 let imported = app_store 6160 .load_local_interop_records() 6161 .expect("load imported records"); 6162 let order_count: i64 = app_store 6163 .connection() 6164 .query_row("SELECT COUNT(*) FROM orders", [], |row| row.get(0)) 6165 .expect("load order count"); 6166 6167 assert_eq!(report.imported_records, 1); 6168 assert_eq!(report.skipped_records, 0); 6169 assert_eq!(imported.len(), 1); 6170 assert_eq!(imported[0].projected_kind, "signed_event"); 6171 assert_eq!( 6172 imported[0].event_id.as_deref(), 6173 Some("malformed-order-event") 6174 ); 6175 assert_eq!(order_count, 0); 6176 } 6177 6178 #[test] 6179 fn imports_cli_local_work_into_app_farm_and_product_projection() { 6180 let app_store = 6181 AppSqliteStore::open(DatabaseTarget::InMemory).expect("open app sqlite store"); 6182 let events = local_events_store(); 6183 let farm_key = "AAAAAAAAAAAAAAAAAAAAAA"; 6184 let listing_key = "BBBBBBBBBBBBBBBBBBBBBB"; 6185 events 6186 .append_record(&local_work_record( 6187 "cli:local_work:farm", 6188 farm_key, 6189 json!({ 6190 "record_kind": "farm_config_v1", 6191 "document": { 6192 "selection": { 6193 "account": "seller-account", 6194 "farm_d_tag": farm_key 6195 }, 6196 "profile": { 6197 "name": "Green Farm", 6198 "display_name": "Green Farm" 6199 }, 6200 "farm": { 6201 "d_tag": farm_key, 6202 "name": "Green Farm", 6203 "location": { 6204 "primary": "farmstand" 6205 } 6206 }, 6207 "listing_defaults": { 6208 "delivery_method": "pickup", 6209 "location": { 6210 "primary": "farmstand" 6211 } 6212 } 6213 } 6214 }), 6215 )) 6216 .expect("append farm local work"); 6217 let mut listing = local_work_record( 6218 "cli:local_work:listing", 6219 farm_key, 6220 json!({ 6221 "record_kind": "listing_draft_v1", 6222 "document": { 6223 "listing": { 6224 "d_tag": listing_key, 6225 "farm_d_tag": farm_key 6226 }, 6227 "seller_actor": { 6228 "account_id": "seller-account", 6229 "pubkey": "seller-pubkey" 6230 }, 6231 "product": { 6232 "key": "eggs", 6233 "title": "Eggs", 6234 "summary": "Fresh eggs" 6235 }, 6236 "primary_bin": { 6237 "quantity_unit": "each", 6238 "price_amount": "6", 6239 "price_currency": "USD" 6240 }, 6241 "inventory": { 6242 "available": "10" 6243 } 6244 } 6245 }), 6246 ); 6247 listing.listing_addr = Some(format!("30402:seller-pubkey:{listing_key}")); 6248 events 6249 .append_record(&listing) 6250 .expect("append listing local work"); 6251 6252 let report = app_store 6253 .import_shared_local_events_from_store(&events) 6254 .expect("import shared local events"); 6255 let second_report = app_store 6256 .import_shared_local_events_from_store(&events) 6257 .expect("import shared local events again"); 6258 6259 assert_eq!(report.scanned_records, 2); 6260 assert_eq!(report.imported_records, 2); 6261 assert!(report.last_change_seq.is_some()); 6262 assert_eq!(second_report.scanned_records, 0); 6263 assert_eq!(second_report.imported_records, 0); 6264 assert_eq!(second_report.skipped_records, 0); 6265 assert_eq!(second_report.self_observed_records, 0); 6266 assert!( 6267 events 6268 .get_cursor("radroots_app_sqlite_projection_v1") 6269 .expect("read shared cursor") 6270 .is_none() 6271 ); 6272 let imported = app_store 6273 .load_local_interop_records() 6274 .expect("load imported records"); 6275 assert_eq!(imported.len(), 2); 6276 assert!( 6277 imported 6278 .iter() 6279 .all(|record| record.local_status == "local_saved") 6280 ); 6281 let farm_setup = app_store 6282 .load_farm_setup("seller-account") 6283 .expect("load farm setup"); 6284 let saved_farm = farm_setup.saved_farm.expect("saved farm"); 6285 assert_eq!(saved_farm.display_name, "Green Farm"); 6286 assert_eq!(farm_setup.draft.farm_name, "Green Farm"); 6287 let products = app_store 6288 .load_products( 6289 saved_farm.farm_id, 6290 "", 6291 Default::default(), 6292 Default::default(), 6293 ) 6294 .expect("load products"); 6295 assert_eq!(products.rows.len(), 1); 6296 assert_eq!(products.rows[0].title, "Eggs"); 6297 assert_eq!(products.rows[0].subtitle.as_deref(), Some("Fresh eggs")); 6298 assert_eq!( 6299 products.rows[0] 6300 .price 6301 .as_ref() 6302 .expect("price") 6303 .amount_minor_units, 6304 600 6305 ); 6306 assert_eq!(products.rows[0].stock.quantity, Some(10)); 6307 assert_eq!( 6308 products.rows[0].status, 6309 radroots_app_view::ProductStatus::Draft 6310 ); 6311 } 6312 6313 #[test] 6314 fn fresh_app_store_replays_existing_shared_records_after_another_app_imported_them() { 6315 let events = local_events_store(); 6316 let farm_key = "AAAAAAAAAAAAAAAAAAAAAA"; 6317 events 6318 .append_record(&local_work_record( 6319 "cli:local_work:farm", 6320 farm_key, 6321 json!({ 6322 "record_kind": "farm_config_v1", 6323 "document": { 6324 "selection": { 6325 "account": "seller-account", 6326 "farm_d_tag": farm_key 6327 }, 6328 "profile": { 6329 "name": "Green Farm", 6330 "display_name": "Green Farm" 6331 }, 6332 "farm": { 6333 "d_tag": farm_key, 6334 "name": "Green Farm", 6335 "location": { 6336 "primary": "farmstand" 6337 } 6338 } 6339 } 6340 }), 6341 )) 6342 .expect("append farm local work"); 6343 let first_store = 6344 AppSqliteStore::open(DatabaseTarget::InMemory).expect("open first app sqlite store"); 6345 let first_report = first_store 6346 .import_shared_local_events_from_store(&events) 6347 .expect("first app imports shared local events"); 6348 let second_same_store_report = first_store 6349 .import_shared_local_events_from_store(&events) 6350 .expect("first app imports unchanged shared local events"); 6351 let second_store = 6352 AppSqliteStore::open(DatabaseTarget::InMemory).expect("open second app sqlite store"); 6353 let fresh_store_report = second_store 6354 .import_shared_local_events_from_store(&events) 6355 .expect("fresh app imports shared local events"); 6356 6357 assert_eq!(first_report.scanned_records, 1); 6358 assert_eq!(first_report.imported_records, 1); 6359 assert_eq!(second_same_store_report.scanned_records, 0); 6360 assert_eq!(second_same_store_report.imported_records, 0); 6361 assert_eq!(fresh_store_report.scanned_records, 1); 6362 assert_eq!(fresh_store_report.imported_records, 1); 6363 assert!( 6364 events 6365 .get_cursor("radroots_app_sqlite_projection_v1") 6366 .expect("read shared cursor") 6367 .is_none() 6368 ); 6369 } 6370 6371 #[test] 6372 fn imports_signed_listing_tags_into_existing_local_product_projection() { 6373 let app_store = 6374 AppSqliteStore::open(DatabaseTarget::InMemory).expect("open app sqlite store"); 6375 let events = local_events_store(); 6376 let farm_key = "AAAAAAAAAAAAAAAAAAAAAA"; 6377 let listing_key = "BBBBBBBBBBBBBBBBBBBBBB"; 6378 events 6379 .append_record(&local_work_record( 6380 "cli:local_work:farm", 6381 farm_key, 6382 json!({ 6383 "record_kind": "farm_config_v1", 6384 "document": { 6385 "selection": { 6386 "account": "seller-account", 6387 "farm_d_tag": farm_key 6388 }, 6389 "profile": { 6390 "name": "Green Farm" 6391 }, 6392 "farm": { 6393 "d_tag": farm_key, 6394 "name": "Green Farm", 6395 "location": { 6396 "primary": "farmstand" 6397 } 6398 } 6399 } 6400 }), 6401 )) 6402 .expect("append farm local work"); 6403 let mut listing = local_work_record( 6404 "cli:local_work:listing", 6405 farm_key, 6406 json!({ 6407 "record_kind": "listing_draft_v1", 6408 "document": { 6409 "listing": { 6410 "d_tag": listing_key, 6411 "farm_d_tag": farm_key 6412 }, 6413 "seller_actor": { 6414 "account_id": "seller-account", 6415 "pubkey": "seller-pubkey" 6416 }, 6417 "product": { 6418 "key": "eggs", 6419 "title": "Eggs", 6420 "summary": "Fresh eggs" 6421 }, 6422 "primary_bin": { 6423 "quantity_unit": "each", 6424 "price_amount": "6", 6425 "price_currency": "USD" 6426 }, 6427 "inventory": { 6428 "available": "10" 6429 } 6430 } 6431 }), 6432 ); 6433 listing.listing_addr = Some(format!("30402:seller-pubkey:{listing_key}")); 6434 events 6435 .append_record(&listing) 6436 .expect("append listing local work"); 6437 app_store 6438 .import_shared_local_events_from_store(&events) 6439 .expect("import local work records"); 6440 events 6441 .append_record(&LocalEventRecordInput { 6442 record_id: "cli:signed_event:listing:event-1".to_owned(), 6443 family: LocalRecordFamily::SignedEvent, 6444 status: LocalRecordStatus::Published, 6445 source_runtime: SourceRuntime::Cli, 6446 created_at_ms: 1100, 6447 inserted_at_ms: 1101, 6448 owner_account_id: Some("seller-account".to_owned()), 6449 owner_pubkey: Some("seller-pubkey".to_owned()), 6450 farm_id: Some(farm_key.to_owned()), 6451 listing_addr: Some(format!("30402:seller-pubkey:{listing_key}")), 6452 local_work_json: None, 6453 event_id: Some("event-1".to_owned()), 6454 event_kind: Some(KIND_LISTING), 6455 event_pubkey: Some("seller-pubkey".to_owned()), 6456 event_created_at: Some(1100), 6457 event_tags_json: Some(json!([ 6458 ["d", listing_key], 6459 ["a", format!("30340:seller-pubkey:{farm_key}")], 6460 ["key", "eggs"], 6461 ["title", "Relay Eggs"], 6462 ["summary", "Published eggs"], 6463 ["radroots:bin", "bin-1", "1", "each"], 6464 ["radroots:price", "bin-1", "8", "USD", "1", "each"], 6465 ["inventory", "9"], 6466 ["status", "active"] 6467 ])), 6468 event_content: Some("# Relay Eggs\n\nPublished eggs".to_owned()), 6469 event_sig: Some("signature".to_owned()), 6470 raw_event_json: Some(json!({ 6471 "id": "event-1", 6472 "kind": KIND_LISTING, 6473 "pubkey": "seller-pubkey", 6474 "content": "# Relay Eggs\n\nPublished eggs" 6475 })), 6476 outbox_status: PublishOutboxStatus::Acknowledged, 6477 relay_set_fingerprint: Some("relay-set".to_owned()), 6478 relay_delivery_json: Some(json!({ 6479 "state": "acknowledged", 6480 "acknowledged_relays": ["ws://127.0.0.1:1234/"] 6481 })), 6482 }) 6483 .expect("append signed listing"); 6484 6485 app_store 6486 .import_shared_local_events_from_store(&events) 6487 .expect("import signed listing"); 6488 let imported = app_store 6489 .load_local_interop_records() 6490 .expect("load imported records"); 6491 let listing_records = imported 6492 .iter() 6493 .filter(|record| record.projected_kind == "listing") 6494 .collect::<Vec<_>>(); 6495 assert_eq!(listing_records.len(), 2); 6496 assert_eq!( 6497 listing_records[0].projected_id, 6498 listing_records[1].projected_id 6499 ); 6500 let product_count: i64 = app_store 6501 .connection() 6502 .query_row("SELECT COUNT(*) FROM products", [], |row| row.get(0)) 6503 .expect("product count"); 6504 let product: (String, String, Option<i64>, Option<i64>) = app_store 6505 .connection() 6506 .query_row( 6507 "SELECT title, status, price_minor_units, stock_count FROM products", 6508 [], 6509 |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?, row.get(3)?)), 6510 ) 6511 .expect("load product"); 6512 assert_eq!(product_count, 1); 6513 assert_eq!(product.0, "Relay Eggs"); 6514 assert_eq!(product.1, "published"); 6515 assert_eq!(product.2, Some(800)); 6516 assert_eq!(product.3, Some(9)); 6517 } 6518 6519 #[test] 6520 fn cli_origin_signed_window_listing_projects_into_buyer_browse_and_search() { 6521 let app_store = 6522 AppSqliteStore::open(DatabaseTarget::InMemory).expect("open app sqlite store"); 6523 let events = local_events_store(); 6524 let farm_key = "AAAAAAAAAAAAAAAAAAAAAA"; 6525 let listing_key = "BBBBBBBBBBBBBBBBBBBBBB"; 6526 events 6527 .append_record(&signed_market_listing_record( 6528 "buyer-visible-cli", 6529 "seller-pubkey", 6530 farm_key, 6531 listing_key, 6532 "Buyer Visible Eggs", 6533 "9", 6534 "active", 6535 "pickup", 6536 "North barn pickup", 6537 4_102_444_800, 6538 4_102_531_200, 6539 LocalRecordStatus::Published, 6540 PublishOutboxStatus::Acknowledged, 6541 )) 6542 .expect("append signed listing"); 6543 6544 let report = app_store 6545 .import_shared_local_events_from_store(&events) 6546 .expect("import signed listing"); 6547 let browse = app_store 6548 .load_buyer_listings("", &BTreeSet::new()) 6549 .expect("buyer browse should load"); 6550 let search = app_store 6551 .load_buyer_listings("eggs", &BTreeSet::from([FarmOrderMethod::Pickup])) 6552 .expect("buyer search should load"); 6553 let detail = app_store 6554 .load_buyer_product_detail(search.rows[0].product_id) 6555 .expect("buyer detail should load") 6556 .expect("buyer detail should exist"); 6557 6558 assert_eq!(report.imported_records, 1); 6559 assert_eq!(browse.rows.len(), 1); 6560 assert_eq!(search.rows.len(), 1); 6561 assert_eq!(search.rows[0].title, "Buyer Visible Eggs"); 6562 assert_eq!( 6563 search.rows[0].availability.state, 6564 ProductAvailabilityState::Scheduled 6565 ); 6566 assert_eq!(search.rows[0].stock.quantity, Some(9)); 6567 assert_eq!( 6568 search.rows[0].fulfillment_methods, 6569 BTreeSet::from([FarmOrderMethod::Pickup]) 6570 ); 6571 assert_eq!(detail.listing.title, "Buyer Visible Eggs"); 6572 } 6573 6574 #[test] 6575 fn app_origin_signed_window_listing_converges_into_buyer_visibility() { 6576 let app_store = 6577 AppSqliteStore::open(DatabaseTarget::InMemory).expect("open app sqlite store"); 6578 let events = local_events_store(); 6579 let farm_uuid = Uuid::from_u128(0x55555555555545558555555555555555); 6580 let product_uuid = Uuid::from_u128(0x66666666666646668666666666666666); 6581 let farm_key = app_d_tag_from_uuid(farm_uuid); 6582 let listing_key = app_d_tag_from_uuid(product_uuid); 6583 let listing_addr = format!("30402:app-seller-pubkey:{listing_key}"); 6584 let app_farm_record = app_local_work_record( 6585 "app:local_work:farm:buyer-visible", 6586 farm_key.as_str(), 6587 json!({ 6588 "record_kind": "farm_config_v1", 6589 "document": { 6590 "selection": { 6591 "account": "seller-account", 6592 "farm_d_tag": farm_key 6593 }, 6594 "profile": { 6595 "display_name": "App Farm" 6596 }, 6597 "farm": { 6598 "d_tag": farm_key, 6599 "name": "App Farm", 6600 "location": { 6601 "primary": "app farmstand" 6602 } 6603 } 6604 } 6605 }), 6606 ); 6607 let mut app_listing_record = app_local_work_record( 6608 "app:local_work:listing:buyer-visible", 6609 farm_key.as_str(), 6610 json!({ 6611 "record_kind": "listing_draft_v1", 6612 "document": { 6613 "listing": { 6614 "d_tag": listing_key, 6615 "farm_d_tag": farm_key 6616 }, 6617 "seller_actor": { 6618 "account_id": "seller-account", 6619 "pubkey": "app-seller-pubkey" 6620 }, 6621 "product": { 6622 "key": listing_key, 6623 "title": "App Draft Eggs", 6624 "summary": "Fresh app-origin eggs" 6625 }, 6626 "primary_bin": { 6627 "quantity_unit": "each", 6628 "price_amount": "7", 6629 "price_currency": "USD" 6630 }, 6631 "inventory": { 6632 "available": "12" 6633 } 6634 } 6635 }), 6636 ); 6637 app_listing_record.listing_addr = Some(listing_addr); 6638 events 6639 .append_record(&app_farm_record) 6640 .expect("append app farm local work"); 6641 events 6642 .append_record(&app_listing_record) 6643 .expect("append app listing local work"); 6644 app_store 6645 .import_shared_local_events_from_store(&events) 6646 .expect("import app local records"); 6647 events 6648 .append_record(&signed_market_listing_record( 6649 "buyer-visible-app-origin", 6650 "app-seller-pubkey", 6651 farm_key.as_str(), 6652 listing_key.as_str(), 6653 "Buyer Visible App Eggs", 6654 "11", 6655 "active", 6656 "pickup", 6657 "App farmstand pickup", 6658 4_102_444_800, 6659 4_102_531_200, 6660 LocalRecordStatus::Published, 6661 PublishOutboxStatus::Acknowledged, 6662 )) 6663 .expect("append signed app-origin listing"); 6664 6665 app_store 6666 .import_shared_local_events_from_store(&events) 6667 .expect("import signed app-origin listing"); 6668 let buyer_listings = app_store 6669 .load_buyer_listings("app eggs", &BTreeSet::new()) 6670 .expect("buyer listings should load"); 6671 6672 assert_eq!(buyer_listings.rows.len(), 1); 6673 assert_eq!(buyer_listings.rows[0].product_id.as_uuid(), product_uuid); 6674 assert_eq!(buyer_listings.rows[0].title, "Buyer Visible App Eggs"); 6675 assert_eq!(buyer_listings.rows[0].stock.quantity, Some(11)); 6676 } 6677 6678 #[test] 6679 fn network_app_origin_listing_cannot_claim_app_product_without_app_owned_evidence() { 6680 let app_store = 6681 AppSqliteStore::open(DatabaseTarget::InMemory).expect("open app sqlite store"); 6682 let events = local_events_store(); 6683 let farm_uuid = Uuid::from_u128(0x77777777777747778777777777777777); 6684 let product_uuid = Uuid::from_u128(0x88888888888848888888888888888888); 6685 let farm_key = app_d_tag_from_uuid(farm_uuid); 6686 let listing_key = app_d_tag_from_uuid(product_uuid); 6687 let listing_addr = format!("30402:app-seller-pubkey:{listing_key}"); 6688 seed_app_projection(&app_store, farm_uuid, product_uuid); 6689 let mut network_listing = signed_market_listing_record( 6690 "network-app-origin", 6691 "app-seller-pubkey", 6692 farm_key.as_str(), 6693 listing_key.as_str(), 6694 "Relay App Eggs", 6695 "11", 6696 "active", 6697 "pickup", 6698 "App farmstand pickup", 6699 4_102_444_800, 6700 4_102_531_200, 6701 LocalRecordStatus::Published, 6702 PublishOutboxStatus::Acknowledged, 6703 ); 6704 network_listing.source_runtime = SourceRuntime::Network; 6705 network_listing.owner_account_id = None; 6706 events 6707 .append_record(&network_listing) 6708 .expect("append network app-origin listing"); 6709 6710 let report = app_store 6711 .import_shared_local_events_from_store(&events) 6712 .expect("import network app-origin listing"); 6713 let imported = app_store 6714 .load_local_interop_records() 6715 .expect("load imported records"); 6716 let product_count: i64 = app_store 6717 .connection() 6718 .query_row("SELECT COUNT(*) FROM products", [], |row| row.get(0)) 6719 .expect("product count"); 6720 let app_product: (String, Option<i64>) = app_store 6721 .connection() 6722 .query_row( 6723 "SELECT title, stock_count FROM products WHERE id = ?1", 6724 [product_uuid.to_string()], 6725 |row| Ok((row.get(0)?, row.get(1)?)), 6726 ) 6727 .expect("load app product"); 6728 let network_product_id = 6729 deterministic_product_id(Some("app-seller-pubkey"), listing_key.as_str()); 6730 let network_product: (String, String, String, Option<i64>) = app_store 6731 .connection() 6732 .query_row( 6733 "SELECT id, farm_id, title, stock_count FROM products WHERE id = ?1", 6734 [network_product_id.to_string()], 6735 |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?, row.get(3)?)), 6736 ) 6737 .expect("load network product"); 6738 let buyer_listings = app_store 6739 .load_buyer_listings("relay app", &BTreeSet::new()) 6740 .expect("buyer listings should load"); 6741 let listing_import = imported 6742 .iter() 6743 .find(|record| record.record_id == "network-app-origin") 6744 .expect("network app-origin listing import"); 6745 6746 assert_eq!(report.imported_records, 1); 6747 assert_eq!(product_count, 2); 6748 assert_eq!(app_product.0, "Origin Eggs"); 6749 assert_eq!(app_product.1, Some(3)); 6750 assert_ne!(network_product_id.as_uuid(), product_uuid); 6751 assert_ne!(network_product.1, farm_uuid.to_string()); 6752 assert_eq!(network_product.2, "Relay App Eggs"); 6753 assert_eq!(network_product.3, Some(11)); 6754 assert_eq!(buyer_listings.rows.len(), 1); 6755 assert_eq!( 6756 buyer_listings.rows[0].product_id.as_uuid(), 6757 network_product_id.as_uuid() 6758 ); 6759 assert_eq!( 6760 listing_import.source_runtime, 6761 SourceRuntime::Network.as_str() 6762 ); 6763 assert_eq!( 6764 listing_import.listing_addr.as_deref(), 6765 Some(listing_addr.as_str()) 6766 ); 6767 assert_eq!( 6768 listing_import.projected_id.as_deref(), 6769 Some(network_product_id.to_string().as_str()) 6770 ); 6771 } 6772 6773 #[test] 6774 fn network_app_origin_listing_reuses_app_product_with_matching_app_owned_evidence() { 6775 let app_store = 6776 AppSqliteStore::open(DatabaseTarget::InMemory).expect("open app sqlite store"); 6777 let events = local_events_store(); 6778 let farm_uuid = Uuid::from_u128(0x79797979797949799797979797979797); 6779 let product_uuid = Uuid::from_u128(0x89898989898949898989898989898989); 6780 let farm_key = app_d_tag_from_uuid(farm_uuid); 6781 let listing_key = app_d_tag_from_uuid(product_uuid); 6782 let listing_addr = format!("30402:app-seller-pubkey:{listing_key}"); 6783 let app_farm_record = app_local_work_record( 6784 "app:local_work:farm:network-claim-gate", 6785 farm_key.as_str(), 6786 json!({ 6787 "record_kind": "farm_config_v1", 6788 "document": { 6789 "selection": { 6790 "account": "seller-account", 6791 "farm_d_tag": farm_key 6792 }, 6793 "profile": { 6794 "display_name": "App Farm" 6795 }, 6796 "farm": { 6797 "d_tag": farm_key, 6798 "name": "App Farm" 6799 } 6800 } 6801 }), 6802 ); 6803 let mut app_listing_record = app_local_work_record( 6804 "app:local_work:listing:network-claim-gate", 6805 farm_key.as_str(), 6806 json!({ 6807 "record_kind": "listing_draft_v1", 6808 "document": { 6809 "listing": { 6810 "d_tag": listing_key, 6811 "farm_d_tag": farm_key 6812 }, 6813 "seller_actor": { 6814 "account_id": "seller-account", 6815 "pubkey": "app-seller-pubkey" 6816 }, 6817 "product": { 6818 "key": listing_key, 6819 "title": "App Draft Eggs", 6820 "summary": "Fresh app-origin eggs" 6821 }, 6822 "primary_bin": { 6823 "quantity_unit": "each", 6824 "price_amount": "7", 6825 "price_currency": "USD" 6826 }, 6827 "inventory": { 6828 "available": "12" 6829 } 6830 } 6831 }), 6832 ); 6833 app_listing_record.listing_addr = Some(listing_addr.clone()); 6834 events 6835 .append_record(&app_farm_record) 6836 .expect("append app farm local work"); 6837 events 6838 .append_record(&app_listing_record) 6839 .expect("append app listing local work"); 6840 app_store 6841 .import_shared_local_events_from_store(&events) 6842 .expect("import app local work"); 6843 let mut network_listing = signed_market_listing_record( 6844 "network-app-origin-matching-evidence", 6845 "app-seller-pubkey", 6846 farm_key.as_str(), 6847 listing_key.as_str(), 6848 "Relay App Eggs", 6849 "11", 6850 "active", 6851 "pickup", 6852 "App farmstand pickup", 6853 4_102_444_800, 6854 4_102_531_200, 6855 LocalRecordStatus::Published, 6856 PublishOutboxStatus::Acknowledged, 6857 ); 6858 network_listing.source_runtime = SourceRuntime::Network; 6859 network_listing.owner_account_id = None; 6860 events 6861 .append_record(&network_listing) 6862 .expect("append network app-origin listing"); 6863 6864 let report = app_store 6865 .import_shared_local_events_from_store(&events) 6866 .expect("import network app-origin listing"); 6867 let product_count: i64 = app_store 6868 .connection() 6869 .query_row("SELECT COUNT(*) FROM products", [], |row| row.get(0)) 6870 .expect("product count"); 6871 let product: (String, String, String, Option<i64>) = app_store 6872 .connection() 6873 .query_row( 6874 "SELECT id, farm_id, title, stock_count FROM products", 6875 [], 6876 |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?, row.get(3)?)), 6877 ) 6878 .expect("load product"); 6879 let imported = app_store 6880 .load_local_interop_records() 6881 .expect("load imported records"); 6882 let listing_import = imported 6883 .iter() 6884 .find(|record| record.record_id == "network-app-origin-matching-evidence") 6885 .expect("network app-origin listing import"); 6886 6887 assert_eq!(report.imported_records, 1); 6888 assert_eq!(product_count, 1); 6889 assert_eq!(product.0, product_uuid.to_string()); 6890 assert_eq!(product.1, farm_uuid.to_string()); 6891 assert_eq!(product.2, "Relay App Eggs"); 6892 assert_eq!(product.3, Some(11)); 6893 assert_eq!( 6894 listing_import.source_runtime, 6895 SourceRuntime::Network.as_str() 6896 ); 6897 assert_eq!( 6898 listing_import.projected_id.as_deref(), 6899 Some(product_uuid.to_string().as_str()) 6900 ); 6901 } 6902 6903 #[test] 6904 fn network_app_origin_listing_requires_matching_event_pubkey_for_app_product_reuse() { 6905 let app_store = 6906 AppSqliteStore::open(DatabaseTarget::InMemory).expect("open app sqlite store"); 6907 let events = local_events_store(); 6908 let farm_uuid = Uuid::from_u128(0x7a7a7a7a7a7a4a7a9a7a7a7a7a7a7a7a); 6909 let product_uuid = Uuid::from_u128(0x8a8a8a8a8a8a4a8aaa8a8a8a8a8a8a8a); 6910 let farm_key = app_d_tag_from_uuid(farm_uuid); 6911 let listing_key = app_d_tag_from_uuid(product_uuid); 6912 let listing_addr = format!("30402:app-seller-pubkey:{listing_key}"); 6913 let app_farm_record = app_local_work_record( 6914 "app:local_work:farm:network-foreign-claim", 6915 farm_key.as_str(), 6916 json!({ 6917 "record_kind": "farm_config_v1", 6918 "document": { 6919 "selection": { 6920 "account": "seller-account", 6921 "farm_d_tag": farm_key 6922 }, 6923 "profile": { 6924 "display_name": "App Farm" 6925 }, 6926 "farm": { 6927 "d_tag": farm_key, 6928 "name": "App Farm" 6929 } 6930 } 6931 }), 6932 ); 6933 let mut app_listing_record = app_local_work_record( 6934 "app:local_work:listing:network-foreign-claim", 6935 farm_key.as_str(), 6936 json!({ 6937 "record_kind": "listing_draft_v1", 6938 "document": { 6939 "listing": { 6940 "d_tag": listing_key, 6941 "farm_d_tag": farm_key 6942 }, 6943 "seller_actor": { 6944 "account_id": "seller-account", 6945 "pubkey": "app-seller-pubkey" 6946 }, 6947 "product": { 6948 "key": listing_key, 6949 "title": "App Draft Eggs", 6950 "summary": "Fresh app-origin eggs" 6951 }, 6952 "primary_bin": { 6953 "quantity_unit": "each", 6954 "price_amount": "7", 6955 "price_currency": "USD" 6956 }, 6957 "inventory": { 6958 "available": "12" 6959 } 6960 } 6961 }), 6962 ); 6963 app_listing_record.listing_addr = Some(listing_addr.clone()); 6964 events 6965 .append_record(&app_farm_record) 6966 .expect("append app farm local work"); 6967 events 6968 .append_record(&app_listing_record) 6969 .expect("append app listing local work"); 6970 app_store 6971 .import_shared_local_events_from_store(&events) 6972 .expect("import app local work"); 6973 let mut network_listing = signed_market_listing_record( 6974 "network-app-origin-foreign-event-pubkey", 6975 "app-seller-pubkey", 6976 farm_key.as_str(), 6977 listing_key.as_str(), 6978 "Foreign Relay App Eggs", 6979 "11", 6980 "active", 6981 "pickup", 6982 "App farmstand pickup", 6983 4_102_444_800, 6984 4_102_531_200, 6985 LocalRecordStatus::Published, 6986 PublishOutboxStatus::Acknowledged, 6987 ); 6988 network_listing.source_runtime = SourceRuntime::Network; 6989 network_listing.owner_account_id = None; 6990 network_listing.event_pubkey = Some("foreign-seller-pubkey".to_owned()); 6991 events 6992 .append_record(&network_listing) 6993 .expect("append foreign network app-origin listing"); 6994 6995 app_store 6996 .import_shared_local_events_from_store(&events) 6997 .expect("import network app-origin listing"); 6998 let product_count: i64 = app_store 6999 .connection() 7000 .query_row("SELECT COUNT(*) FROM products", [], |row| row.get(0)) 7001 .expect("product count"); 7002 let app_product: (String, Option<i64>) = app_store 7003 .connection() 7004 .query_row( 7005 "SELECT title, stock_count FROM products WHERE id = ?1", 7006 [product_uuid.to_string()], 7007 |row| Ok((row.get(0)?, row.get(1)?)), 7008 ) 7009 .expect("load app product"); 7010 let foreign_product_id = 7011 deterministic_product_id(Some("foreign-seller-pubkey"), listing_key.as_str()); 7012 let foreign_product_count: i64 = app_store 7013 .connection() 7014 .query_row( 7015 "SELECT COUNT(*) FROM products WHERE id = ?1", 7016 [foreign_product_id.to_string()], 7017 |row| row.get(0), 7018 ) 7019 .expect("foreign product count"); 7020 7021 assert_eq!(product_count, 2); 7022 assert_eq!(app_product.0, "App Draft Eggs"); 7023 assert_eq!(app_product.1, Some(12)); 7024 assert_eq!(foreign_product_count, 1); 7025 } 7026 7027 #[test] 7028 fn app_signed_duplicate_replaces_network_listing_product_projection() { 7029 let app_store = 7030 AppSqliteStore::open(DatabaseTarget::InMemory).expect("open app sqlite store"); 7031 let events = local_events_store(); 7032 let farm_uuid = Uuid::from_u128(0x99999999999949999999999999999999); 7033 let product_uuid = Uuid::from_u128(0xaaaaaaaaaaaa4aaaaaaaaaaaaaaaaaaa); 7034 let farm_key = app_d_tag_from_uuid(farm_uuid); 7035 let listing_key = app_d_tag_from_uuid(product_uuid); 7036 let seller_pubkey = "app-seller-pubkey"; 7037 let duplicate_event_id = "duplicate-app-origin-listing-event"; 7038 let mut network_listing = signed_market_listing_record( 7039 "duplicate-network-app-origin", 7040 seller_pubkey, 7041 farm_key.as_str(), 7042 listing_key.as_str(), 7043 "Relay App Eggs", 7044 "11", 7045 "active", 7046 "pickup", 7047 "App farmstand pickup", 7048 4_102_444_800, 7049 4_102_531_200, 7050 LocalRecordStatus::Published, 7051 PublishOutboxStatus::Acknowledged, 7052 ); 7053 network_listing.source_runtime = SourceRuntime::Network; 7054 network_listing.owner_account_id = None; 7055 network_listing.record_id = "app:relay_event:duplicate-app-origin".to_owned(); 7056 network_listing.event_id = Some(duplicate_event_id.to_owned()); 7057 events 7058 .append_record(&network_listing) 7059 .expect("append network app-origin listing"); 7060 7061 app_store 7062 .import_shared_local_events_from_store(&events) 7063 .expect("import network app-origin listing"); 7064 let network_product_id = 7065 deterministic_product_id(Some(seller_pubkey), listing_key.as_str()); 7066 let network_product_count: i64 = app_store 7067 .connection() 7068 .query_row("SELECT COUNT(*) FROM products", [], |row| row.get(0)) 7069 .expect("network product count"); 7070 assert_eq!(network_product_count, 1); 7071 assert_ne!(network_product_id.as_uuid(), product_uuid); 7072 let buyer_context = BuyerContext::account("acct_buyer"); 7073 let network_listing = app_store 7074 .load_buyer_product_detail(network_product_id) 7075 .expect("network buyer detail should load") 7076 .expect("network listing should exist") 7077 .listing; 7078 app_store 7079 .replace_buyer_cart( 7080 &buyer_context, 7081 &radroots_app_view::BuyerCartProjection { 7082 farm_id: Some(network_listing.farm_id), 7083 farm_display_name: Some(network_listing.farm_display_name.clone()), 7084 lines: vec![radroots_app_view::BuyerCartLineProjection { 7085 product_id: network_listing.product_id, 7086 farm_id: network_listing.farm_id, 7087 farm_display_name: network_listing.farm_display_name.clone(), 7088 title: network_listing.title.clone(), 7089 quantity: 2, 7090 unit_price: network_listing.price.clone(), 7091 line_total_minor_units: 1600, 7092 fulfillment_summary: network_listing 7093 .next_fulfillment_window_label 7094 .clone() 7095 .expect("network listing fulfillment summary"), 7096 }], 7097 subtotal_minor_units: Some(1600), 7098 currency_code: Some("USD".to_owned()), 7099 replace_confirmation: None, 7100 }, 7101 ) 7102 .expect("buyer cart should save"); 7103 app_store 7104 .save_buyer_order_review_draft( 7105 &buyer_context, 7106 &radroots_app_view::BuyerOrderReviewDraft { 7107 name: "Casey Buyer".to_owned(), 7108 email: "casey@example.test".to_owned(), 7109 phone: String::new(), 7110 order_note: String::new(), 7111 }, 7112 ) 7113 .expect("order review draft should save"); 7114 let order_id = app_store 7115 .place_buyer_order(&buyer_context) 7116 .expect("buyer order should place"); 7117 app_store 7118 .replace_buyer_cart( 7119 &buyer_context, 7120 &radroots_app_view::BuyerCartProjection { 7121 farm_id: Some(network_listing.farm_id), 7122 farm_display_name: Some(network_listing.farm_display_name.clone()), 7123 lines: vec![radroots_app_view::BuyerCartLineProjection { 7124 product_id: network_listing.product_id, 7125 farm_id: network_listing.farm_id, 7126 farm_display_name: network_listing.farm_display_name.clone(), 7127 title: network_listing.title.clone(), 7128 quantity: 3, 7129 unit_price: network_listing.price, 7130 line_total_minor_units: 2400, 7131 fulfillment_summary: network_listing 7132 .next_fulfillment_window_label 7133 .expect("network listing fulfillment summary"), 7134 }], 7135 subtotal_minor_units: Some(2400), 7136 currency_code: Some("USD".to_owned()), 7137 replace_confirmation: None, 7138 }, 7139 ) 7140 .expect("buyer cart should save again"); 7141 7142 seed_app_projection(&app_store, farm_uuid, product_uuid); 7143 let mut app_listing = signed_market_listing_record( 7144 "duplicate-app-signed-origin", 7145 seller_pubkey, 7146 farm_key.as_str(), 7147 listing_key.as_str(), 7148 "Relay App Eggs", 7149 "11", 7150 "active", 7151 "pickup", 7152 "App farmstand pickup", 7153 4_102_444_800, 7154 4_102_531_200, 7155 LocalRecordStatus::Published, 7156 PublishOutboxStatus::Acknowledged, 7157 ); 7158 app_listing.source_runtime = SourceRuntime::App; 7159 app_listing.record_id = "app:signed_event:duplicate-app-origin".to_owned(); 7160 app_listing.event_id = Some(duplicate_event_id.to_owned()); 7161 events 7162 .append_record(&app_listing) 7163 .expect("append app signed duplicate listing"); 7164 7165 app_store 7166 .import_shared_local_events_from_store(&events) 7167 .expect("import app signed duplicate listing"); 7168 let imported = app_store 7169 .load_local_interop_records() 7170 .expect("load imported records"); 7171 let product_count: i64 = app_store 7172 .connection() 7173 .query_row("SELECT COUNT(*) FROM products", [], |row| row.get(0)) 7174 .expect("product count"); 7175 let stale_product_count: i64 = app_store 7176 .connection() 7177 .query_row( 7178 "SELECT COUNT(*) FROM products WHERE id = ?1", 7179 [network_product_id.to_string()], 7180 |row| row.get(0), 7181 ) 7182 .expect("stale product count"); 7183 let listing_import = imported 7184 .iter() 7185 .find(|record| record.record_id == "app:signed_event:duplicate-app-origin") 7186 .expect("app signed duplicate listing import"); 7187 let migrated_cart = app_store 7188 .load_buyer_cart(&buyer_context) 7189 .expect("buyer cart should load after duplicate convergence"); 7190 let order_line_id: String = app_store 7191 .connection() 7192 .query_row( 7193 "SELECT id FROM order_lines WHERE order_id = ?1", 7194 [order_id.to_string()], 7195 |row| row.get(0), 7196 ) 7197 .expect("order line id should load"); 7198 7199 assert_eq!(product_count, 1); 7200 assert_eq!(stale_product_count, 0); 7201 assert_eq!(migrated_cart.lines.len(), 1); 7202 assert_eq!(migrated_cart.lines[0].product_id.as_uuid(), product_uuid); 7203 assert_eq!(migrated_cart.lines[0].quantity, 3); 7204 assert!(order_line_id.contains(network_product_id.to_string().as_str())); 7205 assert_eq!(listing_import.source_runtime, SourceRuntime::App.as_str()); 7206 assert_eq!( 7207 listing_import.projected_id.as_deref(), 7208 Some(product_uuid.to_string().as_str()) 7209 ); 7210 assert!( 7211 imported 7212 .iter() 7213 .all(|record| record.record_id != "app:relay_event:duplicate-app-origin") 7214 ); 7215 app_store 7216 .clear_buyer_cart(&buyer_context) 7217 .expect("buyer cart should clear"); 7218 assert_eq!( 7219 app_store 7220 .apply_buyer_repeat_demand_to_cart(&buyer_context, order_id, false) 7221 .expect("repeat demand should apply"), 7222 BuyerRepeatDemandApplyOutcome::Applied 7223 ); 7224 let repeated_cart = app_store 7225 .load_buyer_cart(&buyer_context) 7226 .expect("buyer cart should load after repeat demand"); 7227 assert_eq!(repeated_cart.lines.len(), 1); 7228 assert_eq!(repeated_cart.lines[0].product_id.as_uuid(), product_uuid); 7229 assert_eq!(repeated_cart.lines[0].quantity, 2); 7230 } 7231 7232 #[test] 7233 fn failed_duplicate_listing_replacement_rolls_back_prior_visible_state() { 7234 let app_store = 7235 AppSqliteStore::open(DatabaseTarget::InMemory).expect("open app sqlite store"); 7236 let events = local_events_store(); 7237 let farm_uuid = Uuid::from_u128(0x9b9b9b9b9b9b4b9bbb9b9b9b9b9b9b9b); 7238 let product_uuid = Uuid::from_u128(0xabababababab4abababababababababa); 7239 let farm_key = app_d_tag_from_uuid(farm_uuid); 7240 let listing_key = app_d_tag_from_uuid(product_uuid); 7241 let seller_pubkey = "app-seller-pubkey"; 7242 let duplicate_event_id = "duplicate-app-origin-rollback-event"; 7243 let mut network_listing = signed_market_listing_record( 7244 "rollback-network-app-origin", 7245 seller_pubkey, 7246 farm_key.as_str(), 7247 listing_key.as_str(), 7248 "Rollback Relay Eggs", 7249 "11", 7250 "active", 7251 "pickup", 7252 "App farmstand pickup", 7253 4_102_444_800, 7254 4_102_531_200, 7255 LocalRecordStatus::Published, 7256 PublishOutboxStatus::Acknowledged, 7257 ); 7258 network_listing.source_runtime = SourceRuntime::Network; 7259 network_listing.owner_account_id = None; 7260 network_listing.record_id = "app:relay_event:rollback-app-origin".to_owned(); 7261 network_listing.event_id = Some(duplicate_event_id.to_owned()); 7262 events 7263 .append_record(&network_listing) 7264 .expect("append network app-origin listing"); 7265 app_store 7266 .import_shared_local_events_from_store(&events) 7267 .expect("import network app-origin listing"); 7268 7269 let network_product_id = 7270 deterministic_product_id(Some(seller_pubkey), listing_key.as_str()); 7271 let network_farm_id = deterministic_farm_id(Some(seller_pubkey), farm_key.as_str()); 7272 seed_app_projection(&app_store, farm_uuid, product_uuid); 7273 app_store 7274 .connection() 7275 .execute( 7276 "INSERT INTO buyer_carts ( 7277 buyer_context_key, 7278 farm_id, 7279 updated_at 7280 ) VALUES ('account:acct_buyer', ?1, '2026-01-01T00:00:00Z')", 7281 [network_farm_id.to_string()], 7282 ) 7283 .expect("insert buyer cart header"); 7284 app_store 7285 .connection() 7286 .execute( 7287 "INSERT INTO buyer_cart_lines ( 7288 buyer_context_key, 7289 product_id, 7290 quantity, 7291 updated_at 7292 ) VALUES ('account:acct_buyer', ?1, 2, '2026-01-01T00:00:00Z')", 7293 [network_product_id.to_string()], 7294 ) 7295 .expect("insert stale buyer cart line"); 7296 app_store 7297 .connection() 7298 .execute_batch( 7299 format!( 7300 "CREATE TEMP TRIGGER fail_duplicate_cart_delete 7301 BEFORE DELETE ON buyer_cart_lines 7302 WHEN old.product_id = '{}' 7303 BEGIN 7304 SELECT RAISE(ABORT, 'forced duplicate cart migration failure'); 7305 END;", 7306 network_product_id 7307 ) 7308 .as_str(), 7309 ) 7310 .expect("create failure trigger"); 7311 7312 let mut app_listing = signed_market_listing_record( 7313 "rollback-app-signed-origin", 7314 seller_pubkey, 7315 farm_key.as_str(), 7316 listing_key.as_str(), 7317 "Rollback App Eggs", 7318 "9", 7319 "active", 7320 "pickup", 7321 "App farmstand pickup", 7322 4_102_444_800, 7323 4_102_531_200, 7324 LocalRecordStatus::Published, 7325 PublishOutboxStatus::Acknowledged, 7326 ); 7327 app_listing.source_runtime = SourceRuntime::App; 7328 app_listing.record_id = "app:signed_event:rollback-app-origin".to_owned(); 7329 app_listing.event_id = Some(duplicate_event_id.to_owned()); 7330 events 7331 .append_record(&app_listing) 7332 .expect("append app signed duplicate listing"); 7333 7334 app_store 7335 .import_shared_local_events_from_store(&events) 7336 .expect_err("duplicate replacement should roll back on cart migration failure"); 7337 let imported = app_store 7338 .load_local_interop_records() 7339 .expect("load imported records"); 7340 let product_count: i64 = app_store 7341 .connection() 7342 .query_row("SELECT COUNT(*) FROM products", [], |row| row.get(0)) 7343 .expect("product count"); 7344 let stale_cart_quantity: i64 = app_store 7345 .connection() 7346 .query_row( 7347 "SELECT quantity FROM buyer_cart_lines WHERE product_id = ?1", 7348 [network_product_id.to_string()], 7349 |row| row.get(0), 7350 ) 7351 .expect("stale cart quantity"); 7352 let canonical_cart_count: i64 = app_store 7353 .connection() 7354 .query_row( 7355 "SELECT COUNT(*) FROM buyer_cart_lines WHERE product_id = ?1", 7356 [product_uuid.to_string()], 7357 |row| row.get(0), 7358 ) 7359 .expect("canonical cart count"); 7360 let network_product_title: String = app_store 7361 .connection() 7362 .query_row( 7363 "SELECT title FROM products WHERE id = ?1", 7364 [network_product_id.to_string()], 7365 |row| row.get(0), 7366 ) 7367 .expect("network product title"); 7368 7369 assert_eq!(product_count, 2); 7370 assert_eq!(stale_cart_quantity, 2); 7371 assert_eq!(canonical_cart_count, 0); 7372 assert_eq!(network_product_title, "Rollback Relay Eggs"); 7373 assert!( 7374 imported 7375 .iter() 7376 .any(|record| record.record_id == "app:relay_event:rollback-app-origin") 7377 ); 7378 assert!( 7379 imported 7380 .iter() 7381 .all(|record| record.record_id != "app:signed_event:rollback-app-origin") 7382 ); 7383 } 7384 7385 #[test] 7386 fn buyer_visibility_rejects_incomplete_unpublished_stale_and_unsupported_records() { 7387 for record in [ 7388 signed_market_listing_record( 7389 "pending-window", 7390 "seller-pubkey", 7391 "AAAAAAAAAAAAAAAAAAAAAA", 7392 "BBBBBBBBBBBBBBBBBBBBBB", 7393 "Pending Eggs", 7394 "8", 7395 "active", 7396 "pickup", 7397 "Pending barn pickup", 7398 4_102_444_800, 7399 4_102_531_200, 7400 LocalRecordStatus::PendingPublish, 7401 PublishOutboxStatus::Pending, 7402 ), 7403 signed_market_listing_record( 7404 "sold-out-window", 7405 "seller-pubkey", 7406 "CCCCCCCCCCCCCCCCCCCCCC", 7407 "DDDDDDDDDDDDDDDDDDDDDD", 7408 "Sold Out Eggs", 7409 "0", 7410 "active", 7411 "pickup", 7412 "South barn pickup", 7413 4_102_444_800, 7414 4_102_531_200, 7415 LocalRecordStatus::Published, 7416 PublishOutboxStatus::Acknowledged, 7417 ), 7418 signed_market_listing_record( 7419 "expired-window", 7420 "seller-pubkey", 7421 "EEEEEEEEEEEEEEEEEEEEEE", 7422 "FFFFFFFFFFFFFFFFFFFFFF", 7423 "Expired Eggs", 7424 "8", 7425 "active", 7426 "pickup", 7427 "East barn pickup", 7428 946_684_800, 7429 946_771_200, 7430 LocalRecordStatus::Published, 7431 PublishOutboxStatus::Acknowledged, 7432 ), 7433 signed_market_listing_record( 7434 "unsupported-fulfillment", 7435 "seller-pubkey", 7436 "GGGGGGGGGGGGGGGGGGGGGG", 7437 "HHHHHHHHHHHHHHHHHHHHHH", 7438 "Unsupported Eggs", 7439 "8", 7440 "active", 7441 "other", 7442 "Unknown exchange point", 7443 4_102_444_800, 7444 4_102_531_200, 7445 LocalRecordStatus::Published, 7446 PublishOutboxStatus::Acknowledged, 7447 ), 7448 signed_listing_record( 7449 "status-only", 7450 "IIIIIIIIIIIIIIIIIIIIII", 7451 "JJJJJJJJJJJJJJJJJJJJJJ", 7452 "active", 7453 ), 7454 ] { 7455 let app_store = 7456 AppSqliteStore::open(DatabaseTarget::InMemory).expect("open app sqlite store"); 7457 let events = local_events_store(); 7458 events.append_record(&record).expect("append record"); 7459 7460 app_store 7461 .import_shared_local_events_from_store(&events) 7462 .expect("import hidden listing record"); 7463 7464 assert!(buyer_listing_titles(&app_store).is_empty()); 7465 } 7466 7467 let app_store = 7468 AppSqliteStore::open(DatabaseTarget::InMemory).expect("open app sqlite store"); 7469 let events = local_events_store(); 7470 let farm_key = "KKKKKKKKKKKKKKKKKKKKKK"; 7471 let listing_key = "LLLLLLLLLLLLLLLLLLLLLL"; 7472 events 7473 .append_record(&local_work_record( 7474 "local-only-listing", 7475 farm_key, 7476 json!({ 7477 "record_kind": "listing_draft_v1", 7478 "document": { 7479 "listing": { 7480 "d_tag": listing_key, 7481 "farm_d_tag": farm_key 7482 }, 7483 "product": { 7484 "title": "Local Only Eggs" 7485 }, 7486 "primary_bin": { 7487 "quantity_unit": "each", 7488 "price_amount": "7", 7489 "price_currency": "USD" 7490 }, 7491 "inventory": { 7492 "available": "7" 7493 } 7494 } 7495 }), 7496 )) 7497 .expect("append local-only listing"); 7498 app_store 7499 .import_shared_local_events_from_store(&events) 7500 .expect("import local-only listing"); 7501 assert!(buyer_listing_titles(&app_store).is_empty()); 7502 7503 let app_store = 7504 AppSqliteStore::open(DatabaseTarget::InMemory).expect("open app sqlite store"); 7505 let events = local_events_store(); 7506 events 7507 .append_record(&signed_market_listing_record( 7508 "current-active-window", 7509 "seller-pubkey", 7510 farm_key, 7511 listing_key, 7512 "Current Eggs", 7513 "8", 7514 "active", 7515 "pickup", 7516 "West barn pickup", 7517 4_102_444_800, 7518 4_102_531_200, 7519 LocalRecordStatus::Published, 7520 PublishOutboxStatus::Acknowledged, 7521 )) 7522 .expect("append active listing"); 7523 app_store 7524 .import_shared_local_events_from_store(&events) 7525 .expect("import active listing"); 7526 assert_eq!(buyer_listing_titles(&app_store), vec!["Current Eggs"]); 7527 events 7528 .append_record(&signed_market_listing_record( 7529 "newer-archived-window", 7530 "seller-pubkey", 7531 farm_key, 7532 listing_key, 7533 "Archived Eggs", 7534 "8", 7535 "archived", 7536 "pickup", 7537 "West barn pickup", 7538 4_102_444_800, 7539 4_102_531_200, 7540 LocalRecordStatus::Published, 7541 PublishOutboxStatus::Acknowledged, 7542 )) 7543 .expect("append archived listing"); 7544 app_store 7545 .import_shared_local_events_from_store(&events) 7546 .expect("import archived listing"); 7547 assert!(buyer_listing_titles(&app_store).is_empty()); 7548 } 7549 7550 #[test] 7551 fn older_signed_listing_import_does_not_roll_back_current_product_state() { 7552 let app_store = 7553 AppSqliteStore::open(DatabaseTarget::InMemory).expect("open app sqlite store"); 7554 let events = local_events_store(); 7555 let farm_key = "CURRENTFARMAAAAAAAAAA"; 7556 let listing_key = "CURRENTLISTINGBBBBBB"; 7557 let mut newer = signed_market_listing_record( 7558 "listing-current-newer", 7559 "seller-pubkey", 7560 farm_key, 7561 listing_key, 7562 "New Eggs", 7563 "12", 7564 "active", 7565 "pickup", 7566 "North barn pickup", 7567 4_102_444_800, 7568 4_102_531_200, 7569 LocalRecordStatus::Published, 7570 PublishOutboxStatus::Acknowledged, 7571 ); 7572 set_listing_event_version( 7573 &mut newer, 7574 "event-listing-current-newer", 7575 2_000, 7576 "New Eggs", 7577 "12", 7578 ); 7579 events.append_record(&newer).expect("append newer listing"); 7580 app_store 7581 .import_shared_local_events_from_store(&events) 7582 .expect("import newer listing"); 7583 7584 let mut older = signed_market_listing_record( 7585 "listing-current-older", 7586 "seller-pubkey", 7587 farm_key, 7588 listing_key, 7589 "Old Eggs", 7590 "3", 7591 "active", 7592 "pickup", 7593 "North barn pickup", 7594 4_102_444_800, 7595 4_102_531_200, 7596 LocalRecordStatus::Published, 7597 PublishOutboxStatus::Acknowledged, 7598 ); 7599 set_listing_event_version( 7600 &mut older, 7601 "event-listing-current-older", 7602 1_000, 7603 "Old Eggs", 7604 "3", 7605 ); 7606 events.append_record(&older).expect("append older listing"); 7607 7608 let report = app_store 7609 .import_shared_local_events_from_store(&events) 7610 .expect("import older listing"); 7611 let product: (String, Option<i64>) = app_store 7612 .connection() 7613 .query_row("SELECT title, stock_count FROM products", [], |row| { 7614 Ok((row.get(0)?, row.get(1)?)) 7615 }) 7616 .expect("load product"); 7617 let imported = app_store 7618 .load_local_interop_records() 7619 .expect("load imported records"); 7620 7621 assert_eq!(report.imported_records, 1); 7622 assert_eq!(product.0, "New Eggs"); 7623 assert_eq!(product.1, Some(12)); 7624 assert_eq!( 7625 imported 7626 .iter() 7627 .filter(|record| record.projected_kind == "listing") 7628 .count(), 7629 2 7630 ); 7631 } 7632 7633 #[test] 7634 fn equal_timestamp_signed_listing_currentness_uses_event_id_tie_breaker() { 7635 let app_store = 7636 AppSqliteStore::open(DatabaseTarget::InMemory).expect("open app sqlite store"); 7637 let events = local_events_store(); 7638 let farm_key = "TIEFARMAAAAAAAAAAAAAA"; 7639 let listing_key = "TIELISTINGBBBBBBBBBB"; 7640 let mut winning = signed_market_listing_record( 7641 "listing-tie-winning", 7642 "seller-pubkey", 7643 farm_key, 7644 listing_key, 7645 "Tie Winner Eggs", 7646 "10", 7647 "active", 7648 "pickup", 7649 "North barn pickup", 7650 4_102_444_800, 7651 4_102_531_200, 7652 LocalRecordStatus::Published, 7653 PublishOutboxStatus::Acknowledged, 7654 ); 7655 set_listing_event_version( 7656 &mut winning, 7657 "event-z-winning", 7658 3_000, 7659 "Tie Winner Eggs", 7660 "10", 7661 ); 7662 events 7663 .append_record(&winning) 7664 .expect("append winning listing"); 7665 app_store 7666 .import_shared_local_events_from_store(&events) 7667 .expect("import winning listing"); 7668 7669 let mut losing = signed_market_listing_record( 7670 "listing-tie-losing", 7671 "seller-pubkey", 7672 farm_key, 7673 listing_key, 7674 "Tie Loser Eggs", 7675 "1", 7676 "active", 7677 "pickup", 7678 "North barn pickup", 7679 4_102_444_800, 7680 4_102_531_200, 7681 LocalRecordStatus::Published, 7682 PublishOutboxStatus::Acknowledged, 7683 ); 7684 set_listing_event_version(&mut losing, "event-a-losing", 3_000, "Tie Loser Eggs", "1"); 7685 events 7686 .append_record(&losing) 7687 .expect("append losing listing"); 7688 7689 app_store 7690 .import_shared_local_events_from_store(&events) 7691 .expect("import losing listing"); 7692 let product: (String, Option<i64>) = app_store 7693 .connection() 7694 .query_row("SELECT title, stock_count FROM products", [], |row| { 7695 Ok((row.get(0)?, row.get(1)?)) 7696 }) 7697 .expect("load product"); 7698 7699 assert_eq!(product.0, "Tie Winner Eggs"); 7700 assert_eq!(product.1, Some(10)); 7701 } 7702 7703 #[test] 7704 fn signed_farm_import_prefers_event_identity_over_local_owner_metadata() { 7705 let app_store = 7706 AppSqliteStore::open(DatabaseTarget::InMemory).expect("open app sqlite store"); 7707 let events = local_events_store(); 7708 let signed_farm_key = "SIGNEDFARMAAAAAAAAAAAA"; 7709 let expected_farm_id = deterministic_farm_id(Some("event-pubkey"), signed_farm_key); 7710 events 7711 .append_record(&LocalEventRecordInput { 7712 record_id: "cli:signed_event:farm:event-identity".to_owned(), 7713 family: LocalRecordFamily::SignedEvent, 7714 status: LocalRecordStatus::Published, 7715 source_runtime: SourceRuntime::Cli, 7716 created_at_ms: 1100, 7717 inserted_at_ms: 1101, 7718 owner_account_id: Some("seller-account".to_owned()), 7719 owner_pubkey: Some("stale-owner-pubkey".to_owned()), 7720 farm_id: Some("STALEFARMTAG".to_owned()), 7721 listing_addr: None, 7722 local_work_json: None, 7723 event_id: Some("event-farm-identity".to_owned()), 7724 event_kind: Some(KIND_FARM), 7725 event_pubkey: Some("event-pubkey".to_owned()), 7726 event_created_at: Some(1100), 7727 event_tags_json: Some(json!([["d", signed_farm_key]])), 7728 event_content: Some( 7729 json!({ 7730 "d_tag": signed_farm_key, 7731 "name": "Signed Farm" 7732 }) 7733 .to_string(), 7734 ), 7735 event_sig: Some("signature".to_owned()), 7736 raw_event_json: Some(json!({ 7737 "id": "event-farm-identity", 7738 "kind": KIND_FARM, 7739 "pubkey": "event-pubkey" 7740 })), 7741 outbox_status: PublishOutboxStatus::Acknowledged, 7742 relay_set_fingerprint: Some("relay-set".to_owned()), 7743 relay_delivery_json: Some(json!({ 7744 "state": "acknowledged", 7745 "acknowledged_relays": ["ws://127.0.0.1:1234/"] 7746 })), 7747 }) 7748 .expect("append signed farm"); 7749 7750 let report = app_store 7751 .import_shared_local_events_from_store(&events) 7752 .expect("import signed farm"); 7753 let imported = app_store 7754 .load_local_interop_records() 7755 .expect("load imported records"); 7756 let stored_farm: (String, String) = app_store 7757 .connection() 7758 .query_row("SELECT id, display_name FROM farms", [], |row| { 7759 Ok((row.get(0)?, row.get(1)?)) 7760 }) 7761 .expect("load farm"); 7762 7763 assert_eq!(report.imported_records, 1); 7764 assert_eq!(imported[0].projected_kind, "farm"); 7765 assert_eq!( 7766 imported[0].projected_id.as_deref(), 7767 Some(expected_farm_id.to_string().as_str()) 7768 ); 7769 assert_eq!(stored_farm.0, expected_farm_id.to_string()); 7770 assert_eq!(stored_farm.1, "Signed Farm"); 7771 } 7772 7773 #[test] 7774 fn cli_signed_listing_import_uses_cli_identity_for_app_shaped_keys() { 7775 let app_store = 7776 AppSqliteStore::open(DatabaseTarget::InMemory).expect("open app sqlite store"); 7777 let events = local_events_store(); 7778 let signed_farm_key = 7779 app_d_tag_from_uuid(Uuid::from_u128(0x77777777777747778777777777777777)); 7780 let signed_listing_key = 7781 app_d_tag_from_uuid(Uuid::from_u128(0x88888888888848888888888888888888)); 7782 let expected_farm_id = 7783 deterministic_farm_id(Some("farm-tag-pubkey"), signed_farm_key.as_str()); 7784 let expected_product_id = 7785 deterministic_product_id(Some("listing-event-pubkey"), signed_listing_key.as_str()); 7786 events 7787 .append_record(&LocalEventRecordInput { 7788 record_id: "cli:signed_event:listing:event-identity".to_owned(), 7789 family: LocalRecordFamily::SignedEvent, 7790 status: LocalRecordStatus::Published, 7791 source_runtime: SourceRuntime::Cli, 7792 created_at_ms: 1100, 7793 inserted_at_ms: 1101, 7794 owner_account_id: Some("seller-account".to_owned()), 7795 owner_pubkey: Some("stale-owner-pubkey".to_owned()), 7796 farm_id: Some("STALEFARMTAG".to_owned()), 7797 listing_addr: Some("30402:stale-owner-pubkey:STALELISTING".to_owned()), 7798 local_work_json: None, 7799 event_id: Some("event-listing-identity".to_owned()), 7800 event_kind: Some(KIND_LISTING), 7801 event_pubkey: Some("listing-event-pubkey".to_owned()), 7802 event_created_at: Some(1100), 7803 event_tags_json: Some(json!([ 7804 ["d", signed_listing_key], 7805 ["a", format!("30340:farm-tag-pubkey:{signed_farm_key}")], 7806 ["title", "Signed Event Eggs"], 7807 ["summary", "Signed event summary"], 7808 ["radroots:bin", "bin-1", "1", "each"], 7809 ["radroots:price", "bin-1", "8", "USD", "1", "each"], 7810 ["inventory", "9"], 7811 ["status", "active"] 7812 ])), 7813 event_content: Some( 7814 json!({ 7815 "product": { 7816 "title": "Signed Event Eggs", 7817 "summary": "Signed event summary" 7818 } 7819 }) 7820 .to_string(), 7821 ), 7822 event_sig: Some("signature".to_owned()), 7823 raw_event_json: Some(json!({ 7824 "id": "event-listing-identity", 7825 "kind": KIND_LISTING, 7826 "pubkey": "listing-event-pubkey" 7827 })), 7828 outbox_status: PublishOutboxStatus::Acknowledged, 7829 relay_set_fingerprint: Some("relay-set".to_owned()), 7830 relay_delivery_json: Some(json!({ 7831 "state": "acknowledged", 7832 "acknowledged_relays": ["ws://127.0.0.1:1234/"] 7833 })), 7834 }) 7835 .expect("append signed listing"); 7836 7837 let report = app_store 7838 .import_shared_local_events_from_store(&events) 7839 .expect("import signed listing"); 7840 let imported = app_store 7841 .load_local_interop_records() 7842 .expect("load imported records"); 7843 let product: (String, String) = app_store 7844 .connection() 7845 .query_row("SELECT id, farm_id FROM products", [], |row| { 7846 Ok((row.get(0)?, row.get(1)?)) 7847 }) 7848 .expect("load product"); 7849 7850 assert_eq!(report.imported_records, 1); 7851 assert_eq!(imported[0].projected_kind, "listing"); 7852 assert_eq!( 7853 imported[0].projected_id.as_deref(), 7854 Some(expected_product_id.to_string().as_str()) 7855 ); 7856 assert_eq!(product.0, expected_product_id.to_string()); 7857 assert_eq!(product.1, expected_farm_id.to_string()); 7858 } 7859 7860 #[test] 7861 fn direct_record_import_dedupes_signed_events_by_event_id() { 7862 let app_store = 7863 AppSqliteStore::open(DatabaseTarget::InMemory).expect("open app sqlite store"); 7864 let events = local_events_store(); 7865 let farm_key = "SIGNEDFARMAAAAAAAAAAAA"; 7866 let listing_key = "SIGNEDLISTINGBBBBBBBB"; 7867 let first = events 7868 .append_record(&signed_listing_record( 7869 "shared-record", 7870 farm_key, 7871 listing_key, 7872 "active", 7873 )) 7874 .expect("append shared signed listing"); 7875 let mut duplicate = signed_listing_record("relay-record", farm_key, listing_key, "active"); 7876 duplicate.event_id = first.event_id.clone(); 7877 let duplicate = events 7878 .append_record(&duplicate) 7879 .expect("append relay signed listing"); 7880 7881 let report = app_store 7882 .import_local_event_records(&[first, duplicate]) 7883 .expect("direct records should import"); 7884 let imported = app_store 7885 .load_local_interop_records() 7886 .expect("load imported records"); 7887 7888 assert_eq!(report.scanned_records, 2); 7889 assert_eq!(report.imported_records, 1); 7890 assert_eq!(report.skipped_records, 1); 7891 assert_eq!( 7892 imported 7893 .iter() 7894 .filter(|record| record.projected_kind == "listing") 7895 .count(), 7896 1 7897 ); 7898 } 7899 7900 #[test] 7901 fn app_order_request_receipt_replaces_prior_relay_duplicate() { 7902 let app_store = 7903 AppSqliteStore::open(DatabaseTarget::InMemory).expect("open app sqlite store"); 7904 let events = local_events_store(); 7905 let seller_pubkey = test_pubkey("seller-pubkey"); 7906 let seller_pubkey = seller_pubkey.as_str(); 7907 let buyer_pubkey = test_pubkey("buyer-pubkey"); 7908 let buyer_pubkey = buyer_pubkey.as_str(); 7909 let listing_addr = format!("30402:{seller_pubkey}:app-order-listing"); 7910 let payload = order_request_payload( 7911 "app-order-receipt-replaces-relay", 7912 listing_addr.as_str(), 7913 buyer_pubkey, 7914 seller_pubkey, 7915 ); 7916 let parts = order_request_event_build(&listing_event_ptr("listing-event"), &payload) 7917 .expect("build order request event"); 7918 let event = event_from_parts("app-order-request-event", buyer_pubkey, parts); 7919 let mut relay_record = signed_order_event_record( 7920 "app:relay_event:order-request:duplicate", 7921 &event, 7922 listing_addr.as_str(), 7923 SourceRuntime::Cli, 7924 None, 7925 ); 7926 relay_record.outbox_status = PublishOutboxStatus::None; 7927 relay_record.relay_delivery_json = Some(json!({ 7928 "state": "observed", 7929 "observed_relays": ["ws://127.0.0.1:1234/"] 7930 })); 7931 let relay_record = events 7932 .append_record(&relay_record) 7933 .expect("append relay order request"); 7934 let app_record = events 7935 .append_record(&signed_order_event_record( 7936 "app:signed_event:order-request:duplicate", 7937 &event, 7938 listing_addr.as_str(), 7939 SourceRuntime::App, 7940 Some("acct_buyer"), 7941 )) 7942 .expect("append app order request receipt"); 7943 7944 let report = app_store 7945 .import_local_event_records(&[relay_record, app_record]) 7946 .expect("import duplicate order request records"); 7947 let imported = app_store 7948 .load_local_interop_records() 7949 .expect("load imported records"); 7950 let stored = imported 7951 .iter() 7952 .find(|record| record.event_id.as_deref() == Some(event.id.as_str())) 7953 .expect("app order request evidence"); 7954 7955 assert_eq!(report.imported_records, 2); 7956 assert_eq!(report.skipped_records, 0); 7957 assert_eq!( 7958 imported 7959 .iter() 7960 .filter(|record| record.event_id.as_deref() == Some(event.id.as_str())) 7961 .count(), 7962 1 7963 ); 7964 assert_eq!(stored.record_id, "app:signed_event:order-request:duplicate"); 7965 assert_eq!(stored.source_runtime, SourceRuntime::App.as_str()); 7966 assert_eq!(stored.owner_account_id.as_deref(), Some("acct_buyer")); 7967 assert_eq!( 7968 stored.outbox_status, 7969 PublishOutboxStatus::Acknowledged.as_str() 7970 ); 7971 assert_eq!(stored.listing_addr.as_deref(), Some(listing_addr.as_str())); 7972 } 7973 7974 #[test] 7975 fn relay_order_decision_duplicate_does_not_downgrade_app_receipt() { 7976 let app_store = 7977 AppSqliteStore::open(DatabaseTarget::InMemory).expect("open app sqlite store"); 7978 let events = local_events_store(); 7979 let seller_pubkey = test_pubkey("seller-pubkey"); 7980 let seller_pubkey = seller_pubkey.as_str(); 7981 let buyer_pubkey = test_pubkey("buyer-pubkey"); 7982 let buyer_pubkey = buyer_pubkey.as_str(); 7983 let listing_addr = format!("30402:{seller_pubkey}:app-decision-listing"); 7984 let request_payload = order_request_payload( 7985 "app-decision-receipt", 7986 listing_addr.as_str(), 7987 buyer_pubkey, 7988 seller_pubkey, 7989 ); 7990 let request_parts = 7991 order_request_event_build(&listing_event_ptr("listing-event"), &request_payload) 7992 .expect("build order request event"); 7993 let request_event = 7994 event_from_parts("app-decision-request-event", buyer_pubkey, request_parts); 7995 let decision_payload = accepted_order_decision_payload( 7996 "app-decision-receipt", 7997 listing_addr.as_str(), 7998 buyer_pubkey, 7999 seller_pubkey, 8000 ); 8001 let decision_parts = order_decision_event_build( 8002 &typed_event_id(request_event.id.as_str()), 8003 &typed_event_id(request_event.id.as_str()), 8004 &decision_payload, 8005 ) 8006 .expect("build order decision event"); 8007 let decision_event = 8008 event_from_parts("app-order-decision-event", seller_pubkey, decision_parts); 8009 let app_record = events 8010 .append_record(&signed_order_event_record( 8011 "app:signed_event:order-decision:duplicate", 8012 &decision_event, 8013 listing_addr.as_str(), 8014 SourceRuntime::App, 8015 Some("acct_seller"), 8016 )) 8017 .expect("append app order decision receipt"); 8018 let mut relay_record = signed_order_event_record( 8019 "app:relay_event:order-decision:duplicate", 8020 &decision_event, 8021 listing_addr.as_str(), 8022 SourceRuntime::Cli, 8023 None, 8024 ); 8025 relay_record.outbox_status = PublishOutboxStatus::None; 8026 relay_record.relay_delivery_json = Some(json!({ 8027 "state": "observed", 8028 "observed_relays": ["ws://127.0.0.1:1234/"] 8029 })); 8030 let relay_record = events 8031 .append_record(&relay_record) 8032 .expect("append relay order decision"); 8033 8034 let report = app_store 8035 .import_local_event_records(&[app_record, relay_record]) 8036 .expect("import duplicate order decision records"); 8037 let imported = app_store 8038 .load_local_interop_records() 8039 .expect("load imported records"); 8040 let stored = imported 8041 .iter() 8042 .find(|record| record.event_id.as_deref() == Some(decision_event.id.as_str())) 8043 .expect("app order decision evidence"); 8044 8045 assert_eq!(report.imported_records, 1); 8046 assert_eq!(report.skipped_records, 1); 8047 assert_eq!( 8048 imported 8049 .iter() 8050 .filter(|record| record.event_id.as_deref() == Some(decision_event.id.as_str())) 8051 .count(), 8052 1 8053 ); 8054 assert_eq!( 8055 stored.record_id, 8056 "app:signed_event:order-decision:duplicate" 8057 ); 8058 assert_eq!(stored.source_runtime, SourceRuntime::App.as_str()); 8059 assert_eq!(stored.owner_account_id.as_deref(), Some("acct_seller")); 8060 assert_eq!( 8061 stored.outbox_status, 8062 PublishOutboxStatus::Acknowledged.as_str() 8063 ); 8064 assert_eq!(stored.listing_addr.as_deref(), Some(listing_addr.as_str())); 8065 } 8066 8067 #[test] 8068 fn local_work_farm_import_preserves_duplicate_relay_signed_ready_farm() { 8069 let app_store = 8070 AppSqliteStore::open(DatabaseTarget::InMemory).expect("open app sqlite store"); 8071 let relay_events = local_events_store(); 8072 let shared_events = local_events_store(); 8073 let farm_uuid = Uuid::from_u128(0x55555555555545558555555555555555); 8074 let farm_key = app_d_tag_from_uuid(farm_uuid); 8075 let signed_event_id = "event-app-relay-ready-farm"; 8076 let relay_record = relay_events 8077 .append_record(&signed_farm_record( 8078 "app:relay_event:farm-ready", 8079 signed_event_id, 8080 SourceRuntime::App, 8081 "app-seller-pubkey", 8082 farm_key.as_str(), 8083 "ready", 8084 "Relay Ready Farm", 8085 )) 8086 .expect("append relay farm"); 8087 let direct_report = app_store 8088 .import_local_event_records(&[relay_record]) 8089 .expect("direct relay import"); 8090 let local_farm_record = app_local_work_record( 8091 "app:local_work:farm:ready-preserve", 8092 farm_key.as_str(), 8093 json!({ 8094 "record_kind": "farm_config_v1", 8095 "document": { 8096 "selection": { 8097 "account": "seller-account", 8098 "farm_d_tag": farm_key 8099 }, 8100 "profile": { 8101 "display_name": "Draft Farm" 8102 }, 8103 "farm": { 8104 "d_tag": farm_key, 8105 "name": "Draft Farm" 8106 } 8107 } 8108 }), 8109 ); 8110 shared_events 8111 .append_record(&local_farm_record) 8112 .expect("append local farm work"); 8113 shared_events 8114 .append_record(&signed_farm_record( 8115 "app:signed_event:farm-ready", 8116 signed_event_id, 8117 SourceRuntime::App, 8118 "app-seller-pubkey", 8119 farm_key.as_str(), 8120 "ready", 8121 "Relay Ready Farm", 8122 )) 8123 .expect("append duplicate signed farm"); 8124 8125 let shared_report = app_store 8126 .import_shared_local_events_from_store(&shared_events) 8127 .expect("import shared local work after relay"); 8128 let stored_farm: (String, String, String) = app_store 8129 .connection() 8130 .query_row("SELECT id, display_name, readiness FROM farms", [], |row| { 8131 Ok((row.get(0)?, row.get(1)?, row.get(2)?)) 8132 }) 8133 .expect("load farm"); 8134 8135 assert_eq!(direct_report.imported_records, 1); 8136 assert_eq!(shared_report.imported_records, 1); 8137 assert_eq!(shared_report.skipped_records, 1); 8138 assert_eq!(stored_farm.0, farm_uuid.to_string()); 8139 assert_eq!(stored_farm.1, "Draft Farm"); 8140 assert_eq!(stored_farm.2, "ready"); 8141 } 8142 8143 #[test] 8144 fn signed_farm_without_readiness_preserves_listing_visible_farm() { 8145 let app_store = 8146 AppSqliteStore::open(DatabaseTarget::InMemory).expect("open app sqlite store"); 8147 let events = local_events_store(); 8148 let farm_key = "SIGNEDFARMAAAAAAAAAAAA"; 8149 let listing_key = "SIGNEDLISTINGBBBBBBBB"; 8150 let expected_farm_id = deterministic_farm_id(Some("seller-pubkey"), farm_key); 8151 events 8152 .append_record(&signed_market_listing_record( 8153 "visible-listing", 8154 "seller-pubkey", 8155 farm_key, 8156 listing_key, 8157 "Relay Ready Eggs", 8158 "8", 8159 "active", 8160 "pickup", 8161 "West barn pickup", 8162 4_102_444_800, 8163 4_102_531_200, 8164 LocalRecordStatus::Published, 8165 PublishOutboxStatus::Acknowledged, 8166 )) 8167 .expect("append visible listing"); 8168 events 8169 .append_record(&LocalEventRecordInput { 8170 record_id: "cli:signed_event:farm:no-readiness".to_owned(), 8171 family: LocalRecordFamily::SignedEvent, 8172 status: LocalRecordStatus::Published, 8173 source_runtime: SourceRuntime::Cli, 8174 created_at_ms: 1200, 8175 inserted_at_ms: 1201, 8176 owner_account_id: Some("seller-account".to_owned()), 8177 owner_pubkey: Some("seller-pubkey".to_owned()), 8178 farm_id: Some(farm_key.to_owned()), 8179 listing_addr: None, 8180 local_work_json: None, 8181 event_id: Some("event-farm-no-readiness".to_owned()), 8182 event_kind: Some(KIND_FARM), 8183 event_pubkey: Some("seller-pubkey".to_owned()), 8184 event_created_at: Some(1200), 8185 event_tags_json: Some(json!([["d", farm_key]])), 8186 event_content: Some( 8187 json!({ 8188 "d_tag": farm_key, 8189 "name": "Relay Ready Farm" 8190 }) 8191 .to_string(), 8192 ), 8193 event_sig: Some("signature".to_owned()), 8194 raw_event_json: Some(json!({ 8195 "id": "event-farm-no-readiness", 8196 "kind": KIND_FARM, 8197 "pubkey": "seller-pubkey" 8198 })), 8199 outbox_status: PublishOutboxStatus::Acknowledged, 8200 relay_set_fingerprint: Some("relay-set".to_owned()), 8201 relay_delivery_json: Some(json!({ 8202 "state": "acknowledged", 8203 "acknowledged_relays": ["ws://127.0.0.1:1234/"] 8204 })), 8205 }) 8206 .expect("append farm without readiness"); 8207 8208 let report = app_store 8209 .import_shared_local_events_from_store(&events) 8210 .expect("import listing and farm"); 8211 let stored_farm: (String, String, String) = app_store 8212 .connection() 8213 .query_row("SELECT id, display_name, readiness FROM farms", [], |row| { 8214 Ok((row.get(0)?, row.get(1)?, row.get(2)?)) 8215 }) 8216 .expect("load farm"); 8217 8218 assert_eq!(report.imported_records, 2); 8219 assert_eq!(stored_farm.0, expected_farm_id.to_string()); 8220 assert_eq!(stored_farm.1, "Relay Ready Farm"); 8221 assert_eq!(stored_farm.2, "ready"); 8222 assert_eq!(buyer_listing_titles(&app_store), vec!["Relay Ready Eggs"]); 8223 } 8224 8225 #[test] 8226 fn maps_acknowledged_signed_listing_lifecycle_statuses() { 8227 for (status_tag, expected_product_status) in [ 8228 ("active", "published"), 8229 ("window", "published"), 8230 ("archived", "archived"), 8231 ("sold", "paused"), 8232 ] { 8233 let app_store = 8234 AppSqliteStore::open(DatabaseTarget::InMemory).expect("open app sqlite store"); 8235 let events = local_events_store(); 8236 let farm_key = "AAAAAAAAAAAAAAAAAAAAAA"; 8237 let listing_key = "BBBBBBBBBBBBBBBBBBBBBB"; 8238 events 8239 .append_record(&signed_listing_record( 8240 status_tag, 8241 farm_key, 8242 listing_key, 8243 status_tag, 8244 )) 8245 .expect("append signed listing"); 8246 8247 let report = app_store 8248 .import_shared_local_events_from_store(&events) 8249 .expect("import signed listing"); 8250 let product_status: String = app_store 8251 .connection() 8252 .query_row("SELECT status FROM products", [], |row| row.get(0)) 8253 .expect("load product status"); 8254 8255 assert_eq!(report.imported_records, 1); 8256 assert_eq!(report.skipped_records, 0); 8257 assert_eq!(product_status, expected_product_status); 8258 } 8259 } 8260 8261 #[test] 8262 fn maps_observed_signed_listing_as_published_without_outbox_acknowledgement() { 8263 let app_store = 8264 AppSqliteStore::open(DatabaseTarget::InMemory).expect("open app sqlite store"); 8265 let events = local_events_store(); 8266 let farm_key = "AAAAAAAAAAAAAAAAAAAAAA"; 8267 let listing_key = "BBBBBBBBBBBBBBBBBBBBBB"; 8268 let mut record = signed_listing_record_with_publish_state( 8269 "observed-listing", 8270 farm_key, 8271 listing_key, 8272 "active", 8273 LocalRecordStatus::Published, 8274 PublishOutboxStatus::None, 8275 ); 8276 record.relay_delivery_json = Some(json!({ 8277 "state": "observed", 8278 "target_relays": ["ws://127.0.0.1:1234"], 8279 "connected_relays": ["ws://127.0.0.1:1234"], 8280 "acknowledged_relays": [], 8281 "observed_relays": ["ws://127.0.0.1:1234"], 8282 "failed_relays": [] 8283 })); 8284 events 8285 .append_record(&record) 8286 .expect("append observed signed listing"); 8287 8288 let report = app_store 8289 .import_shared_local_events_from_store(&events) 8290 .expect("import observed signed listing"); 8291 let product_status: String = app_store 8292 .connection() 8293 .query_row("SELECT status FROM products", [], |row| row.get(0)) 8294 .expect("load product status"); 8295 8296 assert_eq!(report.imported_records, 1); 8297 assert_eq!(report.skipped_records, 0); 8298 assert_eq!(product_status, "published"); 8299 } 8300 8301 #[test] 8302 fn unknown_acknowledged_signed_listing_status_is_not_published() { 8303 let app_store = 8304 AppSqliteStore::open(DatabaseTarget::InMemory).expect("open app sqlite store"); 8305 let events = local_events_store(); 8306 let farm_key = "AAAAAAAAAAAAAAAAAAAAAA"; 8307 let listing_key = "BBBBBBBBBBBBBBBBBBBBBB"; 8308 events 8309 .append_record(&signed_listing_record( 8310 "unknown-status", 8311 farm_key, 8312 listing_key, 8313 "unknown-status", 8314 )) 8315 .expect("append signed listing"); 8316 8317 let report = app_store 8318 .import_shared_local_events_from_store(&events) 8319 .expect("import signed listing"); 8320 let imported = app_store 8321 .load_local_interop_records() 8322 .expect("load imported records"); 8323 let product_count: i64 = app_store 8324 .connection() 8325 .query_row("SELECT COUNT(*) FROM products", [], |row| row.get(0)) 8326 .expect("product count"); 8327 8328 assert_eq!(report.imported_records, 0); 8329 assert_eq!(report.skipped_records, 1); 8330 assert_eq!(imported[0].projected_kind, "unsupported"); 8331 assert_eq!(product_count, 0); 8332 } 8333 8334 #[test] 8335 fn pending_or_failed_signed_listing_records_do_not_downgrade_published_product() { 8336 for (record_status, outbox_status) in [ 8337 ( 8338 LocalRecordStatus::PendingPublish, 8339 PublishOutboxStatus::Pending, 8340 ), 8341 (LocalRecordStatus::Failed, PublishOutboxStatus::Failed), 8342 ] { 8343 let app_store = 8344 AppSqliteStore::open(DatabaseTarget::InMemory).expect("open app sqlite store"); 8345 let events = local_events_store(); 8346 let farm_key = "AAAAAAAAAAAAAAAAAAAAAA"; 8347 let listing_key = "BBBBBBBBBBBBBBBBBBBBBB"; 8348 events 8349 .append_record(&signed_listing_record( 8350 "confirmed", 8351 farm_key, 8352 listing_key, 8353 "active", 8354 )) 8355 .expect("append confirmed signed listing"); 8356 app_store 8357 .import_shared_local_events_from_store(&events) 8358 .expect("import confirmed signed listing"); 8359 events 8360 .append_record(&signed_listing_record_with_publish_state( 8361 record_status.as_str(), 8362 farm_key, 8363 listing_key, 8364 "active", 8365 record_status, 8366 outbox_status, 8367 )) 8368 .expect("append unconfirmed signed listing"); 8369 8370 app_store 8371 .import_shared_local_events_from_store(&events) 8372 .expect("import unconfirmed signed listing"); 8373 let product_status: String = app_store 8374 .connection() 8375 .query_row("SELECT status FROM products", [], |row| row.get(0)) 8376 .expect("load product status"); 8377 let imported = app_store 8378 .load_local_interop_records() 8379 .expect("load imported records"); 8380 8381 assert_eq!(product_status, "published"); 8382 assert!(imported.iter().any(|record| { 8383 record.local_status == record_status.as_str() 8384 && record.outbox_status == outbox_status.as_str() 8385 })); 8386 } 8387 } 8388 8389 #[test] 8390 fn observes_outbox_updates_after_first_import_without_replaying_unchanged_rows() { 8391 let app_store = 8392 AppSqliteStore::open(DatabaseTarget::InMemory).expect("open app sqlite store"); 8393 let events = local_events_store(); 8394 let farm_key = "AAAAAAAAAAAAAAAAAAAAAA"; 8395 let listing_key = "BBBBBBBBBBBBBBBBBBBBBB"; 8396 events 8397 .append_record(&signed_listing_record_with_publish_state( 8398 "pending-listing", 8399 farm_key, 8400 listing_key, 8401 "active", 8402 LocalRecordStatus::PendingPublish, 8403 PublishOutboxStatus::Pending, 8404 )) 8405 .expect("append pending signed listing"); 8406 let first_report = app_store 8407 .import_shared_local_events_from_store(&events) 8408 .expect("import pending listing"); 8409 let unchanged_report = app_store 8410 .import_shared_local_events_from_store(&events) 8411 .expect("import unchanged listing"); 8412 8413 assert_eq!(first_report.scanned_records, 1); 8414 assert_eq!(first_report.imported_records, 1); 8415 assert_eq!(unchanged_report.scanned_records, 0); 8416 8417 events 8418 .update_outbox(&LocalEventRecordUpdate { 8419 record_id: "pending-listing".to_owned(), 8420 status: LocalRecordStatus::Published, 8421 outbox_status: PublishOutboxStatus::Acknowledged, 8422 relay_set_fingerprint: Some("relay-set".to_owned()), 8423 relay_delivery_json: Some(json!({ 8424 "state": "acknowledged", 8425 "acknowledged_relays": ["ws://127.0.0.1:1234/"] 8426 })), 8427 updated_at_ms: 1200, 8428 }) 8429 .expect("update listing outbox"); 8430 let changed_report = app_store 8431 .import_shared_local_events_from_store(&events) 8432 .expect("import updated listing"); 8433 let product_status: String = app_store 8434 .connection() 8435 .query_row("SELECT status FROM products", [], |row| row.get(0)) 8436 .expect("load product status"); 8437 let imported = app_store 8438 .load_local_interop_records() 8439 .expect("load imported records"); 8440 8441 assert_eq!(changed_report.scanned_records, 1); 8442 assert_eq!(changed_report.imported_records, 1); 8443 assert_eq!(product_status, "published"); 8444 assert_eq!(imported.len(), 1); 8445 assert_eq!(imported[0].local_status, "published"); 8446 assert_eq!(imported[0].outbox_status, "acknowledged"); 8447 } 8448 8449 #[test] 8450 fn app_authored_shared_records_replay_into_fresh_store_without_origin_duplicates() { 8451 let events = local_events_store(); 8452 let farm_uuid = Uuid::from_u128(0x11111111111111111111111111111111); 8453 let product_uuid = Uuid::from_u128(0x22222222222222222222222222222222); 8454 let farm_key = app_d_tag_from_uuid(farm_uuid); 8455 let listing_key = app_d_tag_from_uuid(product_uuid); 8456 let app_farm_record = app_local_work_record( 8457 "app:local_work:farm", 8458 farm_key.as_str(), 8459 json!({ 8460 "record_kind": "farm_config_v1", 8461 "document": { 8462 "selection": { 8463 "account": "seller-account", 8464 "farm_d_tag": farm_key 8465 }, 8466 "profile": { 8467 "display_name": "App Farm" 8468 }, 8469 "farm": { 8470 "d_tag": farm_key, 8471 "name": "App Farm", 8472 "location": { 8473 "primary": "app farmstand" 8474 } 8475 }, 8476 "listing_defaults": { 8477 "delivery_method": "pickup", 8478 "location": { 8479 "primary": "app farmstand" 8480 } 8481 } 8482 } 8483 }), 8484 ); 8485 let mut app_listing_record = app_local_work_record( 8486 "app:local_work:listing", 8487 farm_key.as_str(), 8488 json!({ 8489 "record_kind": "listing_draft_v1", 8490 "document": { 8491 "listing": { 8492 "d_tag": listing_key, 8493 "farm_d_tag": farm_key 8494 }, 8495 "seller_actor": { 8496 "account_id": "seller-account", 8497 "pubkey": "app-seller-pubkey" 8498 }, 8499 "product": { 8500 "key": listing_key, 8501 "title": "App Eggs", 8502 "summary": "Fresh app-origin eggs" 8503 }, 8504 "primary_bin": { 8505 "quantity_unit": "each", 8506 "price_amount": "7", 8507 "price_currency": "USD" 8508 }, 8509 "inventory": { 8510 "available": "12" 8511 } 8512 } 8513 }), 8514 ); 8515 app_listing_record.listing_addr = Some(format!("30402:app-seller-pubkey:{listing_key}")); 8516 events 8517 .append_record(&app_farm_record) 8518 .expect("append app farm local work"); 8519 events 8520 .append_record(&app_listing_record) 8521 .expect("append app listing local work"); 8522 8523 let origin_store = 8524 AppSqliteStore::open(DatabaseTarget::InMemory).expect("open origin app sqlite store"); 8525 seed_app_projection(&origin_store, farm_uuid, product_uuid); 8526 let origin_report = origin_store 8527 .import_shared_local_events_from_store(&events) 8528 .expect("import shared local events into origin store"); 8529 let origin_second_report = origin_store 8530 .import_shared_local_events_from_store(&events) 8531 .expect("import unchanged shared local events into origin store"); 8532 let origin_product_count: i64 = origin_store 8533 .connection() 8534 .query_row("SELECT COUNT(*) FROM products", [], |row| row.get(0)) 8535 .expect("origin product count"); 8536 let origin_product: (String, String, String, Option<i64>, Option<i64>) = origin_store 8537 .connection() 8538 .query_row( 8539 "SELECT id, farm_id, title, price_minor_units, stock_count FROM products", 8540 [], 8541 |row| { 8542 Ok(( 8543 row.get(0)?, 8544 row.get(1)?, 8545 row.get(2)?, 8546 row.get(3)?, 8547 row.get(4)?, 8548 )) 8549 }, 8550 ) 8551 .expect("load origin product"); 8552 let origin_imports = origin_store 8553 .load_local_interop_records() 8554 .expect("load origin imported records"); 8555 8556 assert_eq!(origin_report.scanned_records, 2); 8557 assert_eq!(origin_report.imported_records, 2); 8558 assert_eq!(origin_report.skipped_records, 0); 8559 assert_eq!(origin_report.self_observed_records, 0); 8560 assert_eq!(origin_second_report.scanned_records, 0); 8561 assert_eq!(origin_product_count, 1); 8562 assert_eq!(origin_product.0, product_uuid.to_string()); 8563 assert_eq!(origin_product.1, farm_uuid.to_string()); 8564 assert_eq!(origin_product.2, "App Eggs"); 8565 assert_eq!(origin_product.3, Some(700)); 8566 assert_eq!(origin_product.4, Some(12)); 8567 assert_eq!(origin_imports.len(), 2); 8568 8569 let fresh_store = 8570 AppSqliteStore::open(DatabaseTarget::InMemory).expect("open fresh app sqlite store"); 8571 let fresh_report = fresh_store 8572 .import_shared_local_events_from_store(&events) 8573 .expect("import shared local events into fresh store"); 8574 let fresh_product_count: i64 = fresh_store 8575 .connection() 8576 .query_row("SELECT COUNT(*) FROM products", [], |row| row.get(0)) 8577 .expect("fresh product count"); 8578 let fresh_product: (String, String, String) = fresh_store 8579 .connection() 8580 .query_row("SELECT id, farm_id, title FROM products", [], |row| { 8581 Ok((row.get(0)?, row.get(1)?, row.get(2)?)) 8582 }) 8583 .expect("load fresh product"); 8584 let fresh_imports = fresh_store 8585 .load_local_interop_records() 8586 .expect("load fresh imported records"); 8587 8588 assert_eq!(fresh_report.scanned_records, 2); 8589 assert_eq!(fresh_report.imported_records, 2); 8590 assert_eq!(fresh_report.skipped_records, 0); 8591 assert_eq!(fresh_report.self_observed_records, 0); 8592 assert_eq!(fresh_product_count, 1); 8593 assert_eq!(fresh_product.0, product_uuid.to_string()); 8594 assert_eq!(fresh_product.1, farm_uuid.to_string()); 8595 assert_eq!(fresh_product.2, "App Eggs"); 8596 assert_eq!(fresh_imports.len(), 2); 8597 } 8598 8599 #[test] 8600 fn app_authored_records_with_non_uuid_tags_do_not_fallback_to_cli_identity() { 8601 let app_store = 8602 AppSqliteStore::open(DatabaseTarget::InMemory).expect("open app sqlite store"); 8603 let events = local_events_store(); 8604 let app_record = app_local_work_record( 8605 "app:local_work:farm:invalid-tag", 8606 "not-a-uuid-d-tag", 8607 json!({ 8608 "record_kind": "farm_config_v1", 8609 "document": { 8610 "selection": { 8611 "account": "seller-account", 8612 "farm_d_tag": "not-a-uuid-d-tag" 8613 }, 8614 "profile": { 8615 "display_name": "App Farm" 8616 }, 8617 "farm": { 8618 "d_tag": "not-a-uuid-d-tag", 8619 "name": "App Farm" 8620 } 8621 } 8622 }), 8623 ); 8624 events 8625 .append_record(&app_record) 8626 .expect("append app local work"); 8627 8628 let report = app_store 8629 .import_shared_local_events_from_store(&events) 8630 .expect("import shared local events"); 8631 let imported = app_store 8632 .load_local_interop_records() 8633 .expect("load imported records"); 8634 let farm_count: i64 = app_store 8635 .connection() 8636 .query_row("SELECT COUNT(*) FROM farms", [], |row| row.get(0)) 8637 .expect("farm count"); 8638 8639 assert_eq!(report.scanned_records, 1); 8640 assert_eq!(report.imported_records, 0); 8641 assert_eq!(report.skipped_records, 1); 8642 assert_eq!(report.self_observed_records, 0); 8643 assert_eq!(imported.len(), 1); 8644 assert_eq!(imported[0].projected_kind, "unsupported"); 8645 assert_eq!(farm_count, 0); 8646 } 8647 8648 #[test] 8649 fn signed_app_origin_listing_updates_existing_app_projection() { 8650 let app_store = 8651 AppSqliteStore::open(DatabaseTarget::InMemory).expect("open app sqlite store"); 8652 let events = local_events_store(); 8653 let farm_uuid = Uuid::from_u128(0x33333333333343338333333333333333); 8654 let product_uuid = Uuid::from_u128(0x44444444444444448444444444444444); 8655 let farm_key = app_d_tag_from_uuid(farm_uuid); 8656 let listing_key = app_d_tag_from_uuid(product_uuid); 8657 let listing_addr = format!("30402:app-seller-pubkey:{listing_key}"); 8658 let app_farm_record = app_local_work_record( 8659 "app:local_work:farm:signed-convergence", 8660 farm_key.as_str(), 8661 json!({ 8662 "record_kind": "farm_config_v1", 8663 "document": { 8664 "selection": { 8665 "account": "seller-account", 8666 "farm_d_tag": farm_key 8667 }, 8668 "profile": { 8669 "display_name": "App Farm" 8670 }, 8671 "farm": { 8672 "d_tag": farm_key, 8673 "name": "App Farm" 8674 } 8675 } 8676 }), 8677 ); 8678 let mut app_listing_record = app_local_work_record( 8679 "app:local_work:listing:signed-convergence", 8680 farm_key.as_str(), 8681 json!({ 8682 "record_kind": "listing_draft_v1", 8683 "document": { 8684 "listing": { 8685 "d_tag": listing_key, 8686 "farm_d_tag": farm_key 8687 }, 8688 "seller_actor": { 8689 "account_id": "seller-account", 8690 "pubkey": "app-seller-pubkey" 8691 }, 8692 "product": { 8693 "key": listing_key, 8694 "title": "App Draft Eggs", 8695 "summary": "Fresh app-origin eggs" 8696 }, 8697 "primary_bin": { 8698 "quantity_unit": "each", 8699 "price_amount": "7", 8700 "price_currency": "USD" 8701 }, 8702 "inventory": { 8703 "available": "12" 8704 } 8705 } 8706 }), 8707 ); 8708 app_listing_record.listing_addr = Some(listing_addr.clone()); 8709 events 8710 .append_record(&app_farm_record) 8711 .expect("append app farm local work"); 8712 events 8713 .append_record(&app_listing_record) 8714 .expect("append app listing local work"); 8715 8716 let local_report = app_store 8717 .import_shared_local_events_from_store(&events) 8718 .expect("import app local work"); 8719 events 8720 .append_record(&LocalEventRecordInput { 8721 record_id: "cli:signed_event:listing:app-origin".to_owned(), 8722 family: LocalRecordFamily::SignedEvent, 8723 status: LocalRecordStatus::Published, 8724 source_runtime: SourceRuntime::Cli, 8725 created_at_ms: 1100, 8726 inserted_at_ms: 1101, 8727 owner_account_id: Some("seller-account".to_owned()), 8728 owner_pubkey: Some("app-seller-pubkey".to_owned()), 8729 farm_id: Some(farm_key.clone()), 8730 listing_addr: Some(listing_addr.clone()), 8731 local_work_json: None, 8732 event_id: Some("event-app-origin".to_owned()), 8733 event_kind: Some(KIND_LISTING), 8734 event_pubkey: Some("app-seller-pubkey".to_owned()), 8735 event_created_at: Some(1100), 8736 event_tags_json: Some(json!([ 8737 ["d", listing_key], 8738 ["a", format!("30340:app-seller-pubkey:{farm_key}")], 8739 ["title", "Relay App Eggs"], 8740 ["summary", "Published app-origin eggs"], 8741 ["radroots:bin", "bin-1", "1", "each"], 8742 ["radroots:price", "bin-1", "8", "USD", "1", "each"], 8743 ["inventory", "9"], 8744 ["status", "active"] 8745 ])), 8746 event_content: Some("# Relay App Eggs\n\nPublished app-origin eggs".to_owned()), 8747 event_sig: Some("signature".to_owned()), 8748 raw_event_json: Some(json!({ 8749 "id": "event-app-origin", 8750 "kind": KIND_LISTING, 8751 "pubkey": "app-seller-pubkey", 8752 "content": "# Relay App Eggs\n\nPublished app-origin eggs" 8753 })), 8754 outbox_status: PublishOutboxStatus::Acknowledged, 8755 relay_set_fingerprint: Some("relay-set".to_owned()), 8756 relay_delivery_json: Some(json!({ 8757 "state": "acknowledged", 8758 "acknowledged_relays": ["ws://127.0.0.1:1234/"] 8759 })), 8760 }) 8761 .expect("append signed app-origin listing"); 8762 let signed_report = app_store 8763 .import_shared_local_events_from_store(&events) 8764 .expect("import signed app-origin listing"); 8765 let imported = app_store 8766 .load_local_interop_records() 8767 .expect("load imported records"); 8768 let listing_records = imported 8769 .iter() 8770 .filter(|record| record.projected_kind == "listing") 8771 .collect::<Vec<_>>(); 8772 let product_count: i64 = app_store 8773 .connection() 8774 .query_row("SELECT COUNT(*) FROM products", [], |row| row.get(0)) 8775 .expect("product count"); 8776 let product: (String, String, String, Option<i64>, Option<i64>) = app_store 8777 .connection() 8778 .query_row( 8779 "SELECT id, farm_id, status, price_minor_units, stock_count FROM products", 8780 [], 8781 |row| { 8782 Ok(( 8783 row.get(0)?, 8784 row.get(1)?, 8785 row.get(2)?, 8786 row.get(3)?, 8787 row.get(4)?, 8788 )) 8789 }, 8790 ) 8791 .expect("load product"); 8792 8793 assert_eq!(local_report.imported_records, 2); 8794 assert_eq!(signed_report.scanned_records, 1); 8795 assert_eq!(signed_report.imported_records, 1); 8796 assert_eq!(listing_records.len(), 2); 8797 assert_eq!( 8798 listing_records[0].projected_id, 8799 listing_records[1].projected_id 8800 ); 8801 assert_eq!(product_count, 1); 8802 assert_eq!(product.0, product_uuid.to_string()); 8803 assert_eq!(product.1, farm_uuid.to_string()); 8804 assert_eq!(product.2, "published"); 8805 assert_eq!(product.3, Some(800)); 8806 assert_eq!(product.4, Some(9)); 8807 } 8808 }