commit fb1dd87a07c5c8879b86614133be429fbbc2e2ae parent 91fdfc28fbca3289be1e4de3c63299f4d869b365 Author: triesap <tyson@radroots.org> Date: Fri, 26 Jun 2026 07:17:59 +0000 workflow: align studio app with RHI-pending trades - map accepted trade workflow to pending RHI agreement and reserved inventory - add SQLite schema support for pending_rhi workflow agreement state - add localized pending validation copy and suppress seller review actions after agreement - validate with cargo test -p radroots_studio_app_view -p radroots_studio_app_i18n -p radroots_studio_app_sqlite Diffstat:
31 files changed, 882 insertions(+), 401 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock @@ -3025,6 +3025,7 @@ dependencies = [ "tokio", "tokio-rustls", "tower-service", + "webpki-roots 1.0.7", ] [[package]] @@ -3033,13 +3034,16 @@ version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" dependencies = [ + "base64 0.22.1", "bytes", "futures-channel", "futures-util", "http", "http-body", "hyper", + "ipnet", "libc", + "percent-encoding", "pin-project-lite", "socket2", "tokio", @@ -3813,7 +3817,6 @@ dependencies = [ [[package]] name = "mf2_i18n" version = "0.1.0" -source = "git+https://github.com/triesap/mf2_i18n.git?rev=e2ad58d5863d9dd98f2f38d1f08b2140bf34b0a1#e2ad58d5863d9dd98f2f38d1f08b2140bf34b0a1" dependencies = [ "mf2_i18n_build", "mf2_i18n_core", @@ -3826,7 +3829,6 @@ dependencies = [ [[package]] name = "mf2_i18n_build" version = "0.1.0" -source = "git+https://github.com/triesap/mf2_i18n.git?rev=e2ad58d5863d9dd98f2f38d1f08b2140bf34b0a1#e2ad58d5863d9dd98f2f38d1f08b2140bf34b0a1" dependencies = [ "blake3", "hex", @@ -3841,12 +3843,10 @@ dependencies = [ [[package]] name = "mf2_i18n_core" version = "0.1.0" -source = "git+https://github.com/triesap/mf2_i18n.git?rev=e2ad58d5863d9dd98f2f38d1f08b2140bf34b0a1#e2ad58d5863d9dd98f2f38d1f08b2140bf34b0a1" [[package]] name = "mf2_i18n_embedded" version = "0.1.0" -source = "git+https://github.com/triesap/mf2_i18n.git?rev=e2ad58d5863d9dd98f2f38d1f08b2140bf34b0a1#e2ad58d5863d9dd98f2f38d1f08b2140bf34b0a1" dependencies = [ "mf2_i18n_core", ] @@ -3854,7 +3854,6 @@ dependencies = [ [[package]] name = "mf2_i18n_native" version = "0.1.0" -source = "git+https://github.com/triesap/mf2_i18n.git?rev=e2ad58d5863d9dd98f2f38d1f08b2140bf34b0a1#e2ad58d5863d9dd98f2f38d1f08b2140bf34b0a1" dependencies = [ "mf2_i18n_core", "mf2_i18n_embedded", @@ -3866,7 +3865,6 @@ dependencies = [ [[package]] name = "mf2_i18n_runtime" version = "0.1.0" -source = "git+https://github.com/triesap/mf2_i18n.git?rev=e2ad58d5863d9dd98f2f38d1f08b2140bf34b0a1#e2ad58d5863d9dd98f2f38d1f08b2140bf34b0a1" dependencies = [ "ed25519-dalek", "hex", @@ -3881,7 +3879,6 @@ dependencies = [ [[package]] name = "mf2_i18n_std" version = "0.1.0" -source = "git+https://github.com/triesap/mf2_i18n.git?rev=e2ad58d5863d9dd98f2f38d1f08b2140bf34b0a1#e2ad58d5863d9dd98f2f38d1f08b2140bf34b0a1" dependencies = [ "chrono", "intl_pluralrules", @@ -5093,160 +5090,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" [[package]] -name = "radroots_studio_app" -version = "0.1.0" -dependencies = [ - "chrono", - "futures-util", - "gpui", - "gpui-component", - "gpui-component-assets", - "radroots_studio_app_core", - "radroots_studio_app_i18n", - "radroots_studio_app_remote_signer", - "radroots_studio_app_sqlite", - "radroots_studio_app_state", - "radroots_studio_app_sync", - "radroots_studio_app_ui", - "radroots_studio_app_view", - "radroots_core", - "radroots_events", - "radroots_events_codec", - "radroots_identity", - "radroots_local_events", - "radroots_nostr", - "radroots_nostr_accounts", - "radroots_protected_store", - "radroots_sdk", - "radroots_secret_vault", - "radroots_sql_core", - "radroots_trade", - "serde_json", - "thiserror 2.0.18", - "tokio", - "tokio-tungstenite", - "tracing", - "tracing-subscriber", - "uuid", -] - -[[package]] -name = "radroots_studio_app_core" -version = "0.1.0" -dependencies = [ - "chrono", - "radroots_studio_app_view", - "radroots_authority", - "radroots_core", - "radroots_events", - "radroots_local_events", - "radroots_nostr", - "radroots_runtime_paths", - "radroots_sdk", - "serde", - "serde_json", - "thiserror 2.0.18", - "tokio", - "tracing", - "tracing-appender", - "tracing-subscriber", -] - -[[package]] -name = "radroots_studio_app_i18n" -version = "0.1.0" -dependencies = [ - "mf2_i18n", -] - -[[package]] -name = "radroots_studio_app_remote_signer" -version = "0.1.0" -dependencies = [ - "nostr", - "radroots_identity", - "radroots_nostr", - "radroots_nostr_connect", - "serde", - "serde_json", - "tempfile", - "tokio", - "url", -] - -[[package]] -name = "radroots_studio_app_sqlite" -version = "0.1.0" -dependencies = [ - "radroots_studio_app_sync", - "radroots_studio_app_view", - "radroots_core", - "radroots_events", - "radroots_events_codec", - "radroots_local_events", - "radroots_sql_core", - "radroots_trade", - "rusqlite", - "serde_json", - "thiserror 2.0.18", - "uuid", -] - -[[package]] -name = "radroots_studio_app_state" -version = "0.1.0" -dependencies = [ - "radroots_studio_app_sync", - "radroots_studio_app_view", - "serde", - "serde_json", - "thiserror 2.0.18", - "tracing", -] - -[[package]] -name = "radroots_studio_app_sync" -version = "0.1.0" -dependencies = [ - "radroots_studio_app_view", - "radroots_sdk", - "serde", - "serde_json", - "thiserror 2.0.18", -] - -[[package]] -name = "radroots_studio_app_types" -version = "0.1.0" -dependencies = [ - "serde", - "uuid", -] - -[[package]] -name = "radroots_studio_app_ui" -version = "0.1.0" -dependencies = [ - "gpui", - "gpui-component", - "radroots_studio_app_core", - "radroots_studio_app_i18n", -] - -[[package]] -name = "radroots_studio_app_view" -version = "0.1.0" -dependencies = [ - "radroots_studio_app_types", - "radroots_core", - "radroots_events", - "radroots_trade", - "serde", - "url", - "uuid", -] - -[[package]] name = "radroots_authority" version = "0.1.0-alpha.2" dependencies = [ @@ -5298,6 +5141,21 @@ dependencies = [ ] [[package]] +name = "radroots_geocoder" +version = "0.1.0-alpha.2" +dependencies = [ + "hex", + "radroots_runtime_paths", + "reqwest", + "rusqlite", + "serde", + "sha2", + "tempfile", + "thiserror 1.0.69", + "url", +] + +[[package]] name = "radroots_identity" version = "0.1.0-alpha.2" dependencies = [ @@ -5473,17 +5331,20 @@ dependencies = [ "radroots_event_store", "radroots_events", "radroots_events_codec", + "radroots_geocoder", "radroots_identity", "radroots_nostr", "radroots_nostr_connect", "radroots_nostr_signer", "radroots_outbox", "radroots_relay_transport", + "radroots_runtime_paths", "radroots_trade", "serde", "serde_json", "sha2", "sqlx", + "tokio", "uuid", ] @@ -5503,6 +5364,160 @@ dependencies = [ ] [[package]] +name = "radroots_studio_app" +version = "0.1.0" +dependencies = [ + "chrono", + "futures-util", + "gpui", + "gpui-component", + "gpui-component-assets", + "radroots_core", + "radroots_events", + "radroots_events_codec", + "radroots_identity", + "radroots_local_events", + "radroots_nostr", + "radroots_nostr_accounts", + "radroots_protected_store", + "radroots_sdk", + "radroots_secret_vault", + "radroots_sql_core", + "radroots_studio_app_core", + "radroots_studio_app_i18n", + "radroots_studio_app_remote_signer", + "radroots_studio_app_sqlite", + "radroots_studio_app_state", + "radroots_studio_app_sync", + "radroots_studio_app_ui", + "radroots_studio_app_view", + "radroots_trade", + "serde_json", + "thiserror 2.0.18", + "tokio", + "tokio-tungstenite", + "tracing", + "tracing-subscriber", + "uuid", +] + +[[package]] +name = "radroots_studio_app_core" +version = "0.1.0" +dependencies = [ + "chrono", + "radroots_authority", + "radroots_core", + "radroots_events", + "radroots_local_events", + "radroots_nostr", + "radroots_runtime_paths", + "radroots_sdk", + "radroots_studio_app_view", + "serde", + "serde_json", + "thiserror 2.0.18", + "tokio", + "tracing", + "tracing-appender", + "tracing-subscriber", +] + +[[package]] +name = "radroots_studio_app_i18n" +version = "0.1.0" +dependencies = [ + "mf2_i18n", +] + +[[package]] +name = "radroots_studio_app_remote_signer" +version = "0.1.0" +dependencies = [ + "nostr", + "radroots_identity", + "radroots_nostr", + "radroots_nostr_connect", + "serde", + "serde_json", + "tempfile", + "tokio", + "url", +] + +[[package]] +name = "radroots_studio_app_sqlite" +version = "0.1.0" +dependencies = [ + "radroots_core", + "radroots_events", + "radroots_events_codec", + "radroots_local_events", + "radroots_sql_core", + "radroots_studio_app_sync", + "radroots_studio_app_view", + "radroots_trade", + "rusqlite", + "serde_json", + "thiserror 2.0.18", + "uuid", +] + +[[package]] +name = "radroots_studio_app_state" +version = "0.1.0" +dependencies = [ + "radroots_studio_app_sync", + "radroots_studio_app_view", + "serde", + "serde_json", + "thiserror 2.0.18", + "tracing", +] + +[[package]] +name = "radroots_studio_app_sync" +version = "0.1.0" +dependencies = [ + "radroots_sdk", + "radroots_studio_app_view", + "serde", + "serde_json", + "thiserror 2.0.18", +] + +[[package]] +name = "radroots_studio_app_types" +version = "0.1.0" +dependencies = [ + "serde", + "uuid", +] + +[[package]] +name = "radroots_studio_app_ui" +version = "0.1.0" +dependencies = [ + "gpui", + "gpui-component", + "radroots_studio_app_core", + "radroots_studio_app_i18n", +] + +[[package]] +name = "radroots_studio_app_view" +version = "0.1.0" +dependencies = [ + "radroots_core", + "radroots_events", + "radroots_studio_app_types", + "radroots_trade", + "serde", + "url", + "uuid", +] + +[[package]] name = "radroots_trade" version = "0.1.0-alpha.2" dependencies = [ @@ -5770,6 +5785,46 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" [[package]] +name = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots 1.0.7", +] + +[[package]] name = "resvg" version = "0.45.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -7470,6 +7525,24 @@ dependencies = [ ] [[package]] +name = "tower-http" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" +dependencies = [ + "bitflags 2.11.1", + "bytes", + "futures-util", + "http", + "http-body", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", + "url", +] + +[[package]] name = "tower-layer" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" diff --git a/Cargo.toml b/Cargo.toml @@ -29,7 +29,7 @@ futures-util = "0.3" gpui = "0.2.2" gpui-component = "0.5.1" gpui-component-assets = "0.5.1" -mf2_i18n = { git = "https://github.com/triesap/mf2_i18n.git", rev = "e2ad58d5863d9dd98f2f38d1f08b2140bf34b0a1" } +mf2_i18n = { path = "../../../vendor/triesap/mf2_i18n/crates/mf2_i18n" } radroots_core = { path = "../lib/crates/core", default-features = false, features = ["std"] } radroots_authority = { path = "../lib/crates/authority" } radroots_events = { path = "../lib/crates/events", default-features = false, features = ["serde", "std"] } diff --git a/crates/desktop/src/accounts.rs b/crates/desktop/src/accounts.rs @@ -3,12 +3,6 @@ use std::{ path::{Path, PathBuf}, }; -use radroots_studio_app_core::AppSharedAccountsPaths; -use radroots_studio_app_sqlite::{AppSqliteError, AppSqliteStore}; -use radroots_studio_app_view::{ - AccountSummary, AccountSurfaceActivationProjection, ActiveSurface, AppIdentityProjection, - FarmId, FarmerActivationProjection, SelectedAccountProjection, SelectedSurfaceProjection, -}; use radroots_identity::{IdentityError, RadrootsIdentity, RadrootsIdentityId}; use radroots_nostr_accounts::prelude::{ RadrootsNostrAccountRecord, RadrootsNostrAccountStatus, RadrootsNostrAccountsError, @@ -18,6 +12,12 @@ use radroots_secret_vault::{ RadrootsHostVaultCapabilities, RadrootsSecretBackend, RadrootsSecretBackendAvailability, RadrootsSecretBackendSelection, }; +use radroots_studio_app_core::AppSharedAccountsPaths; +use radroots_studio_app_sqlite::{AppSqliteError, AppSqliteStore}; +use radroots_studio_app_view::{ + AccountSummary, AccountSurfaceActivationProjection, ActiveSurface, AppIdentityProjection, + FarmId, FarmerActivationProjection, SelectedAccountProjection, SelectedSurfaceProjection, +}; use thiserror::Error; pub struct DesktopAccountsBootstrap { @@ -359,18 +359,18 @@ mod tests { time::{SystemTime, UNIX_EPOCH}, }; - use radroots_studio_app_core::AppSharedAccountsPaths; - use radroots_studio_app_sqlite::{AppSqliteStore, DatabaseTarget}; - use radroots_studio_app_view::{ - AccountSurfaceActivationProjection, ActiveSurface, AppStartupGate, IdentityReadiness, - SelectedSurfaceProjection, - }; use radroots_identity::RadrootsIdentity; use radroots_nostr_accounts::prelude::{ RadrootsNostrAccountsManager, RadrootsNostrFileAccountStore, RadrootsNostrMemoryAccountStore, RadrootsNostrSecretVaultMemory, }; use radroots_secret_vault::RadrootsHostVaultCapabilities; + use radroots_studio_app_core::AppSharedAccountsPaths; + use radroots_studio_app_sqlite::{AppSqliteStore, DatabaseTarget}; + use radroots_studio_app_view::{ + AccountSurfaceActivationProjection, ActiveSurface, AppStartupGate, IdentityReadiness, + SelectedSurfaceProjection, + }; use super::{ DesktopLocalIdentityImportRequest, account_summary_from_record, @@ -385,7 +385,8 @@ mod tests { .duration_since(UNIX_EPOCH) .expect("clock") .as_nanos(); - let base = std::env::temp_dir().join(format!("radroots_studio_app_accounts_{label}_{suffix}")); + let base = + std::env::temp_dir().join(format!("radroots_studio_app_accounts_{label}_{suffix}")); AppSharedAccountsPaths { data_root: base.join("data/shared/accounts"), @@ -453,7 +454,9 @@ mod tests { let activation = AccountSurfaceActivationProjection::new( account_id.as_str(), SelectedSurfaceProjection::new(ActiveSurface::Farmer), - radroots_studio_app_view::FarmerActivationProjection::active(radroots_studio_app_view::FarmId::new()), + radroots_studio_app_view::FarmerActivationProjection::active( + radroots_studio_app_view::FarmId::new(), + ), ); sqlite_store .save_surface_activation(&activation) @@ -589,7 +592,9 @@ mod tests { let activation = AccountSurfaceActivationProjection::new( second_account_id.as_str(), SelectedSurfaceProjection::new(ActiveSurface::Farmer), - radroots_studio_app_view::FarmerActivationProjection::active(radroots_studio_app_view::FarmId::new()), + radroots_studio_app_view::FarmerActivationProjection::active( + radroots_studio_app_view::FarmId::new(), + ), ); sqlite_store .save_surface_activation(&activation) @@ -648,7 +653,9 @@ mod tests { let activation = AccountSurfaceActivationProjection::new( first_account_id.as_str(), SelectedSurfaceProjection::new(ActiveSurface::Farmer), - radroots_studio_app_view::FarmerActivationProjection::active(radroots_studio_app_view::FarmId::new()), + radroots_studio_app_view::FarmerActivationProjection::active( + radroots_studio_app_view::FarmId::new(), + ), ); sqlite_store .save_surface_activation(&activation) diff --git a/crates/desktop/src/app.rs b/crates/desktop/src/app.rs @@ -150,7 +150,9 @@ mod tests { APP_PROJECTION_SOURCE, AppBuildIdentity, AppRuntimeCapture, AppRuntimeMode, AppRuntimeSnapshot, }; - use radroots_studio_app_state::{AppShellProjection, FarmWorkspaceReadinessProjection, HomeRoute}; + use radroots_studio_app_state::{ + AppShellProjection, FarmWorkspaceReadinessProjection, HomeRoute, + }; use radroots_studio_app_view::{ AppStartupGate, LoggedOutStartupProjection, SettingsAccountProjection, TodayAgendaProjection, diff --git a/crates/desktop/src/pack_day_host_handoff.rs b/crates/desktop/src/pack_day_host_handoff.rs @@ -3,7 +3,9 @@ use std::path::{Component, Path, PathBuf}; #[cfg(target_os = "macos")] use std::process::Command; -use radroots_studio_app_view::{PackDayExportArtifactKind, PackDayExportBundle, PackDayHostHandoffKind}; +use radroots_studio_app_view::{ + PackDayExportArtifactKind, PackDayExportBundle, PackDayHostHandoffKind, +}; use thiserror::Error; #[derive(Clone, Debug, Eq, PartialEq)] diff --git a/crates/desktop/src/pack_day_print.rs b/crates/desktop/src/pack_day_print.rs @@ -915,8 +915,10 @@ mod tests { impl TestDirectory { fn new() -> Self { - let path = std::env::temp_dir() - .join(format!("radroots_studio_app_pack_day_print_{}", Uuid::new_v4())); + let path = std::env::temp_dir().join(format!( + "radroots_studio_app_pack_day_print_{}", + Uuid::new_v4() + )); fs::create_dir_all(&path).expect("test directory should create"); Self { path } } diff --git a/crates/desktop/src/remote_signer.rs b/crates/desktop/src/remote_signer.rs @@ -2,6 +2,13 @@ use std::collections::HashSet; use std::fs; use std::path::{Path, PathBuf}; +use radroots_identity::{IdentityError, RadrootsIdentityId}; +use radroots_nostr_accounts::prelude::{ + RadrootsNostrAccountRecord, RadrootsNostrAccountsError, RadrootsNostrAccountsManager, + account_secret_slot, +}; +use radroots_protected_store::RadrootsProtectedFileSecretVault; +use radroots_secret_vault::{RadrootsSecretVault, RadrootsSecretVaultAccessError}; use radroots_studio_app_core::AppDesktopRuntimePaths; use radroots_studio_app_remote_signer::{ RadrootsAppRemoteSignerApprovedSession, RadrootsAppRemoteSignerError, @@ -10,13 +17,6 @@ use radroots_studio_app_remote_signer::{ RadrootsAppRemoteSignerSessionStoreState, }; use radroots_studio_app_view::{AccountCustody, AppIdentityProjection}; -use radroots_identity::{IdentityError, RadrootsIdentityId}; -use radroots_nostr_accounts::prelude::{ - RadrootsNostrAccountRecord, RadrootsNostrAccountsError, RadrootsNostrAccountsManager, - account_secret_slot, -}; -use radroots_protected_store::RadrootsProtectedFileSecretVault; -use radroots_secret_vault::{RadrootsSecretVault, RadrootsSecretVaultAccessError}; use thiserror::Error; const REMOTE_SIGNER_LABEL: &str = "remote signer"; @@ -382,14 +382,15 @@ mod tests { use std::env; use std::time::{SystemTime, UNIX_EPOCH}; - use radroots_studio_app_remote_signer::{ - RadrootsAppRemoteSignerApprovedSession, RadrootsAppRemoteSignerPendingSession, - RadrootsAppRemoteSignerSessionRecord, radroots_studio_app_remote_signer_requested_permissions, - }; use radroots_identity::{RadrootsIdentity, RadrootsIdentityPublic}; use radroots_nostr_accounts::prelude::{ RadrootsNostrAccountStatus, RadrootsNostrAccountsManager, }; + use radroots_studio_app_remote_signer::{ + RadrootsAppRemoteSignerApprovedSession, RadrootsAppRemoteSignerPendingSession, + RadrootsAppRemoteSignerSessionRecord, + radroots_studio_app_remote_signer_requested_permissions, + }; use super::{ DesktopRemoteSignerPaths, activate_pending_session, apply_remote_signer_custody, diff --git a/crates/desktop/src/runtime.rs b/crates/desktop/src/runtime.rs @@ -6,6 +6,56 @@ use std::sync::{Arc, Mutex, MutexGuard, PoisonError}; use std::time::{Duration as StdDuration, SystemTime, UNIX_EPOCH}; use chrono::{DateTime, Duration, Utc}; +use radroots_core::{ + RadrootsCoreCurrency, RadrootsCoreDecimal, RadrootsCoreMoney, RadrootsCoreQuantity, + RadrootsCoreQuantityPrice, RadrootsCoreUnit, +}; +use radroots_events::{ + ids::{ + RadrootsDTag, RadrootsEventId, RadrootsInventoryBinId, RadrootsListingAddress, + RadrootsOrderId, RadrootsOrderRevisionId, RadrootsPublicKey, + }, + kinds::{ + KIND_FARM, KIND_LISTING, KIND_LISTING_DRAFT, KIND_ORDER_CANCELLATION, KIND_ORDER_DECISION, + KIND_ORDER_REQUEST, KIND_ORDER_REVISION_DECISION, KIND_ORDER_REVISION_PROPOSAL, + KIND_PROFILE, + }, +}; +use radroots_events_codec::order::order_event_context_from_tags; +use radroots_identity::{RadrootsIdentity, RadrootsIdentityId}; +use radroots_local_events::{ + BUYER_ORDER_REQUEST_ACTOR_SOURCE_RESOLVED_ACCOUNT, + BUYER_ORDER_REQUEST_ACTOR_SOURCE_UNRESOLVED_APP, BUYER_ORDER_REQUEST_DOCUMENT_KIND, + BUYER_ORDER_REQUEST_LOCAL_WORK_RECORD_KIND, LocalEventRecord, LocalEventRecordInput, + LocalEventRecordUpdate, LocalEventsStore, LocalRecordFamily, LocalRecordStatus, + PublishOutboxStatus, RelayDeliveryEvidence, RelayDeliveryFailure, SourceRuntime, + buyer_order_request_local_work_record_id, validate_buyer_order_request_local_work_payload, +}; +use radroots_nostr::prelude::{ + RadrootsNostrClient, RadrootsNostrEvent, RadrootsNostrFilter, RadrootsNostrOutput, + RadrootsNostrTimestamp, radroots_nostr_kind, radroots_nostr_parse_pubkey, +}; +use radroots_nostr_accounts::prelude::RadrootsNostrAccountsManager; +use radroots_sdk::protocol::events::{ + RadrootsNostrEvent as SdkRadrootsNostrEvent, RadrootsNostrEventPtr, +}; +use radroots_sdk::protocol::farm::{RadrootsFarm, RadrootsFarmRef}; +use radroots_sdk::protocol::listing::{ + RadrootsListing, RadrootsListingAvailability, RadrootsListingBin, + RadrootsListingDeliveryMethod, RadrootsListingProduct, RadrootsListingStatus, +}; +use radroots_sdk::protocol::order::{ + RadrootsOrderCancellation, RadrootsOrderDecision, RadrootsOrderDecisionOutcome, + RadrootsOrderEconomics, RadrootsOrderInventoryCommitment, RadrootsOrderItem, + RadrootsOrderRequest, RadrootsOrderRevisionDecision, RadrootsOrderRevisionOutcome, + RadrootsOrderRevisionProposal, +}; +use radroots_sdk::{ + FARM_PUBLISH_OPERATION_KIND, LISTING_PUBLISH_OPERATION_KIND, ORDER_CANCELLATION_OPERATION_KIND, + ORDER_DECISION_OPERATION_KIND, ORDER_REVISION_DECISION_OPERATION_KIND, + ORDER_REVISION_PROPOSAL_OPERATION_KIND, ORDER_SUBMIT_OPERATION_KIND, +}; +use radroots_sql_core::SqliteExecutor; use radroots_studio_app_core::{ AppBuildIdentity, AppDesktopRuntimePaths, AppRuntimeCapture, AppRuntimeMode, AppRuntimePathsError, AppRuntimeSnapshot, AppSdkConfig, AppSdkDiagnostics, @@ -67,63 +117,13 @@ use radroots_studio_app_view::{ ReminderUrgency, SettingsAccountProjection, SettingsPreference, SettingsSection, ShellSection, TodayAgendaProjection, }; -use radroots_core::{ - RadrootsCoreCurrency, RadrootsCoreDecimal, RadrootsCoreMoney, RadrootsCoreQuantity, - RadrootsCoreQuantityPrice, RadrootsCoreUnit, -}; -use radroots_events::{ - ids::{ - RadrootsDTag, RadrootsEventId, RadrootsInventoryBinId, RadrootsListingAddress, - RadrootsOrderId, RadrootsOrderRevisionId, RadrootsPublicKey, - }, - kinds::{ - KIND_FARM, KIND_LISTING, KIND_LISTING_DRAFT, KIND_ORDER_CANCELLATION, KIND_ORDER_DECISION, - KIND_ORDER_REQUEST, KIND_ORDER_REVISION_DECISION, KIND_ORDER_REVISION_PROPOSAL, - KIND_PROFILE, - }, -}; -use radroots_events_codec::order::order_event_context_from_tags; -use radroots_identity::{RadrootsIdentity, RadrootsIdentityId}; -use radroots_local_events::{ - BUYER_ORDER_REQUEST_ACTOR_SOURCE_RESOLVED_ACCOUNT, - BUYER_ORDER_REQUEST_ACTOR_SOURCE_UNRESOLVED_APP, BUYER_ORDER_REQUEST_DOCUMENT_KIND, - BUYER_ORDER_REQUEST_LOCAL_WORK_RECORD_KIND, LocalEventRecord, LocalEventRecordInput, - LocalEventRecordUpdate, LocalEventsStore, LocalRecordFamily, LocalRecordStatus, - PublishOutboxStatus, RelayDeliveryEvidence, RelayDeliveryFailure, SourceRuntime, - buyer_order_request_local_work_record_id, validate_buyer_order_request_local_work_payload, -}; -use radroots_nostr::prelude::{ - RadrootsNostrClient, RadrootsNostrEvent, RadrootsNostrFilter, RadrootsNostrOutput, - RadrootsNostrTimestamp, radroots_nostr_kind, radroots_nostr_parse_pubkey, -}; -use radroots_nostr_accounts::prelude::RadrootsNostrAccountsManager; -use radroots_sdk::protocol::events::{ - RadrootsNostrEvent as SdkRadrootsNostrEvent, RadrootsNostrEventPtr, -}; -use radroots_sdk::protocol::farm::{RadrootsFarm, RadrootsFarmRef}; -use radroots_sdk::protocol::listing::{ - RadrootsListing, RadrootsListingAvailability, RadrootsListingBin, - RadrootsListingDeliveryMethod, RadrootsListingLocation, RadrootsListingProduct, - RadrootsListingStatus, -}; -use radroots_sdk::protocol::order::{ - RadrootsOrderCancellation, RadrootsOrderDecision, RadrootsOrderDecisionOutcome, - RadrootsOrderEconomics, RadrootsOrderInventoryCommitment, RadrootsOrderItem, - RadrootsOrderRequest, RadrootsOrderRevisionDecision, RadrootsOrderRevisionOutcome, - RadrootsOrderRevisionProposal, -}; -use radroots_sdk::{ - FARM_PUBLISH_OPERATION_KIND, LISTING_PUBLISH_OPERATION_KIND, ORDER_CANCELLATION_OPERATION_KIND, - ORDER_DECISION_OPERATION_KIND, ORDER_REVISION_DECISION_OPERATION_KIND, - ORDER_REVISION_PROPOSAL_OPERATION_KIND, ORDER_SUBMIT_OPERATION_KIND, -}; -use radroots_sql_core::SqliteExecutor; use radroots_trade::listing::parse_public_listing_address; use radroots_trade::order::{ RadrootsOrderCancellationRecord, RadrootsOrderDecisionRecord, RadrootsOrderReductionInputs, RadrootsOrderRequestRecord, RadrootsOrderRevisionDecisionRecord, - RadrootsOrderRevisionProposalRecord, RadrootsOrderStatus, reduce_order_events, + RadrootsOrderRevisionProposalRecord, reduce_order_events, }; +use radroots_trade::workflow::RadrootsTradeWorkflowState; use serde_json::json; use thiserror::Error; use tokio::runtime::Builder as TokioRuntimeBuilder; @@ -245,7 +245,7 @@ struct ResolvedAppOrderRevisionDecisionEvidence { struct ResolvedAppOrderLifecycleEvidence { evidence_events: Vec<SdkRadrootsNostrEvent>, request_event_id: String, - status: RadrootsOrderStatus, + status: RadrootsTradeWorkflowState, agreement_event_id: Option<String>, last_event_id: Option<String>, decision: Option<ResolvedAppOrderDecisionEvidence>, @@ -2734,7 +2734,8 @@ impl DesktopAppRuntimeState { }); } let lifecycle = self.resolve_order_lifecycle_evidence(&request)?; - if lifecycle.decision.is_some() || lifecycle.status != RadrootsOrderStatus::Requested { + if lifecycle.decision.is_some() || lifecycle.status != RadrootsTradeWorkflowState::Requested + { return Err(AppSqliteError::InvalidProjection { reason: "seller order revision requires an undecided order", }); @@ -2868,7 +2869,8 @@ impl DesktopAppRuntimeState { }); } let lifecycle = self.resolve_order_lifecycle_evidence(&request)?; - if lifecycle.decision.is_some() || lifecycle.status != RadrootsOrderStatus::Requested { + if lifecycle.decision.is_some() || lifecycle.status != RadrootsTradeWorkflowState::Requested + { return Err(AppSqliteError::InvalidProjection { reason: "buyer order revision requires active pre-agreement negotiation", }); @@ -3002,7 +3004,7 @@ impl DesktopAppRuntimeState { }); } let prev_event_id = match lifecycle.status { - RadrootsOrderStatus::Requested => { + RadrootsTradeWorkflowState::Requested => { if active_order_pending_revision_proposal(&lifecycle).is_some() { return Err(AppSqliteError::InvalidProjection { reason: "buyer order cancellation requires no pending seller proposal", @@ -3010,15 +3012,17 @@ impl DesktopAppRuntimeState { } request.request_event_id.clone() } - RadrootsOrderStatus::Accepted => { + RadrootsTradeWorkflowState::RevisionProposed + | RadrootsTradeWorkflowState::AgreedPendingRhi + | RadrootsTradeWorkflowState::Committed => { return Err(AppSqliteError::InvalidProjection { reason: "buyer order cancellation requires an open pre-agreement order", }); } - RadrootsOrderStatus::Missing - | RadrootsOrderStatus::Declined - | RadrootsOrderStatus::Cancelled - | RadrootsOrderStatus::Invalid => { + RadrootsTradeWorkflowState::Missing + | RadrootsTradeWorkflowState::Declined + | RadrootsTradeWorkflowState::Cancelled + | RadrootsTradeWorkflowState::Invalid => { return Err(AppSqliteError::InvalidProjection { reason: "buyer order cancellation requires an open pre-agreement order", }); @@ -5370,7 +5374,9 @@ impl DesktopAppRuntimeState { fn selected_product_editor_id(&self) -> Option<ProductId> { match &self.state_store.products_projection().editor { - radroots_studio_app_state::ProductEditorState::Open(session) => session.selected_product_id, + radroots_studio_app_state::ProductEditorState::Open(session) => { + session.selected_product_id + } radroots_studio_app_state::ProductEditorState::Closed => None, } } @@ -5694,7 +5700,8 @@ impl DesktopAppRuntimeState { cancellations: buckets.cancellations.clone(), }, ); - if !projection.issues.is_empty() || projection.status == RadrootsOrderStatus::Invalid { + if !projection.issues.is_empty() || projection.status == RadrootsTradeWorkflowState::Invalid + { return Err(AppSqliteError::InvalidProjection { reason: "order lifecycle evidence is invalid", }); @@ -7578,19 +7585,7 @@ fn listing_publish_payload_to_sdk_listing( delivery_method: Some(parse_app_listing_delivery_method( payload.fulfillment_method.as_deref().unwrap_or_default(), )?), - location: payload - .fulfillment_location - .as_deref() - .filter(|value| !value.trim().is_empty()) - .map(|primary| RadrootsListingLocation { - primary: primary.trim().to_owned(), - city: None, - region: None, - country: None, - lat: None, - lng: None, - geohash: None, - }), + location: None, published_at: None, images: None, }) @@ -9748,6 +9743,44 @@ mod tests { use chrono::{Duration, Utc}; use futures_util::{SinkExt, StreamExt}; + use radroots_core::{ + RadrootsCoreCurrency, RadrootsCoreDecimal, RadrootsCoreMoney, RadrootsCoreUnit, + }; + use radroots_events::ids::{ + RadrootsEventId, RadrootsInventoryBinId, RadrootsListingAddress, RadrootsOrderId, + RadrootsOrderQuoteId, RadrootsOrderRevisionId, RadrootsPublicKey, + }; + use radroots_events_codec::wire::WireEventParts; + use radroots_identity::{RadrootsIdentity, RadrootsIdentityId}; + use radroots_local_events::{ + BUYER_ORDER_REQUEST_LOCAL_WORK_RECORD_KIND, LocalEventRecord, LocalEventRecordInput, + LocalEventsStore, LocalRecordFamily, LocalRecordStatus, PublishOutboxStatus, + RelayDeliveryEvidence, SourceRuntime, + }; + use radroots_nostr::prelude::{ + RadrootsNostrClient, RadrootsNostrEvent, RadrootsNostrKeys, RadrootsNostrSecretKey, + RadrootsNostrTimestamp, radroots_event_from_nostr, radroots_nostr_build_event, + }; + use radroots_nostr_accounts::prelude::{ + RadrootsNostrAccountsManager, RadrootsNostrFileAccountStore, + RadrootsNostrMemoryAccountStore, RadrootsNostrSecretVaultMemory, RadrootsSecretVault, + account_secret_slot, + }; + use radroots_sdk::protocol::events::{ + RadrootsNostrEvent as SdkRadrootsNostrEvent, RadrootsNostrEventPtr, + }; + use radroots_sdk::protocol::order::{ + RadrootsOrderCancellation, RadrootsOrderDecision, RadrootsOrderDecisionOutcome, + RadrootsOrderEconomicItem, RadrootsOrderEconomics, RadrootsOrderInventoryCommitment, + RadrootsOrderItem, RadrootsOrderPricingBasis, RadrootsOrderRequest, + RadrootsOrderRevisionOutcome, RadrootsOrderRevisionProposal, + }; + use radroots_sdk::{ + LISTING_PUBLISH_OPERATION_KIND, ORDER_CANCELLATION_OPERATION_KIND, + ORDER_DECISION_OPERATION_KIND, ORDER_REVISION_DECISION_OPERATION_KIND, + ORDER_SUBMIT_OPERATION_KIND, + }; + use radroots_sql_core::{SqlExecutor, SqliteExecutor}; use radroots_studio_app_core::{ AppDesktopRuntimePaths, AppRuntimeHostEnvironment, AppRuntimePlatform, AppSdkLifecycleState, AppSdkProjectionLifecycleState, AppSharedAccountsPaths, @@ -9796,44 +9829,6 @@ mod tests { SelectedAccountProjection, SelectedSurfaceProjection, SettingsPreference, SettingsSection, ShellSection, TodayAgendaProjection, TodaySetupTask, TodaySetupTaskKind, TodaySummary, }; - use radroots_core::{ - RadrootsCoreCurrency, RadrootsCoreDecimal, RadrootsCoreMoney, RadrootsCoreUnit, - }; - use radroots_events::ids::{ - RadrootsEventId, RadrootsInventoryBinId, RadrootsListingAddress, RadrootsOrderId, - RadrootsOrderQuoteId, RadrootsOrderRevisionId, RadrootsPublicKey, - }; - use radroots_events_codec::wire::WireEventParts; - use radroots_identity::{RadrootsIdentity, RadrootsIdentityId}; - use radroots_local_events::{ - BUYER_ORDER_REQUEST_LOCAL_WORK_RECORD_KIND, LocalEventRecord, LocalEventRecordInput, - LocalEventsStore, LocalRecordFamily, LocalRecordStatus, PublishOutboxStatus, - RelayDeliveryEvidence, SourceRuntime, - }; - use radroots_nostr::prelude::{ - RadrootsNostrClient, RadrootsNostrEvent, RadrootsNostrKeys, RadrootsNostrSecretKey, - RadrootsNostrTimestamp, radroots_event_from_nostr, radroots_nostr_build_event, - }; - use radroots_nostr_accounts::prelude::{ - RadrootsNostrAccountsManager, RadrootsNostrFileAccountStore, - RadrootsNostrMemoryAccountStore, RadrootsNostrSecretVaultMemory, RadrootsSecretVault, - account_secret_slot, - }; - use radroots_sdk::protocol::events::{ - RadrootsNostrEvent as SdkRadrootsNostrEvent, RadrootsNostrEventPtr, - }; - use radroots_sdk::protocol::order::{ - RadrootsOrderCancellation, RadrootsOrderDecision, RadrootsOrderDecisionOutcome, - RadrootsOrderEconomicItem, RadrootsOrderEconomics, RadrootsOrderInventoryCommitment, - RadrootsOrderItem, RadrootsOrderPricingBasis, RadrootsOrderRequest, - RadrootsOrderRevisionOutcome, RadrootsOrderRevisionProposal, - }; - use radroots_sdk::{ - LISTING_PUBLISH_OPERATION_KIND, ORDER_CANCELLATION_OPERATION_KIND, - ORDER_DECISION_OPERATION_KIND, ORDER_REVISION_DECISION_OPERATION_KIND, - ORDER_SUBMIT_OPERATION_KIND, - }; - use radroots_sql_core::{SqlExecutor, SqliteExecutor}; use serde_json::json; use tokio::net::TcpListener; use tokio::sync::oneshot; diff --git a/crates/desktop/src/source_guards.rs b/crates/desktop/src/source_guards.rs @@ -713,6 +713,7 @@ const REQUIRED_WINDOW_COPY_KEYS: &[&str] = &[ "AppTextKey::TradeWorkflowAxisInventory", "AppTextKey::TradeWorkflowAxisSource", "AppTextKey::TradeWorkflowAgreementOrdered", + "AppTextKey::TradeWorkflowAgreementPendingRhi", "AppTextKey::TradeWorkflowAgreementConfirmed", "AppTextKey::TradeWorkflowAgreementDeclined", "AppTextKey::TradeWorkflowAgreementCancelled", diff --git a/crates/desktop/src/window.rs b/crates/desktop/src/window.rs @@ -11,6 +11,7 @@ use gpui_component::{ menu::PopupMenuItem, select::{SearchableVec, Select, SelectDelegate, SelectEvent, SelectState}, }; +use radroots_nostr::prelude::RadrootsNostrClient; use radroots_studio_app_core::{ AppSdkLifecycleState, AppSdkProjectionLifecycleState, AppSdkRelayUrlPolicy, }; @@ -20,7 +21,8 @@ use radroots_studio_app_remote_signer::{ RadrootsAppRemoteSignerPendingSession, RadrootsAppRemoteSignerSource, radroots_studio_app_remote_signer_connect_pending, radroots_studio_app_remote_signer_poll_pending_session_with_progress, - radroots_studio_app_remote_signer_preview, radroots_studio_app_remote_signer_requested_permissions, + radroots_studio_app_remote_signer_preview, + radroots_studio_app_remote_signer_requested_permissions, }; use radroots_studio_app_sqlite::{AppSqliteError, derive_farm_rules_readiness}; use radroots_studio_app_state::{ @@ -83,7 +85,6 @@ use radroots_studio_app_view::{ TradeValidationReceiptProjection, TradeValidationReceiptResult, TradeValidationReceiptType, TradeWorkflowProjection, TradeWorkflowSource, }; -use radroots_nostr::prelude::RadrootsNostrClient; use std::{ collections::BTreeSet, path::{Component, Path, PathBuf}, @@ -11615,6 +11616,7 @@ fn trade_workflow_value_badge(value_key: AppTextKey) -> AnyElement { fn trade_agreement_status_key(status: TradeAgreementStatus) -> AppTextKey { match status { TradeAgreementStatus::Ordered => AppTextKey::TradeWorkflowAgreementOrdered, + TradeAgreementStatus::PendingRhi => AppTextKey::TradeWorkflowAgreementPendingRhi, TradeAgreementStatus::Confirmed => AppTextKey::TradeWorkflowAgreementConfirmed, TradeAgreementStatus::Declined => AppTextKey::TradeWorkflowAgreementDeclined, TradeAgreementStatus::Cancelled => AppTextKey::TradeWorkflowAgreementCancelled, @@ -12363,7 +12365,8 @@ fn startup_signer_entry_surface( } fn startup_signer_preview_summary(input: &str) -> Result<StartupSignerPreviewSummary, String> { - let target = radroots_studio_app_remote_signer_preview(input).map_err(|error| error.to_string())?; + let target = + radroots_studio_app_remote_signer_preview(input).map_err(|error| error.to_string())?; Ok(StartupSignerPreviewSummary { source_label: startup_signer_source_text(target.source), @@ -16277,6 +16280,7 @@ mod tests { DesktopAppSdkReadyDiagnosticsSummary, DesktopAppSdkStatusSummary, DesktopAppSyncConflictSummary, DesktopAppSyncStatusSummary, }; + use radroots_identity::RadrootsIdentity; use radroots_studio_app_core::{ AppDesktopRuntimePaths, AppRuntimeHostEnvironment, AppRuntimePlatform, AppSdkLifecycleState, AppSdkProjectionLifecycleState, AppSdkRelayUrlPolicy, @@ -16312,7 +16316,6 @@ mod tests { TradeEconomicsProjection, TradeInventoryStatus, TradeRevisionStatus, TradeWorkflowProjection, TradeWorkflowSource, }; - use radroots_identity::RadrootsIdentity; use std::{ fs, path::PathBuf, @@ -16724,6 +16727,10 @@ mod tests { AppTextKey::TradeWorkflowAgreementOrdered, ), ( + TradeAgreementStatus::PendingRhi, + AppTextKey::TradeWorkflowAgreementPendingRhi, + ), + ( TradeAgreementStatus::Confirmed, AppTextKey::TradeWorkflowAgreementConfirmed, ), @@ -17396,7 +17403,9 @@ mod tests { ); runtime.pack_day_projection.export = PackDayExportProjection::running( - radroots_studio_app_state::PackDayExportRequest::for_fulfillment_window(fulfillment_window_id), + radroots_studio_app_state::PackDayExportRequest::for_fulfillment_window( + fulfillment_window_id, + ), ); assert!(!pack_day_export_action_enabled(&runtime)); assert_eq!( @@ -17436,8 +17445,9 @@ mod tests { }, ], }; - let request = - radroots_studio_app_state::PackDayExportRequest::for_fulfillment_window(fulfillment_window_id); + let request = radroots_studio_app_state::PackDayExportRequest::for_fulfillment_window( + fulfillment_window_id, + ); let rows = pack_day_export_detail_rows(&PackDayExportProjection::succeeded( request.clone(), @@ -17508,7 +17518,9 @@ mod tests { assert!(pack_day_host_handoff_action_presentations(&runtime).is_empty()); runtime.pack_day_projection.export = PackDayExportProjection::succeeded( - radroots_studio_app_state::PackDayExportRequest::for_fulfillment_window(fulfillment_window_id), + radroots_studio_app_state::PackDayExportRequest::for_fulfillment_window( + fulfillment_window_id, + ), bundle, ); @@ -17548,7 +17560,9 @@ mod tests { let bundle = sample_pack_day_bundle(temp_dir.path()); let fulfillment_window_id = bundle.fulfillment_window_id; let export_request = - radroots_studio_app_state::PackDayExportRequest::for_fulfillment_window(fulfillment_window_id); + radroots_studio_app_state::PackDayExportRequest::for_fulfillment_window( + fulfillment_window_id, + ); let reveal_request = PackDayHostHandoffRequest::for_bundle(PackDayHostHandoffKind::RevealBundle, &bundle); let open_request = PackDayHostHandoffRequest::for_bundle( @@ -17626,7 +17640,9 @@ mod tests { ); runtime.pack_day_projection.export = PackDayExportProjection::succeeded( - radroots_studio_app_state::PackDayExportRequest::for_fulfillment_window(fulfillment_window_id), + radroots_studio_app_state::PackDayExportRequest::for_fulfillment_window( + fulfillment_window_id, + ), bundle, ); @@ -17674,7 +17690,9 @@ mod tests { assert!(pack_day_print_action_presentations(&runtime).is_empty()); runtime.pack_day_projection.export = PackDayExportProjection::succeeded( - radroots_studio_app_state::PackDayExportRequest::for_fulfillment_window(fulfillment_window_id), + radroots_studio_app_state::PackDayExportRequest::for_fulfillment_window( + fulfillment_window_id, + ), bundle, ); @@ -17717,7 +17735,9 @@ mod tests { assert_eq!(pack_day_batch_print_action_presentation(&runtime), None); runtime.pack_day_projection.export = PackDayExportProjection::succeeded( - radroots_studio_app_state::PackDayExportRequest::for_fulfillment_window(fulfillment_window_id), + radroots_studio_app_state::PackDayExportRequest::for_fulfillment_window( + fulfillment_window_id, + ), bundle, ); @@ -17739,7 +17759,9 @@ mod tests { let bundle = sample_pack_day_bundle(temp_dir.path()); let fulfillment_window_id = bundle.fulfillment_window_id; let export_request = - radroots_studio_app_state::PackDayExportRequest::for_fulfillment_window(fulfillment_window_id); + radroots_studio_app_state::PackDayExportRequest::for_fulfillment_window( + fulfillment_window_id, + ); let batch_request = PackDayBatchPrintRequest::for_bundle(&bundle); let mut runtime = summary( HomeRoute::Today, @@ -17779,7 +17801,9 @@ mod tests { let bundle = sample_pack_day_bundle(temp_dir.path()); let fulfillment_window_id = bundle.fulfillment_window_id; let export_request = - radroots_studio_app_state::PackDayExportRequest::for_fulfillment_window(fulfillment_window_id); + radroots_studio_app_state::PackDayExportRequest::for_fulfillment_window( + fulfillment_window_id, + ); let batch_request = PackDayBatchPrintRequest::for_bundle(&bundle); let mut runtime = summary( HomeRoute::Today, @@ -17862,7 +17886,9 @@ mod tests { let bundle = sample_pack_day_bundle(temp_dir.path()); let fulfillment_window_id = bundle.fulfillment_window_id; let export_request = - radroots_studio_app_state::PackDayExportRequest::for_fulfillment_window(fulfillment_window_id); + radroots_studio_app_state::PackDayExportRequest::for_fulfillment_window( + fulfillment_window_id, + ); let print_request = PackDayPrintRequest::for_bundle(PackDayPrintKind::PrintPackSheet, &bundle); let failed_request = @@ -17964,7 +17990,9 @@ mod tests { let bundle = sample_pack_day_bundle(temp_dir.path()); let fulfillment_window_id = bundle.fulfillment_window_id; let export_request = - radroots_studio_app_state::PackDayExportRequest::for_fulfillment_window(fulfillment_window_id); + radroots_studio_app_state::PackDayExportRequest::for_fulfillment_window( + fulfillment_window_id, + ); let host_handoff_request = PackDayHostHandoffRequest::for_bundle(PackDayHostHandoffKind::RevealBundle, &bundle); let mut runtime = summary( diff --git a/crates/i18n/src/keys.rs b/crates/i18n/src/keys.rs @@ -432,6 +432,7 @@ define_app_text_keys! { TradeWorkflowAxisInventory => "trade.workflow.axis.inventory", TradeWorkflowAxisSource => "trade.workflow.axis.source", TradeWorkflowAgreementOrdered => "trade.workflow.agreement.ordered", + TradeWorkflowAgreementPendingRhi => "trade.workflow.agreement.pending_rhi", TradeWorkflowAgreementConfirmed => "trade.workflow.agreement.confirmed", TradeWorkflowAgreementDeclined => "trade.workflow.agreement.declined", TradeWorkflowAgreementCancelled => "trade.workflow.agreement.cancelled", diff --git a/crates/i18n/src/lib.rs b/crates/i18n/src/lib.rs @@ -639,6 +639,10 @@ mod tests { "Ordered" ); assert_eq!( + app_text(AppTextKey::TradeWorkflowAgreementPendingRhi), + "Pending validation" + ); + assert_eq!( app_text(AppTextKey::TradeWorkflowAgreementConfirmed), "Confirmed" ); diff --git a/crates/runtime/src/pack_day_export.rs b/crates/runtime/src/pack_day_export.rs @@ -474,7 +474,9 @@ mod tests { .duration_since(UNIX_EPOCH) .expect("system time should be after epoch") .as_nanos(); - std::env::temp_dir().join(format!("radroots_studio_app_pack_day_export_{label}_{unique}")) + std::env::temp_dir().join(format!( + "radroots_studio_app_pack_day_export_{label}_{unique}" + )) } fn cleanup_temp_root(roots: &AppRuntimeRoots) { diff --git a/crates/runtime/src/sdk.rs b/crates/runtime/src/sdk.rs @@ -1905,7 +1905,7 @@ mod tests { ids::{RadrootsDTag, RadrootsInventoryBinId}, listing::{ RadrootsListing, RadrootsListingAvailability, RadrootsListingBin, - RadrootsListingDeliveryMethod, RadrootsListingLocation, RadrootsListingProduct, + RadrootsListingDeliveryMethod, RadrootsListingProduct, RadrootsListingPublicLocation, RadrootsListingStatus, }, }; @@ -2342,14 +2342,12 @@ mod tests { status: RadrootsListingStatus::Active, }), delivery_method: Some(RadrootsListingDeliveryMethod::Pickup), - location: Some(RadrootsListingLocation { + location: Some(RadrootsListingPublicLocation { primary: "North Farm".to_owned(), city: None, region: None, country: Some("US".to_owned()), - lat: None, - lng: None, - geohash: None, + geohash: "9q8yy".to_owned(), }), images: None, } diff --git a/crates/signer/src/input.rs b/crates/signer/src/input.rs @@ -39,7 +39,8 @@ impl RadrootsAppRemoteSignerTarget { } } -pub fn radroots_studio_app_remote_signer_requested_permissions() -> RadrootsNostrConnectPermissions { +pub fn radroots_studio_app_remote_signer_requested_permissions() -> RadrootsNostrConnectPermissions +{ vec![ RadrootsNostrConnectPermission::with_parameter( RadrootsNostrConnectMethod::SignEvent, @@ -138,7 +139,8 @@ mod tests { #[test] fn parses_direct_bunker_uri() { - let preview = radroots_studio_app_remote_signer_preview(bunker_uri().as_str()).expect("preview"); + let preview = + radroots_studio_app_remote_signer_preview(bunker_uri().as_str()).expect("preview"); assert_eq!(preview.source, RadrootsAppRemoteSignerSource::BunkerUri); assert_eq!( diff --git a/crates/signer/src/lib.rs b/crates/signer/src/lib.rs @@ -8,7 +8,8 @@ mod session; pub use error::RadrootsAppRemoteSignerError; pub use input::{ RadrootsAppRemoteSignerSource, RadrootsAppRemoteSignerTarget, - radroots_studio_app_remote_signer_preview, radroots_studio_app_remote_signer_requested_permissions, + radroots_studio_app_remote_signer_preview, + radroots_studio_app_remote_signer_requested_permissions, }; pub use protocol::{ RadrootsAppRemoteSignerApprovedSession, RadrootsAppRemoteSignerPendingPollOutcome, diff --git a/crates/signer/src/protocol.rs b/crates/signer/src/protocol.rs @@ -105,8 +105,10 @@ where F: FnMut(RadrootsAppRemoteSignerProgressUpdate), { let mut poller = - radroots_studio_app_remote_signer_open_pending_poller(record, client_secret_key_hex).await?; - radroots_studio_app_remote_signer_poll_pending_poller_with_progress(&mut poller, &mut progress).await + radroots_studio_app_remote_signer_open_pending_poller(record, client_secret_key_hex) + .await?; + radroots_studio_app_remote_signer_poll_pending_poller_with_progress(&mut poller, &mut progress) + .await } pub(crate) async fn radroots_studio_app_remote_signer_open_pending_poller( @@ -770,8 +772,8 @@ mod tests { #[test] fn connect_request_uses_explicit_requested_permissions() { - let target = - radroots_studio_app_remote_signer_preview(fixture_discovery_url().as_str()).expect("preview"); + let target = radroots_studio_app_remote_signer_preview(fixture_discovery_url().as_str()) + .expect("preview"); let request = connect_request_for_target(&target); diff --git a/crates/store/Cargo.toml b/crates/store/Cargo.toml @@ -17,7 +17,7 @@ radroots_studio_app_view.workspace = true radroots_studio_app_sync.workspace = true radroots_sql_core.workspace = true radroots_trade.workspace = true -rusqlite.workspace = true +rusqlite = { version = "0.32.1", features = ["bundled"] } serde_json.workspace = true thiserror.workspace = true uuid.workspace = true diff --git a/crates/store/migrations/0023_order_workflow_display_projection.sql b/crates/store/migrations/0023_order_workflow_display_projection.sql @@ -1,6 +1,6 @@ ALTER TABLE orders ADD COLUMN workflow_agreement TEXT NOT NULL DEFAULT 'ordered' CHECK ( - workflow_agreement IN ('ordered', 'confirmed', 'declined', 'cancelled', 'needs_review') + workflow_agreement IN ('ordered', 'pending_rhi', 'confirmed', 'declined', 'cancelled', 'needs_review') ); ALTER TABLE orders diff --git a/crates/store/migrations/0025_order_workflow_agreement_projection.sql b/crates/store/migrations/0025_order_workflow_agreement_projection.sql @@ -27,7 +27,7 @@ CREATE TABLE orders ( workflow_revision IN ('none', 'change_proposed', 'updated', 'kept_as_placed') ), workflow_agreement TEXT NOT NULL DEFAULT 'ordered' CHECK ( - workflow_agreement IN ('ordered', 'confirmed', 'declined', 'cancelled', 'needs_review') + workflow_agreement IN ('ordered', 'pending_rhi', 'confirmed', 'declined', 'cancelled', 'needs_review') ), workflow_inventory TEXT NOT NULL DEFAULT 'needs_review' CHECK ( workflow_inventory IN ('available', 'reserved', 'sold_out', 'needs_review') diff --git a/crates/store/migrations/0029_order_workflow_pending_rhi_agreement.sql b/crates/store/migrations/0029_order_workflow_pending_rhi_agreement.sql @@ -0,0 +1,248 @@ +DROP INDEX IF EXISTS idx_order_validation_receipts_order_time; +DROP INDEX IF EXISTS idx_order_validation_receipts_root_order; +DROP INDEX IF EXISTS idx_order_lines_order_sort; +DROP INDEX IF EXISTS idx_buyer_order_coordination_context_state_updated_at; +DROP INDEX IF EXISTS idx_buyer_order_coordination_state_updated_at; +DROP INDEX IF EXISTS idx_orders_farm_status; +DROP INDEX IF EXISTS idx_orders_farm_window_status_updated_at; +DROP INDEX IF EXISTS idx_orders_buyer_context_updated_at; + +ALTER TABLE order_validation_receipts RENAME TO order_validation_receipts_pending_rhi_legacy; +ALTER TABLE order_lines RENAME TO order_lines_pending_rhi_legacy; +ALTER TABLE buyer_order_coordination_records RENAME TO buyer_order_coordination_records_pending_rhi_legacy; +ALTER TABLE orders RENAME TO orders_pending_rhi_legacy; + +CREATE TABLE orders ( + id TEXT PRIMARY KEY NOT NULL, + farm_id TEXT NOT NULL REFERENCES farms(id) ON DELETE CASCADE, + fulfillment_window_id TEXT REFERENCES fulfillment_windows(id) ON DELETE SET NULL, + order_number TEXT NOT NULL, + customer_display_name TEXT NOT NULL, + status TEXT NOT NULL CHECK ( + status IN ('needs_action', 'scheduled', 'packed', 'completed', 'declined', 'needs_review') + ), + updated_at TEXT NOT NULL, + buyer_context_key TEXT, + buyer_email TEXT NOT NULL DEFAULT '', + buyer_phone TEXT NOT NULL DEFAULT '', + buyer_order_note TEXT NOT NULL DEFAULT '', + workflow_revision TEXT NOT NULL DEFAULT 'none' CHECK ( + workflow_revision IN ('none', 'change_proposed', 'updated', 'kept_as_placed') + ), + workflow_agreement TEXT NOT NULL DEFAULT 'ordered' CHECK ( + workflow_agreement IN ('ordered', 'pending_rhi', 'confirmed', 'declined', 'cancelled', 'needs_review') + ), + workflow_inventory TEXT NOT NULL DEFAULT 'needs_review' CHECK ( + workflow_inventory IN ('available', 'reserved', 'sold_out', 'needs_review') + ), + workflow_provenance_source TEXT NOT NULL DEFAULT 'unknown' CHECK ( + workflow_provenance_source IN ('app', 'cli', 'relay', 'local_events', 'unknown') + ), + workflow_provenance_last_event_id TEXT +); + +INSERT INTO orders ( + id, + farm_id, + fulfillment_window_id, + order_number, + customer_display_name, + status, + updated_at, + buyer_context_key, + buyer_email, + buyer_phone, + buyer_order_note, + workflow_revision, + workflow_agreement, + workflow_inventory, + workflow_provenance_source, + workflow_provenance_last_event_id +) +SELECT + id, + farm_id, + fulfillment_window_id, + order_number, + customer_display_name, + status, + updated_at, + buyer_context_key, + buyer_email, + buyer_phone, + buyer_order_note, + workflow_revision, + workflow_agreement, + workflow_inventory, + workflow_provenance_source, + workflow_provenance_last_event_id +FROM orders_pending_rhi_legacy; + +CREATE TABLE order_lines ( + id TEXT PRIMARY KEY NOT NULL, + order_id TEXT NOT NULL REFERENCES orders(id) ON DELETE CASCADE, + title TEXT NOT NULL, + quantity_value INTEGER NOT NULL CHECK (quantity_value >= 0), + quantity_unit_label TEXT NOT NULL DEFAULT '', + quantity_display TEXT NOT NULL, + sort_index INTEGER NOT NULL DEFAULT 0, + listing_bin_id TEXT, + unit_price_minor_units INTEGER CHECK ( + unit_price_minor_units IS NULL OR unit_price_minor_units >= 0 + ), + price_currency TEXT NOT NULL DEFAULT 'USD', + farm_key TEXT, + listing_addr TEXT, + listing_event_id TEXT, + seller_pubkey TEXT, + listing_relays_json TEXT +); + +INSERT INTO order_lines ( + id, + order_id, + title, + quantity_value, + quantity_unit_label, + quantity_display, + sort_index, + listing_bin_id, + unit_price_minor_units, + price_currency, + farm_key, + listing_addr, + listing_event_id, + seller_pubkey, + listing_relays_json +) +SELECT + id, + order_id, + title, + quantity_value, + quantity_unit_label, + quantity_display, + sort_index, + listing_bin_id, + unit_price_minor_units, + price_currency, + farm_key, + listing_addr, + listing_event_id, + seller_pubkey, + listing_relays_json +FROM order_lines_pending_rhi_legacy; + +CREATE TABLE buyer_order_coordination_records ( + order_id TEXT PRIMARY KEY NOT NULL REFERENCES orders(id) ON DELETE CASCADE, + buyer_context_key TEXT NOT NULL, + record_id TEXT, + state TEXT NOT NULL CHECK (state IN ('pending', 'synced', 'failed')), + payload_json TEXT, + attempt_count INTEGER NOT NULL DEFAULT 0 CHECK (attempt_count >= 0), + last_error_message TEXT, + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL, + synced_at TEXT +); + +INSERT INTO buyer_order_coordination_records ( + order_id, + buyer_context_key, + record_id, + state, + payload_json, + attempt_count, + last_error_message, + created_at, + updated_at, + synced_at +) +SELECT + order_id, + buyer_context_key, + record_id, + state, + payload_json, + attempt_count, + last_error_message, + created_at, + updated_at, + synced_at +FROM buyer_order_coordination_records_pending_rhi_legacy; + +CREATE TABLE order_validation_receipts ( + event_id TEXT PRIMARY KEY NOT NULL, + order_id TEXT REFERENCES orders(id) ON DELETE CASCADE, + raw_order_id TEXT NOT NULL, + root_event_id TEXT NOT NULL, + listing_event_id TEXT NOT NULL, + target_event_id TEXT NOT NULL, + receipt_type TEXT NOT NULL CHECK ( + receipt_type IN ('listing_validation', 'trade_transition', 'inventory_state', 'state_checkpoint') + ), + result TEXT NOT NULL CHECK ( + result IN ('valid', 'needs_review') + ), + proof_system TEXT NOT NULL CHECK ( + proof_system IN ('none', 'sp1_core', 'sp1_compressed', 'sp1_groth16', 'sp1_plonk') + ), + event_set_root TEXT NOT NULL, + reducer_output_root TEXT NOT NULL, + public_values_hash TEXT NOT NULL, + event_created_at INTEGER NOT NULL CHECK (event_created_at >= 0) +); + +INSERT INTO order_validation_receipts ( + event_id, + order_id, + raw_order_id, + root_event_id, + listing_event_id, + target_event_id, + receipt_type, + result, + proof_system, + event_set_root, + reducer_output_root, + public_values_hash, + event_created_at +) +SELECT + event_id, + order_id, + raw_order_id, + root_event_id, + listing_event_id, + target_event_id, + receipt_type, + result, + proof_system, + event_set_root, + reducer_output_root, + public_values_hash, + event_created_at +FROM order_validation_receipts_pending_rhi_legacy; + +CREATE INDEX idx_orders_farm_status ON orders(farm_id, status); +CREATE INDEX idx_orders_farm_window_status_updated_at + ON orders(farm_id, fulfillment_window_id, status, updated_at DESC, id DESC); +CREATE INDEX idx_orders_buyer_context_updated_at + ON orders(buyer_context_key, updated_at DESC, id DESC) + WHERE buyer_context_key IS NOT NULL AND trim(buyer_context_key) <> ''; +CREATE INDEX idx_order_lines_order_sort + ON order_lines(order_id, sort_index, id); +CREATE INDEX idx_buyer_order_coordination_context_state_updated_at + ON buyer_order_coordination_records(buyer_context_key, state, updated_at); +CREATE INDEX idx_buyer_order_coordination_state_updated_at + ON buyer_order_coordination_records(state, updated_at); +CREATE INDEX idx_order_validation_receipts_order_time + ON order_validation_receipts(order_id, event_created_at DESC, event_id DESC) + WHERE order_id IS NOT NULL; +CREATE INDEX idx_order_validation_receipts_root_order + ON order_validation_receipts(root_event_id, raw_order_id); + +DROP TABLE order_validation_receipts_pending_rhi_legacy; +DROP TABLE order_lines_pending_rhi_legacy; +DROP TABLE buyer_order_coordination_records_pending_rhi_legacy; +DROP TABLE orders_pending_rhi_legacy; diff --git a/crates/store/src/interop.rs b/crates/store/src/interop.rs @@ -1,12 +1,5 @@ use std::{fs, path::Path}; -use radroots_studio_app_view::{ - FarmId, FarmOrderMethod, FarmReadiness, FarmSetupDraft, FarmSetupProjection, FarmSummary, - FulfillmentWindowId, OrderId, PickupLocationId, ProductId, ProductStatus, - TradeProvenanceProjection, TradeRevisionStatus, TradeValidationReceiptProofSystem, - TradeValidationReceiptResult, TradeValidationReceiptType, TradeWorkflowProjection, - TradeWorkflowSource, order_status_from_active_order_projection, -}; use radroots_events::{ RadrootsNostrEvent, ids::{RadrootsEventId, RadrootsOrderId, RadrootsPublicKey}, @@ -35,6 +28,13 @@ use radroots_local_events::{ RelayDeliveryEvidence, RelayDeliveryState, SourceRuntime, }; use radroots_sql_core::{SqlExecutor, SqliteExecutor}; +use radroots_studio_app_view::{ + FarmId, FarmOrderMethod, FarmReadiness, FarmSetupDraft, FarmSetupProjection, FarmSummary, + FulfillmentWindowId, OrderId, PickupLocationId, ProductId, ProductStatus, + TradeProvenanceProjection, TradeRevisionStatus, TradeValidationReceiptProofSystem, + TradeValidationReceiptResult, TradeValidationReceiptType, TradeWorkflowProjection, + TradeWorkflowSource, order_status_from_active_order_projection, +}; use radroots_trade::order::{ RadrootsOrderCancellationRecord, RadrootsOrderDecisionRecord, RadrootsOrderProjection, RadrootsOrderReductionInputs, RadrootsOrderRequestRecord, RadrootsOrderRevisionDecisionRecord, @@ -3730,13 +3730,6 @@ fn farm_readiness_from_storage_key(readiness: &str) -> Result<FarmReadiness, App mod tests { use std::collections::BTreeSet; - use radroots_studio_app_view::{ - BuyerContext, BuyerOrderStatus, FarmId, FarmOrderMethod, OrderId, OrderStatus, - OrdersFilter, OrdersScreenQueryState, ProductAvailabilityState, ProductId, - TradeAgreementStatus, TradeInventoryStatus, TradeRevisionStatus, - TradeValidationReceiptProofSystem, TradeValidationReceiptResult, - TradeValidationReceiptType, TradeWorkflowSource, - }; use radroots_core::{ RadrootsCoreCurrency, RadrootsCoreDecimal, RadrootsCoreMoney, RadrootsCoreUnit, }; @@ -3766,6 +3759,13 @@ mod tests { LocalRecordStatus, PublishOutboxStatus, RelayDeliveryEvidence, SourceRuntime, }; use radroots_sql_core::SqliteExecutor; + use radroots_studio_app_view::{ + BuyerContext, BuyerOrderStatus, FarmId, FarmOrderMethod, OrderId, OrderStatus, + OrdersFilter, OrdersScreenQueryState, ProductAvailabilityState, ProductId, + TradeAgreementStatus, TradeInventoryStatus, TradeRevisionStatus, + TradeValidationReceiptProofSystem, TradeValidationReceiptResult, + TradeValidationReceiptType, TradeWorkflowSource, + }; use radroots_trade::validation_receipt::{ RadrootsTradeValidationReceipt, RadrootsValidationReceiptProof, RadrootsValidationReceiptProofSystem, RadrootsValidationReceiptResult, @@ -5066,15 +5066,19 @@ mod tests { assert_eq!(decision_report.imported_records, 1); assert_eq!(buyer_orders.rows.len(), 1); - assert_eq!(buyer_orders.rows[0].status, BuyerOrderStatus::Scheduled); - assert_eq!(buyer_detail.status, BuyerOrderStatus::Scheduled); - assert_eq!(seller_orders.rows[0].status, OrderStatus::Scheduled); + assert_eq!(buyer_orders.rows[0].status, BuyerOrderStatus::Placed); + assert_eq!(buyer_detail.status, BuyerOrderStatus::Placed); + assert_eq!(seller_orders.rows[0].status, OrderStatus::NeedsAction); assert_eq!(buyer_detail.workflow, buyer_orders.rows[0].workflow); assert_eq!( seller_orders.rows[0].workflow, buyer_orders.rows[0].workflow ); assert_eq!( + buyer_orders.rows[0].workflow.agreement, + TradeAgreementStatus::PendingRhi + ); + assert_eq!( buyer_orders.rows[0].workflow.inventory, TradeInventoryStatus::Reserved ); @@ -5329,7 +5333,11 @@ mod tests { }, ) .expect("load lifecycle seller orders after decision"); - assert_eq!(seller_orders.rows[0].status, OrderStatus::Scheduled); + assert_eq!(seller_orders.rows[0].status, OrderStatus::NeedsAction); + assert_eq!( + seller_orders.rows[0].workflow.agreement, + TradeAgreementStatus::PendingRhi + ); assert_eq!( seller_orders.rows[0].workflow.inventory, TradeInventoryStatus::Reserved @@ -5411,8 +5419,12 @@ mod tests { .load_local_interop_records() .expect("load validation receipt imports"); - assert_eq!(buyer_detail.status, BuyerOrderStatus::Scheduled); - assert_eq!(seller_detail.status, OrderStatus::Scheduled); + assert_eq!(buyer_detail.status, BuyerOrderStatus::Placed); + assert_eq!(seller_detail.status, OrderStatus::NeedsAction); + assert_eq!( + buyer_detail.workflow.agreement, + TradeAgreementStatus::PendingRhi + ); assert_eq!(seller_detail.primary_action, None); assert_eq!(buyer_detail.validation_receipts.len(), 2); assert_eq!( @@ -5681,8 +5693,12 @@ mod tests { assert!(buyer_detail.validation_receipts.is_empty()); assert!(seller_detail.validation_receipts.is_empty()); - assert_eq!(buyer_detail.status, BuyerOrderStatus::Scheduled); - assert_eq!(seller_detail.status, OrderStatus::Scheduled); + assert_eq!(buyer_detail.status, BuyerOrderStatus::Placed); + assert_eq!(seller_detail.status, OrderStatus::NeedsAction); + assert_eq!( + buyer_detail.workflow.agreement, + TradeAgreementStatus::PendingRhi + ); assert_eq!(seller_detail.primary_action, None); } @@ -5848,7 +5864,11 @@ mod tests { }, ) .expect("load revision seller orders after decision"); - assert_eq!(seller_orders.rows[0].status, OrderStatus::Scheduled); + assert_eq!(seller_orders.rows[0].status, OrderStatus::NeedsAction); + assert_eq!( + seller_orders.rows[0].workflow.agreement, + TradeAgreementStatus::PendingRhi + ); assert_eq!( seller_orders.rows[0].workflow.revision, TradeRevisionStatus::Updated diff --git a/crates/store/src/lib.rs b/crates/store/src/lib.rs @@ -1156,6 +1156,30 @@ mod tests { [], ) .expect("agreement-only workflow projection should insert"); + connection + .execute( + "INSERT INTO orders ( + id, + farm_id, + order_number, + customer_display_name, + status, + updated_at, + workflow_agreement, + workflow_inventory + ) VALUES ( + 'order_pending_rhi', + 'farm_schema', + 'pending rhi', + 'Buyer', + 'needs_action', + '2026-01-01T00:00:00Z', + 'pending_rhi', + 'reserved' + )", + [], + ) + .expect("pending rhi workflow projection should insert"); let invalid_result = connection.execute( "INSERT INTO orders ( @@ -1347,6 +1371,12 @@ mod tests { ) .expect("status should load"); assert_eq!(status, "needs_review"); + connection + .execute( + "UPDATE orders SET workflow_agreement = 'pending_rhi' WHERE id = 'order_legacy'", + [], + ) + .expect("pending rhi agreement should satisfy migrated check"); drop(store); remove_database_artifacts(&path); diff --git a/crates/store/src/migration_audit.rs b/crates/store/src/migration_audit.rs @@ -1,6 +1,5 @@ use std::collections::{BTreeMap, BTreeSet}; -use radroots_studio_app_sync::{AppPublishPayload, SyncOperationKind}; use radroots_events::kinds::{ KIND_FARM, KIND_LISTING, KIND_LISTING_DRAFT, KIND_ORDER_CANCELLATION, KIND_ORDER_DECISION, KIND_ORDER_REQUEST, KIND_ORDER_REVISION_DECISION, KIND_ORDER_REVISION_PROPOSAL, @@ -10,6 +9,7 @@ use radroots_local_events::{ LocalEventRecord, LocalEventsStore, LocalRecordFamily, LocalRecordStatus, PublishOutboxStatus, }; use radroots_sql_core::SqlExecutor; +use radroots_studio_app_sync::{AppPublishPayload, SyncOperationKind}; use rusqlite::params; use serde_json::Value; @@ -839,16 +839,16 @@ fn duplicate_candidates_from_map( #[cfg(test)] mod tests { - use radroots_studio_app_sync::{ - AppFarmProfilePublishPayload, AppPublishContext, AppPublishPayload, PendingSyncOperation, - }; - use radroots_studio_app_view::{FarmId, FarmReadiness}; use radroots_events::kinds::{KIND_LISTING, KIND_ORDER_REQUEST, KIND_TRADE_VALIDATION_RECEIPT}; use radroots_local_events::{ LocalEventRecord, LocalEventRecordInput, LocalEventsStore, LocalRecordFamily, LocalRecordStatus, PublishOutboxStatus, SourceRuntime, }; use radroots_sql_core::SqliteExecutor; + use radroots_studio_app_sync::{ + AppFarmProfilePublishPayload, AppPublishContext, AppPublishPayload, PendingSyncOperation, + }; + use radroots_studio_app_view::{FarmId, FarmReadiness}; use rusqlite::params; use serde_json::json; diff --git a/crates/store/src/migrations.rs b/crates/store/src/migrations.rs @@ -118,6 +118,10 @@ const MIGRATIONS: &[Migration] = &[ version: 28, sql: include_str!("../migrations/0028_sdk_migration_receipts.sql"), }, + Migration { + version: 29, + sql: include_str!("../migrations/0029_order_workflow_pending_rhi_agreement.sql"), + }, ]; pub fn latest_schema_version() -> u32 { diff --git a/crates/store/src/repo/orders.rs b/crates/store/src/repo/orders.rs @@ -7,7 +7,7 @@ use radroots_studio_app_view::{ PackDayOutputCustomerOrder, PackDayOutputOrderState, PackDayOutputPackListEntry, PackDayOutputProductTotal, PackDayOutputQuantity, PackDayOutputSource, PackDayOutputWindow, PackDayPackListRow, PackDayProductTotalRow, PackDayProjection, PackDayRosterRow, - PackDayScreenQueryState, ProductId, TradeWorkflowProjection, + PackDayScreenQueryState, ProductId, TradeAgreementStatus, TradeWorkflowProjection, }; use rusqlite::{Connection, OptionalExtension, params}; @@ -1174,9 +1174,10 @@ fn summarize_orders(records: &[OrderRecord]) -> OrdersListSummary { fn primary_action_for_order( status: OrderStatus, - _workflow: &TradeWorkflowProjection, + workflow: &TradeWorkflowProjection, ) -> Option<OrderPrimaryAction> { match status { + OrderStatus::NeedsAction if workflow.agreement == TradeAgreementStatus::PendingRhi => None, OrderStatus::NeedsAction => Some(OrderPrimaryAction::Review), OrderStatus::Scheduled | OrderStatus::Packed diff --git a/crates/store/src/repo/workflow.rs b/crates/store/src/repo/workflow.rs @@ -39,6 +39,7 @@ fn parse_trade_agreement_status( ) -> Result<TradeAgreementStatus, AppSqliteError> { match value.as_str() { "ordered" => Ok(TradeAgreementStatus::Ordered), + "pending_rhi" => Ok(TradeAgreementStatus::PendingRhi), "confirmed" => Ok(TradeAgreementStatus::Confirmed), "declined" => Ok(TradeAgreementStatus::Declined), "cancelled" => Ok(TradeAgreementStatus::Cancelled), diff --git a/crates/sync/src/publish.rs b/crates/sync/src/publish.rs @@ -1,6 +1,3 @@ -use radroots_studio_app_view::{ - FarmId, FarmReadiness, FulfillmentWindowId, OrderId, ProductId, ProductStatus, -}; use radroots_sdk::protocol::order::{ RadrootsOrderEconomics, RadrootsOrderItem, RadrootsOrderRevisionOutcome, }; @@ -9,6 +6,9 @@ use radroots_sdk::{ ORDER_DECISION_OPERATION_KIND, ORDER_REVISION_DECISION_OPERATION_KIND, ORDER_REVISION_PROPOSAL_OPERATION_KIND, ORDER_SUBMIT_OPERATION_KIND, }; +use radroots_studio_app_view::{ + FarmId, FarmReadiness, FulfillmentWindowId, OrderId, ProductId, ProductStatus, +}; use serde::{Deserialize, Serialize}; use thiserror::Error; @@ -680,10 +680,10 @@ mod tests { use crate::{ PendingSyncOperation, PendingSyncOperationState, SyncAggregateRef, SyncOperationKind, }; - use radroots_studio_app_view::{FarmId, FarmReadiness, OrderId, ProductId, ProductStatus}; use radroots_sdk::protocol::order::{ RadrootsOrderEconomics, RadrootsOrderItem, RadrootsOrderRevisionOutcome, }; + use radroots_studio_app_view::{FarmId, FarmReadiness, OrderId, ProductId, ProductStatus}; use serde_json::json; #[test] diff --git a/crates/view/src/lib.rs b/crates/view/src/lib.rs @@ -4,11 +4,11 @@ pub use radroots_studio_app_types::*; use radroots_core::RadrootsCoreMoney; use radroots_events::order::RadrootsOrderEconomics; -use radroots_trade::order::{RadrootsOrderProjection, RadrootsOrderStatus}; use radroots_trade::validation_receipt::{ RadrootsValidationReceiptProofSystem, RadrootsValidationReceiptResult, RadrootsValidationReceiptType, }; +use radroots_trade::{order::RadrootsOrderProjection, workflow::RadrootsTradeWorkflowState}; use serde::{Deserialize, Serialize}; use std::{collections::BTreeSet, error::Error, fmt, str::FromStr}; use url::Url; @@ -1073,6 +1073,7 @@ pub struct BuyerOrderReviewProjection { pub enum TradeAgreementStatus { #[default] Ordered, + PendingRhi, Confirmed, Declined, Cancelled, @@ -1083,6 +1084,7 @@ impl TradeAgreementStatus { pub const fn storage_key(self) -> &'static str { match self { Self::Ordered => "ordered", + Self::PendingRhi => "pending_rhi", Self::Confirmed => "confirmed", Self::Declined => "declined", Self::Cancelled => "cancelled", @@ -1093,6 +1095,7 @@ impl TradeAgreementStatus { pub const fn label_key_id(self) -> &'static str { match self { Self::Ordered => "messages.trade.workflow.agreement.ordered", + Self::PendingRhi => "messages.trade.workflow.agreement.pending_rhi", Self::Confirmed => "messages.trade.workflow.agreement.confirmed", Self::Declined => "messages.trade.workflow.agreement.declined", Self::Cancelled => "messages.trade.workflow.agreement.cancelled", @@ -1100,20 +1103,22 @@ impl TradeAgreementStatus { } } - pub const fn from_active_order_status(status: &RadrootsOrderStatus) -> Self { + pub const fn from_active_order_status(status: &RadrootsTradeWorkflowState) -> Self { match status { - RadrootsOrderStatus::Missing => Self::NeedsReview, - RadrootsOrderStatus::Requested => Self::Ordered, - RadrootsOrderStatus::Accepted => Self::Confirmed, - RadrootsOrderStatus::Declined => Self::Declined, - RadrootsOrderStatus::Cancelled => Self::Cancelled, - RadrootsOrderStatus::Invalid => Self::NeedsReview, + RadrootsTradeWorkflowState::Missing => Self::NeedsReview, + RadrootsTradeWorkflowState::Requested + | RadrootsTradeWorkflowState::RevisionProposed => Self::Ordered, + RadrootsTradeWorkflowState::AgreedPendingRhi => Self::PendingRhi, + RadrootsTradeWorkflowState::Committed => Self::Confirmed, + RadrootsTradeWorkflowState::Declined => Self::Declined, + RadrootsTradeWorkflowState::Cancelled => Self::Cancelled, + RadrootsTradeWorkflowState::Invalid => Self::NeedsReview, } } } -impl From<&RadrootsOrderStatus> for TradeAgreementStatus { - fn from(status: &RadrootsOrderStatus) -> Self { +impl From<&RadrootsTradeWorkflowState> for TradeAgreementStatus { + fn from(status: &RadrootsTradeWorkflowState) -> Self { Self::from_active_order_status(status) } } @@ -1210,10 +1215,16 @@ impl TradeInventoryStatus { pub fn from_active_order_projection(projection: &RadrootsOrderProjection) -> Self { match projection.status { - RadrootsOrderStatus::Requested => Self::NeedsReview, - RadrootsOrderStatus::Accepted => Self::Reserved, - RadrootsOrderStatus::Declined | RadrootsOrderStatus::Cancelled => Self::Available, - RadrootsOrderStatus::Missing | RadrootsOrderStatus::Invalid => Self::NeedsReview, + RadrootsTradeWorkflowState::Requested + | RadrootsTradeWorkflowState::RevisionProposed => Self::NeedsReview, + RadrootsTradeWorkflowState::AgreedPendingRhi + | RadrootsTradeWorkflowState::Committed => Self::Reserved, + RadrootsTradeWorkflowState::Declined | RadrootsTradeWorkflowState::Cancelled => { + Self::Available + } + RadrootsTradeWorkflowState::Missing | RadrootsTradeWorkflowState::Invalid => { + Self::NeedsReview + } } } } @@ -1566,13 +1577,15 @@ pub fn order_status_from_active_order_projection( projection: &RadrootsOrderProjection, ) -> Option<OrderStatus> { match projection.status { - RadrootsOrderStatus::Missing => None, - RadrootsOrderStatus::Requested => Some(OrderStatus::NeedsAction), - RadrootsOrderStatus::Accepted => Some(OrderStatus::Scheduled), - RadrootsOrderStatus::Declined | RadrootsOrderStatus::Cancelled => { + RadrootsTradeWorkflowState::Missing => None, + RadrootsTradeWorkflowState::Requested + | RadrootsTradeWorkflowState::RevisionProposed + | RadrootsTradeWorkflowState::AgreedPendingRhi => Some(OrderStatus::NeedsAction), + RadrootsTradeWorkflowState::Committed => Some(OrderStatus::Scheduled), + RadrootsTradeWorkflowState::Declined | RadrootsTradeWorkflowState::Cancelled => { Some(OrderStatus::Declined) } - RadrootsOrderStatus::Invalid => Some(OrderStatus::NeedsAction), + RadrootsTradeWorkflowState::Invalid => Some(OrderStatus::NeedsAction), } } @@ -2114,11 +2127,11 @@ mod tests { use radroots_events::order::{ RadrootsOrderEconomicItem, RadrootsOrderEconomics, RadrootsOrderPricingBasis, }; - use radroots_trade::order::{RadrootsOrderProjection, RadrootsOrderStatus}; use radroots_trade::validation_receipt::{ RadrootsValidationReceiptProofSystem, RadrootsValidationReceiptResult, RadrootsValidationReceiptType, }; + use radroots_trade::{order::RadrootsOrderProjection, workflow::RadrootsTradeWorkflowState}; use super::{ AccountCustody, AccountSummary, AccountSurfaceActivationProjection, ActiveSurface, @@ -2822,7 +2835,7 @@ mod tests { } } - fn test_active_order_projection(status: RadrootsOrderStatus) -> RadrootsOrderProjection { + fn test_active_order_projection(status: RadrootsTradeWorkflowState) -> RadrootsOrderProjection { RadrootsOrderProjection { order_id: test_order_id("ord_AAAAAAAAAAAAAAAAAAAAAg"), status, @@ -2832,6 +2845,7 @@ mod tests { decision_event_id: Some(test_event_id( "2222222222222222222222222222222222222222222222222222222222222222", )), + validation_receipt_event_id: None, cancellation_event_id: None, lifecycle_terminal: false, economics: Some(test_trade_economics()), @@ -2839,6 +2853,8 @@ mod tests { "2222222222222222222222222222222222222222222222222222222222222222", )), pending_revision_event_id: None, + pending_inventory_reservations: Vec::new(), + committed_inventory_reservations: Vec::new(), listing_addr: Some(test_listing_addr( "30402:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb:AAAAAAAAAAAAAAAAAAAAAg", )), @@ -2858,15 +2874,21 @@ mod tests { #[test] fn trade_workflow_projection_maps_shared_active_order_projection_to_product_axes() { assert_eq!( - TradeAgreementStatus::from_active_order_status(&RadrootsOrderStatus::Requested), + TradeAgreementStatus::from_active_order_status(&RadrootsTradeWorkflowState::Requested), TradeAgreementStatus::Ordered ); assert_eq!( - TradeAgreementStatus::from_active_order_status(&RadrootsOrderStatus::Accepted), + TradeAgreementStatus::from_active_order_status( + &RadrootsTradeWorkflowState::AgreedPendingRhi + ), + TradeAgreementStatus::PendingRhi + ); + assert_eq!( + TradeAgreementStatus::from_active_order_status(&RadrootsTradeWorkflowState::Committed), TradeAgreementStatus::Confirmed ); assert_eq!( - TradeAgreementStatus::from_active_order_status(&RadrootsOrderStatus::Invalid), + TradeAgreementStatus::from_active_order_status(&RadrootsTradeWorkflowState::Invalid), TradeAgreementStatus::NeedsReview ); assert_eq!( @@ -2897,7 +2919,7 @@ mod tests { ); let order_id = OrderId::new(); - let active_order = test_active_order_projection(RadrootsOrderStatus::Accepted); + let active_order = test_active_order_projection(RadrootsTradeWorkflowState::Committed); let projection = TradeWorkflowProjection::from_active_order_projection( order_id, &active_order, @@ -2922,7 +2944,28 @@ mod tests { Some(OrderStatus::Scheduled) ); - let requested_order = test_active_order_projection(RadrootsOrderStatus::Requested); + let pending_rhi_order = + test_active_order_projection(RadrootsTradeWorkflowState::AgreedPendingRhi); + let pending_rhi_projection = TradeWorkflowProjection::from_active_order_projection( + order_id, + &pending_rhi_order, + TradeRevisionStatus::None, + TradeProvenanceProjection::from_primary_source(TradeWorkflowSource::LocalEvents), + ); + assert_eq!( + pending_rhi_projection.agreement, + TradeAgreementStatus::PendingRhi + ); + assert_eq!( + pending_rhi_projection.inventory, + TradeInventoryStatus::Reserved + ); + assert_eq!( + order_status_from_active_order_projection(&pending_rhi_order), + Some(OrderStatus::NeedsAction) + ); + + let requested_order = test_active_order_projection(RadrootsTradeWorkflowState::Requested); let requested_projection = TradeWorkflowProjection::from_active_order_projection( order_id, &requested_order, @@ -2995,12 +3038,16 @@ mod tests { #[test] fn trade_workflow_projection_uses_localization_key_ids_for_visible_status_labels() { assert_eq!( - TradeAgreementStatus::from_active_order_status(&RadrootsOrderStatus::Requested) + TradeAgreementStatus::from_active_order_status(&RadrootsTradeWorkflowState::Requested) .storage_key(), "ordered" ); assert_eq!(TradeAgreementStatus::Ordered.storage_key(), "ordered"); assert_eq!( + TradeAgreementStatus::PendingRhi.storage_key(), + "pending_rhi" + ); + assert_eq!( TradeRevisionStatus::KeptAsPlaced.storage_key(), "kept_as_placed" ); @@ -3015,6 +3062,10 @@ mod tests { "messages.trade.workflow.agreement.ordered" ); assert_eq!( + TradeAgreementStatus::PendingRhi.label_key_id(), + "messages.trade.workflow.agreement.pending_rhi" + ); + assert_eq!( TradeAgreementStatus::NeedsReview.label_key_id(), "messages.trade.workflow.agreement.needs_review" ); diff --git a/i18n/locales/en/messages.json b/i18n/locales/en/messages.json @@ -412,6 +412,7 @@ "trade.workflow.axis.inventory": "Stock", "trade.workflow.axis.source": "Source", "trade.workflow.agreement.ordered": "Ordered", + "trade.workflow.agreement.pending_rhi": "Pending validation", "trade.workflow.agreement.confirmed": "Confirmed", "trade.workflow.agreement.declined": "Declined", "trade.workflow.agreement.cancelled": "Cancelled", diff --git a/i18n/mf2_i18n.toml b/i18n/mf2_i18n.toml @@ -1,3 +1,7 @@ +version = 1 default_locale = "en" -source_dirs = ["locales"] project_salt_path = "id_salt.txt" + +[[sources]] +path = "locales" +layout = "locale-namespace"