//! Midtrans payment provider — Core API (v2/charge). //! //! Gateway-agnostic design: the payments service calls into this module when //! Midtrans credentials are configured (`MIDTRANS_*` env). It returns the //! provider-specific reference that gets persisted into `app_payments.external_ref`: //! a VA number for `va`, or the QR string payload for `qris`. use imphnen_utils::AppError; use serde_json::json; /// Query transaction status for an order via Midtrans Core API. /// /// Returns the raw `transaction_status` string (e.g. "capture", "settlement", /// "pending", "expire", ...). pub async fn get_status( order_id: &str, server_key: &str, ) -> Result { let client = reqwest::Client::new(); let url = format!("{}/{}/status", status_base(), order_id); let resp = client .get(&url) .basic_auth(server_key, Some("")) // Midtrans/istio compresses with gzip even when the client cannot // decompress; reqwest auto-decompress can return an empty body here, // so ask for identity explicitly. .header(reqwest::header::ACCEPT_ENCODING, "identity") .send() .await .map_err(|e| AppError::InternalServerError(format!("Midtrans status request failed: {e}")))?; let status = resp.status(); let hdrs = format!("{:?}", resp.headers()); let text = resp .text() .await .map_err(|e| AppError::InternalServerError(format!("Midtrans status read failed: {e} (http {}, hdrs {})", status, hdrs)))?; let payload: serde_json::Value = serde_json::from_str(&text).map_err(|e| { AppError::InternalServerError(format!( "Midtrans status parse failed: {e} (http {}, hdrs {}, body-len {})", status, hdrs, text.len() )) })?; if !status.is_success() { return Err(AppError::InternalServerError(format!( "Midtrans status error ({}): {}", status, payload["status_message"].as_str().unwrap_or("unknown") ))); } Ok( payload["transaction_status"] .as_str() .unwrap_or("unknown") .to_string(), ) } fn status_base() -> &'static str { if std::env::var("RUST_ENV").as_deref() == Ok("production") { "https://api.midtrans.com/v2" } else { "https://api.sandbox.midtrans.com/v2" } } /// Sandbox vs production endpoint for charge. Sandbox is the default and safe for demo. fn charge_url() -> &'static str { if std::env::var("RUST_ENV").as_deref() == Ok("production") { "https://api.midtrans.com/v2/charge" } else { "https://api.sandbox.midtrans.com/v2/charge" } } /// Create a bank-transfer (Virtual Account) charge via Midtrans Core API. /// /// Returns the VA number to display to the mentee. pub async fn create_va_charge( order_id: &str, gross_amount: i64, bank: &str, server_key: &str, ) -> Result { let client = reqwest::Client::new(); let body = json!({ "payment_type": "bank_transfer", "transaction_details": { "order_id": order_id, "gross_amount": gross_amount, }, "bank_transfer": { "bank": bank, } }); let resp = client .post(charge_url()) .basic_auth(server_key, Some("")) .json(&body) .send() .await .map_err(|e| AppError::InternalServerError(format!("Midtrans request failed: {e}")))?; let status = resp.status(); let payload: serde_json::Value = resp .json() .await .map_err(|e| AppError::InternalServerError(format!("Midtrans response parse failed: {e}")))?; if !status.is_success() { return Err(AppError::InternalServerError(format!( "Midtrans charge error ({}): {}", status, payload["status_message"] .as_str() .unwrap_or("unknown error") ))); } payload["va_numbers"][0]["va_number"] .as_str() .map(|s| s.to_string()) .ok_or_else(|| AppError::InternalServerError("Midtrans response missing va_number".into())) } /// Create a QRIS charge via Midtrans Core API. /// /// Returns the QR string payload (renderable as a QR code). pub async fn create_qris_charge( order_id: &str, gross_amount: i64, server_key: &str, ) -> Result { let client = reqwest::Client::new(); let body = json!({ "payment_type": "qris", "transaction_details": { "order_id": order_id, "gross_amount": gross_amount, }, "qris": { "acquirer": "gopay", } }); let resp = client .post(charge_url()) .basic_auth(server_key, Some("")) .json(&body) .send() .await .map_err(|e| AppError::InternalServerError(format!("Midtrans request failed: {e}")))?; let status = resp.status(); let payload: serde_json::Value = resp .json() .await .map_err(|e| AppError::InternalServerError(format!("Midtrans response parse failed: {e}")))?; if !status.is_success() { return Err(AppError::InternalServerError(format!( "Midtrans charge error ({}): {}", status, payload["status_message"] .as_str() .unwrap_or("unknown error") ))); } payload["qr_string"] .as_str() .map(|s| s.to_string()) .ok_or_else(|| AppError::InternalServerError("Midtrans response missing qr_string".into())) }