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:
asepharyana
2026-08-05 15:15:43 +07:00
parent aa0b659b48
commit 390f46b0e7
9 changed files with 141 additions and 2 deletions
@@ -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"