//! Real PayU integration, replacing the mocked Beeceptor calls in //! `create_order`/`verify_payment`. Matches the checkout contract the //! frontend (`nxtgauge-frontend-solid/src/lib/payu.ts`) already expects: //! a form-redirect to `{payu_base_url}/_payment` with a request hash, and //! a reverse-hash-verified response on callback. //! //! Hash formulas are PayU's standard v1 scheme (documented publicly by //! PayU): request hash covers key/txnid/amount/productinfo/firstname/ //! email/udf1-10/salt; response verification reverses that order and //! substitutes `status` for the trailing empty slots. use sha2::{Digest, Sha512}; #[derive(Clone)] pub struct PayuConfig { pub merchant_key: String, pub merchant_salt: String, pub base_url: String, pub surl: String, pub furl: String, } impl PayuConfig { pub fn from_env() -> Self { Self { merchant_key: std::env::var("PAYU_MERCHANT_KEY").unwrap_or_default(), merchant_salt: std::env::var("PAYU_MERCHANT_SALT").unwrap_or_default(), base_url: std::env::var("PAYU_BASE_URL") .unwrap_or_else(|_| "https://test.payu.in".to_string()), surl: std::env::var("PAYU_SURL") .unwrap_or_else(|_| "/api/payments/payu/callback".to_string()), furl: std::env::var("PAYU_FURL") .unwrap_or_else(|_| "/api/payments/payu/callback".to_string()), } } } fn sha512_hex(input: &str) -> String { let mut hasher = Sha512::new(); hasher.update(input.as_bytes()); hex::encode(hasher.finalize()) } #[allow(clippy::too_many_arguments)] pub fn request_hash( config: &PayuConfig, txnid: &str, amount: &str, productinfo: &str, firstname: &str, email: &str, udf1: &str, udf2: &str, ) -> String { let seq = [ config.merchant_key.as_str(), txnid, amount, productinfo, firstname, email, udf1, udf2, "", "", "", "", "", "", "", "", config.merchant_salt.as_str(), ]; sha512_hex(&seq.join("|")) } /// Verify a PayU response hash. `status` is PayU's returned status string /// (e.g. "success"/"failure") -- it is part of the hashed sequence, not /// just metadata, per PayU's reverse-hash formula. #[allow(clippy::too_many_arguments)] pub fn verify_response_hash( config: &PayuConfig, status: &str, txnid: &str, amount: &str, productinfo: &str, firstname: &str, email: &str, udf1: &str, udf2: &str, received_hash: &str, ) -> bool { let seq = [ config.merchant_salt.as_str(), status, "", "", "", "", "", "", "", "", udf2, udf1, email, firstname, productinfo, amount, txnid, config.merchant_key.as_str(), ]; let expected = sha512_hex(&seq.join("|")); expected.eq_ignore_ascii_case(received_hash) } pub fn generate_txnid() -> String { uuid::Uuid::new_v4().to_string().replace('-', "") } /// Format an integer-paise amount as the decimal-rupee string PayU expects /// in its hash and form fields (e.g. 9900 paise -> "99.00"). pub fn paise_to_rupee_string(paise: i32) -> String { format!("{:.2}", paise as f64 / 100.0) } #[cfg(test)] mod tests { use super::*; fn test_config() -> PayuConfig { PayuConfig { merchant_key: "testkey".to_string(), merchant_salt: "testsalt".to_string(), base_url: "https://test.payu.in".to_string(), surl: "/s".to_string(), furl: "/f".to_string(), } } #[test] fn request_hash_is_deterministic_and_order_sensitive() { let config = test_config(); let h1 = request_hash(&config, "txn1", "99.00", "Starter AI Credits", "John", "john@test.com", "u1", "u2"); let h2 = request_hash(&config, "txn1", "99.00", "Starter AI Credits", "John", "john@test.com", "u1", "u2"); assert_eq!(h1, h2); let h3 = request_hash(&config, "txn2", "99.00", "Starter AI Credits", "John", "john@test.com", "u1", "u2"); assert_ne!(h1, h3, "different txnid must produce a different hash"); } #[test] fn response_hash_round_trips_with_request_hash_shape() { let config = test_config(); // Build a response hash the way PayU would for a success callback, // then confirm verify_response_hash accepts it. let seq = [ config.merchant_salt.as_str(), "success", "", "", "", "", "", "", "", "", "u2", "u1", "john@test.com", "John", "Starter AI Credits", "99.00", "txn1", config.merchant_key.as_str(), ]; let hash = sha512_hex(&seq.join("|")); assert!(verify_response_hash( &config, "success", "txn1", "99.00", "Starter AI Credits", "John", "john@test.com", "u1", "u2", &hash, )); // Tampering with the amount must invalidate the hash. assert!(!verify_response_hash( &config, "success", "txn1", "999.00", "Starter AI Credits", "John", "john@test.com", "u1", "u2", &hash, )); } }