From bb76f4e591dc686b9d5eecbb005a7ebfae606832 Mon Sep 17 00:00:00 2001 From: Shihaam Abdul Rahman Date: Sat, 3 Oct 2026 01:27:29 +0500 Subject: [PATCH] ooredoo billpay via bml card --- .../api/ooredoo/OoredooPaymentClient.kt | 38 +++++++---- .../transfer/CardPayoutTransferHandler.kt | 13 +++- docs/README.md | 2 +- docs/ooredooapi/02-raastas.md | 2 +- docs/ooredooapi/03-bill-pay.md | 65 +++++++++++++++++++ docs/ooredooapi/README.md | 1 + docs/thijooree/07-transfer.md | 8 +-- docs/thijooree/20-transfer-flows.md | 14 ++-- ...card-verification-and-merchant-card-pay.md | 2 +- 9 files changed, 118 insertions(+), 27 deletions(-) create mode 100644 docs/ooredooapi/03-bill-pay.md diff --git a/app/src/main/java/sh/sar/basedbank/api/ooredoo/OoredooPaymentClient.kt b/app/src/main/java/sh/sar/basedbank/api/ooredoo/OoredooPaymentClient.kt index 96e9893..499c150 100644 --- a/app/src/main/java/sh/sar/basedbank/api/ooredoo/OoredooPaymentClient.kt +++ b/app/src/main/java/sh/sar/basedbank/api/ooredoo/OoredooPaymentClient.kt @@ -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" diff --git a/app/src/main/java/sh/sar/basedbank/ui/home/transfer/CardPayoutTransferHandler.kt b/app/src/main/java/sh/sar/basedbank/ui/home/transfer/CardPayoutTransferHandler.kt index fdab6ed..99ced62 100644 --- a/app/src/main/java/sh/sar/basedbank/ui/home/transfer/CardPayoutTransferHandler.kt +++ b/app/src/main/java/sh/sar/basedbank/ui/home/transfer/CardPayoutTransferHandler.kt @@ -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) } diff --git a/docs/README.md b/docs/README.md index c53780e..80c312b 100644 --- a/docs/README.md +++ b/docs/README.md @@ -18,4 +18,4 @@ | [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 | | [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 | diff --git a/docs/ooredooapi/02-raastas.md b/docs/ooredooapi/02-raastas.md index a3a96d4..eb2a104 100644 --- a/docs/ooredooapi/02-raastas.md +++ b/docs/ooredooapi/02-raastas.md @@ -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) · 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) diff --git a/docs/ooredooapi/03-bill-pay.md b/docs/ooredooapi/03-bill-pay.md new file mode 100644 index 0000000..b698c3f --- /dev/null +++ b/docs/ooredooapi/03-bill-pay.md @@ -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…/?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= +``` + +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. + +--- + +  + +--- + +**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) diff --git a/docs/ooredooapi/README.md b/docs/ooredooapi/README.md index 204fde3..08f44c9 100644 --- a/docs/ooredooapi/README.md +++ b/docs/ooredooapi/README.md @@ -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 | | 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 | --- diff --git a/docs/thijooree/07-transfer.md b/docs/thijooree/07-transfer.md index af2ef4c..655eee8 100644 --- a/docs/thijooree/07-transfer.md +++ b/docs/thijooree/07-transfer.md @@ -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 | | 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). @@ -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). -### 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) -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)) +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), [→ 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 4. On success, the result shows inside the dialog; if Dhiraagu couldn't be notified, a toast gives the BML transaction id diff --git a/docs/thijooree/20-transfer-flows.md b/docs/thijooree/20-transfer-flows.md index 2f713a1..02184c9 100644 --- a/docs/thijooree/20-transfer-flows.md +++ b/docs/thijooree/20-transfer-flows.md @@ -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 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 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 `BILL_PAY` | Dhiraagu Bill Pay | | 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 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 Bill Pay | 1 | none | up to 2 places | none | | 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 -isn't known. +Easy Pay itself sets no minimum or maximum; the MVR 1 floor is Thijooree's. The Ooredoo maximums +aren't known. 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 @@ -192,9 +194,9 @@ BML transaction comes from: 1. `CardPayoutTransferHandler.submit()` has the carrier create it for the number and amount (`DhiraaguPaymentClient.createReloadTransaction` / `createBillPayTransaction`, - `OoredooPaymentClient.createRaastasTransaction`, see + `OoredooPaymentClient.createRaastasTransaction` / `createBillPayTransaction`, see [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. That takes a few round trips, so the payment's "Processing..." box shows meanwhile (`TransferFragment.showProcessingDialog`) and closes before the confirm dialog opens. @@ -275,7 +277,7 @@ Source: Fahipay Transfer type: Card (verified BML card) └── Carrier creates a BML merchant transaction → card-only merchant flow - DHIRAAGU_RELOAD, DHIRAAGU_BILL, OOREDOO_RAASTAS + DHIRAAGU_RELOAD, DHIRAAGU_BILL, OOREDOO_RAASTAS, OOREDOO_BILL ``` --- diff --git a/docs/thijooree/29-card-verification-and-merchant-card-pay.md b/docs/thijooree/29-card-verification-and-merchant-card-pay.md index 1cfb7f8..35a8a69 100644 --- a/docs/thijooree/29-card-verification-and-merchant-card-pay.md +++ b/docs/thijooree/29-card-verification-and-merchant-card-pay.md @@ -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 ([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 - 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 > security/PCI liability. See the API doc's