feat(dimentorin): payment status refresh via Midtrans + auto-paid auto-confirm
- store provider_order_id (Midtrans order id) on payment create
- POST /payments/{id}/refresh: query Midtrans v2/{order}/status (settlement/capture → paid + confirm session)
- fix: status URL must end with /status; force Accept-Encoding: identity (istio gzip mangling)
- e2e verified: simulated VA payment in sandbox → refresh → paid + session confirmed
This commit is contained in:
@@ -8,7 +8,67 @@
|
||||
use imphnen_utils::AppError;
|
||||
use serde_json::json;
|
||||
|
||||
/// Sandbox vs production endpoint. Sandbox is the default and safe for demo.
|
||||
/// 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<String, AppError> {
|
||||
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"
|
||||
|
||||
Reference in New Issue
Block a user