use axum::{
extract::{Path, Query, State},
http::StatusCode,
response::Json,
};
use mockforge_core::consistency::{
enrich_order_response, enrich_user_response, get_user_orders_via_graph, ConsistencyEngine,
EntityState, UnifiedState,
};
use mockforge_core::reality::RealityLevel;
use mockforge_data::{LifecyclePreset, LifecycleState, PersonaLifecycle, PersonaProfile};
use serde::Deserialize;
use serde_json::Value as JsonValue;
use serde_json::Value;
use std::sync::Arc;
use tracing::{error, info};
#[derive(Clone)]
pub struct ConsistencyState {
pub engine: Arc<ConsistencyEngine>,
}
#[derive(Debug, Deserialize)]
pub struct SetPersonaRequest {
pub persona: PersonaProfile,
}
#[derive(Debug, Deserialize)]
pub struct SetScenarioRequest {
pub scenario_id: String,
}
#[derive(Debug, Deserialize)]
pub struct SetRealityLevelRequest {
pub level: u8,
}
#[derive(Debug, Deserialize)]
pub struct SetRealityRatioRequest {
pub ratio: f64,
}
#[derive(Debug, Deserialize)]
pub struct RegisterEntityRequest {
pub entity_type: String,
pub entity_id: String,
pub data: Value,
pub persona_id: Option<String>,
}
#[derive(Debug, Deserialize)]
pub struct ActivateChaosRuleRequest {
pub rule: JsonValue, }
#[derive(Debug, Deserialize)]
pub struct DeactivateChaosRuleRequest {
pub rule_name: String,
}
#[derive(Debug, Deserialize)]
pub struct SetPersonaLifecycleRequest {
pub persona_id: String,
pub initial_state: String,
}
#[derive(Debug, Deserialize)]
pub struct ApplyLifecyclePresetRequest {
pub persona_id: String,
pub preset: String,
}
#[derive(Debug, Deserialize)]
pub struct WorkspaceQuery {
#[serde(default = "default_workspace")]
pub workspace: String,
}
fn default_workspace() -> String {
"default".to_string()
}
pub async fn get_state(
State(state): State<ConsistencyState>,
Query(params): Query<WorkspaceQuery>,
) -> Result<Json<UnifiedState>, StatusCode> {
let unified_state = state.engine.get_state(¶ms.workspace).await.ok_or_else(|| {
error!("State not found for workspace: {}", params.workspace);
StatusCode::NOT_FOUND
})?;
Ok(Json(unified_state))
}
pub async fn set_persona(
State(state): State<ConsistencyState>,
Query(params): Query<WorkspaceQuery>,
Json(request): Json<SetPersonaRequest>,
) -> Result<Json<Value>, StatusCode> {
let persona_id = request.persona.id.clone();
state
.engine
.set_active_persona(¶ms.workspace, request.persona)
.await
.map_err(|e| {
error!("Failed to set persona: {}", e);
StatusCode::INTERNAL_SERVER_ERROR
})?;
mockforge_core::pillar_tracking::record_reality_usage(
Some(params.workspace.clone()),
None,
"smart_personas_usage",
serde_json::json!({
"persona_id": persona_id,
"action": "activated"
}),
)
.await;
info!("Set persona for workspace: {}", params.workspace);
Ok(Json(serde_json::json!({
"success": true,
"workspace": params.workspace,
})))
}
pub async fn set_scenario(
State(state): State<ConsistencyState>,
Query(params): Query<WorkspaceQuery>,
Json(request): Json<SetScenarioRequest>,
) -> Result<Json<Value>, StatusCode> {
state
.engine
.set_active_scenario(¶ms.workspace, request.scenario_id)
.await
.map_err(|e| {
error!("Failed to set scenario: {}", e);
StatusCode::INTERNAL_SERVER_ERROR
})?;
info!("Set scenario for workspace: {}", params.workspace);
Ok(Json(serde_json::json!({
"success": true,
"workspace": params.workspace,
})))
}
pub async fn set_reality_level(
State(state): State<ConsistencyState>,
Query(params): Query<WorkspaceQuery>,
Json(request): Json<SetRealityLevelRequest>,
) -> Result<Json<Value>, StatusCode> {
let level = RealityLevel::from_value(request.level).ok_or_else(|| {
error!("Invalid reality level: {}", request.level);
StatusCode::BAD_REQUEST
})?;
state.engine.set_reality_level(¶ms.workspace, level).await.map_err(|e| {
error!("Failed to set reality level: {}", e);
StatusCode::INTERNAL_SERVER_ERROR
})?;
info!("Set reality level for workspace: {}", params.workspace);
Ok(Json(serde_json::json!({
"success": true,
"workspace": params.workspace,
"level": request.level,
})))
}
pub async fn set_reality_ratio(
State(state): State<ConsistencyState>,
Query(params): Query<WorkspaceQuery>,
Json(request): Json<SetRealityRatioRequest>,
) -> Result<Json<Value>, StatusCode> {
if !(0.0..=1.0).contains(&request.ratio) {
return Err(StatusCode::BAD_REQUEST);
}
state
.engine
.set_reality_ratio(¶ms.workspace, request.ratio)
.await
.map_err(|e| {
error!("Failed to set reality ratio: {}", e);
StatusCode::INTERNAL_SERVER_ERROR
})?;
info!("Set reality ratio for workspace: {}", params.workspace);
Ok(Json(serde_json::json!({
"success": true,
"workspace": params.workspace,
"ratio": request.ratio,
})))
}
pub async fn register_entity(
State(state): State<ConsistencyState>,
Query(params): Query<WorkspaceQuery>,
Json(request): Json<RegisterEntityRequest>,
) -> Result<Json<Value>, StatusCode> {
let mut entity = EntityState::new(request.entity_type, request.entity_id, request.data);
if let Some(persona_id) = request.persona_id {
entity.persona_id = Some(persona_id);
}
state
.engine
.register_entity(¶ms.workspace, entity.clone())
.await
.map_err(|e| {
error!("Failed to register entity: {}", e);
StatusCode::INTERNAL_SERVER_ERROR
})?;
info!(
"Registered entity {}:{} for workspace: {}",
entity.entity_type, entity.entity_id, params.workspace
);
Ok(Json(serde_json::json!({
"success": true,
"workspace": params.workspace,
"entity": entity,
})))
}
pub async fn get_entity(
State(state): State<ConsistencyState>,
Path((entity_type, entity_id)): Path<(String, String)>,
Query(params): Query<WorkspaceQuery>,
) -> Result<Json<EntityState>, StatusCode> {
let entity = state
.engine
.get_entity(¶ms.workspace, &entity_type, &entity_id)
.await
.ok_or_else(|| {
error!(
"Entity not found: {}:{} in workspace: {}",
entity_type, entity_id, params.workspace
);
StatusCode::NOT_FOUND
})?;
Ok(Json(entity))
}
pub async fn list_entities(
State(state): State<ConsistencyState>,
Query(params): Query<WorkspaceQuery>,
) -> Result<Json<Value>, StatusCode> {
let unified_state = state.engine.get_state(¶ms.workspace).await.ok_or_else(|| {
error!("State not found for workspace: {}", params.workspace);
StatusCode::NOT_FOUND
})?;
let entities: Vec<&EntityState> = unified_state.entity_state.values().collect();
Ok(Json(serde_json::json!({
"workspace": params.workspace,
"entities": entities,
"count": entities.len(),
})))
}
pub async fn activate_chaos_rule(
State(state): State<ConsistencyState>,
Query(params): Query<WorkspaceQuery>,
Json(request): Json<ActivateChaosRuleRequest>,
) -> Result<Json<Value>, StatusCode> {
state
.engine
.activate_chaos_rule(¶ms.workspace, request.rule)
.await
.map_err(|e| {
error!("Failed to activate chaos rule: {}", e);
StatusCode::INTERNAL_SERVER_ERROR
})?;
info!("Activated chaos rule for workspace: {}", params.workspace);
Ok(Json(serde_json::json!({
"success": true,
"workspace": params.workspace,
})))
}
pub async fn deactivate_chaos_rule(
State(state): State<ConsistencyState>,
Query(params): Query<WorkspaceQuery>,
Json(request): Json<DeactivateChaosRuleRequest>,
) -> Result<Json<Value>, StatusCode> {
state
.engine
.deactivate_chaos_rule(¶ms.workspace, &request.rule_name)
.await
.map_err(|e| {
error!("Failed to deactivate chaos rule: {}", e);
StatusCode::INTERNAL_SERVER_ERROR
})?;
info!("Deactivated chaos rule for workspace: {}", params.workspace);
Ok(Json(serde_json::json!({
"success": true,
"workspace": params.workspace,
"rule_name": request.rule_name,
})))
}
pub async fn set_persona_lifecycle(
State(state): State<ConsistencyState>,
Query(params): Query<WorkspaceQuery>,
Json(request): Json<SetPersonaLifecycleRequest>,
) -> Result<Json<Value>, StatusCode> {
let lifecycle_state = match request.initial_state.to_lowercase().as_str() {
"new" | "new_signup" => LifecycleState::NewSignup,
"active" => LifecycleState::Active,
"power_user" | "poweruser" => LifecycleState::PowerUser,
"churn_risk" | "churnrisk" => LifecycleState::ChurnRisk,
"churned" => LifecycleState::Churned,
"upgrade_pending" | "upgradepending" => LifecycleState::UpgradePending,
"payment_failed" | "paymentfailed" => LifecycleState::PaymentFailed,
_ => {
error!("Invalid lifecycle state: {}", request.initial_state);
return Err(StatusCode::BAD_REQUEST);
}
};
let unified_state = state.engine.get_state(¶ms.workspace).await.ok_or_else(|| {
error!("State not found for workspace: {}", params.workspace);
StatusCode::NOT_FOUND
})?;
if let Some(ref persona) = unified_state.active_persona {
if persona.id == request.persona_id {
let mut persona_mut = persona.clone();
let lifecycle = PersonaLifecycle::new(request.persona_id.clone(), lifecycle_state);
persona_mut.set_lifecycle(lifecycle);
if let Some(ref lifecycle) = persona_mut.lifecycle {
let effects = lifecycle.apply_lifecycle_effects();
for (key, value) in effects {
persona_mut.set_trait(key, value);
}
}
state
.engine
.set_active_persona(¶ms.workspace, persona_mut)
.await
.map_err(|e| {
error!("Failed to set persona lifecycle: {}", e);
StatusCode::INTERNAL_SERVER_ERROR
})?;
info!(
"Set lifecycle state {} for persona {} in workspace: {}",
request.initial_state, request.persona_id, params.workspace
);
return Ok(Json(serde_json::json!({
"success": true,
"workspace": params.workspace,
"persona_id": request.persona_id,
"lifecycle_state": request.initial_state,
})));
}
}
error!(
"Persona {} not found or not active in workspace: {}",
request.persona_id, params.workspace
);
Err(StatusCode::NOT_FOUND)
}
pub async fn get_user_with_graph(
State(state): State<ConsistencyState>,
Path(user_id): Path<String>,
Query(params): Query<WorkspaceQuery>,
) -> Result<Json<Value>, StatusCode> {
let user_entity = state
.engine
.get_entity(¶ms.workspace, "user", &user_id)
.await
.ok_or_else(|| {
error!("User not found: {} in workspace: {}", user_id, params.workspace);
StatusCode::NOT_FOUND
})?;
let mut response = user_entity.data.clone();
enrich_user_response(&state.engine, ¶ms.workspace, &user_id, &mut response).await;
Ok(Json(response))
}
pub async fn get_user_orders_with_graph(
State(state): State<ConsistencyState>,
Path(user_id): Path<String>,
Query(params): Query<WorkspaceQuery>,
) -> Result<Json<Value>, StatusCode> {
state
.engine
.get_entity(¶ms.workspace, "user", &user_id)
.await
.ok_or_else(|| {
error!("User not found: {} in workspace: {}", user_id, params.workspace);
StatusCode::NOT_FOUND
})?;
let orders = get_user_orders_via_graph(&state.engine, ¶ms.workspace, &user_id).await;
let orders_json: Vec<Value> = orders.iter().map(|e| e.data.clone()).collect();
Ok(Json(serde_json::json!({
"user_id": user_id,
"orders": orders_json,
"count": orders_json.len(),
})))
}
pub async fn get_order_with_graph(
State(state): State<ConsistencyState>,
Path(order_id): Path<String>,
Query(params): Query<WorkspaceQuery>,
) -> Result<Json<Value>, StatusCode> {
let order_entity = state
.engine
.get_entity(¶ms.workspace, "order", &order_id)
.await
.ok_or_else(|| {
error!("Order not found: {} in workspace: {}", order_id, params.workspace);
StatusCode::NOT_FOUND
})?;
let mut response = order_entity.data.clone();
enrich_order_response(&state.engine, ¶ms.workspace, &order_id, &mut response).await;
Ok(Json(response))
}
pub async fn update_persona_lifecycles(
State(state): State<ConsistencyState>,
Query(params): Query<WorkspaceQuery>,
) -> Result<Json<Value>, StatusCode> {
use mockforge_core::time_travel::now as get_virtual_time;
let mut unified_state = state.engine.get_state(¶ms.workspace).await.ok_or_else(|| {
error!("State not found for workspace: {}", params.workspace);
StatusCode::NOT_FOUND
})?;
let current_time = get_virtual_time();
let mut updated = false;
if let Some(ref mut persona) = unified_state.active_persona {
let old_state = persona
.lifecycle
.as_ref()
.map(|l| l.current_state)
.unwrap_or(LifecycleState::Active);
persona.update_lifecycle_state(current_time);
let new_state = persona
.lifecycle
.as_ref()
.map(|l| l.current_state)
.unwrap_or(LifecycleState::Active);
if old_state != new_state {
updated = true;
info!(
"Persona {} lifecycle state updated: {:?} -> {:?}",
persona.id, old_state, new_state
);
state
.engine
.set_active_persona(¶ms.workspace, persona.clone())
.await
.map_err(|e| {
error!("Failed to update persona lifecycle: {}", e);
StatusCode::INTERNAL_SERVER_ERROR
})?;
}
}
Ok(Json(serde_json::json!({
"success": true,
"workspace": params.workspace,
"updated": updated,
"current_time": current_time.to_rfc3339(),
})))
}
pub async fn list_lifecycle_presets() -> Json<Value> {
let presets: Vec<Value> = LifecyclePreset::all()
.iter()
.map(|preset| {
serde_json::json!({
"name": preset.name(),
"id": format!("{:?}", preset).to_lowercase(),
"description": preset.description(),
})
})
.collect();
Json(serde_json::json!({
"presets": presets,
}))
}
pub async fn get_lifecycle_preset_details(
Path(preset_name): Path<String>,
) -> Result<Json<Value>, StatusCode> {
let preset = match preset_name.to_lowercase().as_str() {
"subscription" => LifecyclePreset::Subscription,
"loan" => LifecyclePreset::Loan,
"order_fulfillment" | "orderfulfillment" => LifecyclePreset::OrderFulfillment,
"user_engagement" | "userengagement" => LifecyclePreset::UserEngagement,
_ => {
error!("Unknown lifecycle preset: {}", preset_name);
return Err(StatusCode::NOT_FOUND);
}
};
let sample_lifecycle = PersonaLifecycle::from_preset(preset, "sample".to_string());
let response = serde_json::json!({
"preset": {
"name": preset.name(),
"id": format!("{:?}", preset).to_lowercase(),
"description": preset.description(),
},
"initial_state": format!("{:?}", sample_lifecycle.current_state),
"states": sample_lifecycle
.transition_rules
.iter()
.map(|rule| {
serde_json::json!({
"from": format!("{:?}", sample_lifecycle.current_state),
"to": format!("{:?}", rule.to),
"after_days": rule.after_days,
"condition": rule.condition,
})
})
.collect::<Vec<_>>(),
"affected_endpoints": match preset {
LifecyclePreset::Subscription => vec!["billing", "support", "subscription"],
LifecyclePreset::Loan => vec!["loan", "loans", "credit", "application"],
LifecyclePreset::OrderFulfillment => vec!["order", "orders", "fulfillment", "shipment", "delivery"],
LifecyclePreset::UserEngagement => vec!["profile", "user", "users", "activity", "engagement", "notifications"],
},
});
Ok(Json(response))
}
pub async fn apply_lifecycle_preset(
State(state): State<ConsistencyState>,
Query(params): Query<WorkspaceQuery>,
Json(request): Json<ApplyLifecyclePresetRequest>,
) -> Result<Json<Value>, StatusCode> {
let preset = match request.preset.to_lowercase().as_str() {
"subscription" => LifecyclePreset::Subscription,
"loan" => LifecyclePreset::Loan,
"order_fulfillment" | "orderfulfillment" => LifecyclePreset::OrderFulfillment,
"user_engagement" | "userengagement" => LifecyclePreset::UserEngagement,
_ => {
error!("Unknown lifecycle preset: {}", request.preset);
return Err(StatusCode::BAD_REQUEST);
}
};
let mut unified_state = state.engine.get_state(¶ms.workspace).await.ok_or_else(|| {
error!("State not found for workspace: {}", params.workspace);
StatusCode::NOT_FOUND
})?;
if let Some(ref mut persona) = unified_state.active_persona {
if persona.id != request.persona_id {
error!(
"Persona {} not found or not active in workspace: {}",
request.persona_id, params.workspace
);
return Err(StatusCode::NOT_FOUND);
}
let lifecycle = PersonaLifecycle::from_preset(preset, request.persona_id.clone());
persona.set_lifecycle(lifecycle.clone());
let effects = lifecycle.apply_lifecycle_effects();
for (key, value) in effects {
persona.set_trait(key, value);
}
state
.engine
.set_active_persona(¶ms.workspace, persona.clone())
.await
.map_err(|e| {
error!("Failed to apply lifecycle preset: {}", e);
StatusCode::INTERNAL_SERVER_ERROR
})?;
info!(
"Applied lifecycle preset {:?} to persona {} in workspace: {}",
preset, request.persona_id, params.workspace
);
return Ok(Json(serde_json::json!({
"success": true,
"workspace": params.workspace,
"persona_id": request.persona_id,
"preset": preset.name(),
"lifecycle_state": format!("{:?}", lifecycle.current_state),
})));
}
error!("No active persona found in workspace: {}", params.workspace);
Err(StatusCode::NOT_FOUND)
}
pub fn consistency_router(state: ConsistencyState) -> axum::Router {
use axum::routing::{get, post};
axum::Router::new()
.route("/api/v1/consistency/state", get(get_state))
.route("/api/v1/consistency/persona", post(set_persona))
.route("/api/v1/consistency/persona/lifecycle", post(set_persona_lifecycle))
.route("/api/v1/consistency/persona/update-lifecycles", post(update_persona_lifecycles))
.route("/api/v1/consistency/lifecycle-presets", get(list_lifecycle_presets))
.route("/api/v1/consistency/lifecycle-presets/{preset_name}", get(get_lifecycle_preset_details))
.route("/api/v1/consistency/lifecycle-presets/apply", post(apply_lifecycle_preset))
.route("/api/v1/consistency/scenario", post(set_scenario))
.route("/api/v1/consistency/reality-level", post(set_reality_level))
.route("/api/v1/consistency/reality-ratio", post(set_reality_ratio))
.route("/api/v1/consistency/entities", get(list_entities).post(register_entity))
.route(
"/api/v1/consistency/entities/{entity_type}/{entity_id}",
get(get_entity),
)
.route("/api/v1/consistency/users/{id}", get(get_user_with_graph))
.route("/api/v1/consistency/users/{id}/orders", get(get_user_orders_with_graph))
.route("/api/v1/consistency/orders/{id}", get(get_order_with_graph))
.route("/api/v1/consistency/chaos/activate", post(activate_chaos_rule))
.route("/api/v1/consistency/chaos/deactivate", post(deactivate_chaos_rule))
.with_state(state)
}