feat(dimentorin): Midtrans payment provider (sandbox VA + QRIS)
- Env: MIDTRANS_MERCHANT_ID / CLIENT_KEY / SERVER_KEY (masked in Debug) - midtrans_provider: Core API v2/charge for bank_transfer (VA BCA) and qris (gopay) - PaymentServiceImpl: when MIDTRANS_MERCHANT_ID set -> provider=midtrans, external_ref = real VA number / QR string; falls back to manual refs otherwise - SMTP: Google app password working (send-otp 200, OTP stored) - E2E verified (sandbox): VA externalRef=47329093597744219189188, QRIS qr_string EMVCo, confirm->paid - Credentials stored in BWS (dimentorin_midtrans_*, dimentorin_smtp_*)
This commit is contained in:
@@ -0,0 +1,119 @@
|
||||
//! 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;
|
||||
|
||||
/// Sandbox vs production endpoint. 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<String, AppError> {
|
||||
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<String, AppError> {
|
||||
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()))
|
||||
}
|
||||
Reference in New Issue
Block a user