//! Workspace billing admin endpoints (add-credit without membership requirement). use actix_web::{HttpRequest, HttpResponse, Result, web}; use chrono::Utc; use models::*; use service::workspace::billing::WorkspaceBillingAddCreditParams; use service::AppService; use uuid::Uuid; use sea_orm::sea_query::prelude::rust_decimal::prelude::ToPrimitive; use crate::error::ApiError; use crate::ApiResponse; use service::error::AppError; /// Validate the `x-admin-api-key` header against ADMIN_API_SHARED_KEY env var. fn validate_admin_key(req: &HttpRequest) -> Result<(), ApiError> { let expected = std::env::var("ADMIN_API_SHARED_KEY").ok(); let Some(expected) = expected else { return Err(ApiError(AppError::InternalServerError( "ADMIN_API_SHARED_KEY not configured".into(), ))); }; let provided = req .headers() .get("x-admin-api-key") .and_then(|v| v.to_str().ok()) .ok_or(ApiError(AppError::Unauthorized))?; if provided != expected { return Err(ApiError(AppError::Unauthorized)); } Ok(()) } #[utoipa::path( post, path = "/api/admin/workspaces/{slug}/add-credit", params( ("slug" = String, Path, description = "Workspace slug") ), request_body = WorkspaceBillingAddCreditParams, responses( (status = 200, description = "Credit added", body = ApiResponse), (status = 401, description = "Invalid or missing admin API key"), (status = 404, description = "Workspace not found"), (status = 400, description = "Invalid amount"), ), tag = "Admin" )] pub async fn admin_workspace_add_credit( req: HttpRequest, service: web::Data, path: web::Path, body: web::Json, ) -> Result { validate_admin_key(&req)?; let slug = path.into_inner(); if body.amount <= 0.0 { return Err(ApiError(AppError::BadRequest("Amount must be positive".to_string()))); } let ws = service.utils_find_workspace_by_slug(slug.clone()).await?; let billing = service.ensure_workspace_billing(ws.id).await?; let now_utc = Utc::now(); let new_balance = rust_decimal::Decimal::from_f64_retain( billing.balance.to_f64().unwrap_or_default() + body.amount, ) .unwrap_or(rust_decimal::Decimal::ZERO); // Update billing balance (pk is workspace_id, mark unchanged) let _ = models::workspaces::workspace_billing::ActiveModel { workspace_id: sea_orm::ActiveValue::Unchanged(ws.id), balance: sea_orm::ActiveValue::Set(new_balance), updated_at: sea_orm::ActiveValue::Set(now_utc), ..Default::default() } .update(&service.db) .await; // Insert history record with user_id = NULL (admin action) let reason = body.reason.clone().unwrap_or_else(|| "admin_credit".to_string()); let extra = serde_json::json!({ "description": format!("Admin 手动充值: {}", reason) }); let _ = models::workspaces::workspace_billing_history::ActiveModel { uid: sea_orm::ActiveValue::Set(Uuid::now_v7()), workspace_id: sea_orm::ActiveValue::Set(ws.id), user_id: sea_orm::ActiveValue::Set(None), amount: sea_orm::ActiveValue::Set( rust_decimal::Decimal::from_f64_retain(body.amount) .unwrap_or(rust_decimal::Decimal::ZERO), ), currency: sea_orm::ActiveValue::Set(billing.currency.clone()), reason: sea_orm::ActiveValue::Set(reason), extra: sea_orm::ActiveValue::Set(Some(extra)), created_at: sea_orm::ActiveValue::Set(now_utc), } .insert(&service.db) .await; let session = session::Session::no_op(); let resp = service.workspace_billing_current(&session, slug).await?; Ok(ApiResponse::ok(resp).to_response()) }