app

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

commit ab73bde6138b0dbe53dfa9fb05f1941762b914f6
parent 4a44b40aadfa4adf29ae1a177625595b4504cb0f
Author: triesap <tyson@radroots.org>
Date:   Sat, 23 May 2026 06:58:02 +0000

sqlite: preserve confirmed listing lifecycle state

- keep confirmed product status when draft-like local interop evidence arrives later
- retain pending and failed publish posture in imported local interop records
- add coverage for pending and failed signed listings after a published listing
- continue allowing explicit acknowledged lifecycle events to update product status

Diffstat:
Mcrates/shared/sqlite/src/local_interop.rs | 100+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------
1 file changed, 93 insertions(+), 7 deletions(-)

diff --git a/crates/shared/sqlite/src/local_interop.rs b/crates/shared/sqlite/src/local_interop.rs @@ -555,7 +555,12 @@ impl<'a> AppLocalInteropRepository<'a> { farm_id = excluded.farm_id, title = excluded.title, subtitle = excluded.subtitle, - status = excluded.status, + status = CASE + WHEN excluded.status = 'draft' + AND products.status IN ('published', 'paused', 'archived') + THEN products.status + ELSE excluded.status + END, unit_label = excluded.unit_label, price_minor_units = excluded.price_minor_units, price_currency = excluded.price_currency, @@ -993,10 +998,39 @@ mod tests { listing_key: &str, status_tag: &str, ) -> LocalEventRecordInput { + signed_listing_record_with_publish_state( + record_id, + farm_key, + listing_key, + status_tag, + LocalRecordStatus::Published, + PublishOutboxStatus::Acknowledged, + ) + } + + fn signed_listing_record_with_publish_state( + record_id: &str, + farm_key: &str, + listing_key: &str, + status_tag: &str, + record_status: LocalRecordStatus, + outbox_status: PublishOutboxStatus, + ) -> LocalEventRecordInput { + let relay_delivery_json = match outbox_status { + PublishOutboxStatus::Acknowledged => Some(json!({ + "state": "acknowledged", + "acknowledged_relays": ["ws://127.0.0.1:1234/"] + })), + PublishOutboxStatus::Failed => Some(json!({ + "state": "failed", + "failed_relays": ["ws://127.0.0.1:1234/"] + })), + PublishOutboxStatus::Pending | PublishOutboxStatus::None => None, + }; LocalEventRecordInput { record_id: record_id.to_owned(), family: LocalRecordFamily::SignedEvent, - status: LocalRecordStatus::Published, + status: record_status, source_runtime: SourceRuntime::Cli, created_at_ms: 1100, inserted_at_ms: 1101, @@ -1028,12 +1062,9 @@ mod tests { "pubkey": "seller-pubkey", "content": "# Relay Eggs\n\nPublished eggs" })), - outbox_status: PublishOutboxStatus::Acknowledged, + outbox_status, relay_set_fingerprint: Some("relay-set".to_owned()), - relay_delivery_json: Some(json!({ - "state": "acknowledged", - "acknowledged_relays": ["ws://127.0.0.1:1234/"] - })), + relay_delivery_json, } } @@ -1378,4 +1409,59 @@ mod tests { assert_eq!(imported[0].projected_kind, "unsupported"); assert_eq!(product_count, 0); } + + #[test] + fn pending_or_failed_signed_listing_records_do_not_downgrade_published_product() { + for (record_status, outbox_status) in [ + ( + LocalRecordStatus::PendingPublish, + PublishOutboxStatus::Pending, + ), + (LocalRecordStatus::Failed, PublishOutboxStatus::Failed), + ] { + let app_store = + AppSqliteStore::open(DatabaseTarget::InMemory).expect("open app sqlite store"); + let events = local_events_store(); + let farm_key = "AAAAAAAAAAAAAAAAAAAAAA"; + let listing_key = "BBBBBBBBBBBBBBBBBBBBBB"; + events + .append_record(&signed_listing_record( + "confirmed", + farm_key, + listing_key, + "active", + )) + .expect("append confirmed signed listing"); + app_store + .import_shared_local_events_from_store(&events) + .expect("import confirmed signed listing"); + events + .append_record(&signed_listing_record_with_publish_state( + record_status.as_str(), + farm_key, + listing_key, + "active", + record_status, + outbox_status, + )) + .expect("append unconfirmed signed listing"); + + app_store + .import_shared_local_events_from_store(&events) + .expect("import unconfirmed signed listing"); + let product_status: String = app_store + .connection() + .query_row("SELECT status FROM products", [], |row| row.get(0)) + .expect("load product status"); + let imported = app_store + .load_local_interop_records() + .expect("load imported records"); + + assert_eq!(product_status, "published"); + assert!(imported.iter().any(|record| { + record.local_status == record_status.as_str() + && record.outbox_status == outbox_status.as_str() + })); + } + } }