2026-01-27 16:21:58 +00:00
|
|
|
//! Conflict Resolution API endpoints.
|
|
|
|
|
|
|
|
|
|
use axum::{
|
|
|
|
|
extract::{Path, State},
|
|
|
|
|
http::StatusCode,
|
|
|
|
|
routing::{get, post},
|
|
|
|
|
Json, Router,
|
|
|
|
|
};
|
|
|
|
|
use serde::Deserialize;
|
|
|
|
|
use serde_json::{json, Value};
|
|
|
|
|
use sqlx::PgPool;
|
|
|
|
|
use uuid::Uuid;
|
|
|
|
|
|
|
|
|
|
use crate::auth::AuthUser;
|
|
|
|
|
use crate::plugins::builtin::conflict_resolution::{ConflictCase, ConflictService};
|
|
|
|
|
|
|
|
|
|
// ============================================================================
|
|
|
|
|
// Request Types
|
|
|
|
|
// ============================================================================
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
|
|
|
pub struct ReportConflictRequest {
|
|
|
|
|
pub conflict_type: String,
|
|
|
|
|
pub title: String,
|
|
|
|
|
pub description: String,
|
|
|
|
|
pub party_a_id: Uuid,
|
|
|
|
|
pub party_b_id: Option<Uuid>,
|
|
|
|
|
pub anonymous: bool,
|
|
|
|
|
pub severity: i32,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
|
|
|
pub struct TransitionStatusRequest {
|
|
|
|
|
pub new_status: String,
|
|
|
|
|
pub notes: Option<String>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
|
|
|
pub struct ProposeCompromiseRequest {
|
|
|
|
|
pub title: String,
|
|
|
|
|
pub description: String,
|
|
|
|
|
pub proposed_actions: Value,
|
|
|
|
|
pub proposed_by_role: String,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
|
|
|
pub struct RespondToCompromiseRequest {
|
|
|
|
|
pub party: String,
|
|
|
|
|
pub response: String,
|
|
|
|
|
pub feedback: Option<String>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
|
|
|
pub struct ScheduleSessionRequest {
|
|
|
|
|
pub scheduled_for: chrono::DateTime<chrono::Utc>,
|
|
|
|
|
pub duration_minutes: i32,
|
|
|
|
|
pub agenda: Option<String>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
|
|
|
pub struct AddNoteRequest {
|
|
|
|
|
pub content: String,
|
|
|
|
|
pub note_type: String,
|
|
|
|
|
pub session_id: Option<Uuid>,
|
|
|
|
|
pub is_confidential: bool,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
|
|
|
pub struct AddMediatorRequest {
|
|
|
|
|
pub user_id: Uuid,
|
|
|
|
|
pub certification_level: Option<String>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ============================================================================
|
|
|
|
|
// Handlers
|
|
|
|
|
// ============================================================================
|
|
|
|
|
|
|
|
|
|
/// Get active conflicts in a community
|
|
|
|
|
async fn get_active_conflicts(
|
|
|
|
|
_auth: AuthUser,
|
|
|
|
|
Path(community_id): Path<Uuid>,
|
|
|
|
|
State(pool): State<PgPool>,
|
|
|
|
|
) -> Result<Json<Vec<ConflictCase>>, (StatusCode, String)> {
|
|
|
|
|
let conflicts = ConflictService::get_active_conflicts(&pool, community_id)
|
|
|
|
|
.await
|
|
|
|
|
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
|
|
|
|
|
|
|
|
|
|
Ok(Json(conflicts))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Get a specific conflict
|
|
|
|
|
async fn get_conflict(
|
|
|
|
|
_auth: AuthUser,
|
|
|
|
|
Path(conflict_id): Path<Uuid>,
|
|
|
|
|
State(pool): State<PgPool>,
|
|
|
|
|
) -> Result<Json<Option<ConflictCase>>, (StatusCode, String)> {
|
|
|
|
|
let conflict = ConflictService::get_conflict(&pool, conflict_id)
|
|
|
|
|
.await
|
|
|
|
|
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
|
|
|
|
|
|
|
|
|
|
Ok(Json(conflict))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Report a new conflict
|
|
|
|
|
async fn report_conflict(
|
|
|
|
|
auth: AuthUser,
|
|
|
|
|
Path(community_id): Path<Uuid>,
|
|
|
|
|
State(pool): State<PgPool>,
|
|
|
|
|
Json(req): Json<ReportConflictRequest>,
|
|
|
|
|
) -> Result<Json<Value>, (StatusCode, String)> {
|
|
|
|
|
let conflict_id = ConflictService::report_conflict(
|
|
|
|
|
&pool,
|
|
|
|
|
community_id,
|
|
|
|
|
&req.title,
|
|
|
|
|
&req.description,
|
|
|
|
|
&req.conflict_type,
|
|
|
|
|
req.party_a_id,
|
|
|
|
|
req.party_b_id,
|
|
|
|
|
Some(auth.user_id),
|
|
|
|
|
req.anonymous,
|
|
|
|
|
req.severity,
|
|
|
|
|
)
|
|
|
|
|
.await
|
|
|
|
|
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
|
|
|
|
|
|
|
|
|
|
Ok(Json(json!({"id": conflict_id})))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Transition conflict status
|
|
|
|
|
async fn transition_status(
|
|
|
|
|
auth: AuthUser,
|
|
|
|
|
Path(conflict_id): Path<Uuid>,
|
|
|
|
|
State(pool): State<PgPool>,
|
|
|
|
|
Json(req): Json<TransitionStatusRequest>,
|
|
|
|
|
) -> Result<Json<Value>, (StatusCode, String)> {
|
|
|
|
|
ConflictService::transition_status(
|
|
|
|
|
&pool,
|
|
|
|
|
conflict_id,
|
|
|
|
|
&req.new_status,
|
|
|
|
|
auth.user_id,
|
|
|
|
|
req.notes.as_deref(),
|
|
|
|
|
)
|
|
|
|
|
.await
|
|
|
|
|
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
|
|
|
|
|
|
|
|
|
|
Ok(Json(json!({"success": true})))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Propose a compromise
|
|
|
|
|
async fn propose_compromise(
|
|
|
|
|
auth: AuthUser,
|
|
|
|
|
Path(conflict_id): Path<Uuid>,
|
|
|
|
|
State(pool): State<PgPool>,
|
|
|
|
|
Json(req): Json<ProposeCompromiseRequest>,
|
|
|
|
|
) -> Result<Json<Value>, (StatusCode, String)> {
|
|
|
|
|
let proposal_id = ConflictService::propose_compromise(
|
|
|
|
|
&pool,
|
|
|
|
|
conflict_id,
|
|
|
|
|
&req.title,
|
|
|
|
|
&req.description,
|
|
|
|
|
req.proposed_actions,
|
|
|
|
|
auth.user_id,
|
|
|
|
|
&req.proposed_by_role,
|
|
|
|
|
)
|
|
|
|
|
.await
|
|
|
|
|
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
|
|
|
|
|
|
|
|
|
|
Ok(Json(json!({"id": proposal_id})))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Respond to a compromise proposal
|
|
|
|
|
async fn respond_to_compromise(
|
|
|
|
|
_auth: AuthUser,
|
|
|
|
|
Path(proposal_id): Path<Uuid>,
|
|
|
|
|
State(pool): State<PgPool>,
|
|
|
|
|
Json(req): Json<RespondToCompromiseRequest>,
|
|
|
|
|
) -> Result<Json<Value>, (StatusCode, String)> {
|
|
|
|
|
ConflictService::respond_to_compromise(
|
|
|
|
|
&pool,
|
|
|
|
|
proposal_id,
|
|
|
|
|
&req.party,
|
|
|
|
|
&req.response,
|
|
|
|
|
req.feedback.as_deref(),
|
|
|
|
|
)
|
|
|
|
|
.await
|
|
|
|
|
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
|
|
|
|
|
|
|
|
|
|
Ok(Json(json!({"success": true})))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Schedule a mediation session
|
|
|
|
|
async fn schedule_session(
|
|
|
|
|
_auth: AuthUser,
|
|
|
|
|
Path(conflict_id): Path<Uuid>,
|
|
|
|
|
State(pool): State<PgPool>,
|
|
|
|
|
Json(req): Json<ScheduleSessionRequest>,
|
|
|
|
|
) -> Result<Json<Value>, (StatusCode, String)> {
|
|
|
|
|
let session_id = ConflictService::schedule_session(
|
|
|
|
|
&pool,
|
|
|
|
|
conflict_id,
|
|
|
|
|
req.scheduled_for,
|
|
|
|
|
req.duration_minutes,
|
|
|
|
|
req.agenda.as_deref(),
|
|
|
|
|
)
|
|
|
|
|
.await
|
|
|
|
|
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
|
|
|
|
|
|
|
|
|
|
Ok(Json(json!({"id": session_id})))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Add a note to a conflict
|
|
|
|
|
async fn add_note(
|
|
|
|
|
auth: AuthUser,
|
|
|
|
|
Path(conflict_id): Path<Uuid>,
|
|
|
|
|
State(pool): State<PgPool>,
|
|
|
|
|
Json(req): Json<AddNoteRequest>,
|
|
|
|
|
) -> Result<Json<Value>, (StatusCode, String)> {
|
|
|
|
|
let note_id = ConflictService::add_note(
|
|
|
|
|
&pool,
|
|
|
|
|
conflict_id,
|
|
|
|
|
req.session_id,
|
|
|
|
|
auth.user_id,
|
|
|
|
|
&req.content,
|
|
|
|
|
&req.note_type,
|
|
|
|
|
req.is_confidential,
|
|
|
|
|
)
|
|
|
|
|
.await
|
|
|
|
|
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
|
|
|
|
|
|
|
|
|
|
Ok(Json(json!({"id": note_id})))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Get conflict statistics for a community
|
|
|
|
|
async fn get_statistics(
|
|
|
|
|
_auth: AuthUser,
|
|
|
|
|
Path(community_id): Path<Uuid>,
|
|
|
|
|
State(pool): State<PgPool>,
|
|
|
|
|
) -> Result<Json<Value>, (StatusCode, String)> {
|
|
|
|
|
let stats = ConflictService::get_statistics(&pool, community_id)
|
|
|
|
|
.await
|
|
|
|
|
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
|
|
|
|
|
|
|
|
|
|
Ok(Json(stats))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Add user to mediator pool
|
|
|
|
|
async fn add_to_mediator_pool(
|
|
|
|
|
_auth: AuthUser,
|
|
|
|
|
Path(community_id): Path<Uuid>,
|
|
|
|
|
State(pool): State<PgPool>,
|
|
|
|
|
Json(req): Json<AddMediatorRequest>,
|
|
|
|
|
) -> Result<Json<Value>, (StatusCode, String)> {
|
|
|
|
|
let mediator_id = ConflictService::add_to_mediator_pool(
|
|
|
|
|
&pool,
|
|
|
|
|
community_id,
|
|
|
|
|
req.user_id,
|
|
|
|
|
req.certification_level.as_deref(),
|
|
|
|
|
)
|
|
|
|
|
.await
|
|
|
|
|
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
|
|
|
|
|
|
|
|
|
|
Ok(Json(json!({"id": mediator_id})))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ============================================================================
|
|
|
|
|
// Router
|
|
|
|
|
// ============================================================================
|
|
|
|
|
|
|
|
|
|
pub fn router(pool: PgPool) -> Router {
|
|
|
|
|
Router::new()
|
|
|
|
|
// Community conflicts
|
2026-02-03 16:54:39 +00:00
|
|
|
.route(
|
|
|
|
|
"/api/communities/{community_id}/conflicts",
|
|
|
|
|
get(get_active_conflicts).post(report_conflict),
|
|
|
|
|
)
|
|
|
|
|
.route(
|
|
|
|
|
"/api/communities/{community_id}/conflicts/stats",
|
|
|
|
|
get(get_statistics),
|
|
|
|
|
)
|
|
|
|
|
.route(
|
|
|
|
|
"/api/communities/{community_id}/mediators",
|
|
|
|
|
post(add_to_mediator_pool),
|
|
|
|
|
)
|
2026-01-27 16:21:58 +00:00
|
|
|
// Individual conflict operations
|
|
|
|
|
.route("/api/conflicts/{conflict_id}", get(get_conflict))
|
2026-02-03 16:54:39 +00:00
|
|
|
.route(
|
|
|
|
|
"/api/conflicts/{conflict_id}/status",
|
|
|
|
|
post(transition_status),
|
|
|
|
|
)
|
|
|
|
|
.route(
|
|
|
|
|
"/api/conflicts/{conflict_id}/compromise",
|
|
|
|
|
post(propose_compromise),
|
|
|
|
|
)
|
|
|
|
|
.route(
|
|
|
|
|
"/api/conflicts/{conflict_id}/session",
|
|
|
|
|
post(schedule_session),
|
|
|
|
|
)
|
2026-01-27 16:21:58 +00:00
|
|
|
.route("/api/conflicts/{conflict_id}/note", post(add_note))
|
|
|
|
|
// Compromise responses
|
2026-02-03 16:54:39 +00:00
|
|
|
.route(
|
|
|
|
|
"/api/compromises/{proposal_id}/respond",
|
|
|
|
|
post(respond_to_compromise),
|
|
|
|
|
)
|
2026-01-27 16:21:58 +00:00
|
|
|
.with_state(pool)
|
|
|
|
|
}
|