app

Local-first trade for farms and co-ops
git clone https://radroots.dev/git/app.git
Log | Files | Refs | README | LICENSE

commit aadecc93d40c200e594190ed9c5863df811b5f67
parent 9287b509039a6de36453164c750e7777a03a83a6
Author: triesap <tyson@radroots.org>
Date:   Fri, 26 Jun 2026 13:48:12 +0000

runtime: refactor trade workflow projection

Diffstat:
MCargo.lock | 12++++++++++++
Mcrates/desktop/src/runtime.rs | 105+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------
Mcrates/desktop/src/source_guards.rs | 9+++++----
Mcrates/desktop/src/window.rs | 33+++++++++++++++++++++------------
Mcrates/i18n/src/keys.rs | 9+++++----
Mcrates/i18n/src/lib.rs | 18+++++++++++-------
Mcrates/runtime/src/lib.rs | 11++++++-----
Mcrates/runtime/src/sdk.rs | 85+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
Mcrates/store/migrations/0023_order_workflow_display_projection.sql | 13+++++++------
Mcrates/store/migrations/0025_order_workflow_agreement_projection.sql | 30++++++++++++++++++------------
Dcrates/store/migrations/0029_order_workflow_pending_rhi_agreement.sql | 248-------------------------------------------------------------------------------
Acrates/store/migrations/0029_order_workflow_validation_receipt_agreement.sql | 254+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcrates/store/src/interop.rs | 209++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----
Mcrates/store/src/lib.rs | 14+++++++-------
Mcrates/store/src/migrations.rs | 2+-
Mcrates/store/src/repo/buyer.rs | 8++++----
Mcrates/store/src/repo/orders.rs | 14+++++++++-----
Mcrates/store/src/repo/workflow.rs | 9+++++----
Mcrates/view/src/lib.rs | 113++++++++++++++++++++++++++++++++++++++++++++++---------------------------------
Mi18n/locales/en/messages.json | 9+++++----
20 files changed, 796 insertions(+), 409 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -5339,6 +5339,7 @@ dependencies = [ "radroots_outbox", "radroots_relay_transport", "radroots_runtime_paths", + "radroots_sp1_guest_trade", "radroots_trade", "serde", "serde_json", @@ -5353,6 +5354,16 @@ name = "radroots_secret_vault" version = "0.1.0-alpha.2" [[package]] +name = "radroots_sp1_guest_trade" +version = "0.1.0-alpha.2" +dependencies = [ + "serde", + "serde_json", + "sha2", + "thiserror 1.0.69", +] + +[[package]] name = "radroots_sql_core" version = "0.1.0-alpha.2" dependencies = [ @@ -5531,6 +5542,7 @@ dependencies = [ "serde", "serde_json", "sha2", + "sqlx", "thiserror 1.0.69", ] diff --git a/crates/desktop/src/runtime.rs b/crates/desktop/src/runtime.rs @@ -39,10 +39,11 @@ 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::farm::{RadrootsFarm, RadrootsFarmPublicLocation, RadrootsFarmRef}; use radroots_sdk::protocol::listing::{ RadrootsListing, RadrootsListingAvailability, RadrootsListingBin, - RadrootsListingDeliveryMethod, RadrootsListingProduct, RadrootsListingStatus, + RadrootsListingDeliveryMethod, RadrootsListingProduct, RadrootsListingPublicLocation, + RadrootsListingStatus, }; use radroots_sdk::protocol::order::{ RadrootsOrderCancellation, RadrootsOrderDecision, RadrootsOrderDecisionOutcome, @@ -59,9 +60,10 @@ use radroots_sql_core::SqliteExecutor; use radroots_studio_app_core::{ AppBuildIdentity, AppDesktopRuntimePaths, AppRuntimeCapture, AppRuntimeMode, AppRuntimePathsError, AppRuntimeSnapshot, AppSdkConfig, AppSdkDiagnostics, - AppSdkFarmPublishRequest, AppSdkLifecycleState, AppSdkListingPublishRequest, - AppSdkOrderCancellationRequest, AppSdkOrderDecisionRequest, AppSdkOrderRevisionDecisionRequest, - AppSdkOrderRevisionProposalRequest, AppSdkOrderSubmitRequest, AppSdkProjectionLifecycleState, + AppSdkFarmPublicLocationRequest, AppSdkFarmPublishRequest, AppSdkLifecycleState, + AppSdkListingPublishRequest, AppSdkOrderCancellationRequest, AppSdkOrderDecisionRequest, + AppSdkOrderRevisionDecisionRequest, AppSdkOrderRevisionProposalRequest, + AppSdkOrderSubmitRequest, AppSdkProjectionLifecycleState, AppSdkPublicFarmLocation, AppSdkRelayUrlPolicy, AppSdkRuntime, AppSdkRuntimeError, AppSdkRuntimeIssue, AppSdkRuntimeStatus, AppSdkStoragePaths, AppSdkWorkflowReceipt, AppSharedAccountsPaths, PackDayExportWriteError, prepare_pack_day_export_bundle_at_data_root, @@ -4688,11 +4690,16 @@ impl DesktopAppRuntimeState { )) .and_then(|identity| { let actor_pubkey = identity.public_key_hex(); + let public_location = + self.sdk_public_farm_location(actor_pubkey.as_str(), payload.farm_id)?; let request = AppSdkFarmPublishRequest { actor_account_id: payload.context.account_id.clone(), actor_pubkey: actor_pubkey.clone(), signer_keys: identity.into_keys(), - farm: farm_profile_publish_payload_to_sdk_farm(payload), + farm: farm_profile_publish_payload_to_sdk_farm( + payload, + public_location.as_ref(), + ), target_relays: normalized_app_sync_relay_urls(&self.nostr_relay_urls)?, relay_url_policy: sdk_relay_url_policy_for_targets(&self.nostr_relay_urls), idempotency_key: Some(sdk_idempotency_key(source_record_id)), @@ -4732,11 +4739,20 @@ impl DesktopAppRuntimeState { )) .and_then(|identity| { let actor_pubkey = identity.public_key_hex(); + let public_location = match payload.farm_id { + Some(farm_id) => { + self.sdk_public_farm_location(actor_pubkey.as_str(), farm_id)? + } + None => None, + }; let request = AppSdkListingPublishRequest { actor_account_id: payload.context.account_id.clone(), actor_pubkey: actor_pubkey.clone(), signer_keys: identity.into_keys(), - listing: listing_publish_payload_to_sdk_listing(payload)?, + listing: listing_publish_payload_to_sdk_listing( + payload, + public_location.as_ref(), + )?, target_relays: normalized_app_sync_relay_urls(&self.nostr_relay_urls)?, relay_url_policy: sdk_relay_url_policy_for_targets(&self.nostr_relay_urls), idempotency_key: Some(sdk_idempotency_key(source_record_id)), @@ -5052,6 +5068,19 @@ impl DesktopAppRuntimeState { signing_identity_for_publish_payload(accounts_manager, payload) } + fn sdk_public_farm_location( + &self, + actor_pubkey: &str, + farm_id: FarmId, + ) -> Result<Option<AppSdkPublicFarmLocation>, AppSyncTransportError> { + let request = AppSdkFarmPublicLocationRequest { + actor_pubkey: actor_pubkey.to_owned(), + farm_d_tag: d_tag_from_uuid(farm_id.as_uuid()), + }; + self.with_app_sdk_runtime(|runtime| runtime.farm_public_location(request)) + .map_err(sync_transport_error_from_sdk_runtime_error) + } + fn enqueue_app_sdk_farm_publish( &self, request: AppSdkFarmPublishRequest, @@ -7314,6 +7343,7 @@ fn listing_fulfillment_location( fn farm_profile_publish_payload_to_sdk_farm( payload: &AppFarmProfilePublishPayload, + public_location: Option<&AppSdkPublicFarmLocation>, ) -> RadrootsFarm { RadrootsFarm { d_tag: d_tag_from_uuid(payload.farm_id.as_uuid()), @@ -7322,7 +7352,7 @@ fn farm_profile_publish_payload_to_sdk_farm( website: None, picture: None, banner: None, - location: None, + location: public_location.map(public_farm_location_to_protocol), tags: payload.readiness.map(|readiness| match readiness { FarmReadiness::Incomplete => vec!["radroots:readiness:incomplete".to_owned()], FarmReadiness::Ready => vec!["radroots:readiness:ready".to_owned()], @@ -7513,6 +7543,7 @@ fn sync_transport_error_detail_json(error: &AppSyncTransportError) -> serde_json fn listing_publish_payload_to_sdk_listing( payload: &AppListingPublishPayload, + public_location: Option<&AppSdkPublicFarmLocation>, ) -> Result<RadrootsListing, AppSyncTransportError> { let currency = payload .price_currency @@ -7598,12 +7629,36 @@ fn listing_publish_payload_to_sdk_listing( delivery_method: Some(parse_app_listing_delivery_method( payload.fulfillment_method.as_deref().unwrap_or_default(), )?), - location: None, + location: public_location.map(public_listing_location_to_protocol), published_at: None, images: None, }) } +fn public_farm_location_to_protocol( + location: &AppSdkPublicFarmLocation, +) -> RadrootsFarmPublicLocation { + RadrootsFarmPublicLocation { + primary: location.primary.clone(), + city: location.city.clone(), + region: location.region.clone(), + country: location.country.clone(), + geohash: location.geohash5.clone(), + } +} + +fn public_listing_location_to_protocol( + location: &AppSdkPublicFarmLocation, +) -> RadrootsListingPublicLocation { + RadrootsListingPublicLocation { + primary: location.primary.clone(), + city: location.city.clone(), + region: location.region.clone(), + country: location.country.clone(), + geohash: location.geohash5.clone(), + } +} + fn listing_publish_payload_availability( payload: &AppListingPublishPayload, ) -> Result<Option<RadrootsListingAvailability>, AppSyncTransportError> { @@ -9796,8 +9851,8 @@ mod tests { use radroots_sql_core::{SqlExecutor, SqliteExecutor}; use radroots_studio_app_core::{ AppDesktopRuntimePaths, AppRuntimeHostEnvironment, AppRuntimePlatform, - AppSdkLifecycleState, AppSdkProjectionLifecycleState, AppSharedAccountsPaths, - SHARED_ACCOUNTS_STORE_FILE_NAME, SHARED_IDENTITY_FILE_NAME, + AppSdkLifecycleState, AppSdkProjectionLifecycleState, AppSdkPublicFarmLocation, + AppSharedAccountsPaths, SHARED_ACCOUNTS_STORE_FILE_NAME, SHARED_IDENTITY_FILE_NAME, }; use radroots_studio_app_remote_signer::{ RadrootsAppRemoteSignerPendingSession, RadrootsAppRemoteSignerSessionRecord, @@ -10772,17 +10827,16 @@ mod tests { fulfillment_location: Some("Relay barn".to_owned()), status: ProductStatus::Published, }; - let mut listing = super::listing_publish_payload_to_sdk_listing(&listing_payload) - .expect("listing payload should convert to SDK listing"); - listing.location = Some( - radroots_sdk::protocol::listing::RadrootsListingPublicLocation { - primary: "Relay barn".to_owned(), - city: Some("San Francisco".to_owned()), - region: Some("CA".to_owned()), - country: Some("US".to_owned()), - geohash: "9q8yy".to_owned(), - }, - ); + let public_location = AppSdkPublicFarmLocation { + primary: "Relay barn".to_owned(), + city: Some("San Francisco".to_owned()), + region: Some("CA".to_owned()), + country: Some("US".to_owned()), + geohash5: "9q8yy".to_owned(), + }; + let listing = + super::listing_publish_payload_to_sdk_listing(&listing_payload, Some(&public_location)) + .expect("listing payload should convert to SDK listing"); let parts = radroots_sdk::protocol::listing::build_draft(&listing) .expect("listing draft should build") .into_wire_parts(); @@ -15406,7 +15460,7 @@ mod tests { assert_eq!(persisted_order_status(&runtime, order_id), "needs_action"); assert_eq!( persisted_order_workflow_agreement(&runtime, order_id), - "pending_rhi" + "agreed_pending_rhi" ); assert_eq!(relay.event_count(), 1); @@ -16136,7 +16190,10 @@ mod tests { assert_eq!(row.status, BuyerOrderStatus::Placed); assert_eq!(detail.order_id, fixture.order_id); assert_eq!(detail.status, BuyerOrderStatus::Placed); - assert_eq!(detail.workflow.agreement, TradeAgreementStatus::PendingRhi); + assert_eq!( + detail.workflow.agreement, + TradeAgreementStatus::AgreedPendingRhi + ); assert_eq!( detail.workflow.provenance.last_event_id.as_deref(), Some(fixture.decision_event_id.as_str()) diff --git a/crates/desktop/src/source_guards.rs b/crates/desktop/src/source_guards.rs @@ -712,12 +712,13 @@ const REQUIRED_WINDOW_COPY_KEYS: &[&str] = &[ "AppTextKey::TradeWorkflowAxisRevision", "AppTextKey::TradeWorkflowAxisInventory", "AppTextKey::TradeWorkflowAxisSource", - "AppTextKey::TradeWorkflowAgreementOrdered", - "AppTextKey::TradeWorkflowAgreementPendingRhi", - "AppTextKey::TradeWorkflowAgreementConfirmed", + "AppTextKey::TradeWorkflowAgreementRequested", + "AppTextKey::TradeWorkflowAgreementRevisionProposed", + "AppTextKey::TradeWorkflowAgreementAgreedPendingRhi", + "AppTextKey::TradeWorkflowAgreementCommitted", "AppTextKey::TradeWorkflowAgreementDeclined", "AppTextKey::TradeWorkflowAgreementCancelled", - "AppTextKey::TradeWorkflowAgreementNeedsReview", + "AppTextKey::TradeWorkflowAgreementInvalid", "AppTextKey::TradeWorkflowRevisionNone", "AppTextKey::TradeWorkflowRevisionChangeProposed", "AppTextKey::TradeWorkflowRevisionUpdated", diff --git a/crates/desktop/src/window.rs b/crates/desktop/src/window.rs @@ -11615,12 +11615,17 @@ 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::Requested => AppTextKey::TradeWorkflowAgreementRequested, + TradeAgreementStatus::RevisionProposed => { + AppTextKey::TradeWorkflowAgreementRevisionProposed + } + TradeAgreementStatus::AgreedPendingRhi => { + AppTextKey::TradeWorkflowAgreementAgreedPendingRhi + } + TradeAgreementStatus::Committed => AppTextKey::TradeWorkflowAgreementCommitted, TradeAgreementStatus::Declined => AppTextKey::TradeWorkflowAgreementDeclined, TradeAgreementStatus::Cancelled => AppTextKey::TradeWorkflowAgreementCancelled, - TradeAgreementStatus::NeedsReview => AppTextKey::TradeWorkflowAgreementNeedsReview, + TradeAgreementStatus::Invalid => AppTextKey::TradeWorkflowAgreementInvalid, } } @@ -16723,16 +16728,20 @@ mod tests { fn trade_workflow_badge_keys_cover_refactored_status_axes() { for (status, key) in [ ( - TradeAgreementStatus::Ordered, - AppTextKey::TradeWorkflowAgreementOrdered, + TradeAgreementStatus::Requested, + AppTextKey::TradeWorkflowAgreementRequested, + ), + ( + TradeAgreementStatus::RevisionProposed, + AppTextKey::TradeWorkflowAgreementRevisionProposed, ), ( - TradeAgreementStatus::PendingRhi, - AppTextKey::TradeWorkflowAgreementPendingRhi, + TradeAgreementStatus::AgreedPendingRhi, + AppTextKey::TradeWorkflowAgreementAgreedPendingRhi, ), ( - TradeAgreementStatus::Confirmed, - AppTextKey::TradeWorkflowAgreementConfirmed, + TradeAgreementStatus::Committed, + AppTextKey::TradeWorkflowAgreementCommitted, ), ( TradeAgreementStatus::Declined, @@ -16743,8 +16752,8 @@ mod tests { AppTextKey::TradeWorkflowAgreementCancelled, ), ( - TradeAgreementStatus::NeedsReview, - AppTextKey::TradeWorkflowAgreementNeedsReview, + TradeAgreementStatus::Invalid, + AppTextKey::TradeWorkflowAgreementInvalid, ), ] { assert_eq!(trade_agreement_status_key(status), key); diff --git a/crates/i18n/src/keys.rs b/crates/i18n/src/keys.rs @@ -431,12 +431,13 @@ define_app_text_keys! { TradeWorkflowAxisRevision => "trade.workflow.axis.revision", 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", + TradeWorkflowAgreementRequested => "trade.workflow.agreement.requested", + TradeWorkflowAgreementRevisionProposed => "trade.workflow.agreement.revision_proposed", + TradeWorkflowAgreementAgreedPendingRhi => "trade.workflow.agreement.agreed_pending_rhi", + TradeWorkflowAgreementCommitted => "trade.workflow.agreement.committed", TradeWorkflowAgreementDeclined => "trade.workflow.agreement.declined", TradeWorkflowAgreementCancelled => "trade.workflow.agreement.cancelled", - TradeWorkflowAgreementNeedsReview => "trade.workflow.agreement.needs_review", + TradeWorkflowAgreementInvalid => "trade.workflow.agreement.invalid", TradeWorkflowRevisionNone => "trade.workflow.revision.none", TradeWorkflowRevisionChangeProposed => "trade.workflow.revision.change_proposed", TradeWorkflowRevisionUpdated => "trade.workflow.revision.updated", diff --git a/crates/i18n/src/lib.rs b/crates/i18n/src/lib.rs @@ -635,20 +635,24 @@ mod tests { ); assert_eq!(app_text(AppTextKey::TradeWorkflowAxisSource), "Source"); assert_eq!( - app_text(AppTextKey::TradeWorkflowAgreementOrdered), - "Ordered" + app_text(AppTextKey::TradeWorkflowAgreementRequested), + "Requested" + ); + assert_eq!( + app_text(AppTextKey::TradeWorkflowAgreementRevisionProposed), + "Revision proposed" ); assert_eq!( - app_text(AppTextKey::TradeWorkflowAgreementPendingRhi), + app_text(AppTextKey::TradeWorkflowAgreementAgreedPendingRhi), "Pending validation" ); assert_eq!( - app_text(AppTextKey::TradeWorkflowAgreementConfirmed), - "Confirmed" + app_text(AppTextKey::TradeWorkflowAgreementCommitted), + "Committed" ); assert_eq!( - app_text(AppTextKey::TradeWorkflowAgreementNeedsReview), - "Needs review" + app_text(AppTextKey::TradeWorkflowAgreementInvalid), + "Invalid" ); assert_eq!( app_text(AppTextKey::TradeWorkflowRevisionChangeProposed), diff --git a/crates/runtime/src/lib.rs b/crates/runtime/src/lib.rs @@ -37,11 +37,12 @@ pub use runtime::{ }; pub use sdk::{ APP_SDK_DEFAULT_COMMAND_QUEUE_CAPACITY, APP_SDK_STORAGE_DIR_NAME, AppSdkConfig, - AppSdkDiagnostics, AppSdkEventStoreDiagnostics, AppSdkFarmPublishRequest, - AppSdkIntegrityDiagnostics, AppSdkLifecycleState, AppSdkListingPublishRequest, - AppSdkOrderCancellationRequest, AppSdkOrderDecisionRequest, AppSdkOrderRevisionDecisionRequest, - AppSdkOrderRevisionProposalRequest, AppSdkOrderSubmitRequest, AppSdkOutboxDiagnostics, - AppSdkProjectionLifecycleState, AppSdkProjectionLifecycleStatus, AppSdkRelayUrlPolicy, + AppSdkDiagnostics, AppSdkEventStoreDiagnostics, AppSdkFarmPublicLocationRequest, + AppSdkFarmPublishRequest, AppSdkIntegrityDiagnostics, AppSdkLifecycleState, + AppSdkListingPublishRequest, AppSdkOrderCancellationRequest, AppSdkOrderDecisionRequest, + AppSdkOrderRevisionDecisionRequest, AppSdkOrderRevisionProposalRequest, + AppSdkOrderSubmitRequest, AppSdkOutboxDiagnostics, AppSdkProjectionLifecycleState, + AppSdkProjectionLifecycleStatus, AppSdkPublicFarmLocation, AppSdkRelayUrlPolicy, AppSdkRestorePreflightReceipt, AppSdkRestorePreflightRequest, AppSdkRuntime, AppSdkRuntimeError, AppSdkRuntimeIssue, AppSdkRuntimeStatus, AppSdkSqliteStoreDiagnostics, AppSdkStorageDiagnostics, AppSdkStoragePaths, AppSdkSyncDiagnostics, diff --git a/crates/runtime/src/sdk.rs b/crates/runtime/src/sdk.rs @@ -15,6 +15,8 @@ use radroots_events::{ RadrootsNostrEvent, RadrootsNostrEventPtr, contract::RadrootsActorRole, farm::RadrootsFarm, + ids::RadrootsAddressableCoordinate, + kinds::KIND_FARM, listing::RadrootsListing, order::{ RadrootsOrderCancellation, RadrootsOrderDecision, RadrootsOrderRequest, @@ -33,8 +35,8 @@ use radroots_sdk::{ OrderRevisionDecisionReceipt, OrderRevisionProposalEnqueueRequest, OrderRevisionProposalReceipt, OrderSubmitEnqueueRequest, OrderSubmitReceipt, RadrootsClient, RadrootsSdkError, RadrootsSdkStoragePaths, RestoreReceipt, RestoreRequest, - SdkBackupVerification, SdkRelayUrlPolicy as SdkRuntimeRelayUrlPolicy, StorageStatusReceipt, - StorageStatusRequest, SyncStatusReceipt, SyncStatusRequest, + SdkBackupVerification, SdkPublicLocality, SdkRelayUrlPolicy as SdkRuntimeRelayUrlPolicy, + StorageStatusReceipt, StorageStatusRequest, SyncStatusReceipt, SyncStatusRequest, }; use radroots_sdk::{SdkMutationState, SdkRelayTargetPolicy}; use serde::Serialize; @@ -203,6 +205,21 @@ pub struct AppSdkRestorePreflightRequest { pub overwrite_existing_sdk_storage: bool, } +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct AppSdkFarmPublicLocationRequest { + pub actor_pubkey: String, + pub farm_d_tag: String, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct AppSdkPublicFarmLocation { + pub primary: String, + pub city: Option<String>, + pub region: Option<String>, + pub country: Option<String>, + pub geohash5: String, +} + pub struct AppSdkFarmPublishRequest { pub actor_account_id: String, pub actor_pubkey: String, @@ -377,6 +394,10 @@ enum AppSdkWorkerCommand { AppSdkRestorePreflightRequest, mpsc::Sender<Result<AppSdkRestorePreflightReceipt, AppSdkRuntimeIssue>>, ), + FarmPublicLocation( + AppSdkFarmPublicLocationRequest, + mpsc::Sender<Result<Option<AppSdkPublicFarmLocation>, AppSdkRuntimeIssue>>, + ), EnqueueFarmPublish( AppSdkFarmPublishRequest, mpsc::Sender<Result<AppSdkWorkflowReceipt, AppSdkRuntimeIssue>>, @@ -421,6 +442,7 @@ impl fmt::Debug for AppSdkWorkerCommand { Self::SyncStatus(_) => formatter.write_str("SyncStatus"), Self::Diagnostics(_) => formatter.write_str("Diagnostics"), Self::RestorePreflight(_, _) => formatter.write_str("RestorePreflight"), + Self::FarmPublicLocation(_, _) => formatter.write_str("FarmPublicLocation"), Self::EnqueueFarmPublish(_, _) => formatter.write_str("EnqueueFarmPublish"), Self::EnqueueListingPublish(_, _) => formatter.write_str("EnqueueListingPublish"), Self::EnqueueOrderSubmit(_, _) => formatter.write_str("EnqueueOrderSubmit"), @@ -553,6 +575,15 @@ impl AppSdkRuntime { }) } + pub fn farm_public_location( + &self, + request: AppSdkFarmPublicLocationRequest, + ) -> Result<Option<AppSdkPublicFarmLocation>, AppSdkRuntimeError> { + self.run_command(|response_sender| { + AppSdkWorkerCommand::FarmPublicLocation(request, response_sender) + }) + } + pub fn enqueue_farm_publish( &self, request: AppSdkFarmPublishRequest, @@ -790,6 +821,18 @@ impl AppSdkRuntimeIssue { } } +impl From<SdkPublicLocality> for AppSdkPublicFarmLocation { + fn from(value: SdkPublicLocality) -> Self { + Self { + primary: value.primary, + city: value.city, + region: value.region, + country: value.country, + geohash5: value.geohash5, + } + } +} + impl fmt::Display for AppSdkRuntimeIssue { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { write!(formatter, "{}: {}", self.code, self.message) @@ -1081,6 +1124,17 @@ fn run_app_sdk_worker( }; send_worker_result(&shared, response_sender, result); } + AppSdkWorkerCommand::FarmPublicLocation(request, response_sender) => { + let result = if let Some(issue) = lifecycle_busy_issue(&shared) { + Err(issue) + } else { + match sdk.as_ref() { + Some(sdk) => farm_public_location_with_sdk(&runtime, sdk, request), + None => Err(runtime_unavailable_issue(&shared)), + } + }; + send_worker_result(&shared, response_sender, result); + } AppSdkWorkerCommand::EnqueueFarmPublish(request, response_sender) => { let result = if let Some(issue) = lifecycle_busy_issue(&shared) { Err(issue) @@ -1229,6 +1283,13 @@ fn run_degraded_worker( Err(runtime_unavailable_issue(&shared)), ); } + AppSdkWorkerCommand::FarmPublicLocation(_, response_sender) => { + send_worker_result( + &shared, + response_sender, + Err(runtime_unavailable_issue(&shared)), + ); + } AppSdkWorkerCommand::EnqueueFarmPublish(_, response_sender) => { send_worker_result( &shared, @@ -1365,6 +1426,26 @@ async fn collect_sdk_diagnostics( }) } +fn farm_public_location_with_sdk( + runtime: &tokio::runtime::Runtime, + sdk: &RadrootsClient, + request: AppSdkFarmPublicLocationRequest, +) -> Result<Option<AppSdkPublicFarmLocation>, AppSdkRuntimeIssue> { + let farm_addr = RadrootsAddressableCoordinate::parse(format!( + "{KIND_FARM}:{}:{}", + request.actor_pubkey, request.farm_d_tag + )) + .map_err(|error| { + AppSdkRuntimeIssue::from_sdk_error(&RadrootsSdkError::InvalidRequest { + message: format!("farm public location address is invalid: {error}"), + }) + })?; + runtime + .block_on(sdk.farms().private_location(&farm_addr)) + .map(|location| location.map(|receipt| receipt.public_locality.into())) + .map_err(|error| AppSdkRuntimeIssue::from_sdk_error(&error)) +} + fn enqueue_farm_publish_with_sdk( runtime: &tokio::runtime::Runtime, sdk: &RadrootsClient, 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', 'pending_rhi', 'confirmed', 'declined', 'cancelled', 'needs_review') + ADD COLUMN workflow_agreement TEXT NOT NULL DEFAULT 'requested' CHECK ( + workflow_agreement IN ('requested', 'revision_proposed', 'agreed_pending_rhi', 'committed', 'declined', 'cancelled', 'invalid') ); ALTER TABLE orders @@ -19,11 +19,12 @@ ALTER TABLE orders UPDATE orders SET workflow_agreement = CASE status - WHEN 'scheduled' THEN 'confirmed' - WHEN 'packed' THEN 'confirmed' - WHEN 'completed' THEN 'confirmed' + WHEN 'scheduled' THEN 'committed' + WHEN 'packed' THEN 'committed' + WHEN 'completed' THEN 'committed' WHEN 'declined' THEN 'declined' - ELSE 'ordered' + WHEN 'needs_review' THEN 'invalid' + ELSE 'requested' END, workflow_inventory = CASE status WHEN 'scheduled' THEN 'reserved' diff --git a/crates/store/migrations/0025_order_workflow_agreement_projection.sql b/crates/store/migrations/0025_order_workflow_agreement_projection.sql @@ -5,9 +5,9 @@ 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_lines RENAME TO order_lines_agreement_legacy; -ALTER TABLE buyer_order_coordination_records RENAME TO buyer_order_coordination_records_agreement_legacy; -ALTER TABLE orders RENAME TO orders_agreement_legacy; +ALTER TABLE order_lines RENAME TO order_lines_agreement_previous; +ALTER TABLE buyer_order_coordination_records RENAME TO buyer_order_coordination_records_agreement_previous; +ALTER TABLE orders RENAME TO orders_agreement_previous; CREATE TABLE orders ( id TEXT PRIMARY KEY NOT NULL, @@ -26,8 +26,8 @@ CREATE TABLE orders ( 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_agreement TEXT NOT NULL DEFAULT 'requested' CHECK ( + workflow_agreement IN ('requested', 'revision_proposed', 'agreed_pending_rhi', 'committed', 'declined', 'cancelled', 'invalid') ), workflow_inventory TEXT NOT NULL DEFAULT 'needs_review' CHECK ( workflow_inventory IN ('available', 'reserved', 'sold_out', 'needs_review') @@ -69,11 +69,17 @@ SELECT buyer_phone, buyer_order_note, workflow_revision, - workflow_agreement, + CASE workflow_agreement + WHEN 'ordered' THEN 'requested' + WHEN 'pending_rhi' THEN 'agreed_pending_rhi' + WHEN 'confirmed' THEN 'committed' + WHEN 'needs_review' THEN 'invalid' + ELSE workflow_agreement + END, workflow_inventory, workflow_provenance_source, workflow_provenance_last_event_id -FROM orders_agreement_legacy; +FROM orders_agreement_previous; CREATE TABLE order_lines ( id TEXT PRIMARY KEY NOT NULL, @@ -128,7 +134,7 @@ SELECT listing_event_id, seller_pubkey, listing_relays_json -FROM order_lines_agreement_legacy; +FROM order_lines_agreement_previous; CREATE TABLE buyer_order_coordination_records ( order_id TEXT PRIMARY KEY NOT NULL REFERENCES orders(id) ON DELETE CASCADE, @@ -166,7 +172,7 @@ SELECT created_at, updated_at, synced_at -FROM buyer_order_coordination_records_agreement_legacy; +FROM buyer_order_coordination_records_agreement_previous; CREATE INDEX idx_orders_farm_status ON orders(farm_id, status); CREATE INDEX idx_orders_farm_window_status_updated_at @@ -181,6 +187,6 @@ CREATE INDEX idx_buyer_order_coordination_context_state_updated_at CREATE INDEX idx_buyer_order_coordination_state_updated_at ON buyer_order_coordination_records(state, updated_at); -DROP TABLE order_lines_agreement_legacy; -DROP TABLE buyer_order_coordination_records_agreement_legacy; -DROP TABLE orders_agreement_legacy; +DROP TABLE order_lines_agreement_previous; +DROP TABLE buyer_order_coordination_records_agreement_previous; +DROP TABLE orders_agreement_previous; diff --git a/crates/store/migrations/0029_order_workflow_pending_rhi_agreement.sql b/crates/store/migrations/0029_order_workflow_pending_rhi_agreement.sql @@ -1,248 +0,0 @@ -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/migrations/0029_order_workflow_validation_receipt_agreement.sql b/crates/store/migrations/0029_order_workflow_validation_receipt_agreement.sql @@ -0,0 +1,254 @@ +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_workflow_previous; +ALTER TABLE order_lines RENAME TO order_lines_workflow_previous; +ALTER TABLE buyer_order_coordination_records RENAME TO buyer_order_coordination_records_workflow_previous; +ALTER TABLE orders RENAME TO orders_workflow_previous; + +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 'requested' CHECK ( + workflow_agreement IN ('requested', 'revision_proposed', 'agreed_pending_rhi', 'committed', 'declined', 'cancelled', 'invalid') + ), + 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, + CASE workflow_agreement + WHEN 'ordered' THEN 'requested' + WHEN 'pending_rhi' THEN 'agreed_pending_rhi' + WHEN 'confirmed' THEN 'committed' + WHEN 'needs_review' THEN 'invalid' + ELSE workflow_agreement + END, + workflow_inventory, + workflow_provenance_source, + workflow_provenance_last_event_id +FROM orders_workflow_previous; + +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_workflow_previous; + +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_workflow_previous; + +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_workflow_previous; + +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_workflow_previous; +DROP TABLE order_lines_workflow_previous; +DROP TABLE buyer_order_coordination_records_workflow_previous; +DROP TABLE orders_workflow_previous; diff --git a/crates/store/src/interop.rs b/crates/store/src/interop.rs @@ -30,10 +30,10 @@ use radroots_local_events::{ 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, + FulfillmentWindowId, OrderId, OrderStatus, PickupLocationId, ProductId, ProductStatus, + TradeAgreementStatus, TradeInventoryStatus, TradeProvenanceProjection, TradeRevisionStatus, + TradeValidationReceiptProofSystem, TradeValidationReceiptResult, TradeValidationReceiptType, + TradeWorkflowProjection, TradeWorkflowSource, order_status_from_active_order_projection, }; use radroots_trade::order::{ RadrootsOrderCancellationRecord, RadrootsOrderDecisionRecord, RadrootsOrderProjection, @@ -1318,6 +1318,7 @@ impl<'a> AppLocalInteropRepository<'a> { { self.replace_active_order_agreement_lines(workflow.order_id, agreement_source)?; } + self.apply_validation_receipt_workflow_projection(workflow.order_id)?; Ok(()) } @@ -1389,6 +1390,9 @@ impl<'a> AppLocalInteropRepository<'a> { operation: "upsert local interop validation receipt projection", source, })?; + if let Some(order_id) = order_id { + self.apply_validation_receipt_workflow_projection(order_id)?; + } Ok(()) } @@ -1434,6 +1438,109 @@ impl<'a> AppLocalInteropRepository<'a> { operation: "attach local interop validation receipts to request", source, })?; + self.apply_validation_receipt_workflow_projection(order_id)?; + Ok(()) + } + + fn apply_validation_receipt_workflow_projection( + &self, + order_id: OrderId, + ) -> Result<(), AppSqliteError> { + let current: Option<(String, String, Option<String>)> = self + .connection + .query_row( + "SELECT status, workflow_agreement, workflow_provenance_last_event_id + FROM orders + WHERE id = ?1", + params![order_id.to_string()], + |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?)), + ) + .optional() + .map_err(|source| AppSqliteError::Query { + operation: "load order workflow before validation receipt projection", + source, + })?; + let Some((status, agreement, Some(target_event_id))) = current else { + return Ok(()); + }; + if !matches!( + agreement.as_str(), + value if value == TradeAgreementStatus::AgreedPendingRhi.storage_key() + || value == TradeAgreementStatus::Committed.storage_key() + || value == TradeAgreementStatus::Invalid.storage_key() + ) { + return Ok(()); + } + if !matches!( + status.as_str(), + value if value == OrderStatus::NeedsAction.storage_key() + || value == OrderStatus::Scheduled.storage_key() + || value == OrderStatus::NeedsReview.storage_key() + ) { + return Ok(()); + } + let latest_receipt: Option<String> = self + .connection + .query_row( + "SELECT result + FROM order_validation_receipts + WHERE order_id = ?1 + AND receipt_type = 'trade_transition' + AND target_event_id = ?2 + ORDER BY event_created_at DESC, event_id DESC + LIMIT 1", + params![order_id.to_string(), target_event_id.as_str()], + |row| row.get(0), + ) + .optional() + .map_err(|source| AppSqliteError::Query { + operation: "load latest validation receipt workflow projection", + source, + })?; + let Some(result) = latest_receipt else { + return Ok(()); + }; + let (status, agreement, inventory) = match result.as_str() { + "valid" => ( + OrderStatus::Scheduled.storage_key(), + TradeAgreementStatus::Committed.storage_key(), + TradeInventoryStatus::Reserved.storage_key(), + ), + "needs_review" => ( + OrderStatus::NeedsReview.storage_key(), + TradeAgreementStatus::Invalid.storage_key(), + TradeInventoryStatus::NeedsReview.storage_key(), + ), + _ => { + return Err(AppSqliteError::DecodeEnum { + field: "order_validation_receipts.result", + value: result, + }); + } + }; + self.connection + .execute( + "UPDATE orders + SET status = ?2, + workflow_agreement = ?3, + workflow_inventory = ?4, + workflow_provenance_source = ?5, + workflow_provenance_last_event_id = ?6, + updated_at = strftime('%Y-%m-%dT%H:%M:%SZ', 'now') + WHERE id = ?1", + params![ + order_id.to_string(), + status, + agreement, + inventory, + TradeWorkflowSource::LocalEvents.storage_key(), + target_event_id, + ], + ) + .map_err(|source| AppSqliteError::Query { + operation: "apply validation receipt workflow projection", + source, + })?; Ok(()) } @@ -5076,7 +5183,7 @@ mod tests { ); assert_eq!( buyer_orders.rows[0].workflow.agreement, - TradeAgreementStatus::PendingRhi + TradeAgreementStatus::AgreedPendingRhi ); assert_eq!( buyer_orders.rows[0].workflow.inventory, @@ -5336,7 +5443,7 @@ mod tests { assert_eq!(seller_orders.rows[0].status, OrderStatus::NeedsAction); assert_eq!( seller_orders.rows[0].workflow.agreement, - TradeAgreementStatus::PendingRhi + TradeAgreementStatus::AgreedPendingRhi ); assert_eq!( seller_orders.rows[0].workflow.inventory, @@ -5346,8 +5453,72 @@ mod tests { } #[test] - fn validation_receipts_project_passively_on_buyer_and_seller_order_details() { - let fixture = validation_receipt_order_fixture("validation-receipt-passive"); + fn valid_validation_receipt_commits_buyer_and_seller_order_details() { + let fixture = validation_receipt_order_fixture("validation-receipt-valid-commit"); + let valid_event = validation_receipt_event( + hex_event_id(29).as_str(), + fixture.seller_pubkey.as_str(), + fixture.order_id_raw.as_str(), + fixture.listing_event_id.as_str(), + fixture.request_event_id.as_str(), + fixture.decision_event_id.as_str(), + RadrootsValidationReceiptResult::Valid, + 1_777_665_603, + ); + fixture + .events + .append_record(&signed_order_event_record( + "cli:signed_event:validation-receipt:valid-commit", + &valid_event, + fixture.listing_addr.as_str(), + SourceRuntime::Cli, + None, + )) + .expect("append valid validation receipt"); + fixture + .app_store + .import_shared_local_events_from_store(&fixture.events) + .expect("import valid validation receipt"); + + let buyer_detail = fixture + .app_store + .load_buyer_order_detail(&fixture.buyer_context, fixture.order_id) + .expect("load buyer committed validation receipt detail") + .expect("buyer committed validation receipt detail"); + let seller_detail = fixture + .app_store + .load_order_detail(fixture.seller_farm_id, fixture.order_id) + .expect("load seller committed validation receipt detail") + .expect("seller committed validation receipt detail"); + + assert_eq!(buyer_detail.status, BuyerOrderStatus::Scheduled); + assert_eq!(seller_detail.status, OrderStatus::Scheduled); + assert_eq!( + buyer_detail.workflow.agreement, + TradeAgreementStatus::Committed + ); + assert_eq!( + seller_detail.workflow.agreement, + TradeAgreementStatus::Committed + ); + assert_eq!( + buyer_detail.workflow.inventory, + TradeInventoryStatus::Reserved + ); + assert_eq!( + buyer_detail.workflow.provenance.last_event_id.as_deref(), + Some(fixture.decision_event_id.as_str()) + ); + assert_eq!(buyer_detail.validation_receipts.len(), 1); + assert_eq!( + buyer_detail.validation_receipts[0].result, + TradeValidationReceiptResult::Valid + ); + } + + #[test] + fn validation_receipts_drive_buyer_and_seller_order_details() { + let fixture = validation_receipt_order_fixture("validation-receipt-driven"); let valid_event = validation_receipt_event( hex_event_id(31).as_str(), fixture.seller_pubkey.as_str(), @@ -5419,11 +5590,23 @@ mod tests { .load_local_interop_records() .expect("load validation receipt imports"); - assert_eq!(buyer_detail.status, BuyerOrderStatus::Placed); - assert_eq!(seller_detail.status, OrderStatus::NeedsAction); + assert_eq!(buyer_detail.status, BuyerOrderStatus::NeedsReview); + assert_eq!(seller_detail.status, OrderStatus::NeedsReview); assert_eq!( buyer_detail.workflow.agreement, - TradeAgreementStatus::PendingRhi + TradeAgreementStatus::Invalid + ); + assert_eq!( + seller_detail.workflow.agreement, + TradeAgreementStatus::Invalid + ); + assert_eq!( + buyer_detail.workflow.inventory, + TradeInventoryStatus::NeedsReview + ); + assert_eq!( + buyer_detail.workflow.provenance.last_event_id.as_deref(), + Some(fixture.decision_event_id.as_str()) ); assert_eq!(seller_detail.primary_action, None); assert_eq!(buyer_detail.validation_receipts.len(), 2); @@ -5697,7 +5880,7 @@ mod tests { assert_eq!(seller_detail.status, OrderStatus::NeedsAction); assert_eq!( buyer_detail.workflow.agreement, - TradeAgreementStatus::PendingRhi + TradeAgreementStatus::AgreedPendingRhi ); assert_eq!(seller_detail.primary_action, None); } @@ -5867,7 +6050,7 @@ mod tests { assert_eq!(seller_orders.rows[0].status, OrderStatus::NeedsAction); assert_eq!( seller_orders.rows[0].workflow.agreement, - TradeAgreementStatus::PendingRhi + TradeAgreementStatus::AgreedPendingRhi ); assert_eq!( seller_orders.rows[0].workflow.revision, diff --git a/crates/store/src/lib.rs b/crates/store/src/lib.rs @@ -1144,13 +1144,13 @@ mod tests { workflow_agreement, workflow_inventory ) VALUES ( - 'order_needs_review', + 'order_invalid', 'farm_schema', 'needs review', 'Buyer', 'needs_review', '2026-01-01T00:00:00Z', - 'needs_review', + 'invalid', 'needs_review' )", [], @@ -1168,13 +1168,13 @@ mod tests { workflow_agreement, workflow_inventory ) VALUES ( - 'order_pending_rhi', + 'order_agreed_pending_rhi', 'farm_schema', - 'pending rhi', + 'agreed pending rhi', 'Buyer', 'needs_action', '2026-01-01T00:00:00Z', - 'pending_rhi', + 'agreed_pending_rhi', 'reserved' )", [], @@ -1373,10 +1373,10 @@ mod tests { assert_eq!(status, "needs_review"); connection .execute( - "UPDATE orders SET workflow_agreement = 'pending_rhi' WHERE id = 'order_legacy'", + "UPDATE orders SET workflow_agreement = 'agreed_pending_rhi' WHERE id = 'order_legacy'", [], ) - .expect("pending rhi agreement should satisfy migrated check"); + .expect("agreed pending rhi agreement should satisfy migrated check"); drop(store); remove_database_artifacts(&path); diff --git a/crates/store/src/migrations.rs b/crates/store/src/migrations.rs @@ -120,7 +120,7 @@ const MIGRATIONS: &[Migration] = &[ }, Migration { version: 29, - sql: include_str!("../migrations/0029_order_workflow_pending_rhi_agreement.sql"), + sql: include_str!("../migrations/0029_order_workflow_validation_receipt_agreement.sql"), }, ]; diff --git a/crates/store/src/repo/buyer.rs b/crates/store/src/repo/buyer.rs @@ -3823,7 +3823,7 @@ mod tests { set_order_workflow_display_projection( connection, order_id, - "confirmed", + "committed", "reserved", "local_events", Some("agreement-event-1"), @@ -3839,7 +3839,7 @@ mod tests { let row = &list.rows[0]; assert_eq!(list.rows.len(), 1); - assert_eq!(row.workflow.agreement, TradeAgreementStatus::Confirmed); + assert_eq!(row.workflow.agreement, TradeAgreementStatus::Committed); assert_eq!(row.workflow.inventory, TradeInventoryStatus::Reserved); assert_eq!( row.workflow.provenance.primary_source, @@ -3877,7 +3877,7 @@ mod tests { set_order_workflow_display_projection( connection, order_id, - "confirmed", + "committed", "reserved", "local_events", Some("buyer-workflow-event"), @@ -3894,7 +3894,7 @@ mod tests { set_order_workflow_display_projection( connection, order_id, - "confirmed", + "committed", "reserved", "local_events", Some("buyer-workflow-event"), diff --git a/crates/store/src/repo/orders.rs b/crates/store/src/repo/orders.rs @@ -1177,7 +1177,11 @@ fn primary_action_for_order( workflow: &TradeWorkflowProjection, ) -> Option<OrderPrimaryAction> { match status { - OrderStatus::NeedsAction if workflow.agreement == TradeAgreementStatus::PendingRhi => None, + OrderStatus::NeedsAction + if workflow.agreement == TradeAgreementStatus::AgreedPendingRhi => + { + None + } OrderStatus::NeedsAction => Some(OrderPrimaryAction::Review), OrderStatus::Scheduled | OrderStatus::Packed @@ -1602,7 +1606,7 @@ mod tests { set_order_workflow_display_projection( connection, order_id, - "confirmed", + "committed", "reserved", "local_events", Some("seller-workflow-event"), @@ -1623,7 +1627,7 @@ mod tests { .expect("seller detail should exist"); let workflow = &list.rows[0].workflow; - assert_eq!(workflow.agreement, TradeAgreementStatus::Confirmed); + assert_eq!(workflow.agreement, TradeAgreementStatus::Committed); assert_eq!(workflow.revision, TradeRevisionStatus::Updated); assert_eq!(workflow.inventory, TradeInventoryStatus::Reserved); assert_eq!( @@ -1668,7 +1672,7 @@ mod tests { set_order_workflow_display_projection( connection, order_id, - "confirmed", + "committed", "reserved", "local_events", Some("seller-workflow-event"), @@ -1685,7 +1689,7 @@ mod tests { set_order_workflow_display_projection( connection, order_id, - "confirmed", + "committed", "reserved", "local_events", Some("seller-workflow-event"), diff --git a/crates/store/src/repo/workflow.rs b/crates/store/src/repo/workflow.rs @@ -38,12 +38,13 @@ fn parse_trade_agreement_status( value: String, ) -> Result<TradeAgreementStatus, AppSqliteError> { match value.as_str() { - "ordered" => Ok(TradeAgreementStatus::Ordered), - "pending_rhi" => Ok(TradeAgreementStatus::PendingRhi), - "confirmed" => Ok(TradeAgreementStatus::Confirmed), + "requested" => Ok(TradeAgreementStatus::Requested), + "revision_proposed" => Ok(TradeAgreementStatus::RevisionProposed), + "agreed_pending_rhi" => Ok(TradeAgreementStatus::AgreedPendingRhi), + "committed" => Ok(TradeAgreementStatus::Committed), "declined" => Ok(TradeAgreementStatus::Declined), "cancelled" => Ok(TradeAgreementStatus::Cancelled), - "needs_review" => Ok(TradeAgreementStatus::NeedsReview), + "invalid" => Ok(TradeAgreementStatus::Invalid), _ => Err(AppSqliteError::DecodeEnum { field, value }), } } diff --git a/crates/view/src/lib.rs b/crates/view/src/lib.rs @@ -1072,47 +1072,50 @@ pub struct BuyerOrderReviewProjection { #[serde(rename_all = "snake_case")] pub enum TradeAgreementStatus { #[default] - Ordered, - PendingRhi, - Confirmed, + Requested, + RevisionProposed, + AgreedPendingRhi, + Committed, Declined, Cancelled, - NeedsReview, + Invalid, } impl TradeAgreementStatus { pub const fn storage_key(self) -> &'static str { match self { - Self::Ordered => "ordered", - Self::PendingRhi => "pending_rhi", - Self::Confirmed => "confirmed", + Self::Requested => "requested", + Self::RevisionProposed => "revision_proposed", + Self::AgreedPendingRhi => "agreed_pending_rhi", + Self::Committed => "committed", Self::Declined => "declined", Self::Cancelled => "cancelled", - Self::NeedsReview => "needs_review", + Self::Invalid => "invalid", } } 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::Requested => "messages.trade.workflow.agreement.requested", + Self::RevisionProposed => "messages.trade.workflow.agreement.revision_proposed", + Self::AgreedPendingRhi => "messages.trade.workflow.agreement.agreed_pending_rhi", + Self::Committed => "messages.trade.workflow.agreement.committed", Self::Declined => "messages.trade.workflow.agreement.declined", Self::Cancelled => "messages.trade.workflow.agreement.cancelled", - Self::NeedsReview => "messages.trade.workflow.agreement.needs_review", + Self::Invalid => "messages.trade.workflow.agreement.invalid", } } pub const fn from_active_order_status(status: &RadrootsTradeWorkflowState) -> Self { match status { - RadrootsTradeWorkflowState::Missing => Self::NeedsReview, - RadrootsTradeWorkflowState::Requested - | RadrootsTradeWorkflowState::RevisionProposed => Self::Ordered, - RadrootsTradeWorkflowState::AgreedPendingRhi => Self::PendingRhi, - RadrootsTradeWorkflowState::Committed => Self::Confirmed, + RadrootsTradeWorkflowState::Missing => Self::Invalid, + RadrootsTradeWorkflowState::Requested => Self::Requested, + RadrootsTradeWorkflowState::RevisionProposed => Self::RevisionProposed, + RadrootsTradeWorkflowState::AgreedPendingRhi => Self::AgreedPendingRhi, + RadrootsTradeWorkflowState::Committed => Self::Committed, RadrootsTradeWorkflowState::Declined => Self::Declined, RadrootsTradeWorkflowState::Cancelled => Self::Cancelled, - RadrootsTradeWorkflowState::Invalid => Self::NeedsReview, + RadrootsTradeWorkflowState::Invalid => Self::Invalid, } } } @@ -1504,12 +1507,12 @@ impl TradeWorkflowProjection { pub fn from_order_status(order_id: OrderId, status: OrderStatus) -> Self { let mut projection = match status { - OrderStatus::NeedsAction => Self::new(order_id, TradeAgreementStatus::Ordered), - OrderStatus::Scheduled => Self::new(order_id, TradeAgreementStatus::Confirmed), - OrderStatus::Packed => Self::new(order_id, TradeAgreementStatus::Confirmed), - OrderStatus::Completed => Self::new(order_id, TradeAgreementStatus::Confirmed), + OrderStatus::NeedsAction => Self::new(order_id, TradeAgreementStatus::Requested), + OrderStatus::Scheduled => Self::new(order_id, TradeAgreementStatus::Committed), + OrderStatus::Packed => Self::new(order_id, TradeAgreementStatus::Committed), + OrderStatus::Completed => Self::new(order_id, TradeAgreementStatus::Committed), OrderStatus::Declined => Self::new(order_id, TradeAgreementStatus::Declined), - OrderStatus::NeedsReview => Self::new(order_id, TradeAgreementStatus::NeedsReview), + OrderStatus::NeedsReview => Self::new(order_id, TradeAgreementStatus::Invalid), }; match status { @@ -1534,12 +1537,12 @@ impl TradeWorkflowProjection { pub fn from_buyer_order_status(order_id: OrderId, status: BuyerOrderStatus) -> Self { let mut projection = match status { - BuyerOrderStatus::Placed => Self::new(order_id, TradeAgreementStatus::Ordered), - BuyerOrderStatus::Scheduled => Self::new(order_id, TradeAgreementStatus::Confirmed), - BuyerOrderStatus::Ready => Self::new(order_id, TradeAgreementStatus::Confirmed), - BuyerOrderStatus::Completed => Self::new(order_id, TradeAgreementStatus::Confirmed), + BuyerOrderStatus::Placed => Self::new(order_id, TradeAgreementStatus::Requested), + BuyerOrderStatus::Scheduled => Self::new(order_id, TradeAgreementStatus::Committed), + BuyerOrderStatus::Ready => Self::new(order_id, TradeAgreementStatus::Committed), + BuyerOrderStatus::Completed => Self::new(order_id, TradeAgreementStatus::Committed), BuyerOrderStatus::Declined => Self::new(order_id, TradeAgreementStatus::Declined), - BuyerOrderStatus::NeedsReview => Self::new(order_id, TradeAgreementStatus::NeedsReview), + BuyerOrderStatus::NeedsReview => Self::new(order_id, TradeAgreementStatus::Invalid), }; match status { @@ -2875,21 +2878,27 @@ mod tests { fn trade_workflow_projection_maps_shared_active_order_projection_to_product_axes() { assert_eq!( TradeAgreementStatus::from_active_order_status(&RadrootsTradeWorkflowState::Requested), - TradeAgreementStatus::Ordered + TradeAgreementStatus::Requested + ); + assert_eq!( + TradeAgreementStatus::from_active_order_status( + &RadrootsTradeWorkflowState::RevisionProposed + ), + TradeAgreementStatus::RevisionProposed ); assert_eq!( TradeAgreementStatus::from_active_order_status( &RadrootsTradeWorkflowState::AgreedPendingRhi ), - TradeAgreementStatus::PendingRhi + TradeAgreementStatus::AgreedPendingRhi ); assert_eq!( TradeAgreementStatus::from_active_order_status(&RadrootsTradeWorkflowState::Committed), - TradeAgreementStatus::Confirmed + TradeAgreementStatus::Committed ); assert_eq!( TradeAgreementStatus::from_active_order_status(&RadrootsTradeWorkflowState::Invalid), - TradeAgreementStatus::NeedsReview + TradeAgreementStatus::Invalid ); assert_eq!( TradeRevisionStatus::try_from_storage_key("none"), @@ -2927,7 +2936,7 @@ mod tests { TradeProvenanceProjection::from_primary_source(TradeWorkflowSource::LocalEvents), ); assert_eq!(projection.order_id, order_id); - assert_eq!(projection.agreement, TradeAgreementStatus::Confirmed); + assert_eq!(projection.agreement, TradeAgreementStatus::Committed); assert_eq!(projection.revision, TradeRevisionStatus::Updated); assert_eq!(projection.inventory, TradeInventoryStatus::Reserved); assert_eq!(projection.economics.total_minor_units, Some(1234)); @@ -2954,7 +2963,7 @@ mod tests { ); assert_eq!( pending_rhi_projection.agreement, - TradeAgreementStatus::PendingRhi + TradeAgreementStatus::AgreedPendingRhi ); assert_eq!( pending_rhi_projection.inventory, @@ -2974,7 +2983,7 @@ mod tests { ); assert_eq!( requested_projection.agreement, - TradeAgreementStatus::Ordered + TradeAgreementStatus::Requested ); assert_eq!( requested_projection.inventory, @@ -3040,13 +3049,19 @@ mod tests { assert_eq!( TradeAgreementStatus::from_active_order_status(&RadrootsTradeWorkflowState::Requested) .storage_key(), - "ordered" + "requested" + ); + assert_eq!(TradeAgreementStatus::Requested.storage_key(), "requested"); + assert_eq!( + TradeAgreementStatus::RevisionProposed.storage_key(), + "revision_proposed" ); - assert_eq!(TradeAgreementStatus::Ordered.storage_key(), "ordered"); assert_eq!( - TradeAgreementStatus::PendingRhi.storage_key(), - "pending_rhi" + TradeAgreementStatus::AgreedPendingRhi.storage_key(), + "agreed_pending_rhi" ); + assert_eq!(TradeAgreementStatus::Committed.storage_key(), "committed"); + assert_eq!(TradeAgreementStatus::Invalid.storage_key(), "invalid"); assert_eq!( TradeRevisionStatus::KeptAsPlaced.storage_key(), "kept_as_placed" @@ -3058,16 +3073,20 @@ mod tests { ); assert_eq!( - TradeAgreementStatus::Ordered.label_key_id(), - "messages.trade.workflow.agreement.ordered" + TradeAgreementStatus::Requested.label_key_id(), + "messages.trade.workflow.agreement.requested" + ); + assert_eq!( + TradeAgreementStatus::RevisionProposed.label_key_id(), + "messages.trade.workflow.agreement.revision_proposed" ); assert_eq!( - TradeAgreementStatus::PendingRhi.label_key_id(), - "messages.trade.workflow.agreement.pending_rhi" + TradeAgreementStatus::AgreedPendingRhi.label_key_id(), + "messages.trade.workflow.agreement.agreed_pending_rhi" ); assert_eq!( - TradeAgreementStatus::NeedsReview.label_key_id(), - "messages.trade.workflow.agreement.needs_review" + TradeAgreementStatus::Invalid.label_key_id(), + "messages.trade.workflow.agreement.invalid" ); assert_eq!( TradeRevisionStatus::ChangeProposed.label_key_id(), @@ -3491,7 +3510,7 @@ mod tests { assert_eq!(orders_list.rows[0].primary_action, None); assert_eq!( orders_list.rows[0].workflow.agreement, - TradeAgreementStatus::Confirmed + TradeAgreementStatus::Committed ); assert_eq!(order_detail.items[0].quantity_display, "2 bags"); assert_eq!( @@ -3629,7 +3648,7 @@ mod tests { assert_eq!(order_detail.status, BuyerOrderStatus::Scheduled); assert_eq!( order_detail.workflow.agreement, - TradeAgreementStatus::Confirmed + TradeAgreementStatus::Committed ); } diff --git a/i18n/locales/en/messages.json b/i18n/locales/en/messages.json @@ -411,12 +411,13 @@ "trade.workflow.axis.revision": "Change", "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.requested": "Requested", + "trade.workflow.agreement.revision_proposed": "Revision proposed", + "trade.workflow.agreement.agreed_pending_rhi": "Pending validation", + "trade.workflow.agreement.committed": "Committed", "trade.workflow.agreement.declined": "Declined", "trade.workflow.agreement.cancelled": "Cancelled", - "trade.workflow.agreement.needs_review": "Needs review", + "trade.workflow.agreement.invalid": "Invalid", "trade.workflow.revision.none": "No change", "trade.workflow.revision.change_proposed": "Change proposed", "trade.workflow.revision.updated": "Updated",