app

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

commit 91052d6816766603ce4e589b2ae874ff552ca603
parent d6f0edf47f21a09e85319a11c8d3f38704ec8b6c
Author: triesap <tyson@radroots.org>
Date:   Wed,  3 Jun 2026 01:27:08 -0700

app: reserve payment workflow presentation

- keep payment and settlement publish work absent from app sync
- assert every payment display state has no app payment action
- guard localized action copy against checkout and payment terms
- neutralize recovery payment status text without checkout guidance

Diffstat:
Mcrates/desktop/src/runtime.rs | 16++++++++++------
Mcrates/desktop/src/source_guards.rs | 25+++++++++++++++++++++++++
Mcrates/i18n/src/lib.rs | 41+++++++++++++++++++++++++++++++++++++++++
Mcrates/sync/src/publish.rs | 27++++++++++++++++++++++++++-
Mcrates/view/src/lib.rs | 11+++++++++++
5 files changed, 113 insertions(+), 7 deletions(-)

diff --git a/crates/desktop/src/runtime.rs b/crates/desktop/src/runtime.rs @@ -8753,9 +8753,13 @@ fn order_recovery_summary(kind: RecoveryKind, state: RecoveryState) -> &'static (RecoveryKind::MissedPickup, RecoveryState::Resolved) => { "Missed pickup follow-up is resolved" } - (RecoveryKind::RefundFollowUp, RecoveryState::Open) => "Refund follow-up is open", - (RecoveryKind::RefundFollowUp, RecoveryState::InReview) => "Refund follow-up is in review", - (RecoveryKind::RefundFollowUp, RecoveryState::Resolved) => "Refund follow-up is resolved", + (RecoveryKind::RefundFollowUp, RecoveryState::Open) => "Payment status follow-up is open", + (RecoveryKind::RefundFollowUp, RecoveryState::InReview) => { + "Payment status follow-up is in review" + } + (RecoveryKind::RefundFollowUp, RecoveryState::Resolved) => { + "Payment status follow-up is resolved" + } } } @@ -8771,13 +8775,13 @@ fn order_recovery_note(kind: RecoveryKind, state: RecoveryState) -> &'static str "The seller and buyer have agreed on the next step." } (RecoveryKind::RefundFollowUp, RecoveryState::Open) => { - "Review the situation and handle any refund outside the app." + "Review the order record and agree on the next step." } (RecoveryKind::RefundFollowUp, RecoveryState::InReview) => { - "Confirm the outcome and keep payment handling outside the app." + "Confirm the outcome with the order parties." } (RecoveryKind::RefundFollowUp, RecoveryState::Resolved) => { - "The refund follow-up was handled outside the app." + "The payment status follow-up is resolved." } } } diff --git a/crates/desktop/src/source_guards.rs b/crates/desktop/src/source_guards.rs @@ -747,6 +747,17 @@ const REMOVED_WINDOW_HELPER_FAMILIES: &[&str] = &[ "fn home_farm_setup_blocker(", ]; +const FORBIDDEN_PAYMENT_ACTION_COPY_PATTERNS: &[&str] = &[ + "payments are deferred", + "payment is deferred", + "payment deferred", + "checkout unavailable", + "pay now", + "refund outside the app", + "payment handling outside the app", + "handle any refund outside the app", +]; + #[test] fn desktop_menu_source_uses_localized_copy_paths() { assert_eq!( @@ -816,6 +827,20 @@ fn desktop_window_source_does_not_use_about_placeholder_copy() { ); } +#[test] +fn desktop_sources_do_not_expose_reserved_payment_action_copy() { + for (path, source) in launcher_source_files() { + let normalized_source = source.to_lowercase(); + for pattern in FORBIDDEN_PAYMENT_ACTION_COPY_PATTERNS { + assert!( + !normalized_source.contains(pattern), + "{} contains reserved payment action copy `{pattern}`", + path.display() + ); + } + } +} + fn extract_string_literals(source: &str) -> BTreeSet<&str> { let mut literals = BTreeSet::new(); let bytes = source.as_bytes(); diff --git a/crates/i18n/src/lib.rs b/crates/i18n/src/lib.rs @@ -378,6 +378,47 @@ mod tests { } #[test] + fn english_payment_action_copy_remains_unspoken_for_reserved_workflow() { + let action_keys = [ + AppTextKey::PersonalCartContinueCheckoutAction, + AppTextKey::PersonalCheckoutBackAction, + AppTextKey::PersonalCheckoutPlaceOrderAction, + AppTextKey::OrdersRecoveryActionOpenFollowUp, + AppTextKey::OrdersRecoveryActionStartReview, + AppTextKey::OrdersRecoveryActionMarkOpen, + AppTextKey::OrdersRecoveryActionResolve, + ]; + let forbidden_action_terms = [ + "checkout", + "pay", + "refund", + "settlement", + "wallet", + "invoice", + "bank", + "card", + "processor", + ]; + + for key in action_keys { + let copy = app_text(key).to_lowercase(); + for term in forbidden_action_terms { + assert!(!copy.contains(term)); + } + } + + for copy in [ + app_text(AppTextKey::PersonalCheckoutLocalOnlyBody), + app_text(AppTextKey::PersonalOrderCoordinationFailedNotice), + ] { + let normalized_copy = copy.to_lowercase(); + assert!(!normalized_copy.contains("payments are deferred")); + assert!(!normalized_copy.contains("payment deferred")); + assert!(!normalized_copy.contains("checkout unavailable")); + } + } + + #[test] fn english_trade_workflow_copy_matches_the_projection_contract() { assert_eq!( app_text(AppTextKey::TradeWorkflowAxisAgreement), diff --git a/crates/sync/src/publish.rs b/crates/sync/src/publish.rs @@ -763,7 +763,7 @@ mod tests { AppOrderFulfillmentPublishStatus, AppOrderReceiptPublishPayload, AppOrderRequestItemPayload, AppOrderRequestPublishPayload, AppOrderRevisionDecisionPublishPayload, AppOrderRevisionProposalPublishPayload, - AppPublishContext, AppPublishPayload, AppPublishValidationFailure, + AppPublishContext, AppPublishPayload, AppPublishValidationFailure, AppPublishWorkKind, }; use crate::{ PendingSyncOperation, PendingSyncOperationState, SyncAggregateRef, SyncOperationKind, @@ -813,6 +813,31 @@ mod tests { } #[test] + fn publish_work_kinds_keep_payment_and_settlement_events_reserved() { + let work_kinds = [ + AppPublishWorkKind::FarmProfile, + AppPublishWorkKind::Listing, + AppPublishWorkKind::OrderRequest, + AppPublishWorkKind::OrderDecision, + AppPublishWorkKind::OrderRevisionProposal, + AppPublishWorkKind::OrderRevisionDecision, + AppPublishWorkKind::OrderCancellation, + AppPublishWorkKind::OrderFulfillment, + AppPublishWorkKind::OrderReceipt, + ]; + + assert_eq!(work_kinds.len(), 9); + for work_kind in work_kinds { + let storage_key = work_kind.storage_key(); + let sdk_operation = work_kind.sdk_operation(); + assert!(!storage_key.contains("payment")); + assert!(!storage_key.contains("settlement")); + assert!(!sdk_operation.contains("payment")); + assert!(!sdk_operation.contains("settlement")); + } + } + + #[test] fn listing_publish_payload_reports_stable_validation_reason_codes() { let payload = AppPublishPayload::Listing(AppListingPublishPayload { context: AppPublishContext::new("", ""), diff --git a/crates/view/src/lib.rs b/crates/view/src/lib.rs @@ -2821,6 +2821,17 @@ mod tests { } #[test] + fn trade_payment_display_statuses_do_not_enable_payment_actions() { + for status in [ + TradePaymentDisplayStatus::NotRecorded, + TradePaymentDisplayStatus::Recorded, + TradePaymentDisplayStatus::NeedsReview, + ] { + assert!(!status.allows_payment_action()); + } + } + + #[test] fn trade_workflow_projection_uses_localization_key_ids_for_visible_status_labels() { assert_eq!( TradeReducerAgreementStatus::Requested.storage_key(),