Compare commits
12
Commits
9ab7f979fa
...
v1.0.34
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
494a42c1a8
|
||
|
|
9ace8dd724
|
||
|
|
a3449167db
|
||
|
|
1612e21471
|
||
|
|
f02b05b180
|
||
|
|
5abd981096
|
||
|
|
bb76f4e591
|
||
|
|
8795d5f758
|
||
|
|
0d4f0074c7
|
||
|
|
237d25af67
|
||
|
|
a0c515103a
|
||
|
|
56b464b58e
|
@@ -24,20 +24,11 @@ jobs:
|
|||||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||||
echo "version_code=$VERSION_CODE" >> $GITHUB_OUTPUT
|
echo "version_code=$VERSION_CODE" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
BEFORE="${{ github.event.before }}"
|
if git tag -l | grep -q "^v${VERSION}$"; then
|
||||||
if [ -z "$BEFORE" ] || ! git cat-file -e "${BEFORE}^{commit}" 2>/dev/null; then
|
echo "Tag v${VERSION} already exists, skipping"
|
||||||
BEFORE="HEAD~1"
|
|
||||||
fi
|
|
||||||
PREV_VERSION_CODE=$(git show "${BEFORE}:app/build.gradle.kts" 2>/dev/null | grep 'versionCode = ' | sed 's/.*versionCode = \([0-9]*\).*/\1/')
|
|
||||||
|
|
||||||
if [ "$VERSION_CODE" = "$PREV_VERSION_CODE" ]; then
|
|
||||||
echo "versionCode unchanged (${VERSION_CODE}), skipping"
|
|
||||||
echo "should_release=false" >> $GITHUB_OUTPUT
|
echo "should_release=false" >> $GITHUB_OUTPUT
|
||||||
elif git tag -l | grep -q "^v${VERSION}$"; then
|
|
||||||
echo "versionCode changed (${PREV_VERSION_CODE} -> ${VERSION_CODE}) but tag v${VERSION} already exists; bump versionName"
|
|
||||||
exit 1
|
|
||||||
else
|
else
|
||||||
echo "New versionCode detected: ${PREV_VERSION_CODE} -> ${VERSION_CODE} (v${VERSION})"
|
echo "New version detected: v${VERSION}"
|
||||||
echo "should_release=true" >> $GITHUB_OUTPUT
|
echo "should_release=true" >> $GITHUB_OUTPUT
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,9 @@ docs/mibapi/tmp
|
|||||||
docs/bmlapi/tmp
|
docs/bmlapi/tmp
|
||||||
docs/fahipayapi/tmp
|
docs/fahipayapi/tmp
|
||||||
docs/mfaisaapi/tmp
|
docs/mfaisaapi/tmp
|
||||||
|
docs/dhiraaguapi/tmp
|
||||||
|
docs/ooredooapi/tmp
|
||||||
|
docs/ooredooapi/tmp
|
||||||
tmp
|
tmp
|
||||||
app/key.jks
|
app/key.jks
|
||||||
.kotlin/*
|
.kotlin/*
|
||||||
|
|||||||
@@ -21,8 +21,8 @@ android {
|
|||||||
applicationId = "sh.sar.basedbank"
|
applicationId = "sh.sar.basedbank"
|
||||||
minSdk = 26
|
minSdk = 26
|
||||||
targetSdk = 36
|
targetSdk = 36
|
||||||
versionCode = 34
|
versionCode = 35
|
||||||
versionName = "1.0.33"
|
versionName = "1.0.34"
|
||||||
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
|
|
||||||
|
|||||||
@@ -34,10 +34,12 @@ import android.util.Base64
|
|||||||
* 4. The 3-D Secure challenge on BML's Wibmo ACS: the render page auto-posts the `creq`, we pick
|
* 4. The 3-D Secure challenge on BML's Wibmo ACS: the render page auto-posts the `creq`, we pick
|
||||||
* the "Authenticator" channel and submit the BML token's TOTP. The ACS then auto-posts the
|
* the "Authenticator" channel and submit the BML token's TOTP. The ACS then auto-posts the
|
||||||
* result to the Mastercard gateway, which posts it back to BML's `mpgsNotification`.
|
* result to the Mastercard gateway, which posts it back to BML's `mpgsNotification`.
|
||||||
* 5. Poll next-action until TRANSACTION_CONFIRMED.
|
* 5. Poll next-action until TRANSACTION_CONFIRMED. A decline after 3-D Secure (e.g. insufficient
|
||||||
|
* funds) doesn't show up there: it's a new `paymentErrorHistory` entry on the transaction,
|
||||||
|
* checked alongside.
|
||||||
* 6. Return to the merchant: `GET <txn>?wait=1` redirects to the merchant's `redirectUrl` with a
|
* 6. Return to the merchant: `GET <txn>?wait=1` redirects to the merchant's `redirectUrl` with a
|
||||||
* signed `state=CONFIRMED` — the browser's last hop, and how some merchants (Dhiraagu) learn
|
* signed `state=CONFIRMED` — the browser's last hop, and how merchants (Dhiraagu, Ooredoo)
|
||||||
* they were paid. Without it the card is charged but the merchant never delivers.
|
* learn they were paid. Without it the card is charged but the merchant never delivers.
|
||||||
*
|
*
|
||||||
* Every call blocks, so run it on an IO thread. Use one instance per payment — it keeps the ACS
|
* Every call blocks, so run it on an IO thread. Use one instance per payment — it keeps the ACS
|
||||||
* session cookies.
|
* session cookies.
|
||||||
@@ -90,7 +92,10 @@ class BmlMerchantCardPayClient {
|
|||||||
val pk = page.pomeloKey ?: return Result.Failure("This merchant doesn't accept card payments")
|
val pk = page.pomeloKey ?: return Result.Failure("This merchant doesn't accept card payments")
|
||||||
val txnId = page.transactionId
|
val txnId = page.transactionId
|
||||||
|
|
||||||
runCatching { BmlMerchantTxnClient().announceBrowser(txnId) }
|
val txnClient = BmlMerchantTxnClient()
|
||||||
|
val browserId = runCatching { txnClient.announceBrowser(txnId) }.getOrNull()
|
||||||
|
// Earlier attempts' declines are already in the history; only newer ones are ours
|
||||||
|
val priorErrors = browserId?.let { id -> runCatching { txnClient.paymentErrors(txnId, id).size }.getOrNull() }
|
||||||
|
|
||||||
// 1-2. Credentials, then tokenise the card with Pomelo
|
// 1-2. Credentials, then tokenise the card with Pomelo
|
||||||
val creds = getJson("$API_BASE/public-client/credentials/$txnId", pk)
|
val creds = getJson("$API_BASE/public-client/credentials/$txnId", pk)
|
||||||
@@ -154,6 +159,11 @@ class BmlMerchantCardPayClient {
|
|||||||
"TRANSACTION_CONFIRMED" -> return confirmed(txnId)
|
"TRANSACTION_CONFIRMED" -> return confirmed(txnId)
|
||||||
"TRANSACTION_FAILED" -> return Result.Failure("The bank declined the payment")
|
"TRANSACTION_FAILED" -> return Result.Failure("The bank declined the payment")
|
||||||
}
|
}
|
||||||
|
if (browserId != null && priorErrors != null) {
|
||||||
|
runCatching { txnClient.paymentErrors(txnId, browserId) }.getOrNull()
|
||||||
|
?.drop(priorErrors)?.lastOrNull()
|
||||||
|
?.let { return Result.Failure(it.message.ifBlank { "The bank declined the payment" }) }
|
||||||
|
}
|
||||||
Thread.sleep(POLL_MS / 2)
|
Thread.sleep(POLL_MS / 2)
|
||||||
}
|
}
|
||||||
return Result.Failure("Payment status unknown — check with the merchant before retrying")
|
return Result.Failure("Payment status unknown — check with the merchant before retrying")
|
||||||
@@ -165,25 +175,37 @@ class BmlMerchantCardPayClient {
|
|||||||
/**
|
/**
|
||||||
* What the browser does once the payment page sees the confirmation: loads `<txn>?wait=1`,
|
* What the browser does once the payment page sees the confirmation: loads `<txn>?wait=1`,
|
||||||
* which 302s to the merchant's `redirectUrl` (`…?transactionId=<id>&state=CONFIRMED&signature=…`)
|
* which 302s to the merchant's `redirectUrl` (`…?transactionId=<id>&state=CONFIRMED&signature=…`)
|
||||||
* and on to its receipt page. Follows the redirects and retries a couple of times; true when
|
* and on to its receipt page. Some merchants' callback page instead auto-submits a form on
|
||||||
* the chain ended on a 2xx page.
|
* load (Ooredoo's posts the result on to its own site), so those forms are submitted too.
|
||||||
|
* Retries a couple of times; true when the chain ended on a 2xx page.
|
||||||
*/
|
*/
|
||||||
private fun returnToMerchant(txnId: String): Boolean {
|
private fun returnToMerchant(txnId: String): Boolean {
|
||||||
repeat(RETURN_ATTEMPTS) { attempt ->
|
repeat(RETURN_ATTEMPTS) { attempt ->
|
||||||
if (attempt > 0) Thread.sleep(RETURN_RETRY_MS)
|
if (attempt > 0) Thread.sleep(RETURN_RETRY_MS)
|
||||||
val ok = runCatching {
|
val ok = runCatching {
|
||||||
client.newCall(Request.Builder()
|
var request = browserNav(Request.Builder().url("$PAGE_ORIGIN/$txnId?wait=1"))
|
||||||
.url("$PAGE_ORIGIN/$txnId?wait=1")
|
for (hop in 0..MAX_AUTO_SUBMITS) {
|
||||||
.header("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8")
|
val (code, html, url) = client.newCall(request).execute().use {
|
||||||
.header("Accept-Language", "en-US,en;q=0.9")
|
Triple(it.code, it.body?.string().orEmpty(), it.request.url.toString())
|
||||||
.build()
|
}
|
||||||
).execute().use { it.isSuccessful }
|
if (code !in 200..299) return@runCatching false
|
||||||
|
// A page that only exists to post itself onward, like the ACS's own hops
|
||||||
|
if (hop == MAX_AUTO_SUBMITS || !AUTO_SUBMIT.containsMatchIn(html)) return@runCatching true
|
||||||
|
val form = AcsForm.parse(html, url) ?: return@runCatching true
|
||||||
|
request = browserNav(form.toRequest().newBuilder())
|
||||||
|
}
|
||||||
|
true
|
||||||
}.getOrDefault(false)
|
}.getOrDefault(false)
|
||||||
if (ok) return true
|
if (ok) return true
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun browserNav(builder: Request.Builder): Request = builder
|
||||||
|
.header("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8")
|
||||||
|
.header("Accept-Language", "en-US,en;q=0.9")
|
||||||
|
.build()
|
||||||
|
|
||||||
/** Drives the ACS challenge. Returns null on success, or a Failure to stop the payment. */
|
/** Drives the ACS challenge. Returns null on success, or a Failure to stop the payment. */
|
||||||
private fun runThreeDs(threeDsUrl: String, otp: (Boolean) -> String): Result? {
|
private fun runThreeDs(threeDsUrl: String, otp: (Boolean) -> String): Result? {
|
||||||
// render-tds: an auto-submitting form (with an explicit action) that posts the creq to the
|
// render-tds: an auto-submitting form (with an explicit action) that posts the creq to the
|
||||||
@@ -206,6 +228,8 @@ class BmlMerchantCardPayClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// OTP entry. Submit the token code; if it expired, ask for a fresh one once and retry.
|
// OTP entry. Submit the token code; if it expired, ask for a fresh one once and retry.
|
||||||
|
// A rejected code comes back as the OTP page again with "The OTP code you entered is
|
||||||
|
// incorrect Please try again."
|
||||||
var retry = false
|
var retry = false
|
||||||
for (attempt in 0..1) {
|
for (attempt in 0..1) {
|
||||||
form = AcsForm.parse(html, acsUrl) ?: break
|
form = AcsForm.parse(html, acsUrl) ?: break
|
||||||
@@ -213,9 +237,10 @@ class BmlMerchantCardPayClient {
|
|||||||
form.fields["otpValue"] = otp(retry)
|
form.fields["otpValue"] = otp(retry)
|
||||||
form.fields["formReqType"] = "SUBMIT"
|
form.fields["formReqType"] = "SUBMIT"
|
||||||
html = execText(form.toRequest())
|
html = execText(form.toRequest())
|
||||||
if (!html.contains("incorrect", true) && !html.contains("expired", true)) break
|
if (!otpRejected(html)) break
|
||||||
retry = true
|
retry = true
|
||||||
}
|
}
|
||||||
|
if (otpRejected(html)) return Result.Failure("The bank rejected the BML token code. Check the phone's clock and try again.")
|
||||||
// On success the ACS returns an auto-posting form to the gateway; follow it (and the
|
// On success the ACS returns an auto-posting form to the gateway; follow it (and the
|
||||||
// gateway's own auto-post back to BML) so the verdict is recorded before we poll.
|
// gateway's own auto-post back to BML) so the verdict is recorded before we poll.
|
||||||
repeat(3) {
|
repeat(3) {
|
||||||
@@ -226,6 +251,9 @@ class BmlMerchantCardPayClient {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun otpRejected(html: String) = html.contains("name=\"otpValue\"") &&
|
||||||
|
(html.contains("incorrect", true) || html.contains("expired", true))
|
||||||
|
|
||||||
// ── next-action helpers ──────────────────────────────────────────────────
|
// ── next-action helpers ──────────────────────────────────────────────────
|
||||||
|
|
||||||
private fun nextAction(pk: String, body: JSONObject): JSONObject =
|
private fun nextAction(pk: String, body: JSONObject): JSONObject =
|
||||||
@@ -329,5 +357,9 @@ class BmlMerchantCardPayClient {
|
|||||||
private const val MAX_POLLS = 10
|
private const val MAX_POLLS = 10
|
||||||
private const val RETURN_ATTEMPTS = 3
|
private const val RETURN_ATTEMPTS = 3
|
||||||
private const val RETURN_RETRY_MS = 2_000L
|
private const val RETURN_RETRY_MS = 2_000L
|
||||||
|
/** Auto-submitting merchant pages followed on the way back; Ooredoo has one. */
|
||||||
|
private const val MAX_AUTO_SUBMITS = 2
|
||||||
|
/** `<body onload="document.forms['x'].submit()">` and the like. */
|
||||||
|
private val AUTO_SUBMIT = Regex("""onload\s*=\s*("[^"]*|'[^']*)\.submit\(\)""", RegexOption.IGNORE_CASE)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -113,12 +113,36 @@ class BmlMerchantTxnClient {
|
|||||||
return txn.vendorQrCode() ?: throw Exception("Transaction has no QR")
|
return txn.vendorQrCode() ?: throw Exception("Transaction has no QR")
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The PATCHes the page sends on load: register this "browser" and clear any FX selection. */
|
/**
|
||||||
fun announceBrowser(transactionId: String) {
|
* The PATCHes the page sends on load: register this "browser" and clear any FX selection.
|
||||||
patch(transactionId, JSONObject().put("activeBrowserId", "${transactionId}_${System.currentTimeMillis()}"))
|
* Returns the browser id, for [paymentErrors].
|
||||||
|
*/
|
||||||
|
fun announceBrowser(transactionId: String): String {
|
||||||
|
val browserId = "${transactionId}_${System.currentTimeMillis()}"
|
||||||
|
patch(transactionId, JSONObject().put("activeBrowserId", browserId))
|
||||||
patch(transactionId, JSONObject().put("fx", "reset"))
|
patch(transactionId, JSONObject().put("fx", "reset"))
|
||||||
|
return browserId
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The transaction's failed payment attempts, oldest first, read with the page's load PATCH
|
||||||
|
* as [browserId]. A declined card (e.g. `INSUFFICIENT_FUNDS`) lands here while the
|
||||||
|
* transaction stays payable — `state` doesn't change, `hasError` turns true.
|
||||||
|
*/
|
||||||
|
fun paymentErrors(transactionId: String, browserId: String): List<PaymentError> {
|
||||||
|
val history = patch(transactionId, JSONObject().put("activeBrowserId", browserId))
|
||||||
|
.optJSONArray("paymentErrorHistory") ?: return emptyList()
|
||||||
|
return (0 until history.length()).mapNotNull { history.optJSONObject(it) }.map {
|
||||||
|
PaymentError(
|
||||||
|
code = it.optString("code"),
|
||||||
|
message = it.optString("customerVisibleDescription").ifBlank { it.optString("reason") }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** One entry of `paymentErrorHistory`: the gateway's code and the wording BML shows for it. */
|
||||||
|
data class PaymentError(val code: String, val message: String)
|
||||||
|
|
||||||
private fun patch(transactionId: String, body: JSONObject): JSONObject {
|
private fun patch(transactionId: String, body: JSONObject): JSONObject {
|
||||||
val request = Request.Builder()
|
val request = Request.Builder()
|
||||||
.url("$API_BASE/transactions/$transactionId")
|
.url("$API_BASE/transactions/$transactionId")
|
||||||
|
|||||||
+83
-17
@@ -13,14 +13,14 @@ import java.util.Locale
|
|||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Dhiraagu prepaid reload through the dhiraagu.com.mv "Easy TopUp" page, paid by card on BML's
|
* Dhiraagu prepaid reload ("Easy TopUp") and bill payment ("Easy Pay") through dhiraagu.com.mv,
|
||||||
* merchant gateway. Dhiraagu only builds the order; the money moves on the BML Merchant Services
|
* paid by card on BML's merchant gateway. Dhiraagu only builds the order; the money moves on the
|
||||||
* transaction [createBmlTransaction] hands back, which is paid like any card-only BML merchant
|
* BML Merchant Services transaction handed back, which is paid like any card-only BML merchant
|
||||||
* link. See `docs/dhiraaguapi/02-reload.md`.
|
* link. See `docs/dhiraaguapi/02-reload.md` and `docs/dhiraaguapi/03-bill-pay.md`.
|
||||||
*
|
*
|
||||||
* Every call blocks, so run it on an IO thread.
|
* Every call blocks, so run it on an IO thread.
|
||||||
*/
|
*/
|
||||||
class DhiraaguReloadClient {
|
class DhiraaguPaymentClient {
|
||||||
|
|
||||||
private val client = OkHttpClient.Builder()
|
private val client = OkHttpClient.Builder()
|
||||||
.connectTimeout(30, TimeUnit.SECONDS)
|
.connectTimeout(30, TimeUnit.SECONDS)
|
||||||
@@ -33,11 +33,11 @@ class DhiraaguReloadClient {
|
|||||||
* amount paid, GST included. Returns the 24-hex BML transaction id. Throws with Dhiraagu's
|
* amount paid, GST included. Returns the 24-hex BML transaction id. Throws with Dhiraagu's
|
||||||
* wording when a step is refused.
|
* wording when a step is refused.
|
||||||
*/
|
*/
|
||||||
fun createBmlTransaction(number: String, amount: Int): String {
|
fun createReloadTransaction(number: String, amount: Int): String {
|
||||||
val topupNonce = pageNonce("$BASE/services/easy-topup")
|
val topupNonce = pageNonce("$BASE/services/easy-topup")
|
||||||
val gst = gstOf(amount)
|
val gst = gstOf(amount)
|
||||||
val cart = api("cart", "recharge", topupNonce, JSONObject()
|
val cart = api("cart", "recharge", topupNonce, JSONObject()
|
||||||
.put("formId", FORM_ID)
|
.put("formId", FORM_RELOAD)
|
||||||
.put("serviceNumber", number)
|
.put("serviceNumber", number)
|
||||||
.put("amount", amount)
|
.put("amount", amount)
|
||||||
.put("amountGST", gst.toDouble())
|
.put("amountGST", gst.toDouble())
|
||||||
@@ -46,21 +46,66 @@ class DhiraaguReloadClient {
|
|||||||
.put("memberId", "").put("memberName", "").put("memberNId", "")
|
.put("memberId", "").put("memberName", "").put("memberNId", "")
|
||||||
.put("customerId", "").put("customerCode", "")
|
.put("customerId", "").put("customerCode", "")
|
||||||
.put("version", 2))
|
.put("version", 2))
|
||||||
|
return payCart(FORM_RELOAD, cart, BigDecimal(amount))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates the bill payment order for postpaid [number] and the BML transaction paying for
|
||||||
|
* it: lookup (for the billing account) → cart → merchant → payment → BML transaction.
|
||||||
|
* [amount] is MVR, up to 2 decimal places. Returns the 24-hex BML transaction id. Throws
|
||||||
|
* with Dhiraagu's wording when a step is refused.
|
||||||
|
*/
|
||||||
|
fun createBillPayTransaction(number: String, amount: BigDecimal): String {
|
||||||
|
val easyPayNonce = pageNonce("$BASE/services/easy-pay")
|
||||||
|
// The cart needs the billing account the number belongs to, which only the lookup gives
|
||||||
|
val info = call("dhiraaguIO", "infoUnlisted", easyPayNonce, JSONObject().put("number", number))
|
||||||
|
if (info.optJSONArray("serviceDetails")?.optJSONObject(0)?.optString("prepaidIndicator") == "Y") {
|
||||||
|
throw Exception("Prepaid number is not allowed.")
|
||||||
|
}
|
||||||
|
val accountNumber = info.optString("accountNumber").ifBlank { throw Exception("Invalid account/service number") }
|
||||||
|
|
||||||
|
// The page refuses some account statuses and customer types before ordering; so do we
|
||||||
|
val rules = runCatching { get("setting", "bill", easyPayNonce).getJSONObject("resp").getJSONObject("settingAppJson1") }.getOrNull()
|
||||||
|
val status = info.optString("accountStatus")
|
||||||
|
if (rules != null && status in rules.blockedValues("accountStatus")) {
|
||||||
|
throw Exception("Dhiraagu can't accept payment for this service. Contact Dhiraagu customer service. [Account Status: $status]")
|
||||||
|
}
|
||||||
|
val customerType = info.optString("customerType")
|
||||||
|
if (rules != null && customerType in rules.blockedValues("customerType")) {
|
||||||
|
throw Exception("The number is not allowed. [Customer Type: $customerType]")
|
||||||
|
}
|
||||||
|
|
||||||
|
val cart = api("cart", "easyPay", easyPayNonce, JSONObject()
|
||||||
|
.put("formId", FORM_BILL)
|
||||||
|
.put("serviceNumber", number)
|
||||||
|
.put("accountNumber", accountNumber)
|
||||||
|
.put("amount", money(amount))
|
||||||
|
.put("memberId", "").put("memberName", "").put("memberNId", "")
|
||||||
|
.put("billRef", "")
|
||||||
|
.put("billType", if (info.optString("type") == BILL_WRITE_OFF) BILL_WRITE_OFF else BILL_PAYMENT))
|
||||||
|
return payCart(FORM_BILL, cart, amount)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The payment page's half, shared by every form: picks the BML gateway, creates the payment
|
||||||
|
* for [cart] and has Dhiraagu create the BML transaction. Returns its 24-hex id.
|
||||||
|
*/
|
||||||
|
private fun payCart(formId: Int, cart: JSONObject, amount: BigDecimal): String {
|
||||||
val cartId = cart.optString("cartId").ifBlank { throw Exception("Dhiraagu didn't create the order") }
|
val cartId = cart.optString("cartId").ifBlank { throw Exception("Dhiraagu didn't create the order") }
|
||||||
|
|
||||||
// The payment page carries its own nonce, used for the rest of the order
|
// The payment page carries its own nonce, used for the rest of the order
|
||||||
val paymentNonce = pageNonce("$BASE/services/payment-v2?cartid=$cartId")
|
val paymentNonce = pageNonce("$BASE/services/payment-v2?cartid=$cartId")
|
||||||
val merchants = apiList("merchant", "form", paymentNonce, JSONObject().put("formId", FORM_ID))
|
val merchants = apiList("merchant", "form", paymentNonce, JSONObject().put("formId", formId))
|
||||||
val bml = (0 until merchants.length()).map { merchants.getJSONObject(it) }
|
val bml = (0 until merchants.length()).map { merchants.getJSONObject(it) }
|
||||||
.firstOrNull { it.optInt("gatewayId") == GATEWAY_BML }
|
.firstOrNull { it.optInt("gatewayId") == GATEWAY_BML }
|
||||||
?: throw Exception("Dhiraagu isn't taking BML card payments right now")
|
?: throw Exception("Dhiraagu isn't taking BML card payments right now")
|
||||||
|
|
||||||
val payment = api("payment", "create", paymentNonce, JSONObject()
|
val payment = api("payment", "create", paymentNonce, JSONObject()
|
||||||
.put("formId", FORM_ID)
|
.put("formId", formId)
|
||||||
.put("cartId", cartId)
|
.put("cartId", cartId)
|
||||||
.put("gatewayId", GATEWAY_BML)
|
.put("gatewayId", GATEWAY_BML)
|
||||||
.put("dhiraaguPayNumber", "")
|
.put("dhiraaguPayNumber", "")
|
||||||
.put("amount", String.format(Locale.US, "%.2f", amount.toDouble()))
|
.put("amount", money(amount))
|
||||||
.put("paymentMerchantId", bml.getString("merchantId"))
|
.put("paymentMerchantId", bml.getString("merchantId"))
|
||||||
.put("memberId", "").put("tokenize", "").put("paymentType", "")
|
.put("memberId", "").put("tokenize", "").put("paymentType", "")
|
||||||
.put("recurringFrequency", "").put("bmlTokenId", ""))
|
.put("recurringFrequency", "").put("bmlTokenId", ""))
|
||||||
@@ -71,13 +116,22 @@ class DhiraaguReloadClient {
|
|||||||
?: 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))
|
||||||
|
|
||||||
|
/** The `val` list of a `setting` rule, e.g. `{"accountStatus":{"val":["F"]}}`. */
|
||||||
|
private fun JSONObject.blockedValues(rule: String): Set<String> {
|
||||||
|
val vals = optJSONObject(rule)?.optJSONArray("val") ?: return emptySet()
|
||||||
|
return (0 until vals.length()).map { vals.optString(it) }.toSet()
|
||||||
|
}
|
||||||
|
|
||||||
// ── HTTP ─────────────────────────────────────────────────────────────────
|
// ── HTTP ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/** Every page embeds a `var nonce = "…"` that its API calls send as the `nonce` header. */
|
/** Every page embeds a `var nonce = "…"` that its API calls send as the `nonce` header. */
|
||||||
private fun pageNonce(url: String): String =
|
private fun pageNonce(url: String): String =
|
||||||
NONCE.find(get(url))?.groupValues?.get(1) ?: throw Exception("Dhiraagu page didn't load")
|
NONCE.find(page(url))?.groupValues?.get(1) ?: throw Exception("Dhiraagu page didn't load")
|
||||||
|
|
||||||
private fun get(url: String): String = client.newCall(
|
private fun page(url: String): String = client.newCall(
|
||||||
Request.Builder().url(url)
|
Request.Builder().url(url)
|
||||||
.header("User-Agent", UA)
|
.header("User-Agent", UA)
|
||||||
.header("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8")
|
.header("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8")
|
||||||
@@ -94,10 +148,16 @@ class DhiraaguReloadClient {
|
|||||||
call(sub, act, nonce, body).getJSONArray("resp")
|
call(sub, act, nonce, body).getJSONArray("resp")
|
||||||
|
|
||||||
/** POSTs to `sdk-dhr-webapi.ashx`; throws unless `respStatus` is OK. */
|
/** POSTs to `sdk-dhr-webapi.ashx`; throws unless `respStatus` is OK. */
|
||||||
private fun call(sub: String, act: String, nonce: String, body: JSONObject): JSONObject {
|
private fun call(sub: String, act: String, nonce: String, body: JSONObject): JSONObject =
|
||||||
|
send(sub, act, nonce, body.toString().toRequestBody(JSON))
|
||||||
|
|
||||||
|
/** GETs from `sdk-dhr-webapi.ashx` (the settings calls); throws unless `respStatus` is OK. */
|
||||||
|
private fun get(sub: String, act: String, nonce: String): JSONObject = send(sub, act, nonce, null)
|
||||||
|
|
||||||
|
private fun send(sub: String, act: String, nonce: String, body: okhttp3.RequestBody?): JSONObject {
|
||||||
val text = client.newCall(
|
val text = client.newCall(
|
||||||
Request.Builder().url("$API?website_id=$WEBSITE_ID&sub=$sub&act=$act")
|
Request.Builder().url("$API?website_id=$WEBSITE_ID&sub=$sub&act=$act")
|
||||||
.post(body.toString().toRequestBody(JSON))
|
.apply { if (body != null) post(body) }
|
||||||
.header("User-Agent", UA)
|
.header("User-Agent", UA)
|
||||||
.header("Accept", "application/json, text/javascript, */*; q=0.01")
|
.header("Accept", "application/json, text/javascript, */*; q=0.01")
|
||||||
.header("X-Requested-With", "XMLHttpRequest")
|
.header("X-Requested-With", "XMLHttpRequest")
|
||||||
@@ -112,7 +172,7 @@ class DhiraaguReloadClient {
|
|||||||
throw Exception("Unexpected response from Dhiraagu")
|
throw Exception("Unexpected response from Dhiraagu")
|
||||||
}
|
}
|
||||||
if (obj.optString("respStatus") != "OK") {
|
if (obj.optString("respStatus") != "OK") {
|
||||||
throw Exception(obj.optString("respMsg").ifBlank { obj.optString("resp") }.ifBlank { "Dhiraagu refused the reload" })
|
throw Exception(obj.optString("respMsg").ifBlank { obj.optString("resp") }.ifBlank { "Dhiraagu refused the payment" })
|
||||||
}
|
}
|
||||||
return obj
|
return obj
|
||||||
}
|
}
|
||||||
@@ -126,14 +186,20 @@ class DhiraaguReloadClient {
|
|||||||
private val NONCE = Regex("""var nonce = "([^"]+)"""")
|
private val NONCE = Regex("""var nonce = "([^"]+)"""")
|
||||||
private val TXN_URL = Regex("""transaction\.merchants\.bankofmaldives\.com\.mv/([0-9a-fA-F]{24})""")
|
private val TXN_URL = Regex("""transaction\.merchants\.bankofmaldives\.com\.mv/([0-9a-fA-F]{24})""")
|
||||||
|
|
||||||
|
/** Easy Pay's (bill payment) form id across cart / merchant / payment. */
|
||||||
|
private const val FORM_BILL = 1
|
||||||
/** Easy TopUp's form id across cart / merchant / payment. */
|
/** Easy TopUp's form id across cart / merchant / payment. */
|
||||||
private const val FORM_ID = 2
|
private const val FORM_RELOAD = 2
|
||||||
/** Bank of Maldives in `merchant&act=form` (1 = BML, 2 = MIB, 3 = DhiraaguPay). */
|
/** Bank of Maldives in `merchant&act=form` (1 = BML, 2 = MIB, 3 = DhiraaguPay). */
|
||||||
private const val GATEWAY_BML = 1
|
private const val GATEWAY_BML = 1
|
||||||
private const val GST_RATE = 0.08
|
private const val GST_RATE = 0.08
|
||||||
|
|
||||||
|
/** Easy Pay's `billType`s; the lookup's `type` says which applies. */
|
||||||
|
private const val BILL_PAYMENT = "BillPayment"
|
||||||
|
private const val BILL_WRITE_OFF = "writeOffPayments"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The GST inside a GST-inclusive [amount], as the page works it out:
|
* The GST inside a GST-inclusive reload [amount], as the page works it out:
|
||||||
* `amount × rate / (1 + rate)`, to 2 places. The number is credited `amount − gst`.
|
* `amount × rate / (1 + rate)`, to 2 places. The number is credited `amount − gst`.
|
||||||
*/
|
*/
|
||||||
fun gstOf(amount: Int): BigDecimal {
|
fun gstOf(amount: Int): BigDecimal {
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
package sh.sar.basedbank.api.ooredoo
|
||||||
|
|
||||||
|
import okhttp3.MediaType.Companion.toMediaType
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import okhttp3.Request
|
||||||
|
import okhttp3.RequestBody.Companion.toRequestBody
|
||||||
|
import org.json.JSONObject
|
||||||
|
import sh.sar.basedbank.api.models.BankServerException
|
||||||
|
import java.math.BigDecimal
|
||||||
|
import java.math.RoundingMode
|
||||||
|
import java.util.Locale
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
class OoredooPaymentClient {
|
||||||
|
|
||||||
|
private val client = OkHttpClient.Builder()
|
||||||
|
.connectTimeout(30, TimeUnit.SECONDS)
|
||||||
|
.readTimeout(30, TimeUnit.SECONDS)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates the recharge order for [number] (7 digits) and the BML transaction paying for it.
|
||||||
|
* [amount] is what the number is credited, in whole MVR; the card pays [charged], which is
|
||||||
|
* 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 =
|
||||||
|
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", money(charged))
|
||||||
|
.put("amountWithoutGst", amountWithoutGst)
|
||||||
|
.put("receiverMsisdn", msisdn)
|
||||||
|
.put("transType", transType)
|
||||||
|
.put("serviceType", serviceType)
|
||||||
|
.put("serviceTypeDisplayName", "Mobile")
|
||||||
|
val text = client.newCall(
|
||||||
|
Request.Builder().url("$BASE/ooredoo-prod/PaymentGateway/bml")
|
||||||
|
.post(body.toString().toRequestBody(JSON))
|
||||||
|
.header("User-Agent", UA)
|
||||||
|
.header("Accept", "application/json")
|
||||||
|
.header("Origin", BASE)
|
||||||
|
.build()
|
||||||
|
).execute().use { r ->
|
||||||
|
if (r.code in 500..599) throw BankServerException("Ooredoo")
|
||||||
|
r.body?.string().orEmpty()
|
||||||
|
}
|
||||||
|
val obj = try { JSONObject(text) } catch (_: Exception) {
|
||||||
|
throw Exception("Unexpected response from Ooredoo")
|
||||||
|
}
|
||||||
|
if (obj.optString("status") != "OK" || obj.optString("code") != "2000") {
|
||||||
|
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"
|
||||||
|
private val JSON = "application/json".toMediaType()
|
||||||
|
private val TXN_URL = Regex("""transaction\.merchants\.bankofmaldives\.com\.mv/([0-9a-fA-F]{24})""")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -52,6 +52,7 @@ import sh.sar.basedbank.databinding.ItemPickerSectionHeaderBinding
|
|||||||
import sh.sar.basedbank.databinding.ItemTransferTypeBinding
|
import sh.sar.basedbank.databinding.ItemTransferTypeBinding
|
||||||
import sh.sar.basedbank.ui.home.transfer.BmlTransferHandler
|
import sh.sar.basedbank.ui.home.transfer.BmlTransferHandler
|
||||||
import sh.sar.basedbank.ui.home.transfer.BmlVerifiedCards
|
import sh.sar.basedbank.ui.home.transfer.BmlVerifiedCards
|
||||||
|
import sh.sar.basedbank.ui.home.transfer.CardPayoutService
|
||||||
import sh.sar.basedbank.ui.home.transfer.CardPayoutTransferHandler
|
import sh.sar.basedbank.ui.home.transfer.CardPayoutTransferHandler
|
||||||
import sh.sar.basedbank.ui.home.transfer.CarrierLookup
|
import sh.sar.basedbank.ui.home.transfer.CarrierLookup
|
||||||
import sh.sar.basedbank.ui.home.transfer.FahipayService
|
import sh.sar.basedbank.ui.home.transfer.FahipayService
|
||||||
@@ -407,7 +408,7 @@ class TransferFragment : Fragment() {
|
|||||||
return@setFragmentResultListener
|
return@setFragmentResultListener
|
||||||
}
|
}
|
||||||
val label = bundle.getString(ContactPickerSheetFragment.KEY_LABEL) ?: ""
|
val label = bundle.getString(ContactPickerSheetFragment.KEY_LABEL) ?: ""
|
||||||
if (applyFahipayContact(accountNumber, bundle.getString(ContactPickerSheetFragment.KEY_CATEGORY), label)) {
|
if (applyServiceContact(accountNumber, bundle.getString(ContactPickerSheetFragment.KEY_CATEGORY), label)) {
|
||||||
return@setFragmentResultListener
|
return@setFragmentResultListener
|
||||||
}
|
}
|
||||||
val subtitle = bundle.getString(ContactPickerSheetFragment.KEY_SUBTITLE) ?: accountNumber
|
val subtitle = bundle.getString(ContactPickerSheetFragment.KEY_SUBTITLE) ?: accountNumber
|
||||||
@@ -453,7 +454,7 @@ class TransferFragment : Fragment() {
|
|||||||
// Pre-select contact if navigated from contacts page or QR scan
|
// Pre-select contact if navigated from contacts page or QR scan
|
||||||
arguments?.getString(ARG_ACCOUNT)?.let { account ->
|
arguments?.getString(ARG_ACCOUNT)?.let { account ->
|
||||||
val name = arguments?.getString(ARG_NAME) ?: account
|
val name = arguments?.getString(ARG_NAME) ?: account
|
||||||
if (applyFahipayContact(account, arguments?.getString(ARG_CONTACT_CATEGORY), name)) return@let
|
if (applyServiceContact(account, arguments?.getString(ARG_CONTACT_CATEGORY), name)) return@let
|
||||||
prefillToDirectly(
|
prefillToDirectly(
|
||||||
accountNumber = account,
|
accountNumber = account,
|
||||||
displayName = name,
|
displayName = name,
|
||||||
@@ -1125,7 +1126,7 @@ class TransferFragment : Fragment() {
|
|||||||
|
|
||||||
binding.etTo.setOnItemClickListener { _, _, position, _ ->
|
binding.etTo.setOnItemClickListener { _, _, position, _ ->
|
||||||
val contact = adapter.getContact(position) ?: return@setOnItemClickListener
|
val contact = adapter.getContact(position) ?: return@setOnItemClickListener
|
||||||
if (applyFahipayContact(contact.benefAccount, contact.benefCategoryId, contact.benefNickName)) {
|
if (applyServiceContact(contact.benefAccount, contact.benefCategoryId, contact.benefNickName)) {
|
||||||
return@setOnItemClickListener
|
return@setOnItemClickListener
|
||||||
}
|
}
|
||||||
prefillToDirectly(
|
prefillToDirectly(
|
||||||
@@ -1141,15 +1142,27 @@ class TransferFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A saved Fahipay favourite: its list ([categoryId]) already says which service pays it, so
|
* A saved Fahipay favourite, or a recent paid with a Fahipay or card service: its category
|
||||||
* that service is applied as the only transfer type, with no carrier lookup. That switches
|
* ([categoryId]) already says which service pays it, so that service is applied as the only
|
||||||
* the source to the Fahipay wallet and brings in the service's amount rules, the same as a
|
* transfer type, with no carrier lookup. That switches the source to the Fahipay wallet or
|
||||||
* searched number. Returns false, doing nothing, when [categoryId] isn't a Fahipay list.
|
* the default card and brings in the service's amount rules, the same as a searched number.
|
||||||
|
* Returns false, doing nothing, when [categoryId] isn't one of those.
|
||||||
*/
|
*/
|
||||||
private fun applyFahipayContact(number: String, categoryId: String?, name: String): Boolean {
|
private fun applyServiceContact(number: String, categoryId: String?, name: String): Boolean {
|
||||||
val service = FahipayService.fromContactCategory(categoryId) ?: return false
|
val ownerName = name.takeIf { it.isNotBlank() && it != number }
|
||||||
|
val type = FahipayService.fromContactCategory(categoryId)?.let { TransferType.Fahipay(it, ownerName) }
|
||||||
|
?: CardPayoutService.fromContactCategory(categoryId)?.let { service ->
|
||||||
|
// The cards that could pay it then may not be payable now
|
||||||
|
val cards = cardPayoutHandler().payableCards()
|
||||||
|
if (cards.isEmpty()) {
|
||||||
|
Toast.makeText(requireContext(), R.string.bml_card_pay_no_verified, Toast.LENGTH_LONG).show()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
TransferType.Card(service, ownerName, cards)
|
||||||
|
}
|
||||||
|
?: return false
|
||||||
clearRecipient()
|
clearRecipient()
|
||||||
offerTransferTypes(number, listOf(TransferType.Fahipay(service, name.takeIf { it.isNotBlank() && it != number })))
|
offerTransferTypes(number, listOf(type))
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -550,7 +550,14 @@ class BmlTransferHandler(
|
|||||||
fragment.viewLifecycleOwner.lifecycleScope.launch {
|
fragment.viewLifecycleOwner.lifecycleScope.launch {
|
||||||
val result = withContext(Dispatchers.IO) {
|
val result = withContext(Dispatchers.IO) {
|
||||||
runCatching {
|
runCatching {
|
||||||
BmlMerchantCardPayClient().pay(page, card) { _ -> Totp.generate(otpSeed) }
|
BmlMerchantCardPayClient().pay(page, card) { retry ->
|
||||||
|
// A code about to roll over can expire before the ACS checks it, and a
|
||||||
|
// retry in the same window would resend the rejected code: wait for the
|
||||||
|
// next window in both cases. Runs on IO.
|
||||||
|
val left = TOTP_WINDOW_MS - System.currentTimeMillis() % TOTP_WINDOW_MS
|
||||||
|
if (retry || left < TOTP_MIN_LEFT_MS) Thread.sleep(left + 500)
|
||||||
|
Totp.generate(otpSeed)
|
||||||
|
}
|
||||||
}.getOrElse {
|
}.getOrElse {
|
||||||
BmlMerchantCardPayClient.Result.Failure(it.message ?: "Payment failed")
|
BmlMerchantCardPayClient.Result.Failure(it.message ?: "Payment failed")
|
||||||
}
|
}
|
||||||
@@ -957,4 +964,11 @@ class BmlTransferHandler(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun isCard(account: BankAccount) = BmlVerifiedCards.isCard(account)
|
private fun isCard(account: BankAccount) = BmlVerifiedCards.isCard(account)
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
/** The BML token's TOTP window. */
|
||||||
|
const val TOTP_WINDOW_MS = 30_000L
|
||||||
|
/** Don't send a card payment's 3-D Secure code with less than this left in its window. */
|
||||||
|
const val TOTP_MIN_LEFT_MS = 5_000L
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,9 @@ import kotlinx.coroutines.withContext
|
|||||||
import sh.sar.basedbank.R
|
import sh.sar.basedbank.R
|
||||||
import sh.sar.basedbank.api.bml.BmlMerchantTxnClient
|
import sh.sar.basedbank.api.bml.BmlMerchantTxnClient
|
||||||
import sh.sar.basedbank.api.dhiraagu.DhiraaguClient
|
import sh.sar.basedbank.api.dhiraagu.DhiraaguClient
|
||||||
import sh.sar.basedbank.api.dhiraagu.DhiraaguReloadClient
|
import sh.sar.basedbank.api.dhiraagu.DhiraaguPaymentClient
|
||||||
|
import sh.sar.basedbank.api.fahipay.OoredooClient
|
||||||
|
import sh.sar.basedbank.api.ooredoo.OoredooPaymentClient
|
||||||
import sh.sar.basedbank.databinding.FragmentTransferBinding
|
import sh.sar.basedbank.databinding.FragmentTransferBinding
|
||||||
import sh.sar.basedbank.ui.home.HomeViewModel
|
import sh.sar.basedbank.ui.home.HomeViewModel
|
||||||
import sh.sar.basedbank.ui.home.TransferFragment
|
import sh.sar.basedbank.ui.home.TransferFragment
|
||||||
@@ -20,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.
|
||||||
*
|
*
|
||||||
* Only Dhiraagu reload so far. Add the others as constants once their send path lands; the
|
* Add new services as constants; the exhaustive `when`s over this enum point at every site that
|
||||||
* exhaustive `when`s 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,
|
||||||
@@ -31,13 +33,37 @@ enum class CardPayoutService(
|
|||||||
override val maxAmount: Int?,
|
override val maxAmount: Int?,
|
||||||
override val decimalsAllowed: Boolean,
|
override val decimalsAllowed: Boolean,
|
||||||
override val gstPercent: Int?,
|
override val gstPercent: Int?,
|
||||||
|
/**
|
||||||
|
* Tags the recents paid with this service (`RecentPick.contactCategory`), so picking one
|
||||||
|
* again pays the same way. Not a real contact list: card payouts have no favourites.
|
||||||
|
*/
|
||||||
|
val contactCategory: String,
|
||||||
|
override val gstAdded: Boolean = false,
|
||||||
) : PayoutService {
|
) : PayoutService {
|
||||||
DHIRAAGU_RELOAD("Dhiraagu Reload", "Dhiraagu · Reload", R.drawable.dhiraagu_logo,
|
DHIRAAGU_RELOAD("Dhiraagu Reload", "Dhiraagu · Reload", R.drawable.dhiraagu_logo,
|
||||||
minAmount = 20, maxAmount = 1000, decimalsAllowed = false, gstPercent = 8) {
|
minAmount = 20, maxAmount = 1000, decimalsAllowed = false, gstPercent = 8,
|
||||||
|
contactCategory = "CARD_DHIRAAGU_RELOAD") {
|
||||||
// Dhiraagu rounds the GST to 2 places and credits the rest
|
// Dhiraagu rounds the GST to 2 places and credits the rest
|
||||||
override fun creditedAfterGst(amount: BigDecimal): BigDecimal =
|
override fun creditedAfterGst(amount: BigDecimal): BigDecimal =
|
||||||
amount - DhiraaguReloadClient.gstOf(amount.setScale(0, RoundingMode.DOWN).toInt())
|
amount - DhiraaguPaymentClient.gstOf(amount.setScale(0, RoundingMode.DOWN).toInt())
|
||||||
};
|
},
|
||||||
|
// Easy Pay sets no limits of its own: any amount with up to 2 decimals, no GST
|
||||||
|
DHIRAAGU_BILL("Dhiraagu Bill Pay", "Dhiraagu · Bill Pay", R.drawable.dhiraagu_logo,
|
||||||
|
minAmount = 1, maxAmount = null, decimalsAllowed = true, gstPercent = null,
|
||||||
|
contactCategory = "CARD_DHIRAAGU_BILL"),
|
||||||
|
// 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,
|
||||||
|
contactCategory = "CARD_RAASTAS", gstAdded = true),
|
||||||
|
OOREDOO_BILL("Ooredoo Bill Pay", "Ooredoo · Bill Pay", R.drawable.ooredoo_logo,
|
||||||
|
minAmount = 10, maxAmount = null, decimalsAllowed = true, gstPercent = null,
|
||||||
|
contactCategory = "CARD_OOREDOO_BILL");
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
/** The service a recent was paid with, from its [contactCategory], or null when it isn't one. */
|
||||||
|
fun fromContactCategory(categoryId: String?): CardPayoutService? =
|
||||||
|
entries.firstOrNull { it.contactCategory == categoryId }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -78,7 +104,17 @@ class CardPayoutTransferHandler(
|
|||||||
fun typesFor(result: CarrierLookup.Result, cards: Set<String>): List<TransferType.Card> {
|
fun typesFor(result: CarrierLookup.Result, cards: Set<String>): List<TransferType.Card> {
|
||||||
if (cards.isEmpty()) return emptyList()
|
if (cards.isEmpty()) return emptyList()
|
||||||
return buildList {
|
return buildList {
|
||||||
if (result.dhiraagu.type == DhiraaguClient.CustType.RELOAD) add(CardPayoutService.DHIRAAGU_RELOAD)
|
when (result.dhiraagu.type) {
|
||||||
|
DhiraaguClient.CustType.RELOAD -> add(CardPayoutService.DHIRAAGU_RELOAD)
|
||||||
|
DhiraaguClient.CustType.BILL_PAY -> add(CardPayoutService.DHIRAAGU_BILL)
|
||||||
|
DhiraaguClient.CustType.UNSUPPORTED -> {}
|
||||||
|
}
|
||||||
|
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) }
|
}.map { TransferType.Card(it, result.ownerName, cards) }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,7 +151,8 @@ class CardPayoutTransferHandler(
|
|||||||
displayName = displayName,
|
displayName = displayName,
|
||||||
subtitle = "${type.label} · $number",
|
subtitle = "${type.label} · $number",
|
||||||
colorHex = "#E4002B",
|
colorHex = "#E4002B",
|
||||||
imageHash = null
|
imageHash = null,
|
||||||
|
contactCategory = type.service.contactCategory
|
||||||
)
|
)
|
||||||
fragment.focusAmount()
|
fragment.focusAmount()
|
||||||
}
|
}
|
||||||
@@ -136,6 +173,7 @@ class CardPayoutTransferHandler(
|
|||||||
val number = viewModel.transferDraft.transferTypeNumber
|
val number = viewModel.transferDraft.transferTypeNumber
|
||||||
val amount = binding.etAmount.text?.toString()?.trim()?.toBigDecimalOrNull()
|
val amount = binding.etAmount.text?.toString()?.trim()?.toBigDecimalOrNull()
|
||||||
if (number.isBlank() || amount == null || amount.signum() <= 0 || amountProblem != null) return
|
if (number.isBlank() || amount == null || amount.signum() <= 0 || amountProblem != null) return
|
||||||
|
val charged = svc.chargedWithGst(amount)
|
||||||
|
|
||||||
// Creating the order takes a few round trips; show the payment's processing box meanwhile
|
// Creating the order takes a few round trips; show the payment's processing box meanwhile
|
||||||
val processing = fragment.showProcessingDialog(ctx.getString(R.string.transfer))
|
val processing = fragment.showProcessingDialog(ctx.getString(R.string.transfer))
|
||||||
@@ -144,7 +182,13 @@ class CardPayoutTransferHandler(
|
|||||||
runCatching {
|
runCatching {
|
||||||
val txnId = when (svc) {
|
val txnId = when (svc) {
|
||||||
CardPayoutService.DHIRAAGU_RELOAD ->
|
CardPayoutService.DHIRAAGU_RELOAD ->
|
||||||
DhiraaguReloadClient().createBmlTransaction(number, amount.intValueExact())
|
DhiraaguPaymentClient().createReloadTransaction(number, amount.intValueExact())
|
||||||
|
CardPayoutService.DHIRAAGU_BILL ->
|
||||||
|
DhiraaguPaymentClient().createBillPayTransaction(number, amount)
|
||||||
|
CardPayoutService.OOREDOO_RAASTAS ->
|
||||||
|
OoredooPaymentClient().createRaastasTransaction(number, amount.intValueExact(), charged)
|
||||||
|
CardPayoutService.OOREDOO_BILL ->
|
||||||
|
OoredooPaymentClient().createBillPayTransaction(number, amount)
|
||||||
}
|
}
|
||||||
BmlMerchantTxnClient().fetchPayPage(txnId)
|
BmlMerchantTxnClient().fetchPayPage(txnId)
|
||||||
}
|
}
|
||||||
@@ -155,8 +199,8 @@ class CardPayoutTransferHandler(
|
|||||||
when {
|
when {
|
||||||
!it.supportsCard ->
|
!it.supportsCard ->
|
||||||
Toast.makeText(ctx, R.string.transfer_bml_txn_lookup_failed, Toast.LENGTH_LONG).show()
|
Toast.makeText(ctx, R.string.transfer_bml_txn_lookup_failed, Toast.LENGTH_LONG).show()
|
||||||
// The carrier's order must be for exactly what was typed
|
// The carrier's order must be for exactly what was typed (plus GST, if added)
|
||||||
it.amount.toBigDecimal().compareTo(amount) != 0 ->
|
it.amount.toBigDecimal().compareTo(charged) != 0 ->
|
||||||
Toast.makeText(ctx, R.string.transfer_bml_txn_lookup_failed, Toast.LENGTH_LONG).show()
|
Toast.makeText(ctx, R.string.transfer_bml_txn_lookup_failed, Toast.LENGTH_LONG).show()
|
||||||
else -> bmlHandler().confirmCardMerchant(it, src)
|
else -> bmlHandler().confirmCardMerchant(it, src)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,17 +25,33 @@ interface PayoutService {
|
|||||||
val maxAmount: Int?
|
val maxAmount: Int?
|
||||||
/** Whether the amount may have a fractional part (up to 2 decimal places). */
|
/** Whether the amount may have a fractional part (up to 2 decimal places). */
|
||||||
val decimalsAllowed: Boolean
|
val decimalsAllowed: Boolean
|
||||||
/** GST the carrier takes out of the amount before crediting it, in percent, or null for none. */
|
/** GST the carrier charges, in percent, or null for none. [gstAdded] says how. */
|
||||||
val gstPercent: Int?
|
val gstPercent: Int?
|
||||||
|
/**
|
||||||
|
* False (the usual): GST comes out of the amount, so the number is credited less
|
||||||
|
* ([creditedAfterGst]). True: the number is credited the whole amount and GST is charged on
|
||||||
|
* top of it ([chargedWithGst]).
|
||||||
|
*/
|
||||||
|
val gstAdded: Boolean get() = false
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* What the number is credited for a GST-inclusive [amount]: amount / (1 + rate), rounded
|
* What the number is credited for a GST-inclusive [amount]: amount / (1 + rate), rounded
|
||||||
* down. Null when the service charges no GST.
|
* down. Null when the service charges no GST, or adds it on top.
|
||||||
*/
|
*/
|
||||||
fun creditedAfterGst(amount: BigDecimal): BigDecimal? {
|
fun creditedAfterGst(amount: BigDecimal): BigDecimal? {
|
||||||
val gst = gstPercent ?: return null
|
val gst = gstPercent ?: return null
|
||||||
|
if (gstAdded) return null
|
||||||
return amount.divide(BigDecimal.ONE + BigDecimal(gst).movePointLeft(2), 2, RoundingMode.DOWN)
|
return amount.divide(BigDecimal.ONE + BigDecimal(gst).movePointLeft(2), 2, RoundingMode.DOWN)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* What is paid for [amount]: amount + round2(amount × rate) when GST is added on top,
|
||||||
|
* otherwise [amount] itself.
|
||||||
|
*/
|
||||||
|
fun chargedWithGst(amount: BigDecimal): BigDecimal {
|
||||||
|
val gst = gstPercent?.takeIf { gstAdded } ?: return amount
|
||||||
|
return amount + (amount * BigDecimal(gst).movePointLeft(2)).setScale(2, RoundingMode.HALF_UP)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -81,11 +97,18 @@ class PayoutAmountField(
|
|||||||
binding.tilAmount.helperText = if (problem == null) gstNote(svc) else null
|
binding.tilAmount.helperText = if (problem == null) gstNote(svc) else null
|
||||||
}
|
}
|
||||||
|
|
||||||
/** What the recipient is credited once GST comes out, for services that charge it. */
|
/**
|
||||||
|
* For services that charge GST: what the recipient is credited once it comes out, or what
|
||||||
|
* is paid once it's added on top.
|
||||||
|
*/
|
||||||
fun gstNote(svc: PayoutService): String? {
|
fun gstNote(svc: PayoutService): String? {
|
||||||
val gst = svc.gstPercent ?: return null
|
val gst = svc.gstPercent ?: return null
|
||||||
val ctx = context()
|
val ctx = context()
|
||||||
val amount = binding.etAmount.text?.toString()?.trim()?.toBigDecimalOrNull()
|
val amount = binding.etAmount.text?.toString()?.trim()?.toBigDecimalOrNull()
|
||||||
|
if (svc.gstAdded) {
|
||||||
|
if (amount == null || amount.signum() <= 0) return ctx.getString(R.string.transfer_gst_added_hint, gst)
|
||||||
|
return ctx.getString(R.string.transfer_gst_added_pay, "%,.2f".format(svc.chargedWithGst(amount)), gst)
|
||||||
|
}
|
||||||
if (amount == null || amount.signum() <= 0) return ctx.getString(R.string.transfer_fahipay_gst_hint, gst)
|
if (amount == null || amount.signum() <= 0) return ctx.getString(R.string.transfer_fahipay_gst_hint, gst)
|
||||||
val credited = svc.creditedAfterGst(amount) ?: return null
|
val credited = svc.creditedAfterGst(amount) ?: return null
|
||||||
return ctx.getString(R.string.transfer_fahipay_gst_receive, "%,.2f".format(credited), gst)
|
return ctx.getString(R.string.transfer_fahipay_gst_receive, "%,.2f".format(credited), gst)
|
||||||
|
|||||||
@@ -1,149 +1,431 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<resources>
|
<resources>
|
||||||
<string name="app_name">ތިޖޫރީ</string>
|
<string name="app_name">ތިޖޫރީ</string>
|
||||||
|
|
||||||
<!-- Onboarding -->
|
<!-- Onboarding -->
|
||||||
<string name="onboarding_supported_services">ހިދުމަތްތައް</string>
|
<string name="onboarding_supported_services">ޚިދުމަތްތައް</string>
|
||||||
<string name="select_language">ބަސް ހިޔާލު ކުރޭ</string>
|
<string name="select_language">ބަސް އިޚްތިޔާރުކުރޭ</string>
|
||||||
<string name="onboarding_title_1">ތިޔަ ބޭންކްތައް، އެއް އެޕެއްގައި</string>
|
<string name="onboarding_title_1">ހުރިހާ ބޭންކެއް އެއް އެޕަކުން</string>
|
||||||
<string name="onboarding_desc_1">ތިޖޫރީ ގެ ސަބަބުން ތިޔަ ދިވެހި ބޭންކު އެކައުންޓްތައް، ހަމައެއް ތަނަކުން ބެލޭ. ބެލެންސް ބެލޭ، ތަފާތު ތަންތަން ބެލޭ — ތަފާތު އެޕްތަކަށް ބަދަލު ނުވެ.</string>
|
<string name="onboarding_desc_1">ރާއްޖޭގެ ބޭންކުތަކުގައިވާ ތިޔަބޭފުޅާގެ އެކައުންޓްތައް އެއްތަނަކުން ބަލާލެވޭ. އެކި އެޕްތަކަށް ވަންނަންނުޖެހި ބެލެންސް ބެލުމާއި އެކައުންޓްތަކުގެ މަޢުލޫމާތު ފަސޭހަކަމާއެކު ބަލާލެވޭ.</string>
|
||||||
<string name="onboarding_title_2">އިތުރު ބޭންކްތައް ހިމެނެނީ</string>
|
<string name="onboarding_title_2">އިތުރު ބޭންކުތައް ވަރަށް އަވަހަށް</string>
|
||||||
<string name="onboarding_desc_2">އިތުރު ބޭންކްތަކަށް ސަޕޯޓް ލިބޭ ގޮތަށް ތައްޔާރުވަމުން ދަނީ. ދިވެހިރާއްޖޭގެ ބޭންކްތަކަށް ސަޕޯޓް ފަހި ވަމުން ދިޔަ ވަރަކަށް ހިމަނެމުން ދޭ.</string>
|
<string name="onboarding_desc_2">އިތުރު ބޭންކުތަކުގެ ޚިދުމަތްތައް މި އެޕަށް އިތުރުކުރުމުގެ މަސައްކަތް ދަނީ ކުރެވެމުންނެވެ.</string>
|
||||||
<string name="onboarding_title_3">ފެށޭ ގޮތަށް ތައްޔާރު</string>
|
<string name="onboarding_title_3">ސަމާލުކަމަށް</string>
|
||||||
<string name="onboarding_desc_3">ތިޔަ ބޭންކު ކްރެޑެންޝަލް ޖެހި، ތިޔަ އެކައުންޓްތައް ބަލާ. ތިޔަ ޑޭޓާ ހިފެހެއްޓޭ ތަނަކީ ހަމައެކަނި ތިޔަ ފޯނު.\n\nDhiraagu އާއި Ooredoo ގެ API ބޭނުންކޮށްގެން ފޯން ނަންބަރުގެ ތަފްސީލު ބެލޭ.\n\nމި އެޕް ތިޔައާ ބެހޭ ތަފްސީލެއް ނެހެދޭ، ޑިވެލޮޕަރަށް ވެސް ނުފޮނުވާ. ހުރިހާ ޑޭޓާ ހިފެހެއްޓޭ ތަނަކީ ހަމައެކަނި ތިޔަ ފޯނު.</string>
|
<string name="onboarding_desc_3">ތިޖޫރީ އަކީ އެއްވެސް ބޭންކަކާ ނުވަތަ މާލީ އިދާރާއަކާ ގުޅުމެއްނެތި އަމިއްލައަށް އުފައްދާފައިވާ އެޕެކެވެ.\n\nމި އެޕް މަސައްކަތްކުރަނީ ތިޔަބޭފުޅާގެ އިންޓަނެޓް ބޭންކިންގ މަޢުލޫމާތު ބޭނުންކޮށްގެން ސީދާ ބޭންކުގެ އޭޕީއައިތަކާ ގުޅިގެންނެވެ. ބޭންކުތަކުގެ ސިސްޓަމްތަކަށް އަންނަ ބަދަލަކުން އެޕްގެ މަސައްކަތަށް ބުރޫއަރާފާނެއެވެ.\n\nފޯނު ނަންބަރުގެ މަޢުލޫމާތު ހޯދުމަށް ދިރާގާއި އުރީދޫގެ އޭޕީއައި ބޭނުންކުރެވެއެވެ.\n\nމި އެޕުން ތިޔަބޭފުޅާގެ އެއްވެސް އަމިއްލަ މަޢުލޫމާތެއް ނުވަތަ އެޕް ބޭނުންކުރާ ގޮތުގެ މަޢުލޫމާތެއް އެއްނުކުރާނެއެވެ. ހުރިހާ މަޢުލޫމާތެއް ރައްކާކުރެވޭނީ ހަމައެކަނި ތިޔަބޭފުޅާގެ ފޯނުގައެވެ.\n\nކުރިއަށް ވަޑައިގަތުމުން އަންނަނިވި ކަންކަމަށް އެއްބަސްވެވޭނެއެވެ:\n\n• ޚިދުމަތަށް މެދުކެނޑުމެއް ނުވަތަ މައްސަލައެއް ދިމާވެދާނެކަން\n• ބޭންކުން ތިޔަބޭފުޅާގެ އެކައުންޓަށް ފިޔަވަޅެއް އަޅާފާނެކަން\n• އެޕް ބޭނުންކުރުމުން ދިމާވެދާނެ އެއްވެސް ގެއްލުމަކަށް ޑިވެލޮޕަރު ޒިންމާނުވާނެކަން\n• މި އެޕް ބޭނުންކުރަނީ އަމިއްލަ ޒިންމާގައިކަން</string>
|
||||||
<string name="coming_soon">ފަހުން ލިބޭ</string>
|
<string name="coming_soon">ވަރަށް އަވަހަށް އަންނީ</string>
|
||||||
<string name="next">ދެން</string>
|
<string name="next">ކުރިއަށް</string>
|
||||||
<string name="get_started">ފަށާ</string>
|
<string name="get_started">ފަށަމާ</string>
|
||||||
|
|
||||||
<!-- Login -->
|
<!-- Login -->
|
||||||
<string name="select_bank">ބޭންކެއް ހިޔާރު ކުރޭ</string>
|
<string name="select_bank">ބޭންކު އިޚްތިޔާރުކުރޭ</string>
|
||||||
<string name="select_bank_desc">ލޮގިން ވާ ބޭންކު ހިޔާރު ކުރޭ.</string>
|
<string name="select_bank_desc">ލޮގިންވާން ބޭނުންވާ ބޭންކު ނަގާ</string>
|
||||||
<string name="mib_name">Maldives Islamic Bank</string>
|
<string name="mib_name">މޯލްޑިވްސް އިސްލާމިކް ބޭންކް</string>
|
||||||
<string name="mib_desc">ފައިސާނެޓް މޯބައިލް ބޭންކިން</string>
|
<string name="mib_desc">ފައިސާނެޓް މޮބައިލް ބޭންކިންގ</string>
|
||||||
<string name="bml_name">Bank of Maldives</string>
|
<string name="bml_name">ބޭންކް އޮފް މޯލްޑިވްސް</string>
|
||||||
<string name="bml_desc">BML އިންޓަނެޓް ބޭންކިން</string>
|
<string name="bml_desc">ބީއެމްއެލް އިންޓަނެޓް ބޭންކިންގ</string>
|
||||||
<string name="sign_in">ލޮގިން ވޭ</string>
|
<string name="fahipay_name">ފަހިޕޭ</string>
|
||||||
<string name="sign_in_desc">Maldives Islamic Bank ގެ ކްރެޑެންޝަލް ދިވޭ.</string>
|
<string name="fahipay_desc">ޑިޖިޓަލް ވޮލެޓް</string>
|
||||||
<string name="username">ޔޫޒަރ ނޭމް</string>
|
<string name="fahipay_sign_in_desc">އައިޑީ ކާޑު ނަންބަރާއި ޕާސްވޯޑް ޖަހާ</string>
|
||||||
|
<string name="fahipay_id_card">އައިޑީ ކާޑު ނަންބަރު</string>
|
||||||
|
<string name="fahipay_totp_code">އޮތެންޓިކޭޓަރ ކޯޑު (6 ނަންބަރު)</string>
|
||||||
|
<string name="fahipay_totp_hint">އޮތެންޓިކޭޓަރ އެޕުން ލިބޭ ކޯޑު ޖަހާ</string>
|
||||||
|
<string name="fahipay_verify">ޔަގީން</string>
|
||||||
|
<string name="ooredoo_name">އޯރިޑޫ އެމް-ފައިސާ</string>
|
||||||
|
<string name="ooredoo_desc">މޮބައިލް ވޮލެޓް</string>
|
||||||
|
<string name="ooredoo_sign_in_desc">މޮބައިލް ނަންބަރާއި 4 އަދަދުގެ އެމް-ޕިން ޖަހާ</string>
|
||||||
|
<string name="ooredoo_phone">މޮބައިލް ނަންބަރު</string>
|
||||||
|
<string name="ooredoo_pin">އެމް-ޕިން</string>
|
||||||
|
<string name="sign_in">ލޮގިން</string>
|
||||||
|
<string name="sign_in_desc">އެމްއައިބީ އިންޓަނެޓް ބޭންކިންގ މަޢުލޫމާތު ޖަހާ</string>
|
||||||
|
<string name="bml_sign_in_desc">ބީއެމްއެލް އިންޓަނެޓް ބޭންކިންގ މަޢުލޫމާތު ޖަހާ</string>
|
||||||
|
<string name="username">ޔޫސަރނޭމް</string>
|
||||||
<string name="password">ޕާސްވޯޑް</string>
|
<string name="password">ޕާސްވޯޑް</string>
|
||||||
<string name="otp_seed">OTP ސީޑް (TOTP ސިއްރު)</string>
|
<string name="otp_seed">އޯޓީޕީ ސީޑް (ޓީއޯޓީޕީ ސީކްރެޓް)</string>
|
||||||
<string name="otp_seed_hint">ތިޔަ އޮތެންޓިކޭޓަ ދިން Base32 ސިއްރު</string>
|
<string name="otp_seed_hint">އޮތެންޓިކޭޓަރ ސެޓަޕުން ލިބުނު ސީކްރެޓް ކޯޑު</string>
|
||||||
|
<string name="scan_otp_qr">އޯޓީޕީ ކިއުއާރު ސުކޭންކޮއްލާ</string>
|
||||||
<string name="login">ލޮގިން</string>
|
<string name="login">ލޮގިން</string>
|
||||||
|
|
||||||
<!-- Lock screen -->
|
<!-- Lock screen -->
|
||||||
<string name="unlock_app">ތިޖޫރީ ހުޅުވާ</string>
|
<string name="unlock_app">ތިޖޫރީ ހުޅުވާ</string>
|
||||||
<string name="unlock_pin_subtitle">PIN ޖަހާ</string>
|
<string name="unlock_pin_subtitle">ޕިން ނަންބަރު ޖަހާ</string>
|
||||||
<string name="unlock_pattern_subtitle">ހުޅުވާ ޕެޓަން ކަހާ</string>
|
<string name="unlock_pattern_subtitle">ޕެޓަރން ކުރަހާ</string>
|
||||||
<string name="use_biometrics">ބަޔޮމެޓްރިކް ބޭނުން ކުރޭ</string>
|
<string name="use_biometrics">ބަޔޯމެޓްރިކްސް ބޭނުންކުރޭ</string>
|
||||||
<string name="biometric_prompt_subtitle">ފިންގަޕްރިންޓް ނުވަތަ މޫނު ބޭނުން ކޮށްގެން ހުޅުވާ</string>
|
<string name="biometric_prompt_subtitle">އިނގިލީގެ ނިޝާން ނުވަތަ ފޭސް އައިޑީ ބޭނުންކުރޭ</string>
|
||||||
<string name="biometric_negative_btn">PIN / ޕެޓަން ބޭނުން ކުރޭ</string>
|
<string name="biometric_negative_btn">ޕިން / ޕެޓަން ބޭނުންކުރޭ</string>
|
||||||
<string name="unlock_failed">ދިމައެއް ނުވި — އަލުން ކަނޑޭ</string>
|
<string name="unlock_failed">ނުބައި - އަލުން މަސައްކަތްކޮއްލާ</string>
|
||||||
<string name="unlock_attempts_remaining">ދިމައެއް ނުވި — %d ފަހަރު ބާކީ</string>
|
<string name="unlock_attempts_remaining">ނުބައި - ބާކީ %d ފުރުޞަތު</string>
|
||||||
<string name="unlock_locked_out">ވަރަށް ގިނަ ފަހަރު ނުކުރިހުރި. %d ސިކުންތު ފަހުން ލޮކް ހިލޭ.</string>
|
<string name="unlock_locked_out">ގިނަފަހަރު ނުބައިކޮށް ޖެހުމުން ބްލޮކްވެއްޖެ. %d ސިކުންތު ފަހުން އަލުން މަސައްކަތްކޮއްލާ.</string>
|
||||||
|
|
||||||
<!-- Security setup -->
|
<!-- Security setup -->
|
||||||
<string name="security_setup">އެޕް ރައްކާތެރި ކުރޭ</string>
|
<string name="security_setup">އެޕް ރައްކާތެރިކުރޭ</string>
|
||||||
<string name="security_setup_desc">ތިޖޫރީ ހުޅުވަން ބޭނުންވާ ގޮތެއް ހިޔާރު ކުރޭ.</string>
|
<string name="security_setup_desc">އެޕް ތަޅުލުމަށް ބޭނުންކުރާނެ ގޮތެއް އިޚްތިޔާރުކުރޭ.</string>
|
||||||
<string name="method_pin">PIN ކޯޑް</string>
|
<string name="security_already_configured">އެޕް ލޮކް ކުރެވިފައި</string>
|
||||||
<string name="method_pin_desc">4–8 ރިޔަލެއްގެ ނަންބަރު PIN</string>
|
<string name="security_already_configured_desc">އެޕް ތަޅުލާނެ ގޮތް ވަނީ ސެޓްކުރެވިފައި.</string>
|
||||||
<string name="method_pattern">ޕެޓަން ކަހާ</string>
|
|
||||||
<string name="method_pattern_desc">4 ނުވަތަ އެއަށްވުރެ ގިނަ ތިކި ގުޅުވާ</string>
|
<string name="method_pin">ޕިން ކޯޑު</string>
|
||||||
<string name="enter_pin">PIN ޖަހާ</string>
|
<string name="method_pin_desc">4 އާއި 8 އަދަދާ ދެމެދުގެ ނަންބަރެއް</string>
|
||||||
<string name="confirm_pin">PIN ކަށަވަރު ކުރޭ</string>
|
<string name="method_pattern">ޕެޓަރން</string>
|
||||||
<string name="pin_min_digits">މަދުވެގެން 4 ރިޔަލ، ގިނަވެގެން 8</string>
|
<string name="method_pattern_desc">މަދުވެގެން 4 ތިކި ގުޅުވައިގެން ޕެޓަރން ކުރަހާ</string>
|
||||||
<string name="pin_no_match">PIN ދިމައެއް ނުވި — އަލުން ކަނޑޭ</string>
|
<string name="enter_pin">ޕިން ކޯޑެއް ޚިޔާރުކުރޭ</string>
|
||||||
<string name="draw_pattern">ޕެޓަން ކަހާ</string>
|
<string name="confirm_pin">ޕިން ކޯޑު ޔަގީންކުރޭ</string>
|
||||||
<string name="confirm_pattern">ޕެޓަން ކަށަވަރު ކުރޭ</string>
|
<string name="pin_min_digits">މަދުވެގެން 4 އަދަދު، ގިނަވެގެން 8 އަދަދު</string>
|
||||||
<string name="pattern_min_dots">މަދުވެގެން 4 ތިކި ގުޅުވާ</string>
|
<string name="pin_no_match">ޕިން ކޯޑު ދިމައެއްނުވޭ - އަލުން ޖަހާ</string>
|
||||||
<string name="pattern_draw_again">އެ ޕެޓަން އަލުން ކަހާ</string>
|
<string name="draw_pattern">ޕެޓަރންއެއް ކުރަހާ</string>
|
||||||
<string name="pattern_no_match">ޕެޓަން ދިމައެއް ނުވި — އަލުން ކަހާ</string>
|
<string name="confirm_pattern">ޕެޓަރން ޔަގީންކުރޭ</string>
|
||||||
<string name="biometric_title">ބަޔޮމެޓްރިކް ބޭނުން ކުރަންތަ؟</string>
|
<string name="pattern_min_dots">މަދުވެގެން 4 ތިކި ގުޅުވާލާ</string>
|
||||||
<string name="biometric_desc">ފިންގަޕްރިންޓް ނުވަތަ މޫނު ބޭނުން ކޮށްގެން ހުޅުވޭ.</string>
|
<string name="pattern_draw_again">އަލުން އެ ޕެޓަން ކުރަހާ</string>
|
||||||
<string name="biometric_security_note">ބަޔޮމެޓްރިކް ފަސޭހަ ނަމަވެސް PIN ނުވަތަ ޕެޓަނަށްވުރެ ތަންކޮޅެއް ކަށަވަރެއް ނޫން. ރައްކާތެރިކަން ބޭނުން ނަމަ PIN ނުވަތަ ޕެޓަން ހިޔާރު ކުރޭ.</string>
|
<string name="pattern_no_match">ޕެޓަން ދިމައެއްނުވޭ - އަލުން ކުރަހާ</string>
|
||||||
<string name="enable_biometrics">ބަޔޮމެޓްރިކް ހިންގާ</string>
|
<string name="biometric_title">ބަޔޯމެޓްރިކްސް ބޭނުންކުރަންތޯ؟</string>
|
||||||
<string name="skip_biometrics">ސްކިޕް — PIN/ޕެޓަން ބޭނުން ކުރޭ</string>
|
<string name="biometric_desc">ޕިން ނުވަތަ ޕެޓަންގެ ބަދަލުގައި އިނގިލީގެ ނިޝާން ނުވަތަ ފޭސް އައިޑީއިން އެޕް ހުޅުވާލެވޭނެ.</string>
|
||||||
|
<string name="biometric_security_note">ބަޔޯމެޓްރިކްސްއަކީ ފަސޭހަ ގޮތެއް ނަމަވެސް، އެންމެ ރައްކާތެރި ގޮތަކީ ޕިން ނުވަތަ ޕެޓަން ބޭނުންކުރުން.</string>
|
||||||
|
<string name="enable_biometrics">ބަޔޯމެޓްރިކްސް އޮންކުރޭ</string>
|
||||||
|
<string name="skip_biometrics">ބޭނުމެއްނޫން - ހަމައެކަނި ޕިން/ޕެޓަން</string>
|
||||||
<string name="back">ފަހަތަށް</string>
|
<string name="back">ފަހަތަށް</string>
|
||||||
|
|
||||||
<!-- Navigation -->
|
<!-- Navigation -->
|
||||||
<string name="nav_dashboard">ޑޭޝްބޯޑް</string>
|
<string name="nav_dashboard">ޑޭޝްބޯޑު</string>
|
||||||
<string name="nav_add_account">އެކައުންޓް އިތުރު ކުރޭ</string>
|
<string name="nav_add_account">އެކައުންޓެއް އިތުރުކުރޭ</string>
|
||||||
<string name="nav_accounts">އެކައުންޓްތައް</string>
|
<string name="nav_accounts">އެކައުންޓްތައް</string>
|
||||||
<string name="nav_contacts">ކޮންޓެކްޓްތައް</string>
|
<string name="nav_contacts">ކޮންޓެކްޓްތައް</string>
|
||||||
<string name="nav_activities">ހަރަކާތްތައް</string>
|
<string name="nav_activities">އެންމެ ފަހުގެ މުޢާމަލާތްތައް</string>
|
||||||
<string name="nav_transfer_history">ޓްރާންސެކްޝަން ތާރީހް</string>
|
<string name="nav_transfer_history">ހިސްޓްރީ</string>
|
||||||
<string name="nav_finances">ފައިނޭންސް</string>
|
<string name="nav_finances">ފައިނޭންސިންގ</string>
|
||||||
<string name="nav_pay_with_card">ކާޑްތައް</string>
|
<string name="nav_card_settings">ކާޑު ސެޓިންގްސް</string>
|
||||||
<string name="nav_desc_pay_with_card">ކާޑް މެނޭޖްކޮށް ފައިސާ ދައްކާ</string>
|
<string name="nav_otp">އޯޓީޕީ ކޯޑު</string>
|
||||||
<string name="nav_settings">ސެޓިންގ</string>
|
<string name="nav_settings">ސެޓިންގްސް</string>
|
||||||
<string name="nav_desc_accounts">ހުރިހާ ބޭންކް އެކައުންޓްތައް ބަލާ</string>
|
<string name="nav_more">އިތުރު</string>
|
||||||
<string name="nav_desc_contacts">ޓްރާންސްފަ ކޮންޓެކްޓްތައް މެނޭޖް ކުރޭ</string>
|
<string name="nav_desc_accounts">ހުރިހާ އެކައުންޓެއްގެ މަޢުލޫމާތު</string>
|
||||||
<string name="nav_desc_transfer">ކޮންޓެކްޓަކަށް ފައިސާ ފޮނުވާ</string>
|
<string name="nav_desc_contacts">ފައިސާ ފޮނުވާ ކޮންޓެކްޓްތައް</string>
|
||||||
<string name="nav_desc_pay_mv_qr">PayMV QR ކޯޑް ސްކޭން ނުވަތަ ތައްޔާރު ކުރޭ</string>
|
<string name="nav_desc_transfer">ފައިސާ ފޮނުއްވުމަށް</string>
|
||||||
<string name="nav_desc_activities">ފަހުގެ ޓްރާންސްފަތައް ބަލާ</string>
|
<string name="nav_desc_pay_mv_qr">ޕޭއެމްވީ ކިއުއާރު ހެދުމަށް ނުވަތަ ސުކޭން ކުރުމަށް</string>
|
||||||
<string name="nav_desc_transfer_history">އެކައުންޓް ތަކުގެ ޓްރާންސެކްޝަން ތާރީހް</string>
|
<string name="nav_desc_activities">އެންމެ ފަހުން ފޮނުވި ފައިސާގެ ތަފްޞީލު</string>
|
||||||
<string name="nav_desc_finances">ލޯން އަދި ފައިނޭންސިންގ</string>
|
<string name="nav_desc_transfer_history">އެކައުންޓްތަކުގެ ފުރިހަމަ ހިސްޓްރީ</string>
|
||||||
<string name="nav_desc_otp">OTP ކޯޑް ތައްޔާރު ކުރޭ</string>
|
<string name="nav_desc_finances">ލޯނާއި ފައިނޭންސިންގ މަޢުލޫމާތު</string>
|
||||||
<string name="nav_desc_settings">އެޕްލިކޭޝަންގެ ތަރުތީބު</string>
|
<string name="nav_desc_pay_with_card">ކާޑުތައް ބަލަހައްޓުމާއި ކާޑުން ފައިސާ ދެއްކުން</string>
|
||||||
|
<string name="nav_desc_otp">އޮތެންޓިކޭޝަނަށް އޯޓީޕީ ކޯޑު ހޯދުން</string>
|
||||||
|
<string name="nav_desc_settings">އެޕްގެ ސެޓިންގްސް ބަދަލުކުރުމަށް</string>
|
||||||
<string name="nav_open_drawer">ނެވިގޭޝަން ހުޅުވާ</string>
|
<string name="nav_open_drawer">ނެވިގޭޝަން ހުޅުވާ</string>
|
||||||
<string name="nav_close_drawer">ނެވިގޭޝަން ލައްޕާ</string>
|
<string name="nav_close_drawer">ނެވިގޭޝަން ލައްޕާ</string>
|
||||||
<string name="work_in_progress">ތައްޔާރުވަމުން ދަނީ</string>
|
<string name="work_in_progress">މަސައްކަތް ކުރިއަށްދަނީ</string>
|
||||||
<string name="mib_qr_nfc_not_supported">Skill issue on MIB side, Not supported</string>
|
<string name="press_back_to_exit">އެޕުން ނުކުންނެވުމަށް އަނެއްކާވެސް ފަހަތަށް އޮބާލާ</string>
|
||||||
|
|
||||||
<!-- Dashboard -->
|
<!-- Dashboard -->
|
||||||
<string name="dashboard_quick_actions">ހަލުވި ހަރަކާތްތައް</string>
|
<string name="dashboard_pending_finances">ނުދައްކާ ހުރި ފައިނޭންސް</string>
|
||||||
<string name="balance_mvr">ޖުމްލަ MVR</string>
|
<string name="dashboard_quick_actions">އަވަސް ޚިދުމަތްތައް</string>
|
||||||
<string name="balance_usd">ޖުމްލަ USD</string>
|
<string name="balance_mvr">ޖުމްލަ ދިވެހި ރުފިޔާ</string>
|
||||||
<string name="card_support_wip">ކާޑް ސަޕޯޓް</string>
|
<string name="balance_usd">ޖުމްލަ ޔޫއެސް ޑޮލަރު</string>
|
||||||
<string name="transfer">ޓްރާންސްފަ</string>
|
<string name="balance_mvr_credit">ލިބެންހުރި ކްރެޑިޓް (ރުފިޔާ)</string>
|
||||||
<string name="pay_mv_qr">PayMV QR</string>
|
<string name="balance_usd_credit">ލިބެންހުރި ކްރެޑިޓް (ޑޮލަރު)</string>
|
||||||
|
<string name="card_support_wip">ކާޑުގެ ޚިދުމަތް</string>
|
||||||
|
<string name="transfer">ޓްރާންސްފަރ</string>
|
||||||
|
<string name="pay_mv_qr">ޕޭއެމްވީ ކިއުއާރު</string>
|
||||||
|
|
||||||
|
<!-- PayMV QR Generator -->
|
||||||
|
<string name="paymvqr_select_account">އެކައުންޓް ނަގާ</string>
|
||||||
|
<string name="paymvqr_amount_hint">އަދަދު (ބޭނުންނަމަ)</string>
|
||||||
|
<string name="paymvqr_share">ޙިއްޞާކޮއްލާ</string>
|
||||||
|
<string name="paymvqr_save_image">ފޮޓޯ ސޭވްކޮއްލާ</string>
|
||||||
|
<string name="paymvqr_saved">ކިއުއާރު ފޮޓޯ ގެލަރީއަށް ސޭވްކުރެވިއްޖެ</string>
|
||||||
|
<string name="paymvqr_save_failed">ފޮޓޯ ސޭވެއް ނުކުރެވުނު</string>
|
||||||
|
<string name="paymvqr_include_phone">ފޯނު ނަންބަރު ހިމަނާ</string>
|
||||||
|
<string name="paymvqr_reference_hint">ރެފަރެންސް (ބޭނުންނަމަ)</string>
|
||||||
|
|
||||||
|
<!-- Toolbar -->
|
||||||
|
<string name="action_lock">އެޕް ތަޅުލާ</string>
|
||||||
|
<string name="action_hide_amounts">މަޢުލޫމާތު ފޮރުވާ</string>
|
||||||
|
<string name="action_show_amounts">މަޢުލޫމާތު ދައްކާ</string>
|
||||||
|
<string name="autolock_warning_title">އެޕް ތަޅުލެވެނީ</string>
|
||||||
|
<string name="autolock_warning_message">%d ސިކުންތު ތެރޭގައި ތަޅުލެވޭނެ</string>
|
||||||
|
<string name="autolock_stay">ނުތަޅުލާ މަޑުކުރޭ</string>
|
||||||
|
<string name="autolock_lock_now">މިހާރު ތަޅުލާ</string>
|
||||||
|
|
||||||
<!-- Settings -->
|
<!-- Settings -->
|
||||||
<string name="settings_bottom_bar_shortcuts">ތިރި ބާ ޝޯޓްކަޓްތައް</string>
|
<string name="settings_security">ރައްކާތެރިކަން</string>
|
||||||
<string name="settings_bottom_bar_select">ބަޓަން ހިޔާރު ކުރޭ</string>
|
<string name="settings_change_lock">ޕިން / ޕެޓަން ބަދަލުކުރުމަށް</string>
|
||||||
<string name="settings_bottom_bar_slot_1">ތަން 1</string>
|
<string name="settings_biometrics">ބަޔޯމެޓްރިކްސް ބޭނުންކުރުމަށް</string>
|
||||||
<string name="settings_bottom_bar_slot_2">ތަން 2</string>
|
<string name="settings_biometrics_unavailable">މި ފޯނުގައި ބަޔޯމެޓްރިކްސް ސެޓްއަޕްކޮށްފައެއް ނެތް</string>
|
||||||
<string name="settings_bottom_bar_slot_3">ތަން 3</string>
|
<string name="settings_biometrics_unlock">އެޕް ހުޅުވުމަށް</string>
|
||||||
|
<string name="settings_biometrics_transfer">ފައިސާ ފޮނުވުން ކަށަވަރުކުރުމަށް</string>
|
||||||
|
<string name="biometric_transfer_title">ޓްރާންސްފަރ ކަށަވަރުކުރުމަށް</string>
|
||||||
|
<string name="settings_autolock">އަމިއްލައަށް ތަޅުލެވުން</string>
|
||||||
|
<string name="autolock_off">އަމިއްލައަށް ތަޅެއްނުލެވޭނެ</string>
|
||||||
|
<string name="autolock_30s">30 ސިކުންތު</string>
|
||||||
|
<string name="autolock_1m">1 މިނެޓު</string>
|
||||||
|
<string name="autolock_3m">3 މިނެޓު</string>
|
||||||
|
<string name="autolock_5m">5 މިނެޓު</string>
|
||||||
<string name="theme">ތީމް</string>
|
<string name="theme">ތީމް</string>
|
||||||
<string name="theme_system">ސިސްޓަމް</string>
|
<string name="theme_system">ފޯނުގެ ސެޓިންގްސްއާ އެއްގޮތަށް</string>
|
||||||
<string name="theme_light">ލައިޓް</string>
|
<string name="theme_light">އަލި</string>
|
||||||
<string name="theme_dark">ޑާކް</string>
|
<string name="theme_dark">އަނދިރި</string>
|
||||||
|
<string name="settings_pitch_black">ގަދަ އަނދިރި (އޯލެޑް)</string>
|
||||||
|
<string name="settings_accent_color">މައި ކުލަ</string>
|
||||||
|
<string name="settings_receipts">ރަސީދު</string>
|
||||||
|
<string name="settings_always_fullscreen_receipt">މުޅި ސްކްރީނުން ރަސީދު ދައްކާ</string>
|
||||||
|
<string name="accent_blue">ނޫ</string>
|
||||||
|
<string name="accent_orange">ރަތް</string>
|
||||||
|
<string name="accent_green">ފެހި</string>
|
||||||
|
<string name="accent_custom">ބޭނުން ކުލައެއް</string>
|
||||||
|
<string name="accent_custom_pick">ބޭނުން ކުލައެއް އިޚްތިޔާރުކުރޭ</string>
|
||||||
|
<string name="accent_custom_hint">#RRGGBB ކުލައިގެ ކޯޑު</string>
|
||||||
|
<string name="accent_invalid_color">ނުބައި ކުލަ ކޯޑެއް - ރަނގަޅު ކޯޑެއް ޖަހާ</string>
|
||||||
<string name="language">ބަސް</string>
|
<string name="language">ބަސް</string>
|
||||||
<string name="lang_english">English</string>
|
<string name="lang_english">English</string>
|
||||||
<string name="lang_dhivehi">ދިވެހި</string>
|
<string name="lang_dhivehi">ދިވެހި</string>
|
||||||
<string name="settings_privacy">ޕްރައިވެސީ</string>
|
<string name="settings_privacy">ޕްރައިވަސީ</string>
|
||||||
<string name="settings_auto_unlock_pin">ރަނގަޅު ޕިން އެޅުމުން ހުޅުވޭ</string>
|
<string name="settings_hide_amounts">ފައިސާގެ އަދަދުތައް ނިވާކުރުން</string>
|
||||||
<string name="settings_auto_unlock_pin_desc">ޕިންގެ ދިގުމިނާ އެއްވަރަށް ޑިޖިޓް ލިޔުމުން ހުޅުވިދާ</string>
|
<string name="settings_hide_amounts_desc">އެޕްގެ ހުރިހާ ތަނަކުން އެކައުންޓް ބެލެންސާއި ފައިސާގެ އަދަދުތައް ނިވާކޮށްދޭނެ</string>
|
||||||
<string name="settings_block_screenshots">ސްކްރީންޝޮޓް ބްލޮކްކުރޭ</string>
|
<string name="settings_auto_unlock_pin">ރަނގަޅު ޕިން ޖެހުމުން އަމިއްލައަށް ހުޅުވޭ</string>
|
||||||
<string name="settings_block_screenshots_desc">ރިސެންޓްސް ސްކްރީނުންނާއި ސްކްރީން ކެޕްޗާ ހުއްޓުވައިދޭ</string>
|
<string name="settings_auto_unlock_pin_desc">ޕިން ކޯޑު ހަމަވުމާއެކު އަމިއްލައަށް އެޕް ހުޅުވޭނެ</string>
|
||||||
|
<string name="settings_block_screenshots">ސުކްރީންޝޮޓް ނެގުން ހުއްޓުވާ</string>
|
||||||
|
<string name="settings_block_screenshots_desc">އެޕްގެ ސުކްރީންޝޯޓް ނެގުމާއި ރީސެންޓްސްއިން ފެނުން ހުއްޓުވޭނެ</string>
|
||||||
<string name="settings_cache">ކޭޝް</string>
|
<string name="settings_cache">ކޭޝް</string>
|
||||||
<string name="settings_clear_cache">ކޭޝް ސާފުކުރޭ</string>
|
<string name="settings_clear_cache">ކޭޝް ސާފުކޮއްލާ</string>
|
||||||
<string name="settings_cache_cleared">ކޭޝް ސާފުކުރެވިއްޖެ</string>
|
<string name="settings_cache_cleared">ކޭޝް ސާފުކުރެވިއްޖެ</string>
|
||||||
|
<string name="settings_navigation">ނެވިގޭޝަން</string>
|
||||||
|
<string name="settings_nav_drawer">ސައިޑް މެނޫ</string>
|
||||||
|
<string name="settings_nav_bottom">ތިރި މެނޫ</string>
|
||||||
|
<string name="settings_nav_circular">ބުރު މެނޫ</string>
|
||||||
|
<string name="settings_appearance">ފެންނަ ގޮތް</string>
|
||||||
|
<string name="settings_circular_shortcuts">ބުރު މެނޫ ޝޯޓްކަޓް</string>
|
||||||
|
<string name="settings_bottom_bar_shortcuts">ތިރި މެނޫ ޝޯޓްކަޓް</string>
|
||||||
|
<string name="settings_bottom_bar_show_labels">ތިރި މެނޫގެ ނަންތައް އަބަދުވެސް ދައްކާ</string>
|
||||||
|
<string name="settings_bottom_bar_select">ފިއްތައް އިޚްތިޔާރުކުރޭ</string>
|
||||||
|
<string name="settings_bottom_bar_slot_1">ޖާގަ 1</string>
|
||||||
|
<string name="settings_bottom_bar_slot_2">ޖާގަ 2</string>
|
||||||
|
<string name="settings_bottom_bar_slot_3">ޖާގަ 3</string>
|
||||||
|
<string name="settings_privacy_security">ޕްރައިވަސީ އާއި ރައްކާތެރިކަން</string>
|
||||||
|
<string name="settings_storage">ސްޓޯރޭޖް</string>
|
||||||
<string name="settings_logins">ލޮގިންތައް</string>
|
<string name="settings_logins">ލޮގިންތައް</string>
|
||||||
<string name="settings_desc_logins">ބޭންކް ލޮގިންތައް މެނޭޖް ކުރޭ</string>
|
<string name="settings_desc_logins">ބޭންކު އެކައުންޓްތަކުގެ ލޮގިން މެނޭޖްކުރުމަށް</string>
|
||||||
<string name="settings_desc_appearance">ތީމް، ބަސް، އަދި ދައްކުވާ ގޮތް</string>
|
<string name="settings_desc_appearance">ތީމް، ބަސް، އަދި ފެންނަ ގޮތުގެ ސެޓިންގްސް</string>
|
||||||
<string name="settings_desc_privacy_security">އެޕް ލޮކް، ޕިން، އަދި ސަލާމަތީ ސެޓިންގ</string>
|
<string name="settings_desc_privacy_security">އެޕް ލޮކް، ޕިން، އަދި ރައްކާތެރިކަމުގެ ސެޓިންގްސް</string>
|
||||||
<string name="settings_desc_storage">ކޭޝް ޑޭޓާ އަދި ސްޓޯރޭޖް</string>
|
<string name="settings_desc_storage">ކޭޝް ކުރެވިފައިވާ މަޢުލޫމާތާއި ސްޓޯރޭޖް މެނޭޖްކުރުމަށް</string>
|
||||||
|
<string name="settings_notifications">ނޯޓިފިކޭޝަން</string>
|
||||||
|
<string name="settings_desc_notifications">އަލަށް ހިނގާ ފައިސާގެ މުޢާމަލާތްތަކުގެ ނޯޓިފިކޭޝަން</string>
|
||||||
|
<string name="settings_notif_section">ބެކްގްރައުންޑް ޗެކް</string>
|
||||||
|
<string name="settings_notif_enable">ބެކްގްރައުންޑް ނޯޓިފިކޭޝަން އޮންކުރޭ</string>
|
||||||
|
<string name="settings_notif_enable_desc">އާ މުޢާމަލާތްތައް ހިނގުމުން ވަގުތުން ނޯޓިފިކޭޝަން ލިބޭނެ</string>
|
||||||
|
<string name="settings_notif_description">އެޕް ބެކްގްރައުންޑްގައި ހިނގަމުންދާނެއެވެ. އަދި އާ މުޢާމަލާތެއް ހިނގައިފިނަމަ ނޮޓިފިކޭޝަން ފޮނުވާނެއެވެ. މިކަން ހިނގާއިރު ސްޓޭޓަސް ބާގައި ނޯޓިފިކޭޝަނެއް އިންނާނެއެވެ.</string>
|
||||||
|
<string name="settings_notif_open_system">ނޯޓިފިކޭޝަން ސެޓިންގްސް</string>
|
||||||
|
<string name="settings_notif_channels_desc">އަޑާއި ނޯޓިފިކޭޝަން ސެޓިންގްސް ބަދަލުކުރުމަށް</string>
|
||||||
|
<string name="notif_service_title">ތިޖޫރީ</string>
|
||||||
|
<string name="notif_service_desc">އާ މުޢާމަލާތްތައް ޗެކްކުރެވެނީ...</string>
|
||||||
|
<string name="notif_channel_service">ބެކްގްރައުންޑް ޚިދުމަތް</string>
|
||||||
|
<string name="settings_about">އެޕާ ބެހޭ</string>
|
||||||
|
<string name="settings_desc_about">އެޕްގެ މަޢުލޫމާތާއި، ވާޝަން، އަދި ގާނޫނީ</string>
|
||||||
|
<string name="about_version">ވާޝަން %s</string>
|
||||||
|
<string name="about_short_desc">ތިޖޫރީ އަކީ ރާއްޖޭގެ ބޭންކިންގ ޚިދުމަތްތަކަށް ޚާއްޞަކޮށްގެން ތައްޔާރުކުރެވިފައިވާ އެންޑްރޮއިޑް އެޕެއް.</string>
|
||||||
|
<string name="about_terms">ޚިދުމަތުގެ އުޞޫލުތައް</string>
|
||||||
|
<string name="about_donate_title">އެޕް ކުރިއެރުވުމަށް އެހީތެރިވެދެއްވާ</string>
|
||||||
|
<string name="about_donate_desc">މި އެޕަކީ ތިޔަބޭފުޅާއަށް ފައިދާހުރި އެޕެއްނަމަ، އެޕް އިތުރަށް ތަރައްޤީކުރުމަށް ކުޑަ އެހީއެއް ވެދެއްވާ.</string>
|
||||||
|
<string name="about_donate_mvr">ދިވެހި ރުފިޔާއިން އެހީވުމަށް</string>
|
||||||
|
<string name="about_donate_usd">ޑޮލަރުން އެހީވުމަށް</string>
|
||||||
|
<string name="about_legal">ތިޖޫރީ އަކީ އެއްވެސް ބޭންކަކާ ނުވަތަ މާލީ އިދާރާއަކާ ގުޅުމެއްނެތި އަމިއްލައަށް އުފައްދާފައިވާ އެޕެކެވެ.\n\nމި އެޕް މަސައްކަތްކުރަނީ ތިޔަބޭފުޅާގެ އިންޓަނެޓް ބޭންކިންގ މަޢުލޫމާތު ބޭނުންކޮށްގެން ސީދާ ބޭންކުގެ އޭޕީއައިތަކާ ގުޅިގެންނެވެ. ބޭންކުތަކުގެ ސިސްޓަމްތަކަށް އަންނަ ބަދަލަކުން އެޕްގެ މަސައްކަތަށް ބުރޫއަރާފާނެއެވެ.\n\nފޯނު ނަންބަރުގެ މަޢުލޫމާތު ހޯދުމަށް ދިރާގާއި އޯރިޑޫގެ އޭޕީއައި ބޭނުންކުރެވެއެވެ.\n\nމި އެޕުން ތިޔަބޭފުޅާގެ އެއްވެސް އަމިއްލަ މަޢުލޫމާތެއް ނުވަތަ އެޕް ބޭނުންކުރާ ގޮތުގެ މަޢުލޫމާތެއް އެއްނުކުރާނެއެވެ. ހުރިހާ މަޢުލޫމާތެއް ރައްކާކުރެވޭނީ ހަމައެކަނި ތިޔަބޭފުޅާގެ ފޯނުގައެވެ.</string>
|
||||||
<string name="settings_logout">ލޮގްއައުޓް</string>
|
<string name="settings_logout">ލޮގްއައުޓް</string>
|
||||||
<string name="settings_logout_confirm_title">%s އިން ލޮގްއައުޓް ވަންތަ؟</string>
|
<string name="settings_logout_confirm_title">%s އިން ލޮގްއައުޓްވާންވީތޯ؟</string>
|
||||||
<string name="settings_logout_confirm_message">ހުރިހާ ކޭޝް ޑޭޓާ ސާފުވެ، ބާކީ ހުރި އެކައުންޓްތައް އަލުން ލޯޑްވާނެ.</string>
|
<string name="settings_logout_confirm_message">ރައްކާކުރެވިފައިވާ ހުރިހާ މަޢުލޫމާތެއް ފުހެވި، ބާކީ ހުރި އެކައުންޓްތައް ރީފްރެޝްކުރެވޭނެ.</string>
|
||||||
<string name="login_detail_name">ނަން</string>
|
<string name="login_detail_name">ނަން</string>
|
||||||
<string name="login_detail_username">ޔޫޒަރ ނޭމް</string>
|
<string name="login_detail_username">ޔޫސަރނޭމް</string>
|
||||||
<string name="login_detail_email">އީމެއިލް</string>
|
<string name="login_detail_email">އީމެއިލް</string>
|
||||||
<string name="login_detail_mobile">މޮބައިލް</string>
|
<string name="login_detail_mobile">މޮބައިލް ނަންބަރު</string>
|
||||||
<string name="login_detail_customer_id">ކަސްޓަމަ ID</string>
|
<string name="login_detail_customer_id">ކަސްޓަމަރ އައިޑީ</string>
|
||||||
<string name="login_detail_id_card">ID ކާޑް</string>
|
<string name="login_detail_id_card">އައިޑީ ކާޑު</string>
|
||||||
<string name="login_detail_profiles">ޕްރޮފައިލްތައް</string>
|
<string name="login_detail_profiles">ޕްރޮފައިލްތައް</string>
|
||||||
<string name="close">ބަންދު</string>
|
<string name="profile_image_title">ޕްރޮފައިލް ފޮޓޯ</string>
|
||||||
|
<string name="profile_image_select">ފޮޓޯ އިޚްތިޔާރުކުރޭ</string>
|
||||||
|
<string name="profile_image_camera">ކެމެރާއިން ފޮޓޯއެއް ނަގާ</string>
|
||||||
|
<string name="profile_image_remove">ފުހެލާ</string>
|
||||||
|
<string name="profile_image_uploading">ފޮޓޯ އަޕްލޯޑުކުރެވެނީ…</string>
|
||||||
|
<string name="profile_image_upload_failed">ފޮޓޯ އަޕްލޯޑެއް ނުކުރެވުނު</string>
|
||||||
|
<string name="profile_image_deleting">ފޮޓޯ ފުހެލެވެނީ…</string>
|
||||||
|
<string name="close">ލައްޕާ</string>
|
||||||
|
<string name="save">ސޭވްކުރޭ</string>
|
||||||
<string name="cancel">ކެންސަލް</string>
|
<string name="cancel">ކެންސަލް</string>
|
||||||
|
<string name="verify">ޔަގީން</string>
|
||||||
|
|
||||||
<string name="settings_bottom_bar_show_labels">ބޮޓަމް ބާ ލޭބަލް އަބަދުވެސް ދެއްކުން</string>
|
<!-- BML business OTP -->
|
||||||
|
<string name="bml_business_otp_sent">%s މެދުވެރިކޮށް އޯޓީޕީ ފޮނުވިއްޖެ</string>
|
||||||
|
|
||||||
<!-- Home -->
|
<!-- Home -->
|
||||||
|
<string name="transfer_same_account">މިއީ ފައިސާ ފޮނުވާ އެކައުންޓް</string>
|
||||||
<string name="accounts">އެކައުންޓްތައް</string>
|
<string name="accounts">އެކައުންޓްތައް</string>
|
||||||
<string name="available_balance">ލިބެން ހުރި ބެލެންސް</string>
|
<string name="cards">ކާޑުތައް</string>
|
||||||
|
<string name="available_balance">ބޭނުންކުރެވެން ހުރި ބެލެންސް</string>
|
||||||
|
<string name="account_blocked_label">%1$s ހިފެހެއްޓިފައި</string>
|
||||||
|
<string name="dashboard_blocked_mvr">ހިފެހެއްޓިފައިވާ ރުފިޔާ</string>
|
||||||
|
<string name="dashboard_blocked_usd">ހިފެހެއްޓިފައިވާ ޑޮލަރު</string>
|
||||||
|
<string name="dashboard_overdue">މުއްދަތު ހަމަވެފައިވާ ފައިނޭންސް</string>
|
||||||
|
|
||||||
|
<!-- Transfer -->
|
||||||
|
<string name="transfer_tab_quick">އަވަސް ޓްރާންސްފަރ</string>
|
||||||
|
<string name="transfer_tab_contacts">ކޮންޓެކްޓްތައް</string>
|
||||||
|
<string name="transfer_from">ފައިސާ ފޮނުވާ އެކައުންޓް</string>
|
||||||
|
<string name="transfer_to">އެކައުންޓް ނަންބަރު ނުވަތަ ފަވާރާ އައިޑީ</string>
|
||||||
|
<string name="transfer_label_from">ފޮނުވާ އެކައުންޓް</string>
|
||||||
|
<string name="transfer_label_to">ލިބޭ އެކައުންޓް</string>
|
||||||
|
<string name="transfer_select_service">ޚިދުމަތް އިޚްތިޔާރުކުރޭ</string>
|
||||||
|
<string name="transfer_fahipay_phone_only">ފަހިޕޭއިން ފައިސާ ފޮނުވޭނީ 7 އަދަދުގެ ފޯނު ނަންބަރަކަށް</string>
|
||||||
|
<string name="transfer_my_accounts">އަމިއްލަ އެކައުންޓްތައް</string>
|
||||||
|
<string name="transfer_same_as_from">މިއީ ފައިސާ ފޮނުވާ އެކައުންޓް</string>
|
||||||
|
<string name="transfer_lookup_account">އެކައުންޓް ހޯދާ</string>
|
||||||
|
<string name="transfer_clear_recipient">ލިބޭ ފަރާތް ފުހެލާ</string>
|
||||||
|
<string name="transfer_pick_contact">ކޮންޓެކްޓެއް ނަގާ</string>
|
||||||
|
<string name="transfer_scan_qr">ކިއުއާރުން ފައިސާ ދެއްކުމަށް</string>
|
||||||
|
<string name="qr_pick_image">ފޮޓޯއެއް ނަގާ</string>
|
||||||
|
<string name="transfer_qr_invalid">މި ކިއުއާރު ކޯޑު ބޭނުމެއް ނުކުރެވޭނެ</string>
|
||||||
|
<string name="card_qr_paymv_unsupported">ކާޑުން ޕޭއެމްވީ ކިއުއާރަށް ފައިސާއެއް ނުދެއްކޭނެ - ޓްރާންސްފަރއަށް ބަދަލުކުރެވެނީ</string>
|
||||||
|
<string name="qr_camera_permission_title">ކެމެރާގެ ހުއްދަ ބޭނުންވޭ</string>
|
||||||
|
<string name="qr_camera_permission_message">ކިއުއާރު ސުކޭންކުރުމަށް ކެމެރާގެ ހުއްދަ ބޭނުންވެއެވެ. ސެޓިންގްސްއިން ހުއްދަ ދެއްވާ.</string>
|
||||||
|
<string name="camera_permission_profile_message">ފޮޓޯ ނެގުމަށް ކެމެރާގެ ހުއްދަ ބޭނުންވެއެވެ. ސެޓިންގްސްއިން ހުއްދަ ދެއްވާ.</string>
|
||||||
|
<string name="go_to_settings">ސެޓިންގްސްއަށް ދޭ</string>
|
||||||
|
<string name="transfer_select_source_first">ފުރަތަމަ ފައިސާ ފޮނުވާ އެކައުންޓް ނަގާ</string>
|
||||||
|
<string name="transfer_no_from_account">ފުރަތަމަ ފައިސާ ފޮނުވާނެ އެކައުންޓެއް އިޚްތިޔާރުކުރޭ</string>
|
||||||
|
<string name="transfer_enter_account_first">ފުރަތަމަ އެކައުންޓް ނަންބަރު ޖަހާ</string>
|
||||||
|
<string name="transfer_account_not_found">އެކައުންޓެއް ނުފެނުނު</string>
|
||||||
|
<string name="transfer_session_unavailable">ސެޝަން މުއްދަތު ހަމަވެއްޖެ - އަލުން ލޮގިންވެވަޑައިގަންނަވާ</string>
|
||||||
|
<string name="transfer_amount">އަދަދު</string>
|
||||||
|
<string name="transfer_remarks">ތަފްޞީލު / ރިމާކްސް</string>
|
||||||
|
<string name="transfer_confirm">ޔަގީން</string>
|
||||||
|
<string name="transfer_success">ފައިސާ ފޮނުވިއްޖެ</string>
|
||||||
|
<string name="transfer_bml_contact_required_title">ކޮންޓެކްޓް ސޭވްކޮއްލާ</string>
|
||||||
|
<string name="transfer_bml_contact_required_msg">ލިބޭ ފަރާތުގެ އެކައުންޓް ކަރަންސީ ކަށަވަރެއް ނުކުރެވުނެވެ.\n\nމި ފަރާތް ކޮންޓެކްޓެއްގެ ގޮތުގައި ސޭވްކޮށް، ރަނގަޅު ކަރަންސީ އިޚްތިޔާރުކުރުމަށްފަހު އަލުން މަސައްކަތްކޮއްލާ.</string>
|
||||||
|
<string name="transfer_bml_contact_required_msg_bml_limit">ބީއެމްއެލް ނޫން އެކައުންޓަކަށް ޑޮލަރު ފޮނުވޭނީ އެ ފަރާތެއް ކޮންޓެކްޓެއްގެ ގޮތުގައި ސޭވްކުރުމަށްފަހުގައެވެ.\n\nމި ފަރާތް ކޮންޓެކްޓެއްގެ ގޮތުގައި ސޭވްކުރުމަށްފަހު އަލުން މަސައްކަތްކޮއްލާ.</string>
|
||||||
|
<string name="transfer_missing_internal_id">އެކައުންޓް މަޢުލޫމާތު ފުރިހަމައެއް ނޫން - އަލުން ލޮގިންވެވަޑައިގަންނަވާ.</string>
|
||||||
|
<string name="transfer_verify_payment">ފައިސާ ދެއްކުން ކަށަވަރުކޮއްލާ</string>
|
||||||
|
<string name="transfer_send_otp_via">ކޯޑު ފޮނުވާނެ ގޮތް</string>
|
||||||
|
<string name="transfer_otp_code_hint">ކަށަވަރުކުރާ ކޯޑު</string>
|
||||||
|
|
||||||
|
<!-- BML QR Pay -->
|
||||||
|
<string name="bml_qr_looking_up">ވިޔަފާރީގެ މަޢުލޫމާތު ހޯދެނީ…</string>
|
||||||
|
<string name="bml_qr_lookup_failed">ވިޔަފާރީގެ މަޢުލޫމާތު ލިބޭގޮތެއް ނުވި</string>
|
||||||
|
<string name="transfer_bml_txn_lookup_failed">މި ޓްރާންސެކްޝަން އައިޑީގެ މަޢުލޫމާތު ލިބޭގޮތެއް ނުވި</string>
|
||||||
|
<string name="bml_card_pay_no_verified">ކަށަވަރުކުރެވިފައިވާ ކާޑެއް ނެތް. ފުރަތަމަ ކާޑު މެނޭޖްކުރާ ބައިން ބީއެމްއެލް ކާޑެއް ކަށަވަރުކޮއްލާ.</string>
|
||||||
|
<string name="bml_card_pay_already_paid">މި ފައިސާ ދައްކާ ނިމިފައި</string>
|
||||||
|
<string name="bml_qr_payment_success">ފައިސާ ދެއްކިއްޖެ</string>
|
||||||
|
<string name="bml_qr_select_account">ފައިސާ ދައްކާނެ ބީއެމްއެލް އެކައުންޓެއް އިޚްތިޔާރުކުރޭ</string>
|
||||||
|
|
||||||
|
<!-- Accounts -->
|
||||||
|
<string name="accounts_empty">އެކައުންޓެއް ނުފެނުނު</string>
|
||||||
|
|
||||||
|
<!-- Contacts -->
|
||||||
|
<string name="contacts_empty">ކޮންޓެކްޓެއް ނުފެނުނު</string>
|
||||||
|
<string name="contacts_search_hint">ކޮންޓެކްޓް ހޯދާ</string>
|
||||||
|
<string name="contacts_tab_recents">އެންމެ ފަސް</string>
|
||||||
|
<string name="recents_remove">ލިސްޓުން އުނިކުރޭ</string>
|
||||||
|
<string name="contacts_tab_all">ހުރިހާ</string>
|
||||||
|
|
||||||
|
<!-- Add Contact -->
|
||||||
|
<string name="contact_add">ކޮންޓެކްޓެއް އިތުރުކުރޭ</string>
|
||||||
|
<string name="contact_save_to">އެކައުންޓް ސޭވްކުރޭ</string>
|
||||||
|
<string name="contact_alias">ނަން / ވަނަން</string>
|
||||||
|
<string name="contact_currency">ކަރަންސީ</string>
|
||||||
|
<string name="contact_group">ގްރޫޕް</string>
|
||||||
|
<string name="contact_no_group">ގްރޫޕެއް ނެތި</string>
|
||||||
|
<string name="contact_save">ކޮންޓެކްޓް ސޭވްކުރޭ</string>
|
||||||
|
<string name="contact_image">ފޮޓޯ އަޕްލޯޑުކޮއްލާ</string>
|
||||||
|
<string name="contact_saved">ކޮންޓެކްޓް ސޭވްކުރެވިއްޖެ</string>
|
||||||
|
<string name="contact_save_failed">ކޮންޓެކްޓް ސޭވްއެއް ނުކުރެވުނު</string>
|
||||||
|
<string name="contact_no_session">ބޭންކް ސެޝަނެއް ނެތް</string>
|
||||||
|
<string name="contact_lookup_failed">އެކައުންޓެއް ނުފެނުނު</string>
|
||||||
|
<string name="contact_select_destination">ފުރަތަމަ ފައިސާ ލިބޭނެ އެކައުންޓް ނަގާ</string>
|
||||||
|
<string name="contact_already_exists">މި ކޮންޓެކްޓް ސޭވް ވެފައެބައިން: %s</string>
|
||||||
|
<string name="contact_own_account">އަމިއްލަ އެކައުންޓެއް ކޮންޓެކްޓެއްގެ ގޮތުގައި ސޭވްއެއް ނުކުރެވޭނެ</string>
|
||||||
|
|
||||||
|
<!-- Contact expand/delete -->
|
||||||
|
<string name="contact_edit">އުނިއިތުރުގެނޭ</string>
|
||||||
|
<string name="contact_delete">ފުހެލާ</string>
|
||||||
|
<string name="contact_delete_title">ކޮންޓެކްޓް ފުހެލުން</string>
|
||||||
|
<string name="contact_delete_message">%s ކޮންޓެކްޓް ލިސްޓުން ފުހެލަންވީތޯ؟</string>
|
||||||
|
<string name="contact_deleted">ކޮންޓެކްޓް ފުހެލެވިއްޖެ</string>
|
||||||
|
<string name="contact_delete_failed">ކޮންޓެކްޓް ފުހެއެއް ނުލެވުނު</string>
|
||||||
|
<string name="contact_account_number">އެކައުންޓް ނަންބަރު</string>
|
||||||
|
<string name="contact_account_name">އެކައުންޓް ނަން</string>
|
||||||
|
<string name="contact_bank">ބޭންކު</string>
|
||||||
|
<string name="contact_qr">ކިއުއާރު</string>
|
||||||
|
<string name="contact_delete_warning">މި ކަން ނިމުމައްފަހު ބަދަލެއް ނުކުރެވޭނެ.</string>
|
||||||
|
<string name="contact_copy_account">އެކައުންޓް ނަންބަރު ކޮޕީކުރޭ</string>
|
||||||
|
<string name="contact_share_account">އެކައުންޓް މަޢުލޫމާތު ޙިއްޞާކުރޭ</string>
|
||||||
|
<string name="contact_account_copied">އެކައުންޓް ނަންބަރު ކޮޕީކުރެވިއްޖެ</string>
|
||||||
|
|
||||||
|
<!-- Financing -->
|
||||||
|
<string name="financing_empty">ފައިނޭންސިންގއެއް ނެތް</string>
|
||||||
|
<string name="financing_total">ޖުމްލަ</string>
|
||||||
|
<string name="financing_paid">ދައްކާފައި</string>
|
||||||
|
<string name="financing_unpaid">ނުދައްކާ</string>
|
||||||
|
<string name="financing_deal_date">ފެށުނު ތާރީޚު</string>
|
||||||
|
<string name="financing_installment">މަހުން މަހަށް ދައްކަންޖެހޭ</string>
|
||||||
|
<string name="financing_num_installments">ޖުމްލަ އިންސްޓޯލްމަންޓް</string>
|
||||||
|
<string name="financing_last_paid_date">އެންމެ ފަހުން ފައިސާ ދެއްކި ތާރީޚު</string>
|
||||||
|
<string name="financing_last_pay_amount">އެންމެ ފަހުން ދެއްކި އަދަދު</string>
|
||||||
|
<string name="financing_overdue">މުއްދަތު ހަމަވެފައި</string>
|
||||||
|
<string name="financing_completion_done">ފުރިހަމައަށް ދައްކާ ނިމިފައި</string>
|
||||||
|
<string name="financing_deal_no_fmt">ޑީލް #%s</string>
|
||||||
|
<string name="financing_completion_fmt">ނިމޭނީ %s</string>
|
||||||
|
|
||||||
|
<!-- BML Loans -->
|
||||||
|
<string name="loan_outstanding">ނުދައްކާ ބާކީ</string>
|
||||||
|
<string name="loan_monthly_repayment">މަހުން މަހަށް ދައްކަންޖެހޭ</string>
|
||||||
|
<string name="loan_interest_rate">އިންޓްރެސްޓް ރޭޓް</string>
|
||||||
|
<string name="loan_start_date">ފެށުނު ތާރީޚު</string>
|
||||||
|
<string name="loan_end_date">ނިމޭ ތާރީޚު</string>
|
||||||
|
<string name="loan_overdue_payments">މުއްދަތު ހަމަވެ ނުދައްކާ ހުރި</string>
|
||||||
|
<string name="loan_rate_fmt">%.2f%%</string>
|
||||||
|
|
||||||
|
<!-- Cards -->
|
||||||
|
<string name="nav_pay_with_card">ކާޑުތައް</string>
|
||||||
|
<string name="card_pay_qr">ކިއުއާރުން ފައިސާ ދެއްކުމަށް</string>
|
||||||
|
<string name="card_pay_nfc">ޖައްސާލައިގެން ފައިސާ ދެއްކުމަށް</string>
|
||||||
|
<string name="mib_qr_nfc_not_supported">އެމްއައިބީން މި ޚިދުމަތެއް ނުލިބޭ</string>
|
||||||
|
<string name="nfc_unsupported_title">މި ޚިދުމަތް ނުލިބޭ</string>
|
||||||
|
<string name="nfc_unsupported_message">މި ފޯނުން ޖައްސާލައިގެން ފައިސާ ދެއްކުމުގެ ޚިދުމަތެއް ނުލިބޭ.</string>
|
||||||
|
<string name="nfc_disabled_title">އެންއެފްސީ ނިއްވާލެވިފައި</string>
|
||||||
|
<string name="nfc_disabled_message">ޖައްސާލައިގެން ފައިސާ ދެއްކުމަށް އެންއެފްސީ އޮންކުރޭ.</string>
|
||||||
|
<string name="nfc_open_settings">އެންއެފްސީ ސެޓިންގްސް</string>
|
||||||
|
<string name="nfc_not_default_title">މައި އެޕްގެ ގޮތުގައި ހަމަޖަހާ</string>
|
||||||
|
<string name="nfc_not_default_message">ޖައްސާލައިގެން ފައިސާ ދެއްކުމަށް %1$s މައި އެޕްގެ ގޮތުގައި ހަމަޖަހާ.</string>
|
||||||
|
<string name="nfc_payment_open_settings">ޕޭމަންޓް ސެޓިންގްސް</string>
|
||||||
|
<string name="card_manage">ކާޑު މެނޭޖްކުރުން</string>
|
||||||
|
<string name="card_set_as_default">މައި ކާޑުގެ ގޮތުގައި ހަމަޖައްސާ</string>
|
||||||
|
<string name="card_hide_from_dashboard">ޑޭޝްބޯޑުން ފޮރުވާ</string>
|
||||||
|
<string name="card_action_change_pin">ޕިން ބަދަލުކުރޭ</string>
|
||||||
|
<string name="card_action_freeze">ފްރީޒްކުރޭ</string>
|
||||||
|
<string name="card_action_unfreeze">އަންފްރީޒްކުރޭ</string>
|
||||||
|
<string name="card_action_block">ބްލޮކްކުރޭ</string>
|
||||||
|
<string name="card_action_verify">ކަށަވަރުކުރޭ</string>
|
||||||
|
<string name="card_action_verified">ކަށަވަރުކުރެވިފައި</string>
|
||||||
|
<string name="card_verify_already">ކާޑު ވަނީ ކަށަވަރުކުރެވިފައި. ބަދަލުކުރުމަށް އޮއްބައިގެން ހިފަހައްޓަވާ.</string>
|
||||||
|
<string name="card_verify_title">ކާޑު ކަށަވަރުކުރޭ</string>
|
||||||
|
<string name="card_verify_tap">ކަށަވަރުކުރުމަށް ކާޑު ޖައްސާލާ</string>
|
||||||
|
<string name="card_verify_reading">ކާޑު ކިޔަނީ… މަޑުކޮށްލައްވާ</string>
|
||||||
|
<string name="card_verify_matched">ކާޑު ދިމާވެއްޖެ</string>
|
||||||
|
<string name="card_verify_read_failed">ކާޑު ކިޔައެއް ނުގަނެވުނު، އަލުން މަސައްކަތްކޮއްލާ</string>
|
||||||
|
<string name="card_verify_mismatch">ނިމޭ %1$s ކާޑާ ދިމައެއްނުވޭ</string>
|
||||||
|
<string name="card_verify_cancel">ކެންސަލް</string>
|
||||||
|
<string name="card_verify_manual">އަމިއްލައަށް މަޢުލޫމާތު ޖަހާ</string>
|
||||||
|
<string name="card_verify_manual_title">ކާޑުގެ މަޢުލޫމާތު ޖަހާ</string>
|
||||||
|
<string name="card_verify_nfc_disabled_message">ކާޑު ޖައްސާލައިގެން ކަށަވަރުކުރުމަށް އެންއެފްސީ އޮންކުރޭ. ނުވަތަ އަމިއްލައަށް މަޢުލޫމާތު ޖަހާ.</string>
|
||||||
|
<string name="card_verify_cvv_title">ނިމޭ %1$s ކާޑު</string>
|
||||||
|
<string name="card_verify_cvv_hint">ސީވީވީ (CVV)</string>
|
||||||
|
<string name="card_verify_cvv_invalid">3 ނުވަތަ 4 އަދަދުގެ ސީވީވީ ޖަހާ</string>
|
||||||
|
<string name="card_verify_confirm">ކަށަވަރުކޮއްލާ</string>
|
||||||
|
<string name="card_verify_name_hint">ކާޑުގައިވާ ނަން</string>
|
||||||
|
<string name="card_verify_number_hint">ކާޑު ނަންބަރު</string>
|
||||||
|
<string name="card_verify_expiry_hint">މުއްދަތު ހަމަވާ ތާރީޚު (މަސް/އަހަރު)</string>
|
||||||
|
<string name="card_verify_number_invalid">ރަނގަޅު ކާޑު ނަންބަރެއް ޖަހާ</string>
|
||||||
|
<string name="card_verify_number_mismatch">ނަންބަރު ނިމެންވާނީ %1$s އިން</string>
|
||||||
|
<string name="card_verify_expiry_invalid">ރަނގަޅު ތާރީޚެއް ޖަހާ (މިސާލަކަށް: 08/29)</string>
|
||||||
|
<string name="card_verify_success">ކާޑު ކަށަވަރުކުރެވިއްޖެ</string>
|
||||||
|
<string name="card_freeze_confirm_title">ކާޑު ފްރީޒްކުރަންވީތޯ؟</string>
|
||||||
|
<string name="card_freeze_confirm_message">މިކަމުގެ ސަބަބުން ކާޑުގެ ބޭނުން ވަގުތީގޮތުން މެދުކެނޑޭނެއެވެ. ބޭނުންވާ ކޮންމެ ވަގުތަކު އަލުން އަންފްރީޒް ކުރެވޭނެއެވެ.</string>
|
||||||
|
<string name="card_unfreeze_confirm_title">ކާޑު އަންފްރީޒްކުރަންވީތޯ؟</string>
|
||||||
|
<string name="card_unfreeze_confirm_message">މިކަމުގެ ސަބަބުން ކާޑު އަލުން ބޭނުންކުރެވޭނެއެވެ.</string>
|
||||||
|
<string name="card_freeze_success">ކާޑު ފްރީޒްކުރެވިއްޖެ</string>
|
||||||
|
<string name="card_unfreeze_success">ކާޑު އަންފްރީޒްކުރެވިއްޖެ</string>
|
||||||
|
<string name="card_freeze_failed">ކާޑުގެ ހާލަތު ބަދަލެއް ނުކުރެވުނު</string>
|
||||||
|
<string name="card_freeze_comments_hint">ސަބަބު (ބޭނުންނަމަ)</string>
|
||||||
|
<string name="card_status_temp_blocked">ވަގުތީގޮތުން ބްލޮކްކުރެވިފައި</string>
|
||||||
|
<string name="cards_empty">ކާޑެއް ނުފެނުނު</string>
|
||||||
|
|
||||||
<!-- Connectivity banner -->
|
<!-- Connectivity banner -->
|
||||||
<string name="connectivity_no_internet">އިންޓަނެޓް ބައްލަވާ، ދެން ތިޖޫރީ ލޯޑް ކުރޭ</string>
|
<string name="connectivity_no_internet">އިންޓަނެޓް ޗެކްކުރެއްވުމަށްފަހު އަލުން ތިޖޫރީ ރީލޯޑްކޮއްލާ</string>
|
||||||
<string name="connectivity_server_error">%s އާ ގުޅުމުގައި މައްސަލައެއް</string>
|
<string name="connectivity_server_error">%s އާ ގުޅުމުގައި މައްސަލައެއް އުޅޭ</string>
|
||||||
<string name="drag_to_reorder">ދަމާ ތަރުތީބު ބަދަލުކުރޭ</string>
|
<string name="drag_to_reorder">ތަރުތީބު ބަދަލުކުރުމަށް ދަމާލާ</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -269,6 +269,8 @@
|
|||||||
<string name="transfer_fahipay_amount_max">Maximum is MVR %1$s</string>
|
<string name="transfer_fahipay_amount_max">Maximum is MVR %1$s</string>
|
||||||
<string name="transfer_fahipay_gst_hint">%1$d%% GST is deducted from this amount</string>
|
<string name="transfer_fahipay_gst_hint">%1$d%% GST is deducted from this amount</string>
|
||||||
<string name="transfer_fahipay_gst_receive">Recipient receives MVR %1$s after %2$d%% GST</string>
|
<string name="transfer_fahipay_gst_receive">Recipient receives MVR %1$s after %2$d%% GST</string>
|
||||||
|
<string name="transfer_gst_added_hint">%1$d%% GST is charged on top of this amount</string>
|
||||||
|
<string name="transfer_gst_added_pay">You pay MVR %1$s with %2$d%% GST</string>
|
||||||
<string name="transfer_fahipay_phone_only">Fahipay transfers require a 7-digit phone number</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_my_accounts">My Accounts</string>
|
||||||
<string name="transfer_same_as_from">This is the same account as the sender</string>
|
<string name="transfer_same_as_from">This is the same account as the sender</string>
|
||||||
|
|||||||
+2
-2
@@ -17,5 +17,5 @@
|
|||||||
| [bmlapi/](bmlapi/README.md) | Bank of Maldives — hybrid web/OAuth login, dashboard, transfers, cards, QR payments, tap-to-pay |
|
| [bmlapi/](bmlapi/README.md) | Bank of Maldives — hybrid web/OAuth login, dashboard, transfers, cards, QR payments, tap-to-pay |
|
||||||
| [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 — number lookup for reload / bill pay |
|
| [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 for Raastas / bill pay |
|
| [ooredooapi/](ooredooapi/README.md) | Ooredoo Quick Pay — number validation, Raastas and bill pay by BML card |
|
||||||
|
|||||||
@@ -73,6 +73,8 @@ POST <ACS creq url> otpValue=<token TOTP> → auto-POST form (cres → gateway)
|
|||||||
POST <gateway callback> cres → auto-POST form (→ mpgsNotification)
|
POST <gateway callback> cres → auto-POST form (→ mpgsNotification)
|
||||||
POST transactions/mpgsNotification/<id> → records the verdict
|
POST transactions/mpgsNotification/<id> → records the verdict
|
||||||
POST …next-action POLL → TRANSACTION_CONFIRMED
|
POST …next-action POLL → TRANSACTION_CONFIRMED
|
||||||
|
GET transaction…/<id>?wait=1 → 302 merchant redirectUrl (?…&state=CONFIRMED&signature=…)
|
||||||
|
→ 302 merchant receipt page
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -203,8 +205,10 @@ no `action`; their JavaScript posts back to the **same creq URL**. So the creq U
|
|||||||
`destValue=token`, `selectChannel=token`, `authMethod=OOB`, `otpDest=`, `formReqType=SUBMIT`
|
`destValue=token`, `selectChannel=token`, `authMethod=OOB`, `otpDest=`, `formReqType=SUBMIT`
|
||||||
(keep the hidden `creq` / `otpChannels`).
|
(keep the hidden `creq` / `otpChannels`).
|
||||||
3. **POST channel** → the **OTP entry** page (`otpValue` input). Submit `otpValue=<BML token TOTP>`,
|
3. **POST channel** → the **OTP entry** page (`otpValue` input). Submit `otpValue=<BML token TOTP>`,
|
||||||
`formReqType=SUBMIT`. A wrong/expired code re-renders the OTP page with text containing
|
`formReqType=SUBMIT`. A wrong/expired code re-renders the OTP page (still with `otpValue`) and
|
||||||
*"incorrect"* / *"expired"* — regenerate the TOTP and retry once.
|
*"The OTP code you entered is incorrect Please try again."* — wait for the next TOTP window,
|
||||||
|
regenerate and retry once. Rejected twice, the payment stops ("The bank rejected the BML token
|
||||||
|
code"). The app also never sends a code with under 5 s left in its window.
|
||||||
4. On success the ACS returns a form auto-posting **`cres`** to the Mastercard gateway; the gateway
|
4. On success the ACS returns a form auto-posting **`cres`** to the Mastercard gateway; the gateway
|
||||||
returns a form auto-posting the result (`order.id`, `result=SUCCESS`, …) to
|
returns a form auto-posting the result (`order.id`, `result=SUCCESS`, …) to
|
||||||
**`transactions/mpgsNotification/<id>`**. Follow both so the verdict is recorded.
|
**`transactions/mpgsNotification/<id>`**. Follow both so the verdict is recorded.
|
||||||
@@ -223,8 +227,55 @@ Poll `next-action` until the recorded verdict surfaces:
|
|||||||
| `TRANSACTION_CONFIRMED` | Success |
|
| `TRANSACTION_CONFIRMED` | Success |
|
||||||
| `TRANSACTION_FAILED` | Declined |
|
| `TRANSACTION_FAILED` | Declined |
|
||||||
|
|
||||||
The merchant's own backend is also notified out-of-band (e.g.
|
**A decline after 3-D Secure doesn't arrive this way.** In the Ooredoo capture, the card passed
|
||||||
`fahipay.mv/api/bml/gateway/callback/?…state=CONFIRMED`).
|
3-D Secure (`mpgsNotification` got `result=SUCCESS`, `gatewayRecommendation=PROCEED`) and was then
|
||||||
|
declined for insufficient funds. The browser's `?wait=1` went to `?error=1` instead of the
|
||||||
|
merchant, and the transaction stayed payable: `state` still `QR_CODE_GENERATED`, `hasError: true`,
|
||||||
|
`allowRetry: true`, and a new `paymentErrorHistory` entry:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"date":"…","vendor":"mpgs","code":"INSUFFICIENT_FUNDS",
|
||||||
|
"reason":"Transaction declined due to insufficient funds",
|
||||||
|
"customerVisibleDescription":"Insufficient funds. Please use another card or payment method."}
|
||||||
|
```
|
||||||
|
|
||||||
|
The same link was then paid successfully after topping up the card. So while polling,
|
||||||
|
`BmlMerchantCardPayClient` also reads the transaction (the load PATCH,
|
||||||
|
`BmlMerchantTxnClient.paymentErrors`) and stops with `customerVisibleDescription` as soon as an
|
||||||
|
entry newer than the ones there before the attempt shows up.
|
||||||
|
|
||||||
|
## 7. Return to the merchant
|
||||||
|
|
||||||
|
The `mpgsNotification` response is a page whose script sends the browser to
|
||||||
|
`https://transaction.merchants.bankofmaldives.com.mv/<id>?wait=1`. Once the transaction is
|
||||||
|
confirmed, that 302s to the merchant's `redirectUrl` with a BML-signed result, then on to the
|
||||||
|
merchant's own receipt page:
|
||||||
|
|
||||||
|
```
|
||||||
|
GET transaction…/<id>?wait=1
|
||||||
|
→ 302 https://www.dhiraagu.com.mv/api/dhiraagu-bml-response.aspx?transactionId=<id>&state=CONFIRMED&signature=<sha1>
|
||||||
|
→ 302 https://www.dhiraagu.com.mv/services/reload-receipt?pyid=<paymentId> (bill pay: /services/bill-receipt)
|
||||||
|
```
|
||||||
|
|
||||||
|
(FahiPay's is `fahipay.mv/api/bml/gateway/callback/?…state=CONFIRMED`.)
|
||||||
|
|
||||||
|
Ooredoo's callback isn't a redirect: `my.ooredoo.mv/bml/response_new.php?…state=CONFIRMED` is a
|
||||||
|
200 page whose `<body onload="document.forms['wtmpay'].submit()">` posts the result
|
||||||
|
(`order_id`, `bml_transaction_id`, `bml_response=CONFIRMED`, `payment_status=success`, …) on to
|
||||||
|
`www.ooredoo.mv/ooredoo-prod/PaymentGateway/redirect/bml`, which lands on
|
||||||
|
`/payment-status?order_id=…&status=1`. `returnToMerchant` submits such auto-posting forms too
|
||||||
|
(up to 2).
|
||||||
|
|
||||||
|
**This hop is required.** It's how at least Dhiraagu learns it was paid: a test reload that
|
||||||
|
stopped at `TRANSACTION_CONFIRMED` charged the card but never topped up, and opening the
|
||||||
|
`?wait=1` URL in a browser afterwards delivered it. The signature is generated by BML, so the
|
||||||
|
hop can be replayed later from the transaction id alone.
|
||||||
|
|
||||||
|
`BmlMerchantCardPayClient` does it after every confirmed payment (`returnToMerchant`): a browser
|
||||||
|
UA GET that follows the redirects and auto-submitted forms, up to 3 tries, success = the chain
|
||||||
|
ends on a 2xx page. The
|
||||||
|
merchant host may be behind Cloudflare: plain `curl` got a 403 on `dhiraagu.com.mv`, okhttp got
|
||||||
|
through.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -243,6 +294,7 @@ learn of breakage from a failed live payment.
|
|||||||
| **Merchant detection** | BML adds other card providers (UnionPay, Apple/Google Pay); non-`mpgs` card provider | Misroute to the wrong flow |
|
| **Merchant detection** | BML adds other card providers (UnionPay, Apple/Google Pay); non-`mpgs` card provider | Misroute to the wrong flow |
|
||||||
| **`window.appData` parsing** | Key moved/obfuscated or made dynamically signed | No `pomeloJsKey` |
|
| **`window.appData` parsing** | Key moved/obfuscated or made dynamically signed | No `pomeloJsKey` |
|
||||||
| **Double-charge** | Confirm poll times out but the charge went through | Retry risks paying twice |
|
| **Double-charge** | Confirm poll times out but the charge went through | Retry risks paying twice |
|
||||||
|
| **Return to merchant** | The merchant's `redirectUrl` host blocks the client (Cloudflare) or is down | Charged but not delivered — `Success(merchantNotified = false)`, the app toasts the BML transaction id; opening `…/<id>?wait=1` in a browser delivers it |
|
||||||
|
|
||||||
**Maintenance:** re-capture a HAR whenever any party updates; expect to touch the ACS form parser
|
**Maintenance:** re-capture a HAR whenever any party updates; expect to touch the ACS form parser
|
||||||
most often; the flow is effectively untestable in CI (no deterministic 3-D Secure double). Keep the
|
most often; the flow is effectively untestable in CI (no deterministic 3-D Secure double). Keep the
|
||||||
|
|||||||
@@ -0,0 +1,175 @@
|
|||||||
|
# Reload (Easy TopUp, paid by BML card)
|
||||||
|
|
||||||
|
Top up a Dhiraagu prepaid number through the dhiraagu.com.mv **Easy TopUp** page. Dhiraagu only
|
||||||
|
builds the order: the money moves on a **BML Merchant Services transaction** that Dhiraagu creates
|
||||||
|
for it, which is then paid exactly like any card-only BML merchant link
|
||||||
|
([BML API → Merchant Card Payment](../bmlapi/16-card-payment.md)).
|
||||||
|
|
||||||
|
Reconstructed from `docs/dhiraaguapi/tmp/dhiraagu_reload_gateway.md` (a Firefox HAR).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Flow overview
|
||||||
|
|
||||||
|
```
|
||||||
|
GET /services/easy-topup → nonce #1
|
||||||
|
POST cart&act=recharge (nonce #1) → cartId
|
||||||
|
GET /services/payment-v2?cartid=<cartId> → nonce #2
|
||||||
|
POST merchant&act=form (nonce #2) → BML gateway's merchantId
|
||||||
|
POST payment&act=create (nonce #2) → paymentId, oid
|
||||||
|
POST bml&act=createV2 (nonce #2) → BML transaction url ──┐
|
||||||
|
│
|
||||||
|
── from here: the BML card-only merchant flow ── │
|
||||||
|
GET transaction.merchants…/<id>/paynow ←─────────────────────────────┘
|
||||||
|
… Pomelo tokenise, next-action, Wibmo 3-D Secure, MPGS … → TRANSACTION_CONFIRMED
|
||||||
|
GET transaction.merchants…/<id>?wait=1 → 302 dhiraagu-bml-response.aspx (tops up)
|
||||||
|
→ 302 /services/reload-receipt
|
||||||
|
```
|
||||||
|
|
||||||
|
After the payment the browser is sent `transaction…/<id>?wait=1` →
|
||||||
|
`dhiraagu-bml-response.aspx?transactionId=<id>&state=CONFIRMED&signature=…` →
|
||||||
|
`/services/reload-receipt?pyid=<paymentId>`. **This is what makes Dhiraagu top up the number** —
|
||||||
|
a payment that stopped at BML's `TRANSACTION_CONFIRMED` was charged but not delivered until that
|
||||||
|
URL was opened. The card flow follows it for every merchant, see
|
||||||
|
[BML API → Return to the merchant](../bmlapi/16-card-payment.md#7-return-to-the-merchant).
|
||||||
|
|
||||||
|
**Recovering a stuck reload:** open `https://transaction.merchants.bankofmaldives.com.mv/<id>?wait=1`
|
||||||
|
in a browser. BML signs the callback, so the transaction id is all that's needed. (`curl` gets a
|
||||||
|
Cloudflare 403 on the Dhiraagu hop; a browser works.)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Common
|
||||||
|
|
||||||
|
All API calls are `POST https://www.dhiraagu.com.mv/api/sdk-dhr-webapi.ashx?website_id=CA2BB809-3A22-485B-A518-DA6B6DE653A5&sub=<sub>&act=<act>`
|
||||||
|
with a JSON body and these headers:
|
||||||
|
|
||||||
|
| Header | Value |
|
||||||
|
|---|---|
|
||||||
|
| `User-Agent` | a browser UA (same as [Number Lookup](01-number-lookup.md)) |
|
||||||
|
| `Content-Type` | `application/json` |
|
||||||
|
| `X-Requested-With` | `XMLHttpRequest` |
|
||||||
|
| `Origin` | `https://www.dhiraagu.com.mv` |
|
||||||
|
| `nonce` | `var nonce = "…"` from the page that makes the call |
|
||||||
|
|
||||||
|
Every response is `{"respStatus":"OK","resp":…}` on success.
|
||||||
|
|
||||||
|
Each page has its own nonce: the cart call uses the Easy TopUp page's, the rest use the payment
|
||||||
|
page's.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Settings (optional)
|
||||||
|
|
||||||
|
`GET …&sub=setting&act=reload` — the page reads its limits from here. Thijooree hardcodes them.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"gstRate":0.08,"dailyLimit":3000,
|
||||||
|
"amountLimit":{"min":20,"max":1080,"message":"Enter a whole number amount between MVR 20 and 1000"},
|
||||||
|
"reloadPerDay":{"easyTopUp":4,"myAccount":6}, …}
|
||||||
|
```
|
||||||
|
|
||||||
|
| Rule | Value |
|
||||||
|
|---|---|
|
||||||
|
| Amount | whole MVR, **20 – 1000** (the message says 1000; `max` says 1080 — Thijooree uses 1000) |
|
||||||
|
| GST | 8%, **included** in the amount |
|
||||||
|
| Per day | MVR 3000, 4 Easy TopUps |
|
||||||
|
|
||||||
|
GST, as the page works it out: `gst = round2(amount × 0.08 / 1.08)`, credited `amount − gst`
|
||||||
|
(MVR 20 → GST 1.48, credited 18.52).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Cart
|
||||||
|
|
||||||
|
`sub=cart&act=recharge`, nonce from `GET /services/easy-topup`.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"formId":2,"serviceNumber":"7XXXXXX","amount":20,"amountGST":1.48,"amountRecharge":18.52,
|
||||||
|
"gstRate":0.08,"memberId":"","memberName":"","memberNId":"","customerId":"","customerCode":"","version":2}
|
||||||
|
```
|
||||||
|
```json
|
||||||
|
{"cartId":"002773ed-…","formId":2,"cartAmount":20.00,"cartExpiry":"…",
|
||||||
|
"paymentUrl":"https://www.dhiraagu.com.mv/services/payment-v2?cartid=002773ed-…", …}
|
||||||
|
```
|
||||||
|
|
||||||
|
The page also calls `sub=dhiraaguIO&act=infoSubscriberStatus` (`{"number"}`) before this, to show
|
||||||
|
the number's status and balance. Thijooree skips it — [Number Lookup](01-number-lookup.md) has
|
||||||
|
already confirmed a prepaid number.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Payment gateway
|
||||||
|
|
||||||
|
`sub=merchant&act=form`, `{"formId":2}`, nonce from `GET /services/payment-v2?cartid=<cartId>`.
|
||||||
|
Lists the gateways; **`gatewayId: 1` is Bank of Maldives** (2 = MIB, 3 = DhiraaguPay).
|
||||||
|
|
||||||
|
```json
|
||||||
|
[{"merchantId":"98de333c-…","merchantId2":"3f5cf6b7-…","formId":2,"gatewayId":1,
|
||||||
|
"gatewayName":"Bank of Maldives", …}, …]
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Payment
|
||||||
|
|
||||||
|
`sub=payment&act=create`
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"formId":2,"cartId":"<cartId>","gatewayId":1,"dhiraaguPayNumber":"","amount":"20.00",
|
||||||
|
"paymentMerchantId":"<BML merchantId>","memberId":"","tokenize":"","paymentType":"",
|
||||||
|
"recurringFrequency":"","bmlTokenId":""}
|
||||||
|
```
|
||||||
|
```json
|
||||||
|
{"paymentId":"3ea4351b-…","oid":"ET20260006911381","gatewayId":1,"amount":20.00,"paymentStatus":0, …}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. BML transaction
|
||||||
|
|
||||||
|
`sub=bml&act=createV2`, `{"paymentId":"<paymentId>"}`. Returns the BML Merchant Services
|
||||||
|
transaction (amounts in cents):
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"state":"INITIATED","amount":2000,"currency":"MVR","localId":"ET20260006911381",
|
||||||
|
"url":"https://transaction.merchants.bankofmaldives.com.mv/6abfec7afd7f4a4360fc1df4",
|
||||||
|
"redirectUrl":"https://www.dhiraagu.com.mv/api/dhiraagu-bml-response.aspx",
|
||||||
|
"expires":"…(10 min)…","customerReference":"WebApp - Topup", …}
|
||||||
|
```
|
||||||
|
|
||||||
|
The 24-hex id at the end of `url` is the transaction. Its `/paynow` page offers **UnionPay +
|
||||||
|
MPGS cards only, no BML Pay**, so it's paid by card + 3-D Secure.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Reload record
|
||||||
|
|
||||||
|
`sub=reload&act=list`, `{"paymentId"}`, nonce from the receipt page — what the receipt page shows:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"oid":"ET20260006911381","transId":"<BML txn id>","serviceNumber":"7XXXXXX",
|
||||||
|
"amountPay":20.00,"amountTopup":18.52,"amountGST":1.48,
|
||||||
|
"paidStatus":1,"topupStatus":1,"reloadStatusDesc":"Successful", …}
|
||||||
|
```
|
||||||
|
|
||||||
|
Not used by Thijooree yet.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Cloudflare
|
||||||
|
|
||||||
|
`www.dhiraagu.com.mv` is behind Cloudflare. The browser capture carries a `cf_clearance` cookie,
|
||||||
|
but [Number Lookup](01-number-lookup.md) already works from the app with plain okhttp and a browser
|
||||||
|
UA, so these calls are made the same way.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Related:** [Number Lookup](01-number-lookup.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 Lookup](01-number-lookup.md) · [Bill Pay →](03-bill-pay.md)
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
# Bill Pay (Easy Pay, paid by BML card)
|
||||||
|
|
||||||
|
Pay a Dhiraagu postpaid bill through the dhiraagu.com.mv **Easy Pay** page. Like
|
||||||
|
[Reload](02-reload.md), Dhiraagu only builds the order and the money moves on a **BML Merchant
|
||||||
|
Services transaction** paid by card + 3-D Secure
|
||||||
|
([BML API → Merchant Card Payment](../bmlapi/16-card-payment.md)). From the payment page on,
|
||||||
|
the two flows are identical; only the first page, the cart call and the form id differ.
|
||||||
|
|
||||||
|
Reconstructed from `docs/dhiraaguapi/tmp/dhiraagu_billpay_gateway.har` (a Firefox HAR) and the
|
||||||
|
Easy Pay page's inline script.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Flow overview
|
||||||
|
|
||||||
|
```
|
||||||
|
GET /services/easy-pay → nonce #1
|
||||||
|
GET setting&act=bill (nonce #1) → blocked account statuses / customer types
|
||||||
|
POST dhiraaguIO&act=infoUnlisted (nonce #1) → accountNumber, type, accountStatus, customerType
|
||||||
|
POST cart&act=easyPay (nonce #1) → cartId
|
||||||
|
GET /services/payment-v2?cartid=<cartId> → nonce #2
|
||||||
|
POST merchant&act=form {"formId":1} → BML gateway's merchantId
|
||||||
|
POST payment&act=create (formId 1) → paymentId, oid (EP…)
|
||||||
|
POST bml&act=createV2 → BML transaction url
|
||||||
|
── from here: the BML card-only merchant flow ──
|
||||||
|
GET transaction.merchants…/<id>?wait=1 → 302 dhiraagu-bml-response.aspx (posts the payment)
|
||||||
|
→ 302 /services/bill-receipt?pyid=<paymentId>
|
||||||
|
```
|
||||||
|
|
||||||
|
As with reload, the `?wait=1` return hop is what tells Dhiraagu it was paid.
|
||||||
|
|
||||||
|
Common headers and the `{"respStatus":"OK","resp":…}` envelope are as in
|
||||||
|
[Reload → Common](02-reload.md#common).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Settings
|
||||||
|
|
||||||
|
`GET …&sub=setting&act=bill` (no body, so a GET) — rules the page checks the lookup against:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"settingAppJson1":{"accountStatus":{"val":["F"],…},"customerType":{"val":["P"],…}}}
|
||||||
|
```
|
||||||
|
|
||||||
|
A number whose `accountStatus` or `customerType` is in a `val` list is refused before ordering
|
||||||
|
("Payment for this service could not be accepted… [Account Status: F]" / "The number is not
|
||||||
|
allowed. [Customer Type: P]"). Thijooree applies the same rules, skipping them if the call fails.
|
||||||
|
|
||||||
|
`setting&act=maintenance` has `public.easyPay` — `"Y"` means the page is under maintenance.
|
||||||
|
Not checked.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Lookup
|
||||||
|
|
||||||
|
`sub=dhiraaguIO&act=infoUnlisted`, `{"number":"7XXXXXX"}` — the same call as
|
||||||
|
[Number Lookup](01-number-lookup.md), but the bill payment needs more of its answer:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"respStatus":"OK","accountNumber":"1466154","accountStatus":"W","customerType":"S",
|
||||||
|
"type":"BillPayment","serviceDetails":[{"unlisted":"N","prepaidIndicator":"N"}],
|
||||||
|
"accountOwnerInfo":{"name":"…"}}
|
||||||
|
```
|
||||||
|
|
||||||
|
Note the fields are at the top level, not under `resp`.
|
||||||
|
|
||||||
|
| Field | Use |
|
||||||
|
|---|---|
|
||||||
|
| `accountNumber` | the billing account the cart is made out to |
|
||||||
|
| `type` | `BillPayment`, or `writeOffPayments` for a written-off account — sent as `billType` |
|
||||||
|
| `prepaidIndicator` | `"Y"` is refused ("Prepaid number is not allowed.") |
|
||||||
|
|
||||||
|
The page also accepts the account number itself in place of a service number (then
|
||||||
|
`serviceNumber` is sent empty); Thijooree only pays by phone number.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Cart
|
||||||
|
|
||||||
|
`sub=cart&act=easyPay`, nonce from `GET /services/easy-pay`.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"formId":1,"serviceNumber":"7XXXXXX","accountNumber":"1466154","amount":"1.05",
|
||||||
|
"memberId":"","memberName":"","memberNId":"","billRef":"","billType":"BillPayment"}
|
||||||
|
```
|
||||||
|
```json
|
||||||
|
{"cartId":"8fc33fa4-…","formId":1,"cartJson":[{"accountNumber":"1466154","serviceNumber":"7XXXXXX",
|
||||||
|
"amount":1.05,"billRef":"","billType":"BillPayment"}],"cartAmount":1.05,"cartExpiry":"…(20 min)…",
|
||||||
|
"paymentUrl":"https://www.dhiraagu.com.mv/services/payment-v2?cartid=8fc33fa4-…", …}
|
||||||
|
```
|
||||||
|
|
||||||
|
| Rule | Value |
|
||||||
|
|---|---|
|
||||||
|
| Amount | any positive amount, up to 2 decimal places (the page's only check). No min / max. |
|
||||||
|
| GST | none |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Payment page
|
||||||
|
|
||||||
|
Same as [Reload §3–5](02-reload.md#3-payment-gateway) with `formId: 1`:
|
||||||
|
|
||||||
|
- `merchant&act=form` lists DhiraaguPay (3), Bank of Maldives (1) and MIB (2) for "Easy Pay".
|
||||||
|
The BML `merchantId` is the same as reload's.
|
||||||
|
- `payment&act=create` returns an `oid` starting `EP` (reload's start `ET`).
|
||||||
|
- `bml&act=createV2` returns the transaction with `"customerReference":"WebApp - Easy Pay"` and
|
||||||
|
the same `redirectUrl`. Its page is card-only, no BML Pay.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Bill record
|
||||||
|
|
||||||
|
`sub=bill&act=list`, `{"paymentId"}`, nonce from the receipt page — what the receipt shows:
|
||||||
|
|
||||||
|
```json
|
||||||
|
[{"oid":"EP20260006911918","transId":"<BML txn id>","accountNumber":"1466154","serviceNumber":"7XXXXXX",
|
||||||
|
"amount":1.05,"billStatus":1,"paidStatus":1,"cbsStatus":1,"cbsReceipt":"EP…-130","billType":"BillPayment", …}]
|
||||||
|
```
|
||||||
|
|
||||||
|
Not used by Thijooree yet.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Related:** [Number Lookup](01-number-lookup.md) · [Reload](02-reload.md) ·
|
||||||
|
[BML Merchant Card Payment](../bmlapi/16-card-payment.md) ·
|
||||||
|
App side: [Transfer Flows](../thijooree/20-transfer-flows.md#carrier-services-by-bml-card)
|
||||||
|
|
||||||
|
[← Reload](02-reload.md)
|
||||||
@@ -96,6 +96,8 @@ The API only returns a valid result for numbers currently on the Dhiraagu networ
|
|||||||
| # | File | Description |
|
| # | File | Description |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| 1 | [Number Lookup](01-number-lookup.md) | Validate a Dhiraagu number and determine account type |
|
| 1 | [Number Lookup](01-number-lookup.md) | Validate a Dhiraagu number and determine account type |
|
||||||
|
| 2 | [Reload](02-reload.md) | Easy TopUp order → BML merchant transaction, paid by card |
|
||||||
|
| 3 | [Bill Pay](03-bill-pay.md) | Easy Pay order → BML merchant transaction, paid by card |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -129,4 +129,4 @@ Always fall back to the other provider's lookup if this API returns `custType: n
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
[← README](README.md)
|
[← README](README.md) · [Raastas →](02-raastas.md)
|
||||||
|
|||||||
@@ -0,0 +1,107 @@
|
|||||||
|
# Raastas (Quick Pay recharge, paid by BML card)
|
||||||
|
|
||||||
|
Recharge an Ooredoo prepaid number through the ooredoo.mv **Quick Pay** page. Ooredoo creates the
|
||||||
|
order and hands back a **BML Merchant Services transaction**, which is paid like any card-only
|
||||||
|
BML merchant link ([BML API → Merchant Card Payment](../bmlapi/16-card-payment.md)).
|
||||||
|
|
||||||
|
Reconstructed from `docs/ooredooapi/tmp/ooredoo_raastas_bml_card.har` (a Firefox HAR, which also
|
||||||
|
holds a rejected OTP and an insufficient-funds decline on the same transaction).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Flow overview
|
||||||
|
|
||||||
|
```
|
||||||
|
GET /ooredoo-prod/QuickPayPackage/v1/numberTypeValidation?… → custType PRE (Number Validation)
|
||||||
|
POST /ooredoo-prod/PaymentGateway/bml → orderID, bmlUrl ──┐
|
||||||
|
│
|
||||||
|
── from here: the BML card-only merchant flow ── │
|
||||||
|
GET transaction.merchants…/<id> ←──────────────────────────────────────────────────┘
|
||||||
|
… Pomelo tokenise, next-action, Wibmo 3-D Secure, MPGS … → TRANSACTION_CONFIRMED
|
||||||
|
GET transaction.merchants…/<id>?wait=1 → 302 my.ooredoo.mv/bml/response_new.php?…state=CONFIRMED
|
||||||
|
(200, auto-submits) → POST www.ooredoo.mv/ooredoo-prod/PaymentGateway/redirect/bml
|
||||||
|
→ /payment-status?order_id=<orderID>&statusDesc=sucess&status=1
|
||||||
|
```
|
||||||
|
|
||||||
|
Unlike Dhiraagu, there's no nonce or cart: one POST makes the order and the BML transaction.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Order
|
||||||
|
|
||||||
|
`POST https://www.ooredoo.mv/ooredoo-prod/PaymentGateway/bml`
|
||||||
|
|
||||||
|
| Header | Value |
|
||||||
|
|---|---|
|
||||||
|
| `Content-Type` | `application/json` |
|
||||||
|
| `Accept` | `application/json` |
|
||||||
|
| `Origin` | `https://www.ooredoo.mv` |
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"msisdn":"9609XXXXXX","purchaseAmount":"21.60","amountWithoutGst":"20",
|
||||||
|
"receiverMsisdn":"9609XXXXXX","transType":"recharge","serviceType":"prepaid",
|
||||||
|
"serviceTypeDisplayName":"Mobile"}
|
||||||
|
```
|
||||||
|
```json
|
||||||
|
{"status":"OK","msg":"Successfully generated order id","code":"2000",
|
||||||
|
"data":{"orderID":"36447930","purchaseAmount":"2160","hashSignature":"…",
|
||||||
|
"shortUrl":"https://pay.bml.com.mv/7A86qLZ1QV",
|
||||||
|
"bmlUrl":"https://transaction.merchants.bankofmaldives.com.mv/6ac009f7bd9b264b80ba6abf", …}}
|
||||||
|
```
|
||||||
|
|
||||||
|
The 24-hex id at the end of `bmlUrl` is the transaction (`shortUrl` 301s to the same page). The
|
||||||
|
page is card-only, no BML Pay. The transaction's `localId` is the MSISDN, `customerReference` the
|
||||||
|
order id, and it expires after 7 days.
|
||||||
|
|
||||||
|
### GST
|
||||||
|
|
||||||
|
**Added on top**, not taken out: the number is credited `amountWithoutGst` and the card pays
|
||||||
|
`purchaseAmount = amount + toFixed2(amount × 8 / 100)` (MVR 20 → 21.60). The page's payment
|
||||||
|
method list gives `gstPercent: 8` for BML. This is the opposite of Raastas through Fahipay, where
|
||||||
|
GST comes out of the amount.
|
||||||
|
|
||||||
|
### Limits
|
||||||
|
|
||||||
|
Whole MVR, minimum **20** (before GST, so the card pays at least 21.60). The maximum isn't known;
|
||||||
|
the capture starts on the payment page.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Return to Ooredoo
|
||||||
|
|
||||||
|
`?wait=1` 302s to `https://my.ooredoo.mv/bml/response_new.php?transactionId=<id>&state=CONFIRMED&signature=<hash>`.
|
||||||
|
That page is a 200 that auto-submits:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<body onload="document.forms['wtmpay'].submit()">
|
||||||
|
<form method="POST" action="https://www.ooredoo.mv/ooredoo-prod/PaymentGateway/redirect/bml" name="wtmpay">
|
||||||
|
order_id=36447930 amount=21.60000000 msisdn=9609XXXXXX transtype=2
|
||||||
|
bml_transaction_id=<id> bml_hash=<hash> bml_response=CONFIRMED
|
||||||
|
error_code=0 payment_status=success ptype=bml
|
||||||
|
```
|
||||||
|
|
||||||
|
which ends on `https://www.ooredoo.mv/payment-status?order_id=36447930&statusDesc=sucess&status=1`
|
||||||
|
(the receipt: `totalAmount 21.6`, `amountWithoutGst 20`, `gstAmt 1.6`). The card flow submits the
|
||||||
|
form, see [BML API → Return to the merchant](../bmlapi/16-card-payment.md#7-return-to-the-merchant).
|
||||||
|
|
||||||
|
A declined attempt sends `?wait=1` to `transaction…/<id>?error=1` instead, and the same
|
||||||
|
transaction can be paid again.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Cloudflare
|
||||||
|
|
||||||
|
`ooredoo.mv` and `my.ooredoo.mv` are behind Cloudflare. The capture carries a `cf_clearance`
|
||||||
|
cookie; okhttp from the phone gets through without one, as with
|
||||||
|
[Number Validation](01-number-validation.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)
|
||||||
|
|
||||||
|
[← Number Validation](01-number-validation.md) · [Bill Pay →](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…/<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.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**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)
|
||||||
@@ -81,6 +81,8 @@ The API expects the full MSISDN including country code `960` (e.g. `9609654321`)
|
|||||||
| # | File | Description |
|
| # | File | Description |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| 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 |
|
||||||
|
| 3 | [Bill Pay](03-bill-pay.md) | Quick Pay bill payment order → BML merchant transaction, paid by card |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Transfer
|
# Transfer
|
||||||
|
|
||||||
The transfer screen initiates account-to-account fund transfers. It supports MIB, BML, and Fahipay as source banks and handles all bank-specific authentication and OTP steps.
|
The transfer screen initiates account-to-account fund transfers and phone payments. It supports MIB, BML, Fahipay and M-Faisa as sources and handles all bank-specific authentication and OTP steps. A phone number can also be paid as a carrier service (reload, Raastas, bill pay) from the Fahipay wallet or, for Dhiraagu Reload, a verified BML card.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -38,11 +38,12 @@ A dropdown lists all visible accounts parsed via `AccountListParser.from(acc)?.b
|
|||||||
|
|
||||||
## Recipient Entry
|
## Recipient Entry
|
||||||
|
|
||||||
The user can specify a recipient in three ways:
|
The user can specify a recipient in these ways:
|
||||||
|
|
||||||
1. **Manual entry** — type an account number directly
|
1. **Manual entry** — type an account number or phone number directly
|
||||||
2. **Contact picker** — opens `ContactPickerSheetFragment` to select a saved contact
|
2. **Contact picker** — opens `ContactPickerSheetFragment` to select a saved contact. A Fahipay favourite opens as its payout service straight away
|
||||||
3. **QR scan** — launches [QrScannerActivity](25-qr-scanner.md); a PayMV QR result pre-fills the account number, amount, and remarks; a BML ebanking / pay.bml URL switches the form into [BML QR merchant payment](20-transfer-flows.md#bml-qr-merchant-payment-flow) mode
|
3. **QR scan** — launches [QrScannerActivity](25-qr-scanner.md); a PayMV QR result pre-fills the account number, amount, and remarks; a BML ebanking / pay.bml URL switches the form into [BML QR merchant payment](20-transfer-flows.md#bml-qr-merchant-payment-flow) mode
|
||||||
|
4. **BML Merchant Services transaction ID or link** — paid through BML Pay (QR flow) or, for card-only merchants, a verified card ([Card Verification & Merchant Card Pay](29-card-verification-and-merchant-card-pay.md))
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -64,10 +65,22 @@ After the user finishes entering a recipient account number, the app calls the s
|
|||||||
|
|
||||||
- **MIB**: account name lookup via MIB API
|
- **MIB**: account name lookup via MIB API
|
||||||
- **BML**: beneficiary lookup via BML API
|
- **BML**: beneficiary lookup via BML API
|
||||||
- **Fahipay**: account name resolution via Fahipay API
|
- **Fahipay**: phone numbers only — the Dhiraagu / Ooredoo carrier lookup decides which payout services apply
|
||||||
|
|
||||||
The resolved name is displayed below the account number field for the user to confirm.
|
The resolved name is displayed below the account number field for the user to confirm.
|
||||||
|
|
||||||
|
### Phone numbers — Transfer Type picker
|
||||||
|
|
||||||
|
A phone number searched with no source yet (or from a BML card that can pay by card) is looked up every way it can be paid, in parallel: Favara (MIB / BML), and the carrier lookup when the user has a Fahipay wallet or a verified BML card. Each result is a **transfer type**:
|
||||||
|
|
||||||
|
| Type | Example | Pays from |
|
||||||
|
|---|---|---|
|
||||||
|
| 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, 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).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Biometric Gate
|
## Biometric Gate
|
||||||
@@ -100,18 +113,33 @@ When the source is a BML USD account and the destination is a MIB account but no
|
|||||||
5. Re-submits with OTP
|
5. Re-submits with OTP
|
||||||
6. On success, shows `TransferReceiptFragment`
|
6. On success, shows `TransferReceiptFragment`
|
||||||
|
|
||||||
### Fahipay Transfer
|
### Fahipay Payout (reload, Raastas, bill pay)
|
||||||
|
|
||||||
1. Validates fields
|
1. Checks the amount against the picked service's rules
|
||||||
2. (If biometric gate) prompts biometrics
|
2. Confirm dialog (with the GST note for Raastas), then the biometric gate if enabled
|
||||||
3. Submits via Fahipay API using stored `authID` + session cookie
|
3. One POST to the service's Fahipay payment endpoint ([Fahipay API → Payments](../fahipayapi/09-payments.md))
|
||||||
4. On success, shows `TransferReceiptFragment`
|
4. On success, the result shows inside the dialog (no receipt page yet), then the form clears
|
||||||
|
|
||||||
|
See [Transfer Flows → Fahipay source](20-transfer-flows.md#fahipay-source).
|
||||||
|
|
||||||
|
### 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; 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
|
||||||
|
|
||||||
|
See [Transfer Flows → Carrier services by BML card](20-transfer-flows.md#carrier-services-by-bml-card).
|
||||||
|
|
||||||
|
### BML Merchant Payment (QR / card-only link)
|
||||||
|
|
||||||
|
A BML QR, or a BML Merchant Services link whose merchant takes BML Pay, is paid from a BML card through the QR flow. A card-only merchant link is paid with a verified card (Pomelo + 3-D Secure), followed by the return to the merchant. Neither saves a receipt. See [Transfer Flows → BML QR Merchant Payment Flow](20-transfer-flows.md#bml-qr-merchant-payment-flow) and [Card Verification & Merchant Card Pay](29-card-verification-and-merchant-card-pay.md).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Transfer Receipt
|
## Transfer Receipt
|
||||||
|
|
||||||
On success the fragment navigates to `TransferReceiptFragment` passing the completed transfer details.
|
On success of a bank transfer (MIB, BML, M-Faisa) the fragment navigates to `TransferReceiptFragment` passing the completed transfer details. Fahipay payouts, card services and merchant payments show their result inside the confirm dialog instead.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -49,11 +49,13 @@ Each option is a `TransferType` (`ui/home/transfer/TransferType.kt`):
|
|||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| `Favara(info)` | Favara Transfer | `favara_logo` | MIB or BML account |
|
| `Favara(info)` | Favara Transfer | `favara_logo` | MIB or BML account |
|
||||||
| `Fahipay(service, ownerName)` | the service's label, e.g. Raastas | `FahipayService.iconRes`: `ooredoo_logo` / `dhiraagu_logo` | Fahipay wallet |
|
| `Fahipay(service, ownerName)` | the service's label, e.g. Raastas | `FahipayService.iconRes`: `ooredoo_logo` / `dhiraagu_logo` | Fahipay wallet |
|
||||||
|
| `Card(service, ownerName, cards)` | the service's label, e.g. Dhiraagu Reload | `CardPayoutService.iconRes`, with a BML badge | One of `cards`: verified BML cards that can pay by card (see [Carrier services by BML card](#carrier-services-by-bml-card)) |
|
||||||
|
|
||||||
Picking an option also picks the source (`TransferFragment.applyTransferType`). If the selected source can't pay that type, Thijooree switches it:
|
Picking an option also picks the source (`TransferFragment.applyTransferType`). If the selected source can't pay that type, Thijooree switches it:
|
||||||
|
|
||||||
- **Favara:** the default account, if it's MIB or BML. Otherwise the source is cleared and the user is asked to pick one.
|
- **Favara:** the default account, if it's MIB or BML. Otherwise the source is cleared and the user is asked to pick one.
|
||||||
- **Fahipay:** the user's Fahipay wallet.
|
- **Fahipay:** the user's Fahipay wallet.
|
||||||
|
- **Card:** the default card, if it's one of the type's cards. Otherwise the first of them.
|
||||||
|
|
||||||
Then the recipient card is filled in. The options, the number they were looked up for and the pick are kept in `TransferDraft`. If the view is recreated while the popup is still unanswered, it opens again.
|
Then the recipient card is filled in. The options, the number they were looked up for and the pick are kept in `TransferDraft`. If the view is recreated while the popup is still unanswered, it opens again.
|
||||||
|
|
||||||
@@ -64,9 +66,11 @@ The options are dropped when the "To" number is edited, the recipient is cleared
|
|||||||
Two lookups run in parallel:
|
Two lookups run in parallel:
|
||||||
|
|
||||||
- **Favara / IPS lookup.** Uses any logged-in MIB or BML session. The default account's bank goes first, the other is the fallback.
|
- **Favara / IPS lookup.** Uses any logged-in MIB or BML session. The default account's bank goes first, the other is the fallback.
|
||||||
- **Carrier lookup** (see Fahipay source below). Only runs when the user has a Fahipay wallet.
|
- **Carrier lookup** (`CarrierLookup.query`, see Fahipay source below). Only runs when the user has a Fahipay wallet or a BML card that can pay by card. One lookup feeds both.
|
||||||
|
|
||||||
Everything that resolves is offered as a transfer type. Favara comes first, then the Fahipay services. If nothing resolves, the Favara lookup's error is shown as a toast.
|
Everything that resolves is offered as a transfer type. Favara comes first, then the Fahipay services, then the card services.
|
||||||
|
|
||||||
|
The same lookup runs when the source is already a BML card that can pay by card and a phone number is searched. If nothing resolves, the Favara lookup's error is shown as a toast.
|
||||||
|
|
||||||
Any other input with no source selected falls back to the default account as the source, and then the normal lookup for that bank runs.
|
Any other input with no source selected falls back to the default account as the source, and then the normal lookup for that bank runs.
|
||||||
|
|
||||||
@@ -101,7 +105,7 @@ Each Fahipay favourites list is one payout service (`FahipayService.contactCateg
|
|||||||
| `FAHIPAY_OOREDOO_BILL` | Ooredoo Bill Pay |
|
| `FAHIPAY_OOREDOO_BILL` | Ooredoo Bill Pay |
|
||||||
| `FAHIPAY_DHIRAAGU_BILL` | Dhiraagu Bill Pay |
|
| `FAHIPAY_DHIRAAGU_BILL` | Dhiraagu Bill Pay |
|
||||||
|
|
||||||
So picking a favourite skips the carrier lookup and the picker. That service is offered as the only transfer type, so it's picked straight away (`TransferFragment.applyFahipayContact`). As with a searched number, that switches the source to the Fahipay wallet and applies the service's amount rules. This happens wherever a favourite is picked:
|
So picking a favourite skips the carrier lookup and the picker. That service is offered as the only transfer type, so it's picked straight away (`TransferFragment.applyServiceContact`). As with a searched number, that switches the source to the Fahipay wallet and applies the service's amount rules. This happens wherever a favourite is picked:
|
||||||
|
|
||||||
- the contact picker sheet (the row's category goes back as `ContactPickerSheetFragment.KEY_CATEGORY`)
|
- the contact picker sheet (the row's category goes back as `ContactPickerSheetFragment.KEY_CATEGORY`)
|
||||||
- the "To" field's search-as-you-type dropdown
|
- the "To" field's search-as-you-type dropdown
|
||||||
@@ -144,6 +148,74 @@ When the amount breaks a rule, the error replaces the helper text.
|
|||||||
|
|
||||||
None of the Fahipay services take a reference. Picking one clears the Reference field and disables it, the same way BML merchant QR payments do. Clearing the service turns the field back on.
|
None of the Fahipay services take a reference. Picking one clears the Reference field and disables it, the same way BML merchant QR payments do. Clearing the service turns the field back on.
|
||||||
|
|
||||||
|
### Carrier services by BML card
|
||||||
|
|
||||||
|
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**, **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
|
||||||
|
[Card Verification & Merchant Card Pay](29-card-verification-and-merchant-card-pay.md)). The
|
||||||
|
type remembers those cards (`TransferType.Card.cards`). Picking a card that isn't one of them
|
||||||
|
drops the pick, like any other source that can't pay the picked type.
|
||||||
|
|
||||||
|
**Which services.**
|
||||||
|
|
||||||
|
| Carrier result | Service |
|
||||||
|
|---|---|
|
||||||
|
| 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`:
|
||||||
|
|
||||||
|
| Service | Min (MVR) | Max (MVR) | Decimals | GST |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| 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. 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
|
||||||
|
paid ("You pay MVR 21.60 with 8% GST"). The order check in step 2 below compares against
|
||||||
|
`chargedWithGst`.
|
||||||
|
|
||||||
|
**Reference.** None. The field is cleared and disabled, as for the Fahipay services.
|
||||||
|
|
||||||
|
**Recents.** As with Fahipay services, the recent is saved with the service's category
|
||||||
|
(`CardPayoutService.contactCategory`: `CARD_DHIRAAGU_RELOAD`, `CARD_DHIRAAGU_BILL`,
|
||||||
|
`CARD_RAASTAS`, `CARD_OOREDOO_BILL`). Picking it again applies that service with no lookup, so
|
||||||
|
the default card (or another payable card) is selected rather than the default account
|
||||||
|
(`TransferFragment.applyServiceContact`). With no payable card left, it toasts and stops. A number
|
||||||
|
keeps one recent, so paying it another way replaces the category.
|
||||||
|
|
||||||
|
**Sending.** The only part that differs from paying a card-only BML merchant link is where the
|
||||||
|
BML transaction comes from:
|
||||||
|
|
||||||
|
1. `CardPayoutTransferHandler.submit()` has the carrier create it for the number and amount
|
||||||
|
(`DhiraaguPaymentClient.createReloadTransaction` / `createBillPayTransaction`,
|
||||||
|
`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), [→ 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.
|
||||||
|
2. Its payment page is loaded (`BmlMerchantTxnClient.fetchPayPage`). If it doesn't take cards, or
|
||||||
|
its amount isn't the one typed, the payment stops with a toast.
|
||||||
|
3. The page goes to `BmlTransferHandler.confirmCardMerchant`, so from here it's the merchant-link
|
||||||
|
card flow: the same confirm dialog and warning, biometric gate, Pomelo + 3-D Secure payment,
|
||||||
|
and success / failure handling.
|
||||||
|
|
||||||
|
Nothing is charged before the confirm dialog. A cancelled confirm leaves an unpaid Dhiraagu order,
|
||||||
|
which expires on its own.
|
||||||
|
|
||||||
### BML source
|
### BML source
|
||||||
|
|
||||||
1. If the input type is `MIB_ACCOUNT`, calls `BmlValidateClient.verifyMibAccount()`.
|
1. If the input type is `MIB_ACCOUNT`, calls `BmlValidateClient.verifyMibAccount()`.
|
||||||
@@ -208,6 +280,13 @@ Source: Fahipay
|
|||||||
FAHIPAY_TRANSFER, RAASTAS, OOREDOO_BILL, DHIRAAGU_RELOAD, DHIRAAGU_BILL
|
FAHIPAY_TRANSFER, RAASTAS, OOREDOO_BILL, DHIRAAGU_RELOAD, DHIRAAGU_BILL
|
||||||
```
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
Transfer type: Card (verified BML card)
|
||||||
|
|
||||||
|
└── Carrier creates a BML merchant transaction → card-only merchant flow
|
||||||
|
DHIRAAGU_RELOAD, DHIRAAGU_BILL, OOREDOO_RAASTAS, OOREDOO_BILL
|
||||||
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Rejected Combinations
|
## Rejected Combinations
|
||||||
@@ -336,6 +415,7 @@ The transfer button is only enabled when all of the following are true:
|
|||||||
- Amount is greater than `0`
|
- Amount is greater than `0`
|
||||||
- If transfer types are on offer, one has been picked
|
- If transfer types are on offer, one has been picked
|
||||||
- For a Fahipay service, the amount meets that service's rules (see [Amount rules](#amount-rules))
|
- For a Fahipay service, the amount meets that service's rules (see [Amount rules](#amount-rules))
|
||||||
|
- For a carrier service by card, the amount meets that service's rules (see [Carrier services by BML card](#carrier-services-by-bml-card))
|
||||||
- No connectivity error for `NO_INTERNET` or for the source bank
|
- No connectivity error for `NO_INTERNET` or for the source bank
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -7,7 +7,9 @@ Two linked features:
|
|||||||
full card details (PAN, expiry, CVV) encrypted on-device.
|
full card details (PAN, expiry, CVV) encrypted on-device.
|
||||||
2. **Merchant card payment** — on [Transfer](07-transfer.md), a BML Merchant Services transaction ID
|
2. **Merchant card payment** — on [Transfer](07-transfer.md), a BML Merchant Services transaction ID
|
||||||
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)).
|
([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 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
|
||||||
@@ -106,23 +108,28 @@ into the Transfer screen rather than a one-off dialog, mirroring the BML QR merc
|
|||||||
and empties the amount and re-enables remarks.
|
and empties the amount and re-enables remarks.
|
||||||
|
|
||||||
A card is only offered when it is **both** verified **and** belongs to a BML login the app has an
|
A card is only offered when it is **both** verified **and** belongs to a BML login the app has an
|
||||||
OTP seed for (`verifiedCardCandidates`, `:428`; `isCardVerified`, `:463`) — the 3-D Secure step
|
OTP seed for (`BmlVerifiedCards.payable` / `isPayable`, `ui/home/transfer/BmlVerifiedCards.kt`) —
|
||||||
needs that seed.
|
the 3-D Secure step needs that seed.
|
||||||
|
|
||||||
### Send
|
### Send
|
||||||
|
|
||||||
`submitCardPayment` (`:496`) → `confirmCardMerchant` (`:506`) shows the shared transfer confirm
|
`submitCardPayment` → `confirmCardMerchant` shows the shared transfer confirm dialog
|
||||||
dialog (biometric-gated), then `executeCardMerchant` (`:534`) runs, off the main thread:
|
(biometric-gated), then `executeCardMerchant` runs, off the main thread.
|
||||||
|
`CardPayoutTransferHandler` calls `confirmCardMerchant` directly with the page of the
|
||||||
|
transaction a carrier created:
|
||||||
|
|
||||||
```
|
```
|
||||||
BmlMerchantCardPayClient().pay(page, card) { Totp.generate(otpSeed) }
|
BmlMerchantCardPayClient().pay(page, card) { Totp.generate(otpSeed) }
|
||||||
```
|
```
|
||||||
|
|
||||||
where `card` comes from `VerifiedCardStore` (expiry split `MM/YY` → month/year) and `otpSeed` is the
|
where `card` and `otpSeed` come from `BmlVerifiedCards.load` — the `VerifiedCardStore` entry
|
||||||
card's BML login seed. The client (`api/bml/BmlMerchantCardPayClient.kt`) performs the whole
|
(expiry split `MM/YY` → month/year) and the card's BML login seed. The client (`api/bml/BmlMerchantCardPayClient.kt`) performs the whole
|
||||||
Pomelo + MPGS + Wibmo 3-D Secure sequence — feeding the BML token TOTP into the ACS OTP form
|
Pomelo + MPGS + Wibmo 3-D Secure sequence — feeding the BML token TOTP into the ACS OTP form
|
||||||
automatically, retrying once if the first code expired. Outcome is shown in the shared
|
automatically, retrying once if the first code expired. Once BML confirms, it loads
|
||||||
processing/success dialog; failures surface as a toast.
|
`<id>?wait=1` and follows the redirects to the merchant, which is how the merchant learns it was
|
||||||
|
paid (Dhiraagu doesn't top up without it). Outcome is shown in the shared processing/success
|
||||||
|
dialog; failures surface as a toast. If the merchant couldn't be reached, success also toasts
|
||||||
|
the merchant name and BML transaction id (`bml_card_pay_merchant_not_notified`).
|
||||||
|
|
||||||
### Key assumption
|
### Key assumption
|
||||||
|
|
||||||
@@ -145,6 +152,8 @@ another login/person, or a card whose 3-D Secure only offers SMS/email OTP.
|
|||||||
| `api/bml/BmlMerchantTxnClient.kt` | `fetchPayPage` (merchant-type detection), `announceBrowser`, QR payload |
|
| `api/bml/BmlMerchantTxnClient.kt` | `fetchPayPage` (merchant-type detection), `announceBrowser`, QR payload |
|
||||||
| `api/bml/BmlMerchantCardPayClient.kt` | Pomelo tokenise + 3-D Secure card payment |
|
| `api/bml/BmlMerchantCardPayClient.kt` | Pomelo tokenise + 3-D Secure card payment |
|
||||||
| `ui/home/transfer/BmlTransferHandler.kt` | On-screen card merchant mode + payment |
|
| `ui/home/transfer/BmlTransferHandler.kt` | On-screen card merchant mode + payment |
|
||||||
|
| `ui/home/transfer/BmlVerifiedCards.kt` | Which cards can pay by card; loads their details |
|
||||||
|
| `ui/home/transfer/CardPayoutTransferHandler.kt` | Carrier services by card: carrier creates the transaction, then `confirmCardMerchant` |
|
||||||
| `ui/home/TransferFragment.kt` | Transaction-ID lookup + routing |
|
| `ui/home/TransferFragment.kt` | Transaction-ID lookup + routing |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ Documentation for app-specific logic — UI flows, routing decisions, and busine
|
|||||||
| [04 — Accounts](04-accounts.md) | Account list grouped display, AccountsAdapter, profile images, quick-transfer shortcut |
|
| [04 — Accounts](04-accounts.md) | Account list grouped display, AccountsAdapter, profile images, quick-transfer shortcut |
|
||||||
| [05 — Account History](05-account-history.md) | Paginated transaction history, search, infinite scroll |
|
| [05 — Account History](05-account-history.md) | Paginated transaction history, search, infinite scroll |
|
||||||
| [06 — Transfer History](06-transfer-history.md) | Multi-bank merged transfer history, parallel loading |
|
| [06 — Transfer History](06-transfer-history.md) | Multi-bank merged transfer history, parallel loading |
|
||||||
| [07 — Transfer](07-transfer.md) | Recipient lookup, MIB/BML/Fahipay transfer flows, QR, biometric gate, BML OTP |
|
| [07 — Transfer](07-transfer.md) | Recipient lookup, transfer type picker, MIB/BML/Fahipay transfers, Fahipay payouts, Dhiraagu Reload by BML card, QR, biometric gate, BML OTP |
|
||||||
| [08 — Contacts](08-contacts.md) | Contact list, add/edit/delete, categories, contact picker sheet |
|
| [08 — Contacts](08-contacts.md) | Contact list, add/edit/delete, categories, contact picker sheet |
|
||||||
| [09 — Activities](09-activities.md) | Local transfer log, TransferReceiptFragment, share/save receipt |
|
| [09 — Activities](09-activities.md) | Local transfer log, TransferReceiptFragment, share/save receipt |
|
||||||
| [10 — OTP Screen](10-otp-screen.md) | TOTP display, real-time countdown, enrolled bank authenticators |
|
| [10 — OTP Screen](10-otp-screen.md) | TOTP display, real-time countdown, enrolled bank authenticators |
|
||||||
@@ -34,7 +34,7 @@ Documentation for app-specific logic — UI flows, routing decisions, and busine
|
|||||||
| [26 — Circular Nav](26-circular-nav.md) | Radial 4-slot wheel UI with lock centre |
|
| [26 — Circular Nav](26-circular-nav.md) | Radial 4-slot wheel UI with lock centre |
|
||||||
| [27 — Settings: Notifications](27-settings-notifications.md) | Opt-in flow: permission → battery opt → service start |
|
| [27 — Settings: Notifications](27-settings-notifications.md) | Opt-in flow: permission → battery opt → service start |
|
||||||
| [28 — Settings: About](28-settings-about.md) | Version, T&Cs, donate buttons |
|
| [28 — Settings: About](28-settings-about.md) | Version, T&Cs, donate buttons |
|
||||||
| [29 — Card Verification & Merchant Card Pay](29-card-verification-and-merchant-card-pay.md) | NFC/manual card verification + card-only BML merchant payment |
|
| [29 — Card Verification & Merchant Card Pay](29-card-verification-and-merchant-card-pay.md) | NFC/manual card verification + card-only BML merchant payment (links and carrier services), return to merchant |
|
||||||
|
|
||||||
## Reference
|
## Reference
|
||||||
|
|
||||||
@@ -42,5 +42,5 @@ Documentation for app-specific logic — UI flows, routing decisions, and busine
|
|||||||
|---|---|
|
|---|---|
|
||||||
| [18 — PayMV QR Format](18-paymv-qr-format.md) | Decimal TLV encoding, all tags, CRC-16, per-bank references, real samples, Fahipay WIP, parsing reference |
|
| [18 — PayMV QR Format](18-paymv-qr-format.md) | Decimal TLV encoding, all tags, CRC-16, per-bank references, real samples, Fahipay WIP, parsing reference |
|
||||||
| [19 — Parsers](19-parsers.md) | Account display parser architecture — how raw bank API data is normalised into a unified `AccountListDisplay` model |
|
| [19 — Parsers](19-parsers.md) | Account display parser architecture — how raw bank API data is normalised into a unified `AccountListDisplay` model |
|
||||||
| [20 — Transfer Flows](20-transfer-flows.md) | TransferFragment entry points, recipient lookup, transfer type routing, rejected combinations, BML business OTP flow, BML QR merchant payments |
|
| [20 — Transfer Flows](20-transfer-flows.md) | TransferFragment entry points, recipient lookup, transfer type picker, Fahipay services, carrier services by BML card, routing, rejected combinations, BML business OTP flow, BML QR merchant payments |
|
||||||
| [AI Security Audit](AI_SECURITY_CHECK.md) | Full source security audit — credential storage, network layer, manifest, data privacy |
|
| [AI Security Audit](AI_SECURITY_CHECK.md) | Full source security audit — credential storage, network layer, manifest, data privacy |
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
- updated dhivehi transaltions (thank you @quillfires)
|
||||||
|
- improved fahipay support
|
||||||
|
- new UI to select transfer type (Favara, Reload, Raastas, Billpay)
|
||||||
|
- Ooredoo Raastas via Fahipay
|
||||||
|
- Ooredo Billpay via Fahipay
|
||||||
|
- Dhiraagu reload via Fahipay
|
||||||
|
- Dhiraagu billpay via Fahipay
|
||||||
|
- Ooredoo Raastas via BML verified cards
|
||||||
|
- Ooredoo Billpay via BML verified cards
|
||||||
|
- Dhiraagu Reload via BML verifed cards
|
||||||
|
- Dhiraagu billpay via BML verified cards
|
||||||
Reference in New Issue
Block a user