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 import java.util.concurrent.TimeUnit
/** /**
* Ooredoo prepaid recharge (Raastas) through the ooredoo.mv Quick Pay page, paid by card on * Ooredoo prepaid recharge (Raastas) and bill payment through the ooredoo.mv Quick Pay pages,
* BML's merchant gateway. Ooredoo only creates the order; the money moves on the BML Merchant * paid by card on BML's merchant gateway. Ooredoo only creates the order; the money moves on the
* Services transaction it hands back, which is paid like any card-only BML merchant link. See * BML Merchant Services transaction it hands back, which is paid like any card-only BML merchant
* `docs/ooredooapi/02-raastas.md`. * link. See `docs/ooredooapi/02-raastas.md` and `docs/ooredooapi/03-bill-pay.md`.
* *
* Every call blocks, so run it on an IO thread. * 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 * that plus GST. Returns the 24-hex BML transaction id. Throws with Ooredoo's wording when
* the order is refused. * 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 msisdn = "960$number"
val body = JSONObject() val body = JSONObject()
.put("msisdn", msisdn) .put("msisdn", msisdn)
.put("purchaseAmount", String.format(Locale.US, "%.2f", charged.setScale(2, RoundingMode.HALF_UP))) .put("purchaseAmount", money(charged))
.put("amountWithoutGst", amount.toString()) .put("amountWithoutGst", amountWithoutGst)
.put("receiverMsisdn", msisdn) .put("receiverMsisdn", msisdn)
.put("transType", "recharge") .put("transType", transType)
.put("serviceType", "prepaid") .put("serviceType", serviceType)
.put("serviceTypeDisplayName", "Mobile") .put("serviceTypeDisplayName", "Mobile")
val text = client.newCall( val text = client.newCall(
Request.Builder().url("$BASE/ooredoo-prod/PaymentGateway/bml") Request.Builder().url("$BASE/ooredoo-prod/PaymentGateway/bml")
@@ -48,7 +62,6 @@ class OoredooPaymentClient {
.header("User-Agent", UA) .header("User-Agent", UA)
.header("Accept", "application/json") .header("Accept", "application/json")
.header("Origin", BASE) .header("Origin", BASE)
.header("Referer", "$BASE/payment?source=Recharge&transType=2")
.build() .build()
).execute().use { r -> ).execute().use { r ->
if (r.code in 500..599) throw BankServerException("Ooredoo") if (r.code in 500..599) throw BankServerException("Ooredoo")
@@ -58,13 +71,16 @@ class OoredooPaymentClient {
throw Exception("Unexpected response from Ooredoo") throw Exception("Unexpected response from Ooredoo")
} }
if (obj.optString("status") != "OK" || obj.optString("code") != "2000") { 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") val data = obj.optJSONObject("data") ?: throw Exception("Ooredoo didn't create the order")
return TXN_URL.find(data.optString("bmlUrl"))?.groupValues?.get(1) return TXN_URL.find(data.optString("bmlUrl"))?.groupValues?.get(1)
?: throw Exception("BML didn't create the transaction") ?: 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 { companion object {
private const val BASE = "https://www.ooredoo.mv" 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" 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 * 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. * 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 * Add new services as constants; the exhaustive `when`s over this enum point at every site that
* over this enum point at every site that needs updating. * needs updating.
*/ */
enum class CardPayoutService( enum class CardPayoutService(
override val label: String, override val label: String,
@@ -46,7 +46,9 @@ enum class CardPayoutService(
minAmount = 1, maxAmount = null, decimalsAllowed = true, gstPercent = null), minAmount = 1, maxAmount = null, decimalsAllowed = true, gstPercent = null),
// Ooredoo credits the whole amount and charges the card 8% GST on top // Ooredoo credits the whole amount and charges the card 8% GST on top
OOREDOO_RAASTAS("Raastas", "Ooredoo · Raastas", R.drawable.ooredoo_logo, 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) { if (result.ooredoo == OoredooClient.CustType.PRE || result.ooredoo == OoredooClient.CustType.HYBRID) {
add(CardPayoutService.OOREDOO_RAASTAS) 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) } }.map { TransferType.Card(it, result.ownerName, cards) }
} }
@@ -166,6 +171,8 @@ class CardPayoutTransferHandler(
DhiraaguPaymentClient().createBillPayTransaction(number, amount) DhiraaguPaymentClient().createBillPayTransaction(number, amount)
CardPayoutService.OOREDOO_RAASTAS -> CardPayoutService.OOREDOO_RAASTAS ->
OoredooPaymentClient().createRaastasTransaction(number, amount.intValueExact(), charged) OoredooPaymentClient().createRaastasTransaction(number, amount.intValueExact(), charged)
CardPayoutService.OOREDOO_BILL ->
OoredooPaymentClient().createBillPayTransaction(number, amount)
} }
BmlMerchantTxnClient().fetchPayPage(txnId) BmlMerchantTxnClient().fetchPayPage(txnId)
} }
+1 -1
View File
@@ -18,4 +18,4 @@
| [mibapi/](mibapi/README.md) | MIB Faisanet — Blowfish-encrypted API + WebView session, accounts, transfers, contacts | | [mibapi/](mibapi/README.md) | MIB Faisanet — Blowfish-encrypted API + WebView session, accounts, transfers, contacts |
| [fahipayapi/](fahipayapi/README.md) | Fahipay digital wallet — login, balance, history, contacts | | [fahipayapi/](fahipayapi/README.md) | Fahipay digital wallet — login, balance, history, contacts |
| [dhiraaguapi/](dhiraaguapi/README.md) | Dhiraagu Easy Pay / Easy TopUp — number lookup, reload and bill pay by BML card | | [dhiraaguapi/](dhiraaguapi/README.md) | Dhiraagu Easy Pay / Easy TopUp — number lookup, reload and bill pay by BML card |
| [ooredooapi/](ooredooapi/README.md) | Ooredoo Quick Pay — number validation, Raastas by BML card | | [ooredooapi/](ooredooapi/README.md) | Ooredoo Quick Pay — number validation, Raastas and bill pay by BML card |
+1 -1
View File
@@ -104,4 +104,4 @@ cookie; okhttp from the phone gets through without one, as with
**Related:** [Number Validation](01-number-validation.md) · [BML Merchant Card Payment](../bmlapi/16-card-payment.md) · **Related:** [Number Validation](01-number-validation.md) · [BML Merchant Card Payment](../bmlapi/16-card-payment.md) ·
App side: [Transfer Flows](../thijooree/20-transfer-flows.md#carrier-services-by-bml-card) App side: [Transfer Flows](../thijooree/20-transfer-flows.md#carrier-services-by-bml-card)
[← Number Validation](01-number-validation.md) [← Number Validation](01-number-validation.md) · [Bill Pay →](03-bill-pay.md)
+65
View File
@@ -0,0 +1,65 @@
# Bill Pay (Quick Pay, paid by BML card)
Pay an Ooredoo postpaid bill through the ooredoo.mv **Quick Pay** bill-pay page. It is the same
single order call as [Raastas](02-raastas.md) with a different `transType` / `serviceType`, and
no GST. From the order on, it's the BML card-only merchant flow and the same return to Ooredoo.
Reconstructed from `docs/ooredooapi/tmp/ooredoo_billpay_bml_card.har` (a Firefox HAR).
---
## Flow overview
```
GET /bill-pay
GET /ooredoo-prod/QuickPayPackage/v1/numberTypeValidation?… → custType POST (Number Validation)
POST /ooredoo-prod/PaymentGateway/bml → orderID, bmlUrl
── BML card-only merchant flow ── → TRANSACTION_CONFIRMED
GET transaction.merchants…/<id>?wait=1 → 302 my.ooredoo.mv/bml/response_new.php?…state=CONFIRMED
(200, auto-submits, transtype=1) → POST www.ooredoo.mv/ooredoo-prod/PaymentGateway/redirect/bml
→ /payment-status?statusDesc=sucess&status=1&order_id=<orderID>
```
The page doesn't look up the outstanding bill: the amount is whatever is typed.
---
## Order
`POST https://www.ooredoo.mv/ooredoo-prod/PaymentGateway/bml`, headers as in
[Raastas → Order](02-raastas.md#1-order).
```json
{"msisdn":"9609XXXXXX","purchaseAmount":"10.01","amountWithoutGst":"10.01",
"receiverMsisdn":"9609XXXXXX","transType":"billpay","serviceType":"Mobile",
"serviceTypeDisplayName":"Mobile"}
```
```json
{"status":"OK","msg":"Successfully generated order id","code":"2000",
"data":{"orderID":"36447962","purchaseAmount":"1001","shortUrl":"https://pay.bml.com.mv/…",
"bmlUrl":"https://transaction.merchants.bankofmaldives.com.mv/6ac011e099f9d890856e2d33", …}}
```
| | Raastas | Bill Pay |
|---|---|---|
| `transType` | `recharge` | `billpay` |
| `serviceType` | `prepaid` | `Mobile` |
| `amountWithoutGst` | amount credited | = `purchaseAmount` |
| GST | 8% added on top | none |
| Return form `transtype` | `2` | `1` |
### Limits
Minimum **MVR 10**, decimals allowed (up to 2 places). The maximum isn't known.
---
&nbsp;
---
**Related:** [Number Validation](01-number-validation.md) · [Raastas](02-raastas.md) ·
[BML Merchant Card Payment](../bmlapi/16-card-payment.md) ·
App side: [Transfer Flows](../thijooree/20-transfer-flows.md#carrier-services-by-bml-card)
[← Raastas](02-raastas.md)
+1
View File
@@ -82,6 +82,7 @@ The API expects the full MSISDN including country code `960` (e.g. `9609654321`)
|---|---|---| |---|---|---|
| 1 | [Number Validation](01-number-validation.md) | Validate an Ooredoo number and determine account type | | 1 | [Number Validation](01-number-validation.md) | Validate an Ooredoo number and determine account type |
| 2 | [Raastas](02-raastas.md) | Quick Pay recharge order → BML merchant transaction, paid by card | | 2 | [Raastas](02-raastas.md) | Quick Pay recharge order → BML merchant transaction, paid by card |
| 3 | [Bill Pay](03-bill-pay.md) | Quick Pay bill payment order → BML merchant transaction, paid by card |
--- ---
+4 -4
View File
@@ -77,7 +77,7 @@ A phone number searched with no source yet (or from a BML card that can pay by c
|---|---|---| |---|---|---|
| Favara Transfer | bank account behind the number | MIB or BML account | | Favara Transfer | bank account behind the number | MIB or BML account |
| Fahipay service | Raastas, Ooredoo Bill Pay, Dhiraagu Reload, Dhiraagu Bill Pay | Fahipay wallet | | Fahipay service | Raastas, Ooredoo Bill Pay, Dhiraagu Reload, Dhiraagu Bill Pay | Fahipay wallet |
| Card service | Dhiraagu Reload, Dhiraagu Bill Pay, Raastas (BML badge) | Verified BML card | | Card service | Dhiraagu Reload, Dhiraagu Bill Pay, Raastas, Ooredoo Bill Pay (BML badge) | Verified BML card |
One option is applied straight away; with more, a picker opens and Send stays disabled until one is chosen. Picking a type also picks a source that can pay it. Fahipay and card services clear and disable the Remarks field and apply their own amount rules (minimum, maximum, whole amounts, 8% GST note). Details: [Transfer Flows → Transfer Type picker](20-transfer-flows.md#transfer-type-picker). One option is applied straight away; with more, a picker opens and Send stays disabled until one is chosen. Picking a type also picks a source that can pay it. Fahipay and card services clear and disable the Remarks field and apply their own amount rules (minimum, maximum, whole amounts, 8% GST note). Details: [Transfer Flows → Transfer Type picker](20-transfer-flows.md#transfer-type-picker).
@@ -122,10 +122,10 @@ When the source is a BML USD account and the destination is a MIB account but no
See [Transfer Flows → Fahipay source](20-transfer-flows.md#fahipay-source). See [Transfer Flows → Fahipay source](20-transfer-flows.md#fahipay-source).
### Carrier Service by BML Card (Dhiraagu Reload / Bill Pay, Ooredoo Raastas) ### Carrier Service by BML Card (Dhiraagu Reload / Bill Pay, Ooredoo Raastas / Bill Pay)
1. Checks the amount against the carrier's rules (Dhiraagu reload: MVR 20–1000, whole amounts, 8% GST included; Dhiraagu bill pay: from MVR 1, up to 2 decimals, no GST; Raastas: from MVR 20, whole amounts, 8% GST added on top) 1. Checks the amount against the carrier's rules (Dhiraagu reload: MVR 20–1000, whole amounts, 8% GST included; Dhiraagu bill pay: from MVR 1, up to 2 decimals, no GST; Raastas: from MVR 20, whole amounts, 8% GST added on top; Ooredoo bill pay: from MVR 10, up to 2 decimals, no GST)
2. A "Processing..." dialog shows while the carrier creates the order and its BML merchant transaction ([Dhiraagu API → Reload](../dhiraaguapi/02-reload.md), [→ Bill Pay](../dhiraaguapi/03-bill-pay.md), [Ooredoo API → Raastas](../ooredooapi/02-raastas.md)) 2. A "Processing..." dialog shows while the carrier creates the order and its BML merchant transaction ([Dhiraagu API → Reload](../dhiraaguapi/02-reload.md), [→ Bill Pay](../dhiraaguapi/03-bill-pay.md), [Ooredoo API → Raastas](../ooredooapi/02-raastas.md), [→ Bill Pay](../ooredooapi/03-bill-pay.md))
3. From there it is the card-only merchant flow: the same confirm dialog and warning, biometric gate, card + 3-D Secure payment, and the return to the carrier (`?wait=1`) that tops the number up or posts the bill payment. A decline (e.g. insufficient funds) or a rejected token code ends it with the bank's message 3. From there it is the card-only merchant flow: the same confirm dialog and warning, biometric gate, card + 3-D Secure payment, and the return to the carrier (`?wait=1`) that tops the number up or posts the bill payment. A decline (e.g. insufficient funds) or a rejected token code ends it with the bank's message
4. On success, the result shows inside the dialog; if Dhiraagu couldn't be notified, a toast gives the BML transaction id 4. On success, the result shows inside the dialog; if Dhiraagu couldn't be notified, a toast gives the BML transaction id
+8 -6
View File
@@ -152,7 +152,7 @@ None of the Fahipay services take a reference. Picking one clears the Reference
A carrier service can also be paid with a verified BML card, through the carrier's own website A carrier service can also be paid with a verified BML card, through the carrier's own website
and its BML merchant gateway, instead of the Fahipay wallet: **Dhiraagu Reload**, **Dhiraagu and its BML merchant gateway, instead of the Fahipay wallet: **Dhiraagu Reload**, **Dhiraagu
Bill Pay** and **Ooredoo Raastas** so far (`CardPayoutService`, `ui/home/transfer/CardPayoutTransferHandler.kt`). Bill Pay**, **Ooredoo Raastas** and **Ooredoo Bill Pay** (`CardPayoutService`, `ui/home/transfer/CardPayoutTransferHandler.kt`).
**Which cards.** A card qualifies when it's verified and its BML login has an OTP seed, the same **Which cards.** A card qualifies when it's verified and its BML login has an OTP seed, the same
rule as card-only merchant links (`BmlVerifiedCards`, see rule as card-only merchant links (`BmlVerifiedCards`, see
@@ -167,6 +167,7 @@ drops the pick, like any other source that can't pay the picked type.
| Dhiraagu `RELOAD` | Dhiraagu Reload | | Dhiraagu `RELOAD` | Dhiraagu Reload |
| Dhiraagu `BILL_PAY` | Dhiraagu Bill Pay | | Dhiraagu `BILL_PAY` | Dhiraagu Bill Pay |
| Ooredoo `PRE` or `HYBRID` | Raastas | | Ooredoo `PRE` or `HYBRID` | Raastas |
| Ooredoo `POST` or `HYBRID` | Ooredoo Bill Pay |
**Amount rules.** The carrier website's, not Fahipay's. They're checked the same way, through **Amount rules.** The carrier website's, not Fahipay's. They're checked the same way, through
the shared `PayoutAmountField`: the shared `PayoutAmountField`:
@@ -176,9 +177,10 @@ the shared `PayoutAmountField`:
| Dhiraagu Reload | 20 | 1,000 | no | 8%, included (credit = amount − round2(amount × 0.08 / 1.08)) | | Dhiraagu Reload | 20 | 1,000 | no | 8%, included (credit = amount − round2(amount × 0.08 / 1.08)) |
| Dhiraagu Bill Pay | 1 | none | up to 2 places | none | | Dhiraagu Bill Pay | 1 | none | up to 2 places | none |
| Raastas | 20 | none | no | 8%, **added** (charged = amount + round2(amount × 0.08)) | | Raastas | 20 | none | no | 8%, **added** (charged = amount + round2(amount × 0.08)) |
| Ooredoo Bill Pay | 10 | none | up to 2 places | none |
Easy Pay itself sets no minimum or maximum; the MVR 1 floor is Thijooree's. Raastas' maximum Easy Pay itself sets no minimum or maximum; the MVR 1 floor is Thijooree's. The Ooredoo maximums
isn't known. aren't known.
Raastas by card is the one service where GST is added on top (`PayoutService.gstAdded`): the Raastas by card is the one service where GST is added on top (`PayoutService.gstAdded`): the
number is credited what's typed, the card pays more, and the note under the amount says what's number is credited what's typed, the card pays more, and the note under the amount says what's
@@ -192,9 +194,9 @@ BML transaction comes from:
1. `CardPayoutTransferHandler.submit()` has the carrier create it for the number and amount 1. `CardPayoutTransferHandler.submit()` has the carrier create it for the number and amount
(`DhiraaguPaymentClient.createReloadTransaction` / `createBillPayTransaction`, (`DhiraaguPaymentClient.createReloadTransaction` / `createBillPayTransaction`,
`OoredooPaymentClient.createRaastasTransaction`, see `OoredooPaymentClient.createRaastasTransaction` / `createBillPayTransaction`, see
[Dhiraagu API → Reload](../dhiraaguapi/02-reload.md), [→ Bill Pay](../dhiraaguapi/03-bill-pay.md) [Dhiraagu API → Reload](../dhiraaguapi/02-reload.md), [→ Bill Pay](../dhiraaguapi/03-bill-pay.md)
and [Ooredoo API → Raastas](../ooredooapi/02-raastas.md)). and [Ooredoo API → Raastas](../ooredooapi/02-raastas.md), [→ Bill Pay](../ooredooapi/03-bill-pay.md)).
Bill pay looks the number up again first, for the billing account the order is made out to. Bill pay looks the number up again first, for the billing account the order is made out to.
That takes a few round trips, so the payment's "Processing..." box shows meanwhile That takes a few round trips, so the payment's "Processing..." box shows meanwhile
(`TransferFragment.showProcessingDialog`) and closes before the confirm dialog opens. (`TransferFragment.showProcessingDialog`) and closes before the confirm dialog opens.
@@ -275,7 +277,7 @@ Source: Fahipay
Transfer type: Card (verified BML card) Transfer type: Card (verified BML card)
└── Carrier creates a BML merchant transaction → card-only merchant flow └── Carrier creates a BML merchant transaction → card-only merchant flow
DHIRAAGU_RELOAD, DHIRAAGU_BILL, OOREDOO_RAASTAS DHIRAAGU_RELOAD, DHIRAAGU_BILL, OOREDOO_RAASTAS, OOREDOO_BILL
``` ```
--- ---
@@ -9,7 +9,7 @@ Two linked features:
whose merchant has **no BML Pay** is paid with a verified card via the Pomelo + 3-D Secure flow whose merchant has **no BML Pay** is paid with a verified card via the Pomelo + 3-D Secure flow
([BML API → Merchant Card Payment](../bmlapi/16-card-payment.md)). The same flow pays ([BML API → Merchant Card Payment](../bmlapi/16-card-payment.md)). The same flow pays
[carrier services by BML card](20-transfer-flows.md#carrier-services-by-bml-card) (Dhiraagu [carrier services by BML card](20-transfer-flows.md#carrier-services-by-bml-card) (Dhiraagu
Reload and Bill Pay, Ooredoo Raastas), once the carrier has created the transaction. Reload and Bill Pay, Ooredoo Raastas and Bill Pay), once the carrier has created the transaction.
> ⚠️ The merchant card flow is scraped browser/ACS traffic, not a stable API. Storing the CVV is a > ⚠️ The merchant card flow is scraped browser/ACS traffic, not a stable API. Storing the CVV is a
> security/PCI liability. See the API doc's > security/PCI liability. See the API doc's