- Task 1: Admin endpoints for wallet management
- Task 2: AI Credits admin UI (pricing.tsx, credit.tsx)
- Task 3: Ollama security (NetworkPolicy, prompt validation, audit)
- Task 4: LiteLLM integration (litellm.rs, migrated AI feature handlers)
- Task 5: Refund architecture (ai_refunds table, endpoints)
- Task 6: Coupons, promotions, referrals (order creation with coupon)
- Task 7: Subscription lifecycle (plan upgrades/downgrades, cron jobs)
- Task 8: Credit expiration enforcement (daily cron task)
- Task 9: Token cost engine (ai_model_cost_config, margin view)
- Task 10: Observability (metrics tables, aggregation function)
Cherry-picked from main branch commit 3c0f45f
188 lines
5.3 KiB
Rust
188 lines
5.3 KiB
Rust
//! 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,
|
|
));
|
|
}
|
|
}
|