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:
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(),