diff --git a/apps/payments/src/ai_credits.rs b/apps/payments/src/ai_credits.rs index 9744d6b..cfaea6d 100644 --- a/apps/payments/src/ai_credits.rs +++ b/apps/payments/src/ai_credits.rs @@ -7,7 +7,6 @@ use axum::{ Json, Router, }; use contracts::auth_middleware::AuthUser; -use hmac::Mac; use serde::{Deserialize, Serialize}; use sqlx::FromRow; use uuid::Uuid; @@ -19,9 +18,22 @@ struct CreateAiCreditOrderRequest { #[derive(Debug, Deserialize, Serialize)] struct VerifyAiCreditOrderRequest { - order_id: String, - payment_id: String, - signature: Option, + txnid: String, + mihpayid: String, + status: String, + hash: String, + #[serde(default)] + amount: String, + #[serde(default)] + firstname: String, + #[serde(default)] + email: String, + #[serde(default)] + productinfo: String, + #[serde(default)] + udf1: String, + #[serde(default)] + udf2: String, } #[derive(Debug, Serialize, FromRow)] @@ -95,78 +107,53 @@ async fn create_ai_credit_order( } }; - let gateway_config = match crate::resolve_razorpay_config(&state).await { + let gateway_config = match crate::resolve_payu_config(&state).await { Ok(config) => config, Err((status, message)) => { return (status, Json(serde_json::json!({ "error": message }))).into_response(); } }; - let resp = match state - .client - .post(format!("{}/v1/orders", gateway_config.base_url.trim_end_matches('/'))) - .basic_auth(&gateway_config.key_id, Some(&gateway_config.key_secret)) - .json(&serde_json::json!({ - "amount": package.price_inr, - "currency": "INR", - "receipt": format!("ai_{}", Uuid::new_v4().simple()), - "notes": { - "package_id": package.id.to_string(), - "user_id": auth.user_id.to_string(), - "package_type": "AI_CREDITS" - } - })) - .send() - .await - { - Ok(resp) => resp, - Err(e) => { - tracing::error!("Razorpay error creating AI credit order: {}", e); - return ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(serde_json::json!({ "error": "Payment gateway error" })), - ) - .into_response(); + let contact = sqlx::query_as::<_, crate::UserContactRow>( + "SELECT email, full_name, phone FROM users WHERE id = $1", + ) + .bind(auth.user_id) + .fetch_optional(&state.pool) + .await; + + let (email, firstname, phone) = match contact { + Ok(Some(row)) => { + let firstname = row + .full_name + .as_deref() + .and_then(|n| n.split_whitespace().next()) + .unwrap_or("Customer") + .to_string(); + (row.email, firstname, row.phone.unwrap_or_default()) } + _ => (auth.email.clone(), "Customer".to_string(), String::new()), }; - let status = resp.status(); - let body: serde_json::Value = match resp.json().await { - Ok(body) => body, - Err(e) => { - tracing::error!("Failed to parse Razorpay order response: {}", e); - return ( - StatusCode::BAD_GATEWAY, - Json(serde_json::json!({ "error": "Invalid payment gateway response" })), - ) - .into_response(); - } - }; + let txnid = crate::build_txnid(); + let amount = format!("{:.2}", package.price_inr as f64 / 100.0); + let productinfo = package.name.clone(); + let udf1 = package.id.to_string(); + let udf2 = auth.user_id.to_string(); - if status == StatusCode::UNAUTHORIZED { - return ( - StatusCode::UNAUTHORIZED, - Json(serde_json::json!({ "error": "Razorpay authentication failed" })), - ) - .into_response(); - } - - if !status.is_success() { - return ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(serde_json::json!({ - "error": body.get("message").and_then(|m| m.as_str()).unwrap_or("Order creation failed") - })), - ) - .into_response(); - } - - let order_id = body - .get("id") - .or_else(|| body.get("order_id")) - .and_then(|v| v.as_str()) - .unwrap_or_default() - .to_string(); + let hash = crate::payu_request_hash( + &gateway_config.key, + &txnid, + &amount, + &productinfo, + &firstname, + &email, + &udf1, + &udf2, + "", + "", + "", + &gateway_config.salt, + ); if let Err(e) = sqlx::query( r#" @@ -176,7 +163,7 @@ async fn create_ai_credit_order( ) .bind(auth.user_id) .bind(package.id) - .bind(&order_id) + .bind(&txnid) .bind(package.price_inr) .execute(&state.pool) .await @@ -184,14 +171,31 @@ async fn create_ai_credit_order( tracing::error!("Failed to record AI credit payment: {}", e); } + let return_url = format!( + "{}/dashboard/wallet/payu-return", + state.frontend_url.trim_end_matches('/') + ); + ( StatusCode::OK, Json(serde_json::json!({ - "order_id": order_id, - "amount": package.price_inr, + "key": gateway_config.key, + "txnid": txnid, + "order_id": txnid, + "amount": amount, + "productinfo": productinfo, + "firstname": firstname, + "email": email, + "phone": phone, + "surl": return_url, + "furl": return_url, + "hash": hash, + "payu_base_url": gateway_config.base_url, + "udf1": udf1, + "udf2": udf2, "currency": "INR", "credits": package.credits, - "status": body.get("status").and_then(|v| v.as_str()).unwrap_or("created"), + "status": "created", })), ) .into_response() @@ -202,47 +206,30 @@ async fn verify_ai_credit_order( State(state): State, Json(payload): Json, ) -> impl IntoResponse { - let gateway_config = match crate::resolve_razorpay_config(&state).await { + let gateway_config = match crate::resolve_payu_config(&state).await { Ok(config) => config, Err((status, message)) => { return (status, Json(serde_json::json!({ "error": message }))).into_response(); } }; - let signature = match payload.signature.clone() { - Some(signature) => signature, - None => { - return ( - StatusCode::BAD_REQUEST, - Json(serde_json::json!({ "error": "razorpay_signature is required" })), - ) - .into_response(); - } - }; + let expected_hash = crate::payu_response_hash( + &gateway_config.salt, + &payload.status, + &payload.udf1, + &payload.udf2, + "", + "", + "", + &payload.email, + &payload.firstname, + &payload.productinfo, + &payload.amount, + &payload.txnid, + &gateway_config.key, + ); - let provided_signature = match hex::decode(signature) { - Ok(sig) => sig, - Err(_) => { - return ( - StatusCode::BAD_REQUEST, - Json(serde_json::json!({ "error": "Invalid signature format" })), - ) - .into_response(); - } - }; - - let mut mac = match hmac::Hmac::::new_from_slice(gateway_config.key_secret.as_bytes()) { - Ok(mac) => mac, - Err(_) => { - return ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(serde_json::json!({ "error": "Unable to initialize signature verifier" })), - ) - .into_response(); - } - }; - mac.update(format!("{}|{}", payload.order_id, payload.payment_id).as_bytes()); - if mac.verify_slice(&provided_signature).is_err() { + if !expected_hash.eq_ignore_ascii_case(&payload.hash) { return ( StatusCode::BAD_REQUEST, Json(serde_json::json!({ "error": "Signature mismatch" })), @@ -250,6 +237,14 @@ async fn verify_ai_credit_order( .into_response(); } + if !payload.status.eq_ignore_ascii_case("success") { + return ( + StatusCode::BAD_REQUEST, + Json(serde_json::json!({ "error": "PayU reported a non-success status." })), + ) + .into_response(); + } + let payment = match sqlx::query_as::<_, crate::PaymentRow>( r#" SELECT id, user_id, package_id, tracecoins_credited, amount_inr, status, payu_mihpayid @@ -257,7 +252,7 @@ async fn verify_ai_credit_order( WHERE payu_txnid = $1 AND status = 'PENDING' "#, ) - .bind(&payload.order_id) + .bind(&payload.txnid) .fetch_optional(&state.pool) .await { @@ -313,7 +308,7 @@ async fn verify_ai_credit_order( WHERE id = $2 "#, ) - .bind(&payload.payment_id) + .bind(&payload.mihpayid) .bind(payment.id) .execute(&state.pool) .await @@ -393,7 +388,7 @@ async fn verify_ai_credit_order( Json(serde_json::json!({ "verified": true, "credits_added": package.credits, - "payment_id": payload.payment_id, + "payment_id": payload.mihpayid, "status": "success" })), ) diff --git a/apps/payments/src/main.rs b/apps/payments/src/main.rs index 4eb7f96..3ce66e2 100644 --- a/apps/payments/src/main.rs +++ b/apps/payments/src/main.rs @@ -5,9 +5,8 @@ use axum::{ Json, Router, }; use contracts::auth_middleware::AuthUser; -use hmac::{Hmac, Mac}; use serde::{Deserialize, Serialize}; -use sha2::Sha256; +use sha2::{Digest, Sha512}; use sqlx::{postgres::PgPool, FromRow}; use std::net::SocketAddr; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; @@ -16,40 +15,65 @@ use uuid::Uuid; pub mod ai_credits; pub mod packages; -type HmacSha256 = Hmac; - #[derive(Clone)] pub struct AppState { - client: reqwest::Client, pool: PgPool, - fallback_razorpay_key_id: String, - fallback_razorpay_key_secret: String, + pub(crate) client: reqwest::Client, + fallback_payu_key: String, + fallback_payu_salt: String, + frontend_url: String, } #[derive(Debug, Deserialize)] struct CreateOrderRequest { amount: u64, + #[allow(dead_code)] currency: Option, + #[allow(dead_code)] receipt: Option, package_id: Option, } #[derive(Debug, Serialize)] -struct CreateOrderResponse { - order_id: String, - amount: u64, - currency: String, - status: String, +struct PayuOrderResponse { + key: String, + txnid: String, + amount: String, + productinfo: String, + firstname: String, + email: String, + phone: String, + surl: String, + furl: String, + hash: String, + payu_base_url: String, + udf1: String, + udf2: String, } #[derive(Debug, Deserialize)] struct VerifyPaymentRequest { - #[serde(alias = "razorpay_order_id")] - order_id: Option, - #[serde(alias = "razorpay_payment_id")] - payment_id: Option, - #[serde(alias = "razorpay_signature")] - signature: Option, + status: String, + txnid: String, + amount: String, + mihpayid: String, + hash: String, + #[serde(default)] + firstname: String, + #[serde(default)] + email: String, + #[serde(default)] + productinfo: String, + #[serde(default)] + udf1: String, + #[serde(default)] + udf2: String, + #[serde(default)] + udf3: String, + #[serde(default)] + udf4: String, + #[serde(default)] + udf5: String, } #[derive(Debug, Serialize)] @@ -70,14 +94,23 @@ struct PaymentStatusResponse { #[derive(Debug, FromRow)] struct PricingPackageRow { + name: String, tracecoins_amount: i32, price_inr: i32, } +#[derive(Debug, FromRow)] +pub(crate) struct UserContactRow { + pub(crate) email: String, + pub(crate) full_name: Option, + pub(crate) phone: Option, +} + #[derive(Debug, FromRow)] struct PaymentRow { id: Uuid, user_id: Uuid, + #[allow(dead_code)] package_id: Option, tracecoins_credited: Option, amount_inr: i32, @@ -85,28 +118,10 @@ struct PaymentRow { payu_mihpayid: Option, } -#[derive(Debug, Serialize)] -struct RazorpayOrderRequest { - amount: u64, - currency: String, - receipt: String, - notes: serde_json::Value, -} - -#[derive(Debug, Deserialize)] -struct RazorpayOrderResponse { - id: String, - amount: u64, - currency: String, - status: String, -} - #[derive(Debug, Deserialize)] struct StoredGatewayConfig { mode: Option, base_url: Option, - callback_url: Option, - webhook_url: Option, merchant_id: Option, api_key: Option, secret_key: Option, @@ -114,37 +129,38 @@ struct StoredGatewayConfig { #[derive(Debug, FromRow)] struct PaymentGatewayConfigRow { + #[allow(dead_code)] display_name: Option, config_json: Option, is_active: bool, } #[derive(Debug, Clone)] -struct RazorpayConfig { - key_id: String, - key_secret: String, - base_url: String, - mode: String, - enabled: bool, +pub(crate) struct PayuConfig { + pub(crate) key: String, + pub(crate) salt: String, + pub(crate) base_url: String, } fn error_response(status: StatusCode, message: impl Into) -> (StatusCode, String) { (status, message.into()) } -fn build_receipt(receipt: Option) -> String { - let candidate = receipt.unwrap_or_else(|| format!("tc_{}", Uuid::new_v4().simple())); - candidate.chars().take(40).collect() +pub(crate) fn build_txnid() -> String { + format!("tc{}", Uuid::new_v4().simple()) + .chars() + .take(40) + .collect() } -pub(crate) async fn resolve_razorpay_config( - state: &AppState, -) -> Result { +/// Resolves the PayU merchant key/salt from the admin-configured gateway settings, +/// falling back to the deployment's PAYU_MERCHANT_KEY / PAYU_SALT env vars. +pub(crate) async fn resolve_payu_config(state: &AppState) -> Result { let row = sqlx::query_as::<_, PaymentGatewayConfigRow>( r#" SELECT display_name, config_json, is_active FROM payment_gateway_configs - WHERE gateway_key = 'RAZORPAY' + WHERE gateway_key = 'PAYU' ORDER BY is_active DESC, created_at DESC LIMIT 1 "#, @@ -153,9 +169,9 @@ pub(crate) async fn resolve_razorpay_config( .await .map_err(|e| error_response(StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?; - let mut key_id = state.fallback_razorpay_key_id.clone(); - let mut key_secret = state.fallback_razorpay_key_secret.clone(); - let mut base_url = "https://api.razorpay.com".to_string(); + let mut key = state.fallback_payu_key.clone(); + let mut salt = state.fallback_payu_salt.clone(); + let mut base_url = "https://test.payu.in".to_string(); let mut mode = "sandbox".to_string(); let mut enabled = true; @@ -163,51 +179,104 @@ pub(crate) async fn resolve_razorpay_config( enabled = row.is_active; if let Some(config_json) = row.config_json { if let Ok(stored) = serde_json::from_value::(config_json) { - if let Some(value) = stored.api_key.filter(|v| !v.trim().is_empty()) { - key_id = value; + // merchant_id / api_key hold the PayU merchant key; secret_key holds the PayU salt. + if let Some(value) = stored + .api_key + .filter(|v| !v.trim().is_empty()) + .or_else(|| stored.merchant_id.filter(|v| !v.trim().is_empty())) + { + key = value; } if let Some(value) = stored.secret_key.filter(|v| !v.trim().is_empty()) { - key_secret = value; - } - if let Some(value) = stored.base_url.filter(|v| !v.trim().is_empty()) { - base_url = value; + salt = value; } if let Some(value) = stored.mode.filter(|v| !v.trim().is_empty()) { mode = value; } + if let Some(value) = stored.base_url.filter(|v| !v.trim().is_empty()) { + base_url = value; + } } } } + if mode.eq_ignore_ascii_case("live") && base_url == "https://test.payu.in" { + base_url = "https://secure.payu.in".to_string(); + } + if !enabled { return Err(error_response( StatusCode::SERVICE_UNAVAILABLE, - "Razorpay gateway is disabled", + "PayU gateway is disabled", )); } - if key_id.trim().is_empty() || key_secret.trim().is_empty() { + if key.trim().is_empty() || salt.trim().is_empty() { return Err(error_response( StatusCode::INTERNAL_SERVER_ERROR, - "Razorpay credentials are not configured", + "PayU credentials are not configured", )); } - Ok(RazorpayConfig { - key_id, - key_secret, - base_url, - mode, - enabled, - }) + Ok(PayuConfig { key, salt, base_url }) +} + +fn sha512_hex(raw: &str) -> String { + let digest = Sha512::digest(raw.as_bytes()); + hex::encode(digest) +} + +/// PayU's documented request hash: sha512(key|txnid|amount|productinfo|firstname|email|udf1..udf5|||||SALT) +#[allow(clippy::too_many_arguments)] +pub(crate) fn payu_request_hash( + key: &str, + txnid: &str, + amount: &str, + productinfo: &str, + firstname: &str, + email: &str, + udf1: &str, + udf2: &str, + udf3: &str, + udf4: &str, + udf5: &str, + salt: &str, +) -> String { + let raw = format!( + "{key}|{txnid}|{amount}|{productinfo}|{firstname}|{email}|{udf1}|{udf2}|{udf3}|{udf4}|{udf5}||||||{salt}" + ); + sha512_hex(&raw) +} + +/// PayU's documented response hash: sha512(SALT|status|||||udf5|udf4|udf3|udf2|udf1|email|firstname|productinfo|amount|txnid|key) +#[allow(clippy::too_many_arguments)] +pub(crate) fn payu_response_hash( + salt: &str, + status: &str, + udf1: &str, + udf2: &str, + udf3: &str, + udf4: &str, + udf5: &str, + email: &str, + firstname: &str, + productinfo: &str, + amount: &str, + txnid: &str, + key: &str, +) -> String { + let raw = format!( + "{salt}|{status}||||||{udf5}|{udf4}|{udf3}|{udf2}|{udf1}|{email}|{firstname}|{productinfo}|{amount}|{txnid}|{key}" + ); + sha512_hex(&raw) } async fn create_order( auth: AuthUser, State(state): State, Json(payload): Json, -) -> Result, (StatusCode, String)> { - let gateway_config = resolve_razorpay_config(&state).await?; +) -> Result, (StatusCode, String)> { + let gateway_config = resolve_payu_config(&state).await?; let package_id_str = payload .package_id .as_ref() @@ -222,9 +291,8 @@ async fn create_order( )); } - let currency = payload.currency.unwrap_or_else(|| "INR".to_string()); let package = sqlx::query_as::<_, PricingPackageRow>( - "SELECT tracecoins_amount, price_inr FROM pricing_packages WHERE id = $1 AND is_active = true", + "SELECT name, tracecoins_amount, price_inr FROM pricing_packages WHERE id = $1 AND is_active = true", ) .bind(package_id) .fetch_optional(&state.pool) @@ -242,48 +310,47 @@ async fn create_order( )); } - let razorpay_request = RazorpayOrderRequest { - amount: payload.amount, - currency: currency.clone(), - receipt: build_receipt(payload.receipt), - notes: serde_json::json!({ - "package_id": package_id_str, - "user_id": auth.user_id.to_string(), - }), + let contact = sqlx::query_as::<_, UserContactRow>( + "SELECT email, full_name, phone FROM users WHERE id = $1", + ) + .bind(auth.user_id) + .fetch_optional(&state.pool) + .await + .map_err(|e| error_response(StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?; + + let (email, firstname, phone) = match contact { + Some(row) => { + let firstname = row + .full_name + .as_deref() + .and_then(|n| n.split_whitespace().next()) + .unwrap_or("Customer") + .to_string(); + (row.email, firstname, row.phone.unwrap_or_default()) + } + None => (auth.email.clone(), "Customer".to_string(), String::new()), }; - let resp = state - .client - .post(format!("{}/v1/orders", gateway_config.base_url.trim_end_matches('/'))) - .basic_auth(&gateway_config.key_id, Some(&gateway_config.key_secret)) - .json(&razorpay_request) - .send() - .await - .map_err(|e| error_response(StatusCode::INTERNAL_SERVER_ERROR, format!("Razorpay request failed: {e}")))?; + let txnid = build_txnid(); + let amount = format!("{:.2}", payload.amount as f64 / 100.0); + let productinfo = package.name.clone(); + let udf1 = package_id_str.clone(); + let udf2 = auth.user_id.to_string(); - let status = resp.status(); - if status == StatusCode::UNAUTHORIZED { - return Err(error_response( - StatusCode::UNAUTHORIZED, - "Razorpay authentication failed", - )); - } - - if !status.is_success() { - let body = resp - .text() - .await - .unwrap_or_else(|_| "Unable to read Razorpay error response".to_string()); - return Err(error_response( - StatusCode::INTERNAL_SERVER_ERROR, - format!("Razorpay order creation failed: {body}"), - )); - } - - let razorpay_order = resp - .json::() - .await - .map_err(|e| error_response(StatusCode::INTERNAL_SERVER_ERROR, format!("Invalid Razorpay response: {e}")))?; + let hash = payu_request_hash( + &gateway_config.key, + &txnid, + &amount, + &productinfo, + &firstname, + &email, + &udf1, + &udf2, + "", + "", + "", + &gateway_config.salt, + ); sqlx::query( r#" @@ -293,18 +360,29 @@ async fn create_order( ) .bind(auth.user_id) .bind(package_id) - .bind(&razorpay_order.id) - .bind(razorpay_order.amount as i64) + .bind(&txnid) + .bind(payload.amount as i32) .bind(package.tracecoins_amount) .execute(&state.pool) .await .map_err(|e| error_response(StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?; - Ok(Json(CreateOrderResponse { - order_id: razorpay_order.id, - amount: razorpay_order.amount, - currency: razorpay_order.currency, - status: razorpay_order.status, + let return_url = format!("{}/dashboard/wallet/payu-return", state.frontend_url.trim_end_matches('/')); + + Ok(Json(PayuOrderResponse { + key: gateway_config.key, + txnid, + amount, + productinfo, + firstname, + email, + phone, + surl: return_url.clone(), + furl: return_url, + hash, + payu_base_url: gateway_config.base_url, + udf1, + udf2, })) } @@ -313,25 +391,27 @@ async fn verify_payment( State(state): State, Json(payload): Json, ) -> Result, (StatusCode, String)> { - let gateway_config = resolve_razorpay_config(&state).await?; - let order_id = payload - .order_id - .ok_or_else(|| error_response(StatusCode::BAD_REQUEST, "order_id is required"))?; - let payment_id = payload - .payment_id - .ok_or_else(|| error_response(StatusCode::BAD_REQUEST, "payment_id is required"))?; - let signature = payload - .signature - .ok_or_else(|| error_response(StatusCode::BAD_REQUEST, "razorpay_signature is required"))?; + let gateway_config = resolve_payu_config(&state).await?; - let provided_signature = hex::decode(signature) - .map_err(|_| error_response(StatusCode::BAD_REQUEST, "Invalid signature format"))?; + let expected_hash = payu_response_hash( + &gateway_config.salt, + &payload.status, + &payload.udf1, + &payload.udf2, + &payload.udf3, + &payload.udf4, + &payload.udf5, + &payload.email, + &payload.firstname, + &payload.productinfo, + &payload.amount, + &payload.txnid, + &gateway_config.key, + ); - let mut mac = HmacSha256::new_from_slice(gateway_config.key_secret.as_bytes()) - .map_err(|_| error_response(StatusCode::INTERNAL_SERVER_ERROR, "Unable to initialize signature verifier"))?; - mac.update(format!("{order_id}|{payment_id}").as_bytes()); - mac.verify_slice(&provided_signature) - .map_err(|_| error_response(StatusCode::BAD_REQUEST, "Signature mismatch"))?; + if !expected_hash.eq_ignore_ascii_case(&payload.hash) { + return Err(error_response(StatusCode::BAD_REQUEST, "Signature mismatch")); + } let payment = sqlx::query_as::<_, PaymentRow>( r#" @@ -340,7 +420,7 @@ async fn verify_payment( WHERE payu_txnid = $1 AND status = 'PENDING' "#, ) - .bind(&order_id) + .bind(&payload.txnid) .fetch_optional(&state.pool) .await .map_err(|e| error_response(StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?; @@ -362,6 +442,22 @@ async fn verify_payment( )); } + if !payload.status.eq_ignore_ascii_case("success") { + sqlx::query("UPDATE payments SET status = 'FAILED', payu_mihpayid = $1 WHERE id = $2") + .bind(&payload.mihpayid) + .bind(payment.id) + .execute(&state.pool) + .await + .map_err(|e| error_response(StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?; + + return Ok(Json(VerifyPaymentResponse { + verified: false, + payment_id: payload.mihpayid, + status: "failed".to_string(), + message: "PayU reported a non-success status.".to_string(), + })); + } + let tracecoins = payment.tracecoins_credited.unwrap_or(0); sqlx::query( @@ -373,7 +469,7 @@ async fn verify_payment( WHERE id = $2 "#, ) - .bind(&payment_id) + .bind(&payload.mihpayid) .bind(payment.id) .execute(&state.pool) .await @@ -432,7 +528,7 @@ async fn verify_payment( Ok(Json(VerifyPaymentResponse { verified: true, - payment_id, + payment_id: payload.mihpayid, status: "success".to_string(), message: "Payment verified successfully".to_string(), })) @@ -501,12 +597,12 @@ async fn main() { .expect("Failed to connect to database"); let state = AppState { - client: reqwest::Client::new(), pool, - fallback_razorpay_key_id: std::env::var("RAZORPAY_KEY_ID") - .expect("RAZORPAY_KEY_ID must be set"), - fallback_razorpay_key_secret: std::env::var("RAZORPAY_KEY_SECRET") - .expect("RAZORPAY_KEY_SECRET must be set"), + client: reqwest::Client::new(), + fallback_payu_key: std::env::var("PAYU_MERCHANT_KEY").unwrap_or_default(), + fallback_payu_salt: std::env::var("PAYU_SALT").unwrap_or_default(), + frontend_url: std::env::var("FRONTEND_URL") + .unwrap_or_else(|_| "http://localhost:3000".to_string()), }; let app = Router::new() diff --git a/apps/users/src/handlers/payment_gateway.rs b/apps/users/src/handlers/payment_gateway.rs index 2e31b9d..3f7d17b 100644 --- a/apps/users/src/handlers/payment_gateway.rs +++ b/apps/users/src/handlers/payment_gateway.rs @@ -9,7 +9,7 @@ use axum::{ use contracts::auth_middleware::{require_admin, AuthUser}; use serde::{Deserialize, Serialize}; -const RAZORPAY_GATEWAY_KEY: &str = "RAZORPAY"; +const PAYU_GATEWAY_KEY: &str = "PAYU"; pub fn router() -> Router { Router::new() @@ -42,10 +42,10 @@ struct PaymentGatewayRow { impl Default for PaymentGatewayConfigPayload { fn default() -> Self { Self { - provider: "Razorpay".to_string(), + provider: "PayU".to_string(), mode: "sandbox".to_string(), enabled: true, - base_url: "https://api.razorpay.com".to_string(), + base_url: "https://test.payu.in".to_string(), callback_url: "".to_string(), webhook_url: "".to_string(), merchant_id: "".to_string(), @@ -58,7 +58,7 @@ impl Default for PaymentGatewayConfigPayload { fn normalize_payload(payload: PaymentGatewayConfigPayload) -> PaymentGatewayConfigPayload { PaymentGatewayConfigPayload { provider: if payload.provider.trim().is_empty() { - "Razorpay".to_string() + "PayU".to_string() } else { payload.provider.trim().to_string() }, @@ -69,7 +69,7 @@ fn normalize_payload(payload: PaymentGatewayConfigPayload) -> PaymentGatewayConf }, enabled: payload.enabled, base_url: if payload.base_url.trim().is_empty() { - "https://api.razorpay.com".to_string() + "https://test.payu.in".to_string() } else { payload.base_url.trim().to_string() }, @@ -86,7 +86,7 @@ fn row_to_payload(row: PaymentGatewayRow) -> PaymentGatewayConfigPayload { payload.provider = row .display_name .filter(|v| !v.trim().is_empty()) - .unwrap_or_else(|| "Razorpay".to_string()); + .unwrap_or_else(|| "PayU".to_string()); payload.enabled = row.is_active; if let Some(config) = row.config_json { @@ -98,7 +98,7 @@ fn row_to_payload(row: PaymentGatewayRow) -> PaymentGatewayConfigPayload { payload.base_url = config .get("base_url") .and_then(|v| v.as_str()) - .unwrap_or("https://api.razorpay.com") + .unwrap_or("https://test.payu.in") .to_string(); payload.callback_url = config .get("callback_url") @@ -145,7 +145,7 @@ async fn get_payment_gateway_config( LIMIT 1 "#, ) - .bind(RAZORPAY_GATEWAY_KEY) + .bind(PAYU_GATEWAY_KEY) .fetch_optional(&state.pool) .await .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?; @@ -154,8 +154,8 @@ async fn get_payment_gateway_config( Some(row) => row_to_payload(row), None => { let mut defaults = PaymentGatewayConfigPayload::default(); - defaults.api_key = std::env::var("RAZORPAY_KEY_ID").unwrap_or_default(); - defaults.secret_key = std::env::var("RAZORPAY_KEY_SECRET").unwrap_or_default(); + defaults.api_key = std::env::var("PAYU_MERCHANT_KEY").unwrap_or_default(); + defaults.secret_key = std::env::var("PAYU_SALT").unwrap_or_default(); defaults } }; @@ -191,7 +191,7 @@ async fn upsert_payment_gateway_config( LIMIT 1 "#, ) - .bind(RAZORPAY_GATEWAY_KEY) + .bind(PAYU_GATEWAY_KEY) .fetch_optional(&state.pool) .await .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?; @@ -222,7 +222,7 @@ async fn upsert_payment_gateway_config( RETURNING display_name, config_json, is_active "#, ) - .bind(RAZORPAY_GATEWAY_KEY) + .bind(PAYU_GATEWAY_KEY) .bind(&payload.provider) .bind(config_json) .bind(payload.enabled) diff --git a/apps/users/src/handlers/support.rs b/apps/users/src/handlers/support.rs index 24be59e..db1ba05 100644 --- a/apps/users/src/handlers/support.rs +++ b/apps/users/src/handlers/support.rs @@ -1,7 +1,7 @@ use crate::AppState; use axum::{ extract::{Path, Query, State}, - http::StatusCode, + http::{HeaderMap, StatusCode}, response::IntoResponse, routing::{get, post}, Json, Router, @@ -107,8 +107,23 @@ struct AiCreateTicketBody { async fn ai_create_ticket( State(state): State, + headers: HeaderMap, axum::extract::Json(body): axum::extract::Json, ) -> impl IntoResponse { + let expected_key = std::env::var("AI_SERVICE_KEY").unwrap_or_default(); + let provided_key = headers + .get("X-AI-Service-Key") + .and_then(|v| v.to_str().ok()) + .unwrap_or_default(); + + if expected_key.is_empty() || provided_key != expected_key { + return ( + StatusCode::UNAUTHORIZED, + Json(serde_json::json!({ "error": "Invalid or missing AI service key" })), + ) + .into_response(); + } + let user_id = body.user_id.unwrap_or_else(|| Uuid::nil()); let category = body.category.clone().unwrap_or_else(|| "ai_assisted".to_string()); let priority = body.priority.clone().unwrap_or_else(|| "medium".to_string());