ooredoo billpay via bml card

This commit is contained in:
2026-10-03 01:27:29 +05:00
parent 8795d5f758
commit bb76f4e591
9 changed files with 118 additions and 27 deletions
@@ -12,10 +12,10 @@ import java.util.Locale
import java.util.concurrent.TimeUnit
/**
* Ooredoo prepaid recharge (Raastas) through the ooredoo.mv Quick Pay page, paid by card on
* BML's merchant gateway. Ooredoo only creates the order; the money moves on the BML Merchant
* Services transaction it hands back, which is paid like any card-only BML merchant link. See
* `docs/ooredooapi/02-raastas.md`.
* Ooredoo prepaid recharge (Raastas) and bill payment through the ooredoo.mv Quick Pay pages,
* paid by card on BML's merchant gateway. Ooredoo only creates the order; the money moves on the
* BML Merchant Services transaction it hands back, which is paid like any card-only BML merchant
* link. See `docs/ooredooapi/02-raastas.md` and `docs/ooredooapi/03-bill-pay.md`.
*
* Every call blocks, so run it on an IO thread.
*/
@@ -32,15 +32,29 @@ class OoredooPaymentClient {
* that plus GST. Returns the 24-hex BML transaction id. Throws with Ooredoo's wording when
* the order is refused.
*/
fun createRaastasTransaction(number: String, amount: Int, charged: BigDecimal): String {
fun createRaastasTransaction(number: String, amount: Int, charged: BigDecimal): String =
createOrder(number, charged, amount.toString(), transType = "recharge", serviceType = "prepaid")
/**
* Creates the bill payment order for postpaid [number] (7 digits) and the BML transaction
* paying for it. [amount] is MVR, up to 2 decimal places; no GST. Returns the 24-hex BML
* transaction id. Throws with Ooredoo's wording when the order is refused.
*/
fun createBillPayTransaction(number: String, amount: BigDecimal): String =
createOrder(number, amount, money(amount), transType = "billpay", serviceType = "Mobile")
/** `POST PaymentGateway/bml` — one call makes the order and its BML transaction. */
private fun createOrder(
number: String, charged: BigDecimal, amountWithoutGst: String, transType: String, serviceType: String,
): String {
val msisdn = "960$number"
val body = JSONObject()
.put("msisdn", msisdn)
.put("purchaseAmount", String.format(Locale.US, "%.2f", charged.setScale(2, RoundingMode.HALF_UP)))
.put("amountWithoutGst", amount.toString())
.put("purchaseAmount", money(charged))
.put("amountWithoutGst", amountWithoutGst)
.put("receiverMsisdn", msisdn)
.put("transType", "recharge")
.put("serviceType", "prepaid")
.put("transType", transType)
.put("serviceType", serviceType)
.put("serviceTypeDisplayName", "Mobile")
val text = client.newCall(
Request.Builder().url("$BASE/ooredoo-prod/PaymentGateway/bml")
@@ -48,7 +62,6 @@ class OoredooPaymentClient {
.header("User-Agent", UA)
.header("Accept", "application/json")
.header("Origin", BASE)
.header("Referer", "$BASE/payment?source=Recharge&transType=2")
.build()
).execute().use { r ->
if (r.code in 500..599) throw BankServerException("Ooredoo")
@@ -58,13 +71,16 @@ class OoredooPaymentClient {
throw Exception("Unexpected response from Ooredoo")
}
if (obj.optString("status") != "OK" || obj.optString("code") != "2000") {
throw Exception(obj.optString("msg").ifBlank { "Ooredoo refused the recharge" })
throw Exception(obj.optString("msg").ifBlank { "Ooredoo refused the payment" })
}
val data = obj.optJSONObject("data") ?: throw Exception("Ooredoo didn't create the order")
return TXN_URL.find(data.optString("bmlUrl"))?.groupValues?.get(1)
?: throw Exception("BML didn't create the transaction")
}
private fun money(amount: BigDecimal) =
String.format(Locale.US, "%.2f", amount.setScale(2, RoundingMode.HALF_UP))
companion object {
private const val BASE = "https://www.ooredoo.mv"
private const val UA = "Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0"
@@ -22,8 +22,8 @@ import java.math.RoundingMode
* A carrier service a verified BML card can pay, through the carrier's own website and its BML
* merchant gateway. The limits are the carrier website's, not Fahipay's.
*
* No Ooredoo bill pay yet. Add it as a constant once its send path lands; the exhaustive `when`s
* over this enum point at every site that needs updating.
* Add new services as constants; the exhaustive `when`s over this enum point at every site that
* needs updating.
*/
enum class CardPayoutService(
override val label: String,
@@ -46,7 +46,9 @@ enum class CardPayoutService(
minAmount = 1, maxAmount = null, decimalsAllowed = true, gstPercent = null),
// Ooredoo credits the whole amount and charges the card 8% GST on top
OOREDOO_RAASTAS("Raastas", "Ooredoo · Raastas", R.drawable.ooredoo_logo,
minAmount = 20, maxAmount = null, decimalsAllowed = false, gstPercent = 8, gstAdded = true);
minAmount = 20, maxAmount = null, decimalsAllowed = false, gstPercent = 8, gstAdded = true),
OOREDOO_BILL("Ooredoo Bill Pay", "Ooredoo · Bill Pay", R.drawable.ooredoo_logo,
minAmount = 10, maxAmount = null, decimalsAllowed = true, gstPercent = null);
}
/**
@@ -95,6 +97,9 @@ class CardPayoutTransferHandler(
if (result.ooredoo == OoredooClient.CustType.PRE || result.ooredoo == OoredooClient.CustType.HYBRID) {
add(CardPayoutService.OOREDOO_RAASTAS)
}
if (result.ooredoo == OoredooClient.CustType.POST || result.ooredoo == OoredooClient.CustType.HYBRID) {
add(CardPayoutService.OOREDOO_BILL)
}
}.map { TransferType.Card(it, result.ownerName, cards) }
}
@@ -166,6 +171,8 @@ class CardPayoutTransferHandler(
DhiraaguPaymentClient().createBillPayTransaction(number, amount)
CardPayoutService.OOREDOO_RAASTAS ->
OoredooPaymentClient().createRaastasTransaction(number, amount.intValueExact(), charged)
CardPayoutService.OOREDOO_BILL ->
OoredooPaymentClient().createBillPayTransaction(number, amount)
}
BmlMerchantTxnClient().fetchPayPage(txnId)
}