use async_trait::async_trait;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use turnframe_core::case::CaseKey;
use turnframe_core::ids::{
AccountId, CaseRevision, ConversationId, EventId, InteractionId, OptionId, TurnId,
};
use turnframe_core::interaction::{Interaction, InteractionStatus};
use crate::error::StoreError;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum ResolutionOutcome {
Resolved {
event_ids: Vec<EventId>,
},
Failed {
code: String,
},
RestoreActive,
}
impl ResolutionOutcome {
#[must_use]
pub fn target_status(&self) -> InteractionStatus {
match self {
Self::Resolved { .. } => InteractionStatus::Resolved,
Self::Failed { .. } => InteractionStatus::Failed,
Self::RestoreActive => InteractionStatus::Active,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
#[non_exhaustive]
pub enum InvalidationReason {
RevisionChanged,
Superseded {
by: InteractionId,
},
CaseClosed,
Administrative {
code: String,
},
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct InvalidationRecord {
pub reason: InvalidationReason,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub new_revision: Option<CaseRevision>,
pub at: DateTime<Utc>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct InteractionRecord {
pub interaction: Interaction,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub resolved_by_turn: Option<TurnId>,
#[serde(default)]
pub resolution_event_ids: Vec<EventId>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub failure_code: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub invalidation: Option<InvalidationRecord>,
}
impl InteractionRecord {
#[must_use]
pub fn new(interaction: Interaction) -> Self {
Self {
interaction,
resolved_by_turn: None,
resolution_event_ids: Vec::new(),
failure_code: None,
invalidation: None,
}
}
#[must_use]
pub fn id(&self) -> InteractionId {
self.interaction.id
}
#[must_use]
pub fn status(&self) -> InteractionStatus {
self.interaction.status
}
}
#[async_trait]
pub trait InteractionReader: Send + Sync {
async fn get(
&self,
account: &AccountId,
id: &InteractionId,
) -> Result<InteractionRecord, StoreError>;
async fn list_open_for_conversation(
&self,
account: &AccountId,
conversation: &ConversationId,
) -> Result<Vec<Interaction>, StoreError>;
async fn list_open_for_case(
&self,
account: &AccountId,
case_key: &CaseKey,
) -> Result<Vec<Interaction>, StoreError>;
async fn blocking_answered_at(
&self,
account: &AccountId,
case_key: &CaseKey,
revision: CaseRevision,
) -> Result<bool, StoreError>;
}
#[async_trait]
pub trait InteractionWriter: Send + Sync {
async fn insert(&self, interaction: Interaction) -> Result<(), StoreError>;
async fn insert_replacing_blocking(
&self,
interaction: Interaction,
) -> Result<Vec<InteractionId>, StoreError>;
async fn begin_resolution(
&self,
account: &AccountId,
id: &InteractionId,
expected_status: InteractionStatus,
option_id: OptionId,
resolved_by: TurnId,
) -> Result<InteractionRecord, StoreError>;
async fn finish_resolution(
&self,
account: &AccountId,
id: &InteractionId,
outcome: ResolutionOutcome,
) -> Result<InteractionRecord, StoreError>;
async fn invalidate_for_case(
&self,
account: &AccountId,
case_key: &CaseKey,
new_revision: CaseRevision,
reason: InvalidationReason,
) -> Result<Vec<InteractionId>, StoreError>;
async fn invalidate_case_cards(
&self,
account: &AccountId,
case_key: &CaseKey,
reason: InvalidationReason,
) -> Result<Vec<InteractionId>, StoreError>;
async fn expire_due(&self, now: DateTime<Utc>) -> Result<Vec<InteractionId>, StoreError>;
}
pub trait InteractionStore: InteractionReader + InteractionWriter {}
impl<T: InteractionReader + InteractionWriter + ?Sized> InteractionStore for T {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn outcome_targets() {
assert_eq!(
ResolutionOutcome::Resolved { event_ids: vec![] }.target_status(),
InteractionStatus::Resolved
);
assert_eq!(
ResolutionOutcome::Failed { code: "x".into() }.target_status(),
InteractionStatus::Failed
);
assert_eq!(
ResolutionOutcome::RestoreActive.target_status(),
InteractionStatus::Active
);
}
#[test]
fn reason_serializes_tagged() {
let json = serde_json::to_value(InvalidationReason::Superseded {
by: InteractionId::nil(),
})
.unwrap();
assert_eq!(json["kind"], "superseded");
}
}