use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum ConditionType {
SellerFulfilled,
BuyerConfirmed,
TimeLock,
Milestone,
}
impl std::fmt::Display for ConditionType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::SellerFulfilled => write!(f, "seller_fulfilled"),
Self::BuyerConfirmed => write!(f, "buyer_confirmed"),
Self::TimeLock => write!(f, "time_lock"),
Self::Milestone => write!(f, "milestone"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Condition {
#[serde(rename = "type")]
pub condition_type: ConditionType,
#[serde(default)]
pub completed: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub quote_id: Option<Uuid>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub release_after: Option<DateTime<Utc>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
}
impl Condition {
#[must_use]
pub const fn seller_fulfilled(quote_id: Option<Uuid>) -> Self {
Self {
condition_type: ConditionType::SellerFulfilled,
completed: false,
quote_id,
release_after: None,
description: None,
}
}
#[must_use]
pub const fn buyer_confirmed() -> Self {
Self {
condition_type: ConditionType::BuyerConfirmed,
completed: false,
quote_id: None,
release_after: None,
description: None,
}
}
#[must_use]
pub const fn time_lock(release_after: DateTime<Utc>) -> Self {
Self {
condition_type: ConditionType::TimeLock,
completed: false,
quote_id: None,
release_after: Some(release_after),
description: None,
}
}
#[must_use]
pub fn milestone(description: impl Into<String>) -> Self {
Self {
condition_type: ConditionType::Milestone,
completed: false,
quote_id: None,
release_after: None,
description: Some(description.into()),
}
}
pub const fn confirm(&mut self) {
self.completed = true;
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConditionEvaluation {
pub condition: Condition,
pub met: bool,
}
#[must_use]
pub const fn evaluate_buyer_confirmed(condition: &Condition) -> bool {
condition.completed
}
#[must_use]
pub fn evaluate_time_lock(condition: &Condition, now: DateTime<Utc>) -> bool {
condition
.release_after
.is_some_and(|release_after| now >= release_after)
}
#[must_use]
pub const fn evaluate_milestone(condition: &Condition) -> bool {
condition.completed
}
#[must_use]
pub fn evaluate_seller_fulfilled(quote_status: Option<&str>) -> bool {
quote_status == Some("fulfilled")
}
#[must_use]
pub fn evaluate_condition(
condition: &Condition,
now: DateTime<Utc>,
quote_status: Option<&str>,
) -> bool {
match condition.condition_type {
ConditionType::SellerFulfilled => evaluate_seller_fulfilled(quote_status),
ConditionType::BuyerConfirmed => evaluate_buyer_confirmed(condition),
ConditionType::TimeLock => evaluate_time_lock(condition, now),
ConditionType::Milestone => evaluate_milestone(condition),
}
}
pub fn evaluate_all_conditions<F>(
conditions: &[Condition],
now: DateTime<Utc>,
quote_status_fn: F,
) -> (bool, Vec<ConditionEvaluation>)
where
F: Fn(Option<&Uuid>) -> Option<String>,
{
if conditions.is_empty() {
return (true, Vec::new());
}
let mut evaluations = Vec::with_capacity(conditions.len());
for condition in conditions {
let quote_status = if condition.condition_type == ConditionType::SellerFulfilled {
quote_status_fn(condition.quote_id.as_ref())
} else {
None
};
let met = evaluate_condition(condition, now, quote_status.as_deref());
evaluations.push(ConditionEvaluation {
condition: condition.clone(),
met,
});
}
let all_met = evaluations.iter().all(|e| e.met);
(all_met, evaluations)
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::TimeZone;
fn now() -> DateTime<Utc> {
Utc.with_ymd_and_hms(2026, 2, 22, 12, 0, 0).unwrap()
}
fn past() -> DateTime<Utc> {
Utc.with_ymd_and_hms(2026, 2, 20, 12, 0, 0).unwrap()
}
fn future() -> DateTime<Utc> {
Utc.with_ymd_and_hms(2026, 3, 1, 12, 0, 0).unwrap()
}
#[test]
fn seller_fulfilled_met_when_quote_fulfilled() {
assert!(evaluate_seller_fulfilled(Some("fulfilled")));
}
#[test]
fn seller_fulfilled_not_met_when_quote_pending() {
assert!(!evaluate_seller_fulfilled(Some("pending")));
}
#[test]
fn seller_fulfilled_not_met_when_no_quote() {
assert!(!evaluate_seller_fulfilled(None));
}
#[test]
fn buyer_confirmed_met_when_completed() {
let mut c = Condition::buyer_confirmed();
c.confirm();
assert!(evaluate_buyer_confirmed(&c));
}
#[test]
fn buyer_confirmed_not_met_by_default() {
let c = Condition::buyer_confirmed();
assert!(!evaluate_buyer_confirmed(&c));
}
#[test]
fn time_lock_met_when_past_release() {
let c = Condition::time_lock(past());
assert!(evaluate_time_lock(&c, now()));
}
#[test]
fn time_lock_met_when_exactly_at_release() {
let t = now();
let c = Condition::time_lock(t);
assert!(evaluate_time_lock(&c, t));
}
#[test]
fn time_lock_not_met_when_before_release() {
let c = Condition::time_lock(future());
assert!(!evaluate_time_lock(&c, now()));
}
#[test]
fn time_lock_not_met_without_release_after() {
let c = Condition {
condition_type: ConditionType::TimeLock,
completed: false,
quote_id: None,
release_after: None,
description: None,
};
assert!(!evaluate_time_lock(&c, now()));
}
#[test]
fn milestone_met_when_completed() {
let mut c = Condition::milestone("Design review");
c.confirm();
assert!(evaluate_milestone(&c));
}
#[test]
fn milestone_not_met_by_default() {
let c = Condition::milestone("Design review");
assert!(!evaluate_milestone(&c));
}
#[test]
fn evaluate_condition_dispatches_buyer_confirmed() {
let mut c = Condition::buyer_confirmed();
c.confirm();
assert!(evaluate_condition(&c, now(), None));
}
#[test]
fn evaluate_condition_dispatches_time_lock() {
let c = Condition::time_lock(past());
assert!(evaluate_condition(&c, now(), None));
}
#[test]
fn evaluate_condition_dispatches_seller_fulfilled() {
let c = Condition::seller_fulfilled(None);
assert!(evaluate_condition(&c, now(), Some("fulfilled")));
assert!(!evaluate_condition(&c, now(), Some("pending")));
}
#[test]
fn evaluate_condition_dispatches_milestone() {
let c = Condition::milestone("Ship prototype");
assert!(!evaluate_condition(&c, now(), None));
}
#[test]
fn all_conditions_met_empty_list() {
let (all_met, evals) = evaluate_all_conditions(&[], now(), |_| None);
assert!(all_met);
assert!(evals.is_empty());
}
#[test]
fn all_conditions_met_single_confirmed() {
let mut c = Condition::buyer_confirmed();
c.confirm();
let (all_met, evals) = evaluate_all_conditions(&[c], now(), |_| None);
assert!(all_met);
assert_eq!(evals.len(), 1);
assert!(evals[0].met);
}
#[test]
fn all_conditions_not_met_one_unconfirmed() {
let c1 = Condition::buyer_confirmed(); let c2 = Condition::time_lock(past()); let (all_met, evals) = evaluate_all_conditions(&[c1, c2], now(), |_| None);
assert!(!all_met);
assert!(!evals[0].met);
assert!(evals[1].met);
}
#[test]
fn all_conditions_met_mixed_types() {
let mut c1 = Condition::buyer_confirmed();
c1.confirm();
let c2 = Condition::time_lock(past());
let mut c3 = Condition::milestone("Ship it");
c3.confirm();
let (all_met, evals) = evaluate_all_conditions(&[c1, c2, c3], now(), |_| None);
assert!(all_met);
assert_eq!(evals.len(), 3);
}
#[test]
fn seller_fulfilled_with_quote_lookup() {
let quote_id = Uuid::new_v4();
let c = Condition::seller_fulfilled(Some(quote_id));
let (all_met, _) = evaluate_all_conditions(std::slice::from_ref(&c), now(), |id| {
if id == Some("e_id) {
Some("fulfilled".into())
} else {
None
}
});
assert!(all_met);
let (all_met, _) = evaluate_all_conditions(&[c], now(), |id| {
if id == Some("e_id) {
Some("pending".into())
} else {
None
}
});
assert!(!all_met);
}
#[test]
fn condition_seller_fulfilled_defaults() {
let c = Condition::seller_fulfilled(None);
assert_eq!(c.condition_type, ConditionType::SellerFulfilled);
assert!(!c.completed);
assert!(c.quote_id.is_none());
}
#[test]
fn condition_time_lock_has_release_after() {
let t = now();
let c = Condition::time_lock(t);
assert_eq!(c.condition_type, ConditionType::TimeLock);
assert_eq!(c.release_after, Some(t));
}
#[test]
fn condition_milestone_has_description() {
let c = Condition::milestone("Phase 1 complete");
assert_eq!(c.condition_type, ConditionType::Milestone);
assert_eq!(c.description, Some("Phase 1 complete".into()));
}
#[test]
fn condition_type_display() {
assert_eq!(ConditionType::SellerFulfilled.to_string(), "seller_fulfilled");
assert_eq!(ConditionType::BuyerConfirmed.to_string(), "buyer_confirmed");
assert_eq!(ConditionType::TimeLock.to_string(), "time_lock");
assert_eq!(ConditionType::Milestone.to_string(), "milestone");
}
#[test]
fn condition_serializes_to_json() {
let c = Condition::buyer_confirmed();
let json = serde_json::to_string(&c).unwrap();
assert!(json.contains("\"type\":\"buyer_confirmed\""));
assert!(json.contains("\"completed\":false"));
}
#[test]
fn condition_round_trips_json() {
let original = Condition::milestone("Test milestone");
let json = serde_json::to_string(&original).unwrap();
let deserialized: Condition = serde_json::from_str(&json).unwrap();
assert_eq!(
deserialized.condition_type,
ConditionType::Milestone
);
assert_eq!(deserialized.description, Some("Test milestone".into()));
}
}