1use axum::{
6 extract::{Path, Query, State},
7 http::StatusCode,
8 response::Json,
9};
10use mockforge_core::consistency::{
12 enrich_order_response, enrich_user_response, get_user_orders_via_graph, ConsistencyEngine,
13 EntityState, UnifiedState,
14};
15use mockforge_core::reality::RealityLevel;
16use mockforge_data::{LifecyclePreset, LifecycleState, PersonaLifecycle, PersonaProfile};
17use serde::Deserialize;
18use serde_json::Value as JsonValue;
19use serde_json::Value;
20use std::sync::Arc;
21use tracing::{error, info};
22
23#[derive(Clone)]
25pub struct ConsistencyState {
26 pub engine: Arc<ConsistencyEngine>,
28}
29
30#[derive(Debug, Deserialize)]
32pub struct SetPersonaRequest {
33 pub persona: PersonaProfile,
35}
36
37#[derive(Debug, Deserialize)]
39pub struct SetScenarioRequest {
40 pub scenario_id: String,
42}
43
44#[derive(Debug, Deserialize)]
46pub struct SetRealityLevelRequest {
47 pub level: u8,
49}
50
51#[derive(Debug, Deserialize)]
53pub struct SetRealityRatioRequest {
54 pub ratio: f64,
56}
57
58#[derive(Debug, Deserialize)]
60pub struct RegisterEntityRequest {
61 pub entity_type: String,
63 pub entity_id: String,
65 pub data: Value,
67 pub persona_id: Option<String>,
69}
70
71#[derive(Debug, Deserialize)]
73pub struct ActivateChaosRuleRequest {
74 pub rule: JsonValue, }
77
78#[derive(Debug, Deserialize)]
80pub struct DeactivateChaosRuleRequest {
81 pub rule_name: String,
83}
84
85#[derive(Debug, Deserialize)]
87pub struct SetPersonaLifecycleRequest {
88 pub persona_id: String,
90 pub initial_state: String,
92}
93
94#[derive(Debug, Deserialize)]
96pub struct ApplyLifecyclePresetRequest {
97 pub persona_id: String,
99 pub preset: String,
101}
102
103#[derive(Debug, Deserialize)]
105pub struct WorkspaceQuery {
106 #[serde(default = "default_workspace")]
108 pub workspace: String,
109}
110
111fn default_workspace() -> String {
112 "default".to_string()
113}
114
115pub async fn get_state(
119 State(state): State<ConsistencyState>,
120 Query(params): Query<WorkspaceQuery>,
121) -> Result<Json<UnifiedState>, StatusCode> {
122 let unified_state = state.engine.get_state(¶ms.workspace).await.ok_or_else(|| {
123 error!("State not found for workspace: {}", params.workspace);
124 StatusCode::NOT_FOUND
125 })?;
126
127 Ok(Json(unified_state))
128}
129
130pub async fn set_persona(
134 State(state): State<ConsistencyState>,
135 Query(params): Query<WorkspaceQuery>,
136 Json(request): Json<SetPersonaRequest>,
137) -> Result<Json<Value>, StatusCode> {
138 let persona_id = request.persona.id.clone();
140
141 state
142 .engine
143 .set_active_persona(¶ms.workspace, request.persona)
144 .await
145 .map_err(|e| {
146 error!("Failed to set persona: {}", e);
147 StatusCode::INTERNAL_SERVER_ERROR
148 })?;
149
150 mockforge_core::pillar_tracking::record_reality_usage(
152 Some(params.workspace.clone()),
153 None,
154 "smart_personas_usage",
155 serde_json::json!({
156 "persona_id": persona_id,
157 "action": "activated"
158 }),
159 )
160 .await;
161
162 info!("Set persona for workspace: {}", params.workspace);
163 Ok(Json(serde_json::json!({
164 "success": true,
165 "workspace": params.workspace,
166 })))
167}
168
169pub async fn set_scenario(
173 State(state): State<ConsistencyState>,
174 Query(params): Query<WorkspaceQuery>,
175 Json(request): Json<SetScenarioRequest>,
176) -> Result<Json<Value>, StatusCode> {
177 state
178 .engine
179 .set_active_scenario(¶ms.workspace, request.scenario_id)
180 .await
181 .map_err(|e| {
182 error!("Failed to set scenario: {}", e);
183 StatusCode::INTERNAL_SERVER_ERROR
184 })?;
185
186 info!("Set scenario for workspace: {}", params.workspace);
187 Ok(Json(serde_json::json!({
188 "success": true,
189 "workspace": params.workspace,
190 })))
191}
192
193pub async fn set_reality_level(
197 State(state): State<ConsistencyState>,
198 Query(params): Query<WorkspaceQuery>,
199 Json(request): Json<SetRealityLevelRequest>,
200) -> Result<Json<Value>, StatusCode> {
201 let level = RealityLevel::from_value(request.level).ok_or_else(|| {
202 error!("Invalid reality level: {}", request.level);
203 StatusCode::BAD_REQUEST
204 })?;
205
206 state.engine.set_reality_level(¶ms.workspace, level).await.map_err(|e| {
207 error!("Failed to set reality level: {}", e);
208 StatusCode::INTERNAL_SERVER_ERROR
209 })?;
210
211 info!("Set reality level for workspace: {}", params.workspace);
212 Ok(Json(serde_json::json!({
213 "success": true,
214 "workspace": params.workspace,
215 "level": request.level,
216 })))
217}
218
219pub async fn set_reality_ratio(
223 State(state): State<ConsistencyState>,
224 Query(params): Query<WorkspaceQuery>,
225 Json(request): Json<SetRealityRatioRequest>,
226) -> Result<Json<Value>, StatusCode> {
227 if !(0.0..=1.0).contains(&request.ratio) {
228 return Err(StatusCode::BAD_REQUEST);
229 }
230
231 state
232 .engine
233 .set_reality_ratio(¶ms.workspace, request.ratio)
234 .await
235 .map_err(|e| {
236 error!("Failed to set reality ratio: {}", e);
237 StatusCode::INTERNAL_SERVER_ERROR
238 })?;
239
240 info!("Set reality ratio for workspace: {}", params.workspace);
241 Ok(Json(serde_json::json!({
242 "success": true,
243 "workspace": params.workspace,
244 "ratio": request.ratio,
245 })))
246}
247
248pub async fn register_entity(
252 State(state): State<ConsistencyState>,
253 Query(params): Query<WorkspaceQuery>,
254 Json(request): Json<RegisterEntityRequest>,
255) -> Result<Json<Value>, StatusCode> {
256 let mut entity = EntityState::new(request.entity_type, request.entity_id, request.data);
257 if let Some(persona_id) = request.persona_id {
258 entity.persona_id = Some(persona_id);
259 }
260
261 state
262 .engine
263 .register_entity(¶ms.workspace, entity.clone())
264 .await
265 .map_err(|e| {
266 error!("Failed to register entity: {}", e);
267 StatusCode::INTERNAL_SERVER_ERROR
268 })?;
269
270 info!(
271 "Registered entity {}:{} for workspace: {}",
272 entity.entity_type, entity.entity_id, params.workspace
273 );
274 Ok(Json(serde_json::json!({
275 "success": true,
276 "workspace": params.workspace,
277 "entity": entity,
278 })))
279}
280
281pub async fn get_entity(
285 State(state): State<ConsistencyState>,
286 Path((entity_type, entity_id)): Path<(String, String)>,
287 Query(params): Query<WorkspaceQuery>,
288) -> Result<Json<EntityState>, StatusCode> {
289 let entity = state
290 .engine
291 .get_entity(¶ms.workspace, &entity_type, &entity_id)
292 .await
293 .ok_or_else(|| {
294 error!(
295 "Entity not found: {}:{} in workspace: {}",
296 entity_type, entity_id, params.workspace
297 );
298 StatusCode::NOT_FOUND
299 })?;
300
301 Ok(Json(entity))
302}
303
304pub async fn list_entities(
308 State(state): State<ConsistencyState>,
309 Query(params): Query<WorkspaceQuery>,
310) -> Result<Json<Value>, StatusCode> {
311 let unified_state = state.engine.get_state(¶ms.workspace).await.ok_or_else(|| {
312 error!("State not found for workspace: {}", params.workspace);
313 StatusCode::NOT_FOUND
314 })?;
315
316 let entities: Vec<&EntityState> = unified_state.entity_state.values().collect();
317 Ok(Json(serde_json::json!({
318 "workspace": params.workspace,
319 "entities": entities,
320 "count": entities.len(),
321 })))
322}
323
324pub async fn activate_chaos_rule(
328 State(state): State<ConsistencyState>,
329 Query(params): Query<WorkspaceQuery>,
330 Json(request): Json<ActivateChaosRuleRequest>,
331) -> Result<Json<Value>, StatusCode> {
332 state
333 .engine
334 .activate_chaos_rule(¶ms.workspace, request.rule)
335 .await
336 .map_err(|e| {
337 error!("Failed to activate chaos rule: {}", e);
338 StatusCode::INTERNAL_SERVER_ERROR
339 })?;
340
341 info!("Activated chaos rule for workspace: {}", params.workspace);
342 Ok(Json(serde_json::json!({
343 "success": true,
344 "workspace": params.workspace,
345 })))
346}
347
348pub async fn deactivate_chaos_rule(
352 State(state): State<ConsistencyState>,
353 Query(params): Query<WorkspaceQuery>,
354 Json(request): Json<DeactivateChaosRuleRequest>,
355) -> Result<Json<Value>, StatusCode> {
356 state
357 .engine
358 .deactivate_chaos_rule(¶ms.workspace, &request.rule_name)
359 .await
360 .map_err(|e| {
361 error!("Failed to deactivate chaos rule: {}", e);
362 StatusCode::INTERNAL_SERVER_ERROR
363 })?;
364
365 info!("Deactivated chaos rule for workspace: {}", params.workspace);
366 Ok(Json(serde_json::json!({
367 "success": true,
368 "workspace": params.workspace,
369 "rule_name": request.rule_name,
370 })))
371}
372
373pub async fn set_persona_lifecycle(
377 State(state): State<ConsistencyState>,
378 Query(params): Query<WorkspaceQuery>,
379 Json(request): Json<SetPersonaLifecycleRequest>,
380) -> Result<Json<Value>, StatusCode> {
381 let lifecycle_state = match request.initial_state.to_lowercase().as_str() {
383 "new" | "new_signup" => LifecycleState::NewSignup,
384 "active" => LifecycleState::Active,
385 "power_user" | "poweruser" => LifecycleState::PowerUser,
386 "churn_risk" | "churnrisk" => LifecycleState::ChurnRisk,
387 "churned" => LifecycleState::Churned,
388 "upgrade_pending" | "upgradepending" => LifecycleState::UpgradePending,
389 "payment_failed" | "paymentfailed" => LifecycleState::PaymentFailed,
390 _ => {
391 error!("Invalid lifecycle state: {}", request.initial_state);
392 return Err(StatusCode::BAD_REQUEST);
393 }
394 };
395
396 let unified_state = state.engine.get_state(¶ms.workspace).await.ok_or_else(|| {
398 error!("State not found for workspace: {}", params.workspace);
399 StatusCode::NOT_FOUND
400 })?;
401
402 if let Some(ref persona) = unified_state.active_persona {
404 if persona.id == request.persona_id {
405 let mut persona_mut = persona.clone();
406 let lifecycle = PersonaLifecycle::new(request.persona_id.clone(), lifecycle_state);
407 persona_mut.set_lifecycle(lifecycle);
408
409 if let Some(ref lifecycle) = persona_mut.lifecycle {
411 let effects = lifecycle.apply_lifecycle_effects();
412 for (key, value) in effects {
413 persona_mut.set_trait(key, value);
414 }
415 }
416
417 state
419 .engine
420 .set_active_persona(¶ms.workspace, persona_mut)
421 .await
422 .map_err(|e| {
423 error!("Failed to set persona lifecycle: {}", e);
424 StatusCode::INTERNAL_SERVER_ERROR
425 })?;
426
427 info!(
428 "Set lifecycle state {} for persona {} in workspace: {}",
429 request.initial_state, request.persona_id, params.workspace
430 );
431
432 return Ok(Json(serde_json::json!({
433 "success": true,
434 "workspace": params.workspace,
435 "persona_id": request.persona_id,
436 "lifecycle_state": request.initial_state,
437 })));
438 }
439 }
440
441 error!(
442 "Persona {} not found or not active in workspace: {}",
443 request.persona_id, params.workspace
444 );
445 Err(StatusCode::NOT_FOUND)
446}
447
448pub async fn get_user_with_graph(
453 State(state): State<ConsistencyState>,
454 Path(user_id): Path<String>,
455 Query(params): Query<WorkspaceQuery>,
456) -> Result<Json<Value>, StatusCode> {
457 let user_entity = state
459 .engine
460 .get_entity(¶ms.workspace, "user", &user_id)
461 .await
462 .ok_or_else(|| {
463 error!("User not found: {} in workspace: {}", user_id, params.workspace);
464 StatusCode::NOT_FOUND
465 })?;
466
467 let mut response = user_entity.data.clone();
469 enrich_user_response(&state.engine, ¶ms.workspace, &user_id, &mut response).await;
470
471 Ok(Json(response))
472}
473
474pub async fn get_user_orders_with_graph(
479 State(state): State<ConsistencyState>,
480 Path(user_id): Path<String>,
481 Query(params): Query<WorkspaceQuery>,
482) -> Result<Json<Value>, StatusCode> {
483 state
485 .engine
486 .get_entity(¶ms.workspace, "user", &user_id)
487 .await
488 .ok_or_else(|| {
489 error!("User not found: {} in workspace: {}", user_id, params.workspace);
490 StatusCode::NOT_FOUND
491 })?;
492
493 let orders = get_user_orders_via_graph(&state.engine, ¶ms.workspace, &user_id).await;
495
496 let orders_json: Vec<Value> = orders.iter().map(|e| e.data.clone()).collect();
498
499 Ok(Json(serde_json::json!({
500 "user_id": user_id,
501 "orders": orders_json,
502 "count": orders_json.len(),
503 })))
504}
505
506pub async fn get_order_with_graph(
511 State(state): State<ConsistencyState>,
512 Path(order_id): Path<String>,
513 Query(params): Query<WorkspaceQuery>,
514) -> Result<Json<Value>, StatusCode> {
515 let order_entity = state
517 .engine
518 .get_entity(¶ms.workspace, "order", &order_id)
519 .await
520 .ok_or_else(|| {
521 error!("Order not found: {} in workspace: {}", order_id, params.workspace);
522 StatusCode::NOT_FOUND
523 })?;
524
525 let mut response = order_entity.data.clone();
527 enrich_order_response(&state.engine, ¶ms.workspace, &order_id, &mut response).await;
528
529 Ok(Json(response))
530}
531
532pub async fn update_persona_lifecycles(
538 State(state): State<ConsistencyState>,
539 Query(params): Query<WorkspaceQuery>,
540) -> Result<Json<Value>, StatusCode> {
541 use mockforge_core::time_travel::now as get_virtual_time;
542
543 let mut unified_state = state.engine.get_state(¶ms.workspace).await.ok_or_else(|| {
545 error!("State not found for workspace: {}", params.workspace);
546 StatusCode::NOT_FOUND
547 })?;
548
549 let current_time = get_virtual_time();
551
552 let mut updated = false;
554 if let Some(ref mut persona) = unified_state.active_persona {
555 let old_state = persona
556 .lifecycle
557 .as_ref()
558 .map(|l| l.current_state)
559 .unwrap_or(mockforge_data::LifecycleState::Active);
560
561 persona.update_lifecycle_state(current_time);
563
564 let new_state = persona
565 .lifecycle
566 .as_ref()
567 .map(|l| l.current_state)
568 .unwrap_or(mockforge_data::LifecycleState::Active);
569
570 if old_state != new_state {
571 updated = true;
572 info!(
573 "Persona {} lifecycle state updated: {:?} -> {:?}",
574 persona.id, old_state, new_state
575 );
576
577 state
579 .engine
580 .set_active_persona(¶ms.workspace, persona.clone())
581 .await
582 .map_err(|e| {
583 error!("Failed to update persona lifecycle: {}", e);
584 StatusCode::INTERNAL_SERVER_ERROR
585 })?;
586 }
587 }
588
589 Ok(Json(serde_json::json!({
590 "success": true,
591 "workspace": params.workspace,
592 "updated": updated,
593 "current_time": current_time.to_rfc3339(),
594 })))
595}
596
597pub async fn list_lifecycle_presets() -> Json<Value> {
601 let presets: Vec<serde_json::Value> = LifecyclePreset::all()
602 .iter()
603 .map(|preset| {
604 serde_json::json!({
605 "name": preset.name(),
606 "id": format!("{:?}", preset).to_lowercase(),
607 "description": preset.description(),
608 })
609 })
610 .collect();
611
612 Json(serde_json::json!({
613 "presets": presets,
614 }))
615}
616
617pub async fn get_lifecycle_preset_details(
621 Path(preset_name): Path<String>,
622) -> Result<Json<Value>, StatusCode> {
623 let preset = match preset_name.to_lowercase().as_str() {
625 "subscription" => LifecyclePreset::Subscription,
626 "loan" => LifecyclePreset::Loan,
627 "order_fulfillment" | "orderfulfillment" => LifecyclePreset::OrderFulfillment,
628 "user_engagement" | "userengagement" => LifecyclePreset::UserEngagement,
629 _ => {
630 error!("Unknown lifecycle preset: {}", preset_name);
631 return Err(StatusCode::NOT_FOUND);
632 }
633 };
634
635 let sample_lifecycle = PersonaLifecycle::from_preset(preset, "sample".to_string());
637
638 let response = serde_json::json!({
640 "preset": {
641 "name": preset.name(),
642 "id": format!("{:?}", preset).to_lowercase(),
643 "description": preset.description(),
644 },
645 "initial_state": format!("{:?}", sample_lifecycle.current_state),
646 "states": sample_lifecycle
647 .transition_rules
648 .iter()
649 .map(|rule| {
650 serde_json::json!({
651 "from": format!("{:?}", sample_lifecycle.current_state),
652 "to": format!("{:?}", rule.to),
653 "after_days": rule.after_days,
654 "condition": rule.condition,
655 })
656 })
657 .collect::<Vec<_>>(),
658 "affected_endpoints": match preset {
659 LifecyclePreset::Subscription => vec!["billing", "support", "subscription"],
660 LifecyclePreset::Loan => vec!["loan", "loans", "credit", "application"],
661 LifecyclePreset::OrderFulfillment => vec!["order", "orders", "fulfillment", "shipment", "delivery"],
662 LifecyclePreset::UserEngagement => vec!["profile", "user", "users", "activity", "engagement", "notifications"],
663 },
664 });
665
666 Ok(Json(response))
667}
668
669pub async fn apply_lifecycle_preset(
673 State(state): State<ConsistencyState>,
674 Query(params): Query<WorkspaceQuery>,
675 Json(request): Json<ApplyLifecyclePresetRequest>,
676) -> Result<Json<Value>, StatusCode> {
677 let preset = match request.preset.to_lowercase().as_str() {
679 "subscription" => LifecyclePreset::Subscription,
680 "loan" => LifecyclePreset::Loan,
681 "order_fulfillment" | "orderfulfillment" => LifecyclePreset::OrderFulfillment,
682 "user_engagement" | "userengagement" => LifecyclePreset::UserEngagement,
683 _ => {
684 error!("Unknown lifecycle preset: {}", request.preset);
685 return Err(StatusCode::BAD_REQUEST);
686 }
687 };
688
689 let mut unified_state = state.engine.get_state(¶ms.workspace).await.ok_or_else(|| {
691 error!("State not found for workspace: {}", params.workspace);
692 StatusCode::NOT_FOUND
693 })?;
694
695 if let Some(ref mut persona) = unified_state.active_persona {
697 if persona.id != request.persona_id {
698 error!(
699 "Persona {} not found or not active in workspace: {}",
700 request.persona_id, params.workspace
701 );
702 return Err(StatusCode::NOT_FOUND);
703 }
704
705 let lifecycle = PersonaLifecycle::from_preset(preset, request.persona_id.clone());
707
708 persona.set_lifecycle(lifecycle.clone());
710
711 let effects = lifecycle.apply_lifecycle_effects();
713 for (key, value) in effects {
714 persona.set_trait(key, value);
715 }
716
717 state
719 .engine
720 .set_active_persona(¶ms.workspace, persona.clone())
721 .await
722 .map_err(|e| {
723 error!("Failed to apply lifecycle preset: {}", e);
724 StatusCode::INTERNAL_SERVER_ERROR
725 })?;
726
727 info!(
728 "Applied lifecycle preset {:?} to persona {} in workspace: {}",
729 preset, request.persona_id, params.workspace
730 );
731
732 return Ok(Json(serde_json::json!({
733 "success": true,
734 "workspace": params.workspace,
735 "persona_id": request.persona_id,
736 "preset": preset.name(),
737 "lifecycle_state": format!("{:?}", lifecycle.current_state),
738 })));
739 }
740
741 error!("No active persona found in workspace: {}", params.workspace);
742 Err(StatusCode::NOT_FOUND)
743}
744
745pub fn consistency_router(state: ConsistencyState) -> axum::Router {
747 use axum::routing::{get, post};
748
749 axum::Router::new()
750 .route("/api/v1/consistency/state", get(get_state))
752 .route("/api/v1/consistency/persona", post(set_persona))
754 .route("/api/v1/consistency/persona/lifecycle", post(set_persona_lifecycle))
755 .route("/api/v1/consistency/persona/update-lifecycles", post(update_persona_lifecycles))
756 .route("/api/v1/consistency/lifecycle-presets", get(list_lifecycle_presets))
758 .route("/api/v1/consistency/lifecycle-presets/{preset_name}", get(get_lifecycle_preset_details))
759 .route("/api/v1/consistency/lifecycle-presets/apply", post(apply_lifecycle_preset))
760 .route("/api/v1/consistency/scenario", post(set_scenario))
762 .route("/api/v1/consistency/reality-level", post(set_reality_level))
764 .route("/api/v1/consistency/reality-ratio", post(set_reality_ratio))
766 .route("/api/v1/consistency/entities", get(list_entities).post(register_entity))
768 .route(
769 "/api/v1/consistency/entities/{entity_type}/{entity_id}",
770 get(get_entity),
771 )
772 .route("/api/v1/consistency/users/{id}", get(get_user_with_graph))
774 .route("/api/v1/consistency/users/{id}/orders", get(get_user_orders_with_graph))
775 .route("/api/v1/consistency/orders/{id}", get(get_order_with_graph))
776 .route("/api/v1/consistency/chaos/activate", post(activate_chaos_rule))
778 .route("/api/v1/consistency/chaos/deactivate", post(deactivate_chaos_rule))
779 .with_state(state)
780}