commit ab015cb372c34340665b1e67ad87e6325ed87a16
parent 20731e6024a36485ed511b16934b131087249930
Author: triesap <tyson@radroots.org>
Date: Sun, 24 May 2026 10:28:46 +0000
local_events: add buyer order work contract
- add shared local-work constants for app-authored buyer order requests
- add deterministic app order request record-id derivation
- validate current no-payment order request payload shape
- cover the order work contract with focused local-events tests
Diffstat:
3 files changed, 183 insertions(+), 0 deletions(-)
diff --git a/crates/local_events/src/lib.rs b/crates/local_events/src/lib.rs
@@ -3,6 +3,7 @@
mod error;
mod migrations;
mod models;
+mod order_work;
mod store;
pub use error::LocalEventsError;
@@ -11,4 +12,10 @@ pub use models::{
LocalEventRecord, LocalEventRecordInput, LocalEventRecordUpdate, LocalEventsCursor,
LocalRecordFamily, LocalRecordStatus, PublishOutboxStatus, SourceRuntime,
};
+pub use order_work::{
+ 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, buyer_order_request_local_work_record_id,
+ validate_buyer_order_request_local_work_payload,
+};
pub use store::LocalEventsStore;
diff --git a/crates/local_events/src/order_work.rs b/crates/local_events/src/order_work.rs
@@ -0,0 +1,99 @@
+use serde_json::Value;
+
+use crate::LocalEventsError;
+use crate::models::validate_non_empty;
+
+pub const BUYER_ORDER_REQUEST_LOCAL_WORK_RECORD_KIND: &str = "buyer_order_request_v1";
+pub const BUYER_ORDER_REQUEST_DOCUMENT_KIND: &str = "order_draft_v1";
+pub const BUYER_ORDER_REQUEST_ACTOR_SOURCE_RESOLVED_ACCOUNT: &str = "resolved_account";
+pub const BUYER_ORDER_REQUEST_ACTOR_SOURCE_UNRESOLVED_APP: &str = "app_unresolved";
+
+pub fn buyer_order_request_local_work_record_id(
+ order_id: &str,
+) -> Result<String, LocalEventsError> {
+ let order_id = order_id.trim();
+ validate_non_empty("order_id", order_id)?;
+ Ok(format!("app:local_work:order_request:{order_id}"))
+}
+
+pub fn validate_buyer_order_request_local_work_payload(
+ payload: &Value,
+) -> Result<(), LocalEventsError> {
+ validate_string_field(
+ payload,
+ &["record_kind"],
+ BUYER_ORDER_REQUEST_LOCAL_WORK_RECORD_KIND,
+ )?;
+ validate_string_field(
+ payload,
+ &["document", "kind"],
+ BUYER_ORDER_REQUEST_DOCUMENT_KIND,
+ )?;
+ validate_required_string(payload, &["document", "order", "order_id"], "order_id")?;
+ validate_bool_field(payload, &["currentness", "current"], true)?;
+ validate_bool_field(payload, &["no_payment", "payment_required"], false)?;
+ validate_bool_field(payload, &["no_payment", "settlement_deferred"], true)?;
+ Ok(())
+}
+
+fn validate_string_field(
+ payload: &Value,
+ path: &[&str],
+ expected: &str,
+) -> Result<(), LocalEventsError> {
+ let Some(value) = value_at(payload, path).and_then(Value::as_str) else {
+ return Err(LocalEventsError::InvalidRecord(format!(
+ "missing required local order field `{}`",
+ path.join(".")
+ )));
+ };
+ if value != expected {
+ return Err(LocalEventsError::InvalidRecord(format!(
+ "local order field `{}` must be `{expected}`",
+ path.join(".")
+ )));
+ }
+ Ok(())
+}
+
+fn validate_required_string(
+ payload: &Value,
+ path: &[&str],
+ field: &str,
+) -> Result<(), LocalEventsError> {
+ let Some(value) = value_at(payload, path).and_then(Value::as_str) else {
+ return Err(LocalEventsError::InvalidRecord(format!(
+ "missing required local order field `{}`",
+ path.join(".")
+ )));
+ };
+ validate_non_empty(field, value)
+}
+
+fn validate_bool_field(
+ payload: &Value,
+ path: &[&str],
+ expected: bool,
+) -> Result<(), LocalEventsError> {
+ let Some(value) = value_at(payload, path).and_then(Value::as_bool) else {
+ return Err(LocalEventsError::InvalidRecord(format!(
+ "missing required local order field `{}`",
+ path.join(".")
+ )));
+ };
+ if value != expected {
+ return Err(LocalEventsError::InvalidRecord(format!(
+ "local order field `{}` must be `{expected}`",
+ path.join(".")
+ )));
+ }
+ Ok(())
+}
+
+fn value_at<'a>(payload: &'a Value, path: &[&str]) -> Option<&'a Value> {
+ let mut current = payload;
+ for part in path {
+ current = current.get(*part)?;
+ }
+ Some(current)
+}
diff --git a/crates/local_events/tests/order_work.rs b/crates/local_events/tests/order_work.rs
@@ -0,0 +1,77 @@
+use radroots_local_events::{
+ BUYER_ORDER_REQUEST_ACTOR_SOURCE_RESOLVED_ACCOUNT, BUYER_ORDER_REQUEST_DOCUMENT_KIND,
+ BUYER_ORDER_REQUEST_LOCAL_WORK_RECORD_KIND, buyer_order_request_local_work_record_id,
+ validate_buyer_order_request_local_work_payload,
+};
+use serde_json::json;
+
+#[test]
+fn buyer_order_request_record_id_is_deterministic_for_app_orders() {
+ assert_eq!(
+ buyer_order_request_local_work_record_id(" order-1 ").expect("record id"),
+ "app:local_work:order_request:order-1"
+ );
+}
+
+#[test]
+fn buyer_order_request_payload_requires_current_no_payment_order_document() {
+ let payload = json!({
+ "record_kind": BUYER_ORDER_REQUEST_LOCAL_WORK_RECORD_KIND,
+ "scope": "app",
+ "currentness": {
+ "current": true
+ },
+ "no_payment": {
+ "payment_required": false,
+ "settlement_deferred": true
+ },
+ "document": {
+ "version": 1,
+ "kind": BUYER_ORDER_REQUEST_DOCUMENT_KIND,
+ "order": {
+ "order_id": "ord_1",
+ "listing_addr": "30402:seller:listing",
+ "buyer_pubkey": "buyer",
+ "seller_pubkey": "seller",
+ "items": [
+ {
+ "bin_id": "bin-1",
+ "bin_count": 1
+ }
+ ]
+ },
+ "buyer_actor": {
+ "account_id": "buyer-account",
+ "pubkey": "buyer",
+ "source": BUYER_ORDER_REQUEST_ACTOR_SOURCE_RESOLVED_ACCOUNT
+ }
+ }
+ });
+
+ validate_buyer_order_request_local_work_payload(&payload).expect("valid payload");
+}
+
+#[test]
+fn buyer_order_request_payload_rejects_payment_required_documents() {
+ let payload = json!({
+ "record_kind": BUYER_ORDER_REQUEST_LOCAL_WORK_RECORD_KIND,
+ "currentness": {
+ "current": true
+ },
+ "no_payment": {
+ "payment_required": true,
+ "settlement_deferred": true
+ },
+ "document": {
+ "kind": BUYER_ORDER_REQUEST_DOCUMENT_KIND,
+ "order": {
+ "order_id": "ord_1"
+ }
+ }
+ });
+
+ let error =
+ validate_buyer_order_request_local_work_payload(&payload).expect_err("invalid payload");
+
+ assert!(error.to_string().contains("payment_required"));
+}