add support to detect dhiraagu and ooredoo packages
All checks were successful
Auto Tag on Version Change / check-version (push) Successful in 2s
All checks were successful
Auto Tag on Version Change / check-version (push) Successful in 2s
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
package sh.sar.basedbank.api.dhiraagu
|
||||
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import org.json.JSONObject
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class DhiraaguClient {
|
||||
|
||||
private val client = OkHttpClient.Builder()
|
||||
.connectTimeout(15, TimeUnit.SECONDS)
|
||||
.readTimeout(15, TimeUnit.SECONDS)
|
||||
.build()
|
||||
|
||||
enum class CustType { RELOAD, BILL_PAY, UNSUPPORTED }
|
||||
|
||||
companion object {
|
||||
private const val UA = "Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0"
|
||||
}
|
||||
|
||||
data class Result(val type: CustType, val ownerName: String = "")
|
||||
|
||||
fun validateNumber(number: String): Result {
|
||||
// Step 1: fetch Easy Pay page to extract the nonce
|
||||
val pageResp = client.newCall(
|
||||
Request.Builder()
|
||||
.url("https://www.dhiraagu.com.mv/services/easy-pay")
|
||||
.header("User-Agent", UA)
|
||||
.get()
|
||||
.build()
|
||||
).execute()
|
||||
val html = pageResp.body?.string() ?: return Result(CustType.UNSUPPORTED)
|
||||
pageResp.close()
|
||||
|
||||
val nonce = Regex("""var nonce = "([^"]+)"""").find(html)
|
||||
?.groupValues?.getOrNull(1) ?: return Result(CustType.UNSUPPORTED)
|
||||
|
||||
// Step 2: look up number
|
||||
val body = """{"number":"$number"}"""
|
||||
.toRequestBody("application/json".toMediaType())
|
||||
val resp = client.newCall(
|
||||
Request.Builder()
|
||||
.url("https://www.dhiraagu.com.mv/api/sdk-dhr-webapi.ashx?website_id=CA2BB809-3A22-485B-A518-DA6B6DE653A5&sub=dhiraaguIO&act=infoUnlisted")
|
||||
.post(body)
|
||||
.header("User-Agent", UA)
|
||||
.header("nonce", nonce)
|
||||
.header("Origin", "https://dhiraagu.com.mv")
|
||||
.build()
|
||||
).execute()
|
||||
val json = resp.body?.string() ?: return Result(CustType.UNSUPPORTED)
|
||||
resp.close()
|
||||
|
||||
return try {
|
||||
val obj = JSONObject(json)
|
||||
if (obj.optString("respStatus") != "OK") return Result(CustType.UNSUPPORTED)
|
||||
val prepaid = obj.optJSONArray("serviceDetails")
|
||||
?.optJSONObject(0)
|
||||
?.optString("prepaidIndicator")
|
||||
val ownerName = obj.optJSONObject("accountOwnerInfo")
|
||||
?.optString("name") ?: ""
|
||||
val type = when (prepaid) {
|
||||
"Y" -> CustType.RELOAD
|
||||
"N" -> CustType.BILL_PAY
|
||||
else -> CustType.UNSUPPORTED
|
||||
}
|
||||
Result(type, ownerName)
|
||||
} catch (_: Exception) { Result(CustType.UNSUPPORTED) }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package sh.sar.basedbank.api.fahipay
|
||||
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import org.json.JSONObject
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class OoredooClient {
|
||||
|
||||
private val client = OkHttpClient.Builder()
|
||||
.connectTimeout(15, TimeUnit.SECONDS)
|
||||
.readTimeout(15, TimeUnit.SECONDS)
|
||||
.build()
|
||||
|
||||
enum class CustType { PRE, POST, HYBRID, UNSUPPORTED }
|
||||
|
||||
/**
|
||||
* Validates an Ooredoo number and returns which services are supported.
|
||||
* @param number 7-digit Maldivian phone number (without country code)
|
||||
*/
|
||||
fun validateNumber(number: String): CustType {
|
||||
val resp = client.newCall(
|
||||
Request.Builder()
|
||||
.url("https://www.ooredoo.mv/ooredoo-prod/QuickPayPackage/v1/numberTypeValidation?action=cust_details&msisdn=960$number")
|
||||
.get()
|
||||
.build()
|
||||
).execute()
|
||||
val json = resp.body?.string() ?: return CustType.UNSUPPORTED
|
||||
resp.close()
|
||||
return try {
|
||||
val custType = JSONObject(json)
|
||||
.optJSONObject("data")
|
||||
?.optString("custType")
|
||||
?.takeIf { it.isNotBlank() && it != "null" }
|
||||
when (custType) {
|
||||
"PRE" -> CustType.PRE
|
||||
"POST" -> CustType.POST
|
||||
"HYBRID" -> CustType.HYBRID
|
||||
else -> CustType.UNSUPPORTED
|
||||
}
|
||||
} catch (_: Exception) { CustType.UNSUPPORTED }
|
||||
}
|
||||
}
|
||||
@@ -32,6 +32,8 @@ import kotlinx.coroutines.withContext
|
||||
import sh.sar.basedbank.BasedBankApp
|
||||
import sh.sar.basedbank.R
|
||||
import sh.sar.basedbank.api.bml.BmlLoginFlow
|
||||
import sh.sar.basedbank.api.dhiraagu.DhiraaguClient
|
||||
import sh.sar.basedbank.api.fahipay.OoredooClient
|
||||
import sh.sar.basedbank.api.bml.BmlTransferResult
|
||||
import sh.sar.basedbank.api.mib.MibAccount
|
||||
import sh.sar.basedbank.api.mib.MibBeneficiary
|
||||
@@ -65,6 +67,10 @@ class TransferFragment : Fragment() {
|
||||
private var resolvedRecipientName = ""
|
||||
private var resolvedBankName = ""
|
||||
|
||||
// Selected Fahipay service when source is Fahipay and destination is a phone number
|
||||
// Values: "FAHIPAY_TRANSFER", "RAASTAS", "OOREDOO_BILL"
|
||||
private var selectedFahipayService: String? = null
|
||||
|
||||
private val qrLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||
if (result.resultCode != Activity.RESULT_OK) return@registerForActivityResult
|
||||
val raw = result.data?.getStringExtra(QrScannerActivity.EXTRA_QR_CONTENT) ?: return@registerForActivityResult
|
||||
@@ -220,7 +226,9 @@ class TransferFragment : Fragment() {
|
||||
binding.btnClearToInfo.setOnClickListener {
|
||||
resolvedAccountNumber = ""
|
||||
resolvedRecipientName = ""
|
||||
selectedFahipayService = null
|
||||
binding.cardToInfo.visibility = View.GONE
|
||||
binding.layoutServiceSelector.visibility = View.INVISIBLE
|
||||
binding.tilTo.visibility = View.VISIBLE
|
||||
binding.btnPickContact.visibility = View.VISIBLE
|
||||
binding.btnScanQr.visibility = View.VISIBLE
|
||||
@@ -265,6 +273,16 @@ class TransferFragment : Fragment() {
|
||||
return
|
||||
}
|
||||
|
||||
// Fahipay source: only phone numbers are supported
|
||||
if (selectedAccount?.profileType == "FAHIPAY") {
|
||||
if (AccountInputParser.detect(accountNumber) == AccountInputParser.InputType.PHONE) {
|
||||
lookupFahipayTarget(accountNumber)
|
||||
} else {
|
||||
binding.tilTo.error = getString(R.string.transfer_fahipay_phone_only)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
val mibSess = session
|
||||
val bmlSess = bmlSession
|
||||
if (mibSess == null && bmlSess == null) {
|
||||
@@ -352,6 +370,99 @@ class TransferFragment : Fragment() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun lookupFahipayTarget(number: String) {
|
||||
binding.tilTo.isEnabled = false
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
data class LookupResult(
|
||||
val dhiraagu: DhiraaguClient.Result,
|
||||
val ooredoo: OoredooClient.CustType
|
||||
)
|
||||
val result = withContext(Dispatchers.IO) {
|
||||
if (number.startsWith("7")) {
|
||||
// Dhiraagu first, fall back to Ooredoo
|
||||
val d = try { DhiraaguClient().validateNumber(number) }
|
||||
catch (_: Exception) { DhiraaguClient.Result(DhiraaguClient.CustType.UNSUPPORTED) }
|
||||
val o = if (d.type == DhiraaguClient.CustType.UNSUPPORTED)
|
||||
try { OoredooClient().validateNumber(number) }
|
||||
catch (_: Exception) { OoredooClient.CustType.UNSUPPORTED }
|
||||
else OoredooClient.CustType.UNSUPPORTED
|
||||
LookupResult(d, o)
|
||||
} else {
|
||||
// Ooredoo first, fall back to Dhiraagu
|
||||
val o = try { OoredooClient().validateNumber(number) }
|
||||
catch (_: Exception) { OoredooClient.CustType.UNSUPPORTED }
|
||||
val d = if (o == OoredooClient.CustType.UNSUPPORTED)
|
||||
try { DhiraaguClient().validateNumber(number) }
|
||||
catch (_: Exception) { DhiraaguClient.Result(DhiraaguClient.CustType.UNSUPPORTED) }
|
||||
else DhiraaguClient.Result(DhiraaguClient.CustType.UNSUPPORTED)
|
||||
LookupResult(d, o)
|
||||
}
|
||||
}
|
||||
binding.tilTo.isEnabled = true
|
||||
|
||||
val dhiraaguName = result.dhiraagu.ownerName.takeIf { it.isNotBlank() }
|
||||
|
||||
// Collect all applicable services
|
||||
val services = buildList {
|
||||
if (result.dhiraagu.type == DhiraaguClient.CustType.RELOAD) add("DHIRAAGU_RELOAD")
|
||||
if (result.dhiraagu.type == DhiraaguClient.CustType.BILL_PAY) add("DHIRAAGU_BILL")
|
||||
if (result.ooredoo == OoredooClient.CustType.PRE || result.ooredoo == OoredooClient.CustType.HYBRID) add("RAASTAS")
|
||||
if (result.ooredoo == OoredooClient.CustType.POST || result.ooredoo == OoredooClient.CustType.HYBRID) add("OOREDOO_BILL")
|
||||
}
|
||||
|
||||
if (services.isEmpty()) return@launch
|
||||
|
||||
// Only one option — auto-select, no chip UI needed
|
||||
if (services.size == 1) {
|
||||
selectFahipayService(services[0], number, dhiraaguName)
|
||||
return@launch
|
||||
}
|
||||
|
||||
// Multiple options (Ooredoo HYBRID) — show chips
|
||||
binding.chipDhiraaguReload.visibility = if ("DHIRAAGU_RELOAD" in services) View.VISIBLE else View.GONE
|
||||
binding.chipDhiraaguBill.visibility = if ("DHIRAAGU_BILL" in services) View.VISIBLE else View.GONE
|
||||
binding.chipRaastas.visibility = if ("RAASTAS" in services) View.VISIBLE else View.GONE
|
||||
binding.chipOoredooBill.visibility = if ("OOREDOO_BILL" in services) View.VISIBLE else View.GONE
|
||||
binding.layoutServiceSelector.visibility = View.VISIBLE
|
||||
binding.chipGroupService.clearCheck()
|
||||
|
||||
binding.chipDhiraaguReload.setOnCheckedChangeListener { _, checked ->
|
||||
if (checked) { selectFahipayService("DHIRAAGU_RELOAD", number, dhiraaguName); binding.layoutServiceSelector.visibility = View.INVISIBLE }
|
||||
}
|
||||
binding.chipDhiraaguBill.setOnCheckedChangeListener { _, checked ->
|
||||
if (checked) { selectFahipayService("DHIRAAGU_BILL", number, dhiraaguName); binding.layoutServiceSelector.visibility = View.INVISIBLE }
|
||||
}
|
||||
binding.chipRaastas.setOnCheckedChangeListener { _, checked ->
|
||||
if (checked) { selectFahipayService("RAASTAS", number); binding.layoutServiceSelector.visibility = View.INVISIBLE }
|
||||
}
|
||||
binding.chipOoredooBill.setOnCheckedChangeListener { _, checked ->
|
||||
if (checked) { selectFahipayService("OOREDOO_BILL", number); binding.layoutServiceSelector.visibility = View.INVISIBLE }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun selectFahipayService(service: String, number: String, ownerName: String? = null) {
|
||||
selectedFahipayService = service
|
||||
val contacts = viewModel.contacts.value ?: emptyList()
|
||||
val displayName = ownerName
|
||||
?: contacts.firstOrNull { it.benefAccount == number }?.benefNickName
|
||||
?: number
|
||||
val serviceLabel = when (service) {
|
||||
"RAASTAS" -> "Raastas"
|
||||
"OOREDOO_BILL" -> "Ooredoo Bill Pay"
|
||||
"DHIRAAGU_RELOAD" -> "Dhiraagu Reload"
|
||||
"DHIRAAGU_BILL" -> "Dhiraagu Bill Pay"
|
||||
else -> service
|
||||
}
|
||||
prefillToDirectly(
|
||||
accountNumber = number,
|
||||
displayName = displayName,
|
||||
subtitle = "$serviceLabel · $number",
|
||||
colorHex = "#FF6B00",
|
||||
imageHash = null
|
||||
)
|
||||
}
|
||||
|
||||
private fun prefillToDirectly(
|
||||
accountNumber: String,
|
||||
displayName: String,
|
||||
@@ -661,7 +772,9 @@ class TransferFragment : Fragment() {
|
||||
resolvedAccountNumber = ""
|
||||
resolvedRecipientName = ""
|
||||
resolvedBankName = ""
|
||||
selectedFahipayService = null
|
||||
binding.cardToInfo.visibility = View.GONE
|
||||
binding.chipGroupService.visibility = View.GONE
|
||||
binding.tilTo.visibility = View.VISIBLE
|
||||
binding.btnPickContact.visibility = View.VISIBLE
|
||||
binding.btnScanQr.visibility = View.VISIBLE
|
||||
|
||||
@@ -26,14 +26,22 @@
|
||||
android:orientation="vertical"
|
||||
android:padding="20dp">
|
||||
|
||||
<!-- From account dropdown -->
|
||||
<!-- From label + account dropdown -->
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="6dp"
|
||||
android:text="@string/transfer_label_from"
|
||||
android:textAppearance="?attr/textAppearanceLabelMedium"
|
||||
android:textColor="?attr/colorOnSurfaceVariant" />
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/tilFrom"
|
||||
style="@style/Widget.Material3.TextInputLayout.OutlinedBox.ExposedDropdownMenu"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/transfer_from"
|
||||
android:layout_marginBottom="16dp">
|
||||
android:layout_marginBottom="8dp">
|
||||
|
||||
<AutoCompleteTextView
|
||||
android:id="@+id/actvFrom"
|
||||
@@ -118,7 +126,15 @@
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
<!-- To field row: input + pick contact button -->
|
||||
<!-- To label + field row: input + pick contact button -->
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="6dp"
|
||||
android:text="@string/transfer_label_to"
|
||||
android:textAppearance="?attr/textAppearanceLabelMedium"
|
||||
android:textColor="?attr/colorOnSurfaceVariant" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
@@ -171,6 +187,7 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="gone"
|
||||
android:layout_marginTop="8dp"
|
||||
app:cardCornerRadius="4dp"
|
||||
app:cardElevation="0dp"
|
||||
app:strokeWidth="1dp"
|
||||
@@ -228,6 +245,66 @@
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
<!-- Service selector: invisible to reserve space, prevents amount/remarks from shifting -->
|
||||
<LinearLayout
|
||||
android:id="@+id/layoutServiceSelector"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:layout_marginTop="8dp"
|
||||
android:visibility="invisible">
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="4dp"
|
||||
android:text="@string/transfer_select_service"
|
||||
android:textAppearance="?attr/textAppearanceLabelMedium"
|
||||
android:textColor="?attr/colorOnSurfaceVariant" />
|
||||
|
||||
<com.google.android.material.chip.ChipGroup
|
||||
android:id="@+id/chipGroupService"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:singleSelection="true"
|
||||
app:selectionRequired="true">
|
||||
|
||||
<com.google.android.material.chip.Chip
|
||||
android:id="@+id/chipDhiraaguReload"
|
||||
style="@style/Widget.Material3.Chip.Filter"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Dhiraagu Reload"
|
||||
android:visibility="gone" />
|
||||
|
||||
<com.google.android.material.chip.Chip
|
||||
android:id="@+id/chipDhiraaguBill"
|
||||
style="@style/Widget.Material3.Chip.Filter"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Dhiraagu Bill Pay"
|
||||
android:visibility="gone" />
|
||||
|
||||
<com.google.android.material.chip.Chip
|
||||
android:id="@+id/chipRaastas"
|
||||
style="@style/Widget.Material3.Chip.Filter"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Raastas"
|
||||
android:visibility="gone" />
|
||||
|
||||
<com.google.android.material.chip.Chip
|
||||
android:id="@+id/chipOoredooBill"
|
||||
style="@style/Widget.Material3.Chip.Filter"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Ooredoo Bill Pay"
|
||||
android:visibility="gone" />
|
||||
|
||||
</com.google.android.material.chip.ChipGroup>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<View
|
||||
android:id="@+id/spacerTo"
|
||||
android:layout_width="match_parent"
|
||||
|
||||
@@ -147,6 +147,10 @@
|
||||
<string name="transfer_tab_contacts">Contacts</string>
|
||||
<string name="transfer_from">From account</string>
|
||||
<string name="transfer_to">Account Number or Favara ID</string>
|
||||
<string name="transfer_label_from">From</string>
|
||||
<string name="transfer_label_to">To</string>
|
||||
<string name="transfer_select_service">Select Service</string>
|
||||
<string name="transfer_fahipay_phone_only">Fahipay transfers require a 7-digit phone number</string>
|
||||
<string name="transfer_my_accounts">My Accounts</string>
|
||||
<string name="transfer_same_as_from">This is the same account as the sender</string>
|
||||
<string name="transfer_lookup_account">Look up account</string>
|
||||
|
||||
172
docs/dhiraaguapi/01-number-lookup.md
Normal file
172
docs/dhiraaguapi/01-number-lookup.md
Normal file
@@ -0,0 +1,172 @@
|
||||
# Number Lookup
|
||||
|
||||
Validate a Dhiraagu mobile number and determine whether it is prepaid (reload) or postpaid (bill pay).
|
||||
|
||||
This is a two-step process: first fetch the Easy Pay page to extract the nonce, then POST the number to the IO API.
|
||||
|
||||
---
|
||||
|
||||
## Step 1 — Fetch Nonce
|
||||
|
||||
### Endpoint
|
||||
|
||||
```
|
||||
GET https://www.dhiraagu.com.mv/services/easy-pay
|
||||
```
|
||||
|
||||
### Request Headers
|
||||
|
||||
```
|
||||
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0
|
||||
```
|
||||
|
||||
### curl Example
|
||||
|
||||
```bash
|
||||
curl --request GET \
|
||||
--url 'https://www.dhiraagu.com.mv/services/easy-pay' \
|
||||
--header 'User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0'
|
||||
```
|
||||
|
||||
### Response
|
||||
|
||||
The server returns an HTML page. The nonce is embedded in a `<script>` block:
|
||||
|
||||
```html
|
||||
<script>
|
||||
var nonce = "feb6acb9-a532-428e-a4d6-c4e5515b4bc3";
|
||||
...
|
||||
</script>
|
||||
```
|
||||
|
||||
**Extraction regex:**
|
||||
|
||||
```
|
||||
var nonce = "([^"]+)"
|
||||
```
|
||||
|
||||
The nonce is a UUID (36-char string). It is tied to this page load and must be used immediately in Step 2.
|
||||
|
||||
---
|
||||
|
||||
## Step 2 — Number Lookup
|
||||
|
||||
### Endpoint
|
||||
|
||||
```
|
||||
POST https://www.dhiraagu.com.mv/api/sdk-dhr-webapi.ashx?website_id=CA2BB809-3A22-485B-A518-DA6B6DE653A5&sub=dhiraaguIO&act=infoUnlisted
|
||||
```
|
||||
|
||||
### Request Headers
|
||||
|
||||
| Header | Value |
|
||||
|---|---|
|
||||
| `Content-Type` | `application/json` |
|
||||
| `User-Agent` | `Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0` |
|
||||
| `nonce` | UUID extracted from Step 1 |
|
||||
| `Origin` | `https://dhiraagu.com.mv` |
|
||||
|
||||
### Request Body
|
||||
|
||||
```json
|
||||
{
|
||||
"number": "7654321"
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Notes |
|
||||
|---|---|---|
|
||||
| `number` | `string` | 7-digit Dhiraagu mobile number (local format, no country code) |
|
||||
|
||||
### curl Example
|
||||
|
||||
```bash
|
||||
curl --request POST \
|
||||
--url 'https://www.dhiraagu.com.mv/api/sdk-dhr-webapi.ashx?website_id=CA2BB809-3A22-485B-A518-DA6B6DE653A5&sub=dhiraaguIO&act=infoUnlisted' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--header 'User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0' \
|
||||
--header 'nonce: feb6acb9-a532-428e-a4d6-c4e5515b4bc3' \
|
||||
--header 'Origin: https://dhiraagu.com.mv' \
|
||||
--data '{"number":"7654321"}'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Responses
|
||||
|
||||
### Success — Prepaid (Reload)
|
||||
|
||||
```json
|
||||
{
|
||||
"respStatus": "OK",
|
||||
"serviceDetails": [
|
||||
{
|
||||
"prepaidIndicator": "Y",
|
||||
"serviceType": "GSM"
|
||||
}
|
||||
],
|
||||
"accountOwnerInfo": {
|
||||
"name": "Ahmed Mohamed",
|
||||
"accountNo": "7654321"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Description |
|
||||
|---|---|---|
|
||||
| `respStatus` | `string` | `"OK"` on success |
|
||||
| `serviceDetails[0].prepaidIndicator` | `string` | `"Y"` = prepaid (reload), `"N"` = postpaid (bill pay) |
|
||||
| `accountOwnerInfo.name` | `string` | Account holder's full name |
|
||||
|
||||
### Success — Postpaid (Bill Pay)
|
||||
|
||||
Same structure as above, with `prepaidIndicator: "N"`.
|
||||
|
||||
```json
|
||||
{
|
||||
"respStatus": "OK",
|
||||
"serviceDetails": [
|
||||
{
|
||||
"prepaidIndicator": "N",
|
||||
"serviceType": "GSM"
|
||||
}
|
||||
],
|
||||
"accountOwnerInfo": {
|
||||
"name": "Fatima Ali",
|
||||
"accountNo": "7123456"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Failure — Number Not Found
|
||||
|
||||
```json
|
||||
{
|
||||
"respStatus": "FAILED",
|
||||
"respMessage": "Number not found"
|
||||
}
|
||||
```
|
||||
|
||||
`respStatus` is not `"OK"`. Treat any non-OK status as unsupported.
|
||||
|
||||
### Failure — Invalid / Expired Nonce
|
||||
|
||||
If the nonce is missing, invalid, or has already been used, the server returns an error response (non-OK status or HTTP 4xx). Re-fetch the Easy Pay page to get a fresh nonce.
|
||||
|
||||
---
|
||||
|
||||
## Result Mapping
|
||||
|
||||
| `prepaidIndicator` | Service |
|
||||
|---|---|
|
||||
| `"Y"` | Dhiraagu Reload |
|
||||
| `"N"` | Dhiraagu Bill Pay |
|
||||
| absent / other | Unsupported — fall back to Ooredoo lookup |
|
||||
|
||||
---
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
[← README](README.md)
|
||||
100
docs/dhiraaguapi/README.md
Normal file
100
docs/dhiraaguapi/README.md
Normal file
@@ -0,0 +1,100 @@
|
||||
# Dhiraagu API Documentation
|
||||
|
||||
Reverse-engineered from traffic captures of the Dhiraagu Easy Pay web service (`dhiraagu.com.mv`).
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
Dhiraagu exposes a number-lookup API used by their Easy Pay web page to validate mobile numbers before payment. The API uses a two-step flow:
|
||||
|
||||
1. **Nonce extraction** — fetch the Easy Pay HTML page and extract a one-time nonce token embedded in the page JavaScript.
|
||||
2. **Number lookup** — POST the mobile number with the nonce to the IO API endpoint, which returns the account type and owner name.
|
||||
|
||||
The nonce is single-use and page-session-specific; it must be re-fetched for each lookup.
|
||||
|
||||
---
|
||||
|
||||
## Base URL
|
||||
|
||||
```
|
||||
https://www.dhiraagu.com.mv
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Authentication Model
|
||||
|
||||
| Value | How obtained | How used |
|
||||
|---|---|---|
|
||||
| `nonce` | Extracted from Easy Pay page HTML | Sent as `nonce` request header on the lookup POST |
|
||||
|
||||
No persistent session or login is required. The nonce is valid for a single lookup request.
|
||||
|
||||
---
|
||||
|
||||
## Common Request Headers
|
||||
|
||||
```
|
||||
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0
|
||||
Origin: https://dhiraagu.com.mv
|
||||
```
|
||||
|
||||
The `User-Agent` header is required; requests without it are rejected by the server.
|
||||
|
||||
---
|
||||
|
||||
## Number Lookup Flow
|
||||
|
||||
```
|
||||
Client Server
|
||||
| |
|
||||
| GET /services/easy-pay | ← fetch page to get nonce
|
||||
|----------------------------------------->|
|
||||
| 200 OK (HTML page) |
|
||||
| var nonce = "feb6acb9-a532-428e-..." |
|
||||
|<-----------------------------------------|
|
||||
| |
|
||||
| POST /api/sdk-dhr-webapi.ashx |
|
||||
| ?website_id=CA2BB809... |
|
||||
| &sub=dhiraaguIO&act=infoUnlisted |
|
||||
| nonce: feb6acb9-a532-428e-... |
|
||||
| { "number": "7654321" } |
|
||||
|----------------------------------------->|
|
||||
| { respStatus: "OK", |
|
||||
| serviceDetails: [{prepaidIndicator}], |
|
||||
| accountOwnerInfo: { name } } |
|
||||
|<-----------------------------------------|
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Result Interpretation
|
||||
|
||||
| `prepaidIndicator` | Meaning |
|
||||
|---|---|
|
||||
| `"Y"` | Prepaid — use **Dhiraagu Reload** |
|
||||
| `"N"` | Postpaid — use **Dhiraagu Bill Pay** |
|
||||
| absent / other | Number not found or unsupported |
|
||||
|
||||
---
|
||||
|
||||
## Applicable Numbers
|
||||
|
||||
Only Dhiraagu mobile numbers are returned by this API. Dhiraagu numbers in the Maldives start with **7** (7-digit local format, e.g. `7654321`).
|
||||
|
||||
---
|
||||
|
||||
## Documents
|
||||
|
||||
| # | File | Description |
|
||||
|---|---|---|
|
||||
| 1 | [Number Lookup](01-number-lookup.md) | Validate a Dhiraagu number and determine account type |
|
||||
|
||||
---
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
> **Next →** [Number Lookup](01-number-lookup.md)
|
||||
113
docs/ooredooapi/01-number-validation.md
Normal file
113
docs/ooredooapi/01-number-validation.md
Normal file
@@ -0,0 +1,113 @@
|
||||
# Number Validation
|
||||
|
||||
Validate an Ooredoo mobile number and determine its account type (prepaid, postpaid, or hybrid).
|
||||
|
||||
---
|
||||
|
||||
## Endpoint
|
||||
|
||||
```
|
||||
GET https://www.ooredoo.mv/ooredoo-prod/QuickPayPackage/v1/numberTypeValidation
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Request
|
||||
|
||||
### Query Parameters
|
||||
|
||||
| Parameter | Value | Notes |
|
||||
|---|---|---|
|
||||
| `action` | `cust_details` | Always `cust_details` |
|
||||
| `msisdn` | `9607654321` | Full MSISDN — country code `960` + 7-digit local number |
|
||||
|
||||
### curl Example
|
||||
|
||||
```bash
|
||||
curl --request GET \
|
||||
--url 'https://www.ooredoo.mv/ooredoo-prod/QuickPayPackage/v1/numberTypeValidation?action=cust_details&msisdn=9609654321'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Responses
|
||||
|
||||
### Success — Prepaid
|
||||
|
||||
```json
|
||||
{
|
||||
"custType": "PRE",
|
||||
"msisdn": "9609654321"
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Description |
|
||||
|---|---|---|
|
||||
| `custType` | `string` | `"PRE"` = prepaid customer |
|
||||
| `msisdn` | `string` | The MSISDN that was queried |
|
||||
|
||||
→ Offer **Raastas** top-up only.
|
||||
|
||||
---
|
||||
|
||||
### Success — Postpaid
|
||||
|
||||
```json
|
||||
{
|
||||
"custType": "POST",
|
||||
"msisdn": "9609123456"
|
||||
}
|
||||
```
|
||||
|
||||
→ Offer **Ooredoo Bill Pay** only.
|
||||
|
||||
---
|
||||
|
||||
### Success — Hybrid
|
||||
|
||||
```json
|
||||
{
|
||||
"custType": "HYBRID",
|
||||
"msisdn": "9609789012"
|
||||
}
|
||||
```
|
||||
|
||||
→ Offer both **Raastas** and **Ooredoo Bill Pay**. The user must select which service to use.
|
||||
|
||||
---
|
||||
|
||||
### Failure — Number Not Found
|
||||
|
||||
```json
|
||||
{
|
||||
"custType": null,
|
||||
"errorMessage": "Data Not Found",
|
||||
"msisdn": "9609000000"
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Description |
|
||||
|---|---|---|
|
||||
| `custType` | `null` | Number is not an Ooredoo subscriber |
|
||||
| `errorMessage` | `string` | `"Data Not Found"` |
|
||||
|
||||
Treat `custType: null` as unsupported — fall back to Dhiraagu lookup.
|
||||
|
||||
---
|
||||
|
||||
## Result Mapping
|
||||
|
||||
| `custType` | Service(s) |
|
||||
|---|---|
|
||||
| `"PRE"` | Raastas only |
|
||||
| `"POST"` | Ooredoo Bill Pay only |
|
||||
| `"HYBRID"` | Raastas + Ooredoo Bill Pay (show chip selector) |
|
||||
| `null` / absent | Unsupported — fall back to Dhiraagu lookup |
|
||||
|
||||
---
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
[← README](README.md)
|
||||
85
docs/ooredooapi/README.md
Normal file
85
docs/ooredooapi/README.md
Normal file
@@ -0,0 +1,85 @@
|
||||
# Ooredoo API Documentation
|
||||
|
||||
Reverse-engineered from traffic captures of the Ooredoo Quick Pay web service (`ooredoo.mv`).
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
Ooredoo exposes a number-validation API used by their Quick Pay service to check mobile numbers before payment. It is a single unauthenticated GET request that returns the customer type for a given MSISDN.
|
||||
|
||||
The response determines which payment service to offer:
|
||||
|
||||
- **PRE** (prepaid) → Raastas top-up only
|
||||
- **POST** (postpaid) → Ooredoo Bill Pay only
|
||||
- **HYBRID** → both Raastas and Ooredoo Bill Pay (user must choose)
|
||||
- No match → number is not an Ooredoo number
|
||||
|
||||
---
|
||||
|
||||
## Base URL
|
||||
|
||||
```
|
||||
https://www.ooredoo.mv
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Authentication Model
|
||||
|
||||
No authentication required. The endpoint is publicly accessible.
|
||||
|
||||
---
|
||||
|
||||
## Common Request Headers
|
||||
|
||||
No special headers are required. Standard HTTP/HTTPS headers apply.
|
||||
|
||||
---
|
||||
|
||||
## Number Lookup Flow
|
||||
|
||||
```
|
||||
Client Server
|
||||
| |
|
||||
| GET /ooredoo-prod/QuickPayPackage/v1/ |
|
||||
| numberTypeValidation |
|
||||
| ?action=cust_details |
|
||||
| &msisdn=9607654321 |
|
||||
|------------------------------------------->|
|
||||
| { custType: "PRE" } |
|
||||
|<-------------------------------------------|
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Result Interpretation
|
||||
|
||||
| `custType` | Meaning |
|
||||
|---|---|
|
||||
| `"PRE"` | Prepaid — offer **Raastas** top-up |
|
||||
| `"POST"` | Postpaid — offer **Ooredoo Bill Pay** |
|
||||
| `"HYBRID"` | Both prepaid and postpaid — offer both (user selects) |
|
||||
| `null` / absent | Not an Ooredoo number |
|
||||
|
||||
---
|
||||
|
||||
## Applicable Numbers
|
||||
|
||||
Only Ooredoo mobile numbers are returned by this API. Ooredoo numbers in the Maldives start with **9** (7-digit local format, e.g. `9654321`). The API expects the full MSISDN including country code `960`.
|
||||
|
||||
---
|
||||
|
||||
## Documents
|
||||
|
||||
| # | File | Description |
|
||||
|---|---|---|
|
||||
| 1 | [Number Validation](01-number-validation.md) | Validate an Ooredoo number and determine account type |
|
||||
|
||||
---
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
> **Next →** [Number Validation](01-number-validation.md)
|
||||
Reference in New Issue
Block a user