forked from thijooree/android
prep for reload/raastas part 3: add GST notice, max and min values and make contact picker and contacts page behave correctly for fahipay contacts
This commit is contained in:
@@ -15,7 +15,7 @@ import sh.sar.basedbank.databinding.ItemPickerSectionHeaderBinding
|
||||
|
||||
class ContactPickerAdapter(
|
||||
private val imageCache: MutableMap<String, Bitmap>,
|
||||
private val onItemClick: (accountNumber: String, label: String) -> Unit,
|
||||
private val onItemClick: (accountNumber: String, label: String, categoryId: String?) -> Unit,
|
||||
private val onSameAsFrom: () -> Unit,
|
||||
private val onImageNeeded: ((hash: String) -> Unit)? = null,
|
||||
private val onItemLongClick: ((accountNumber: String, anchor: android.view.View) -> Boolean)? = null
|
||||
@@ -33,7 +33,9 @@ class ContactPickerAdapter(
|
||||
val imageHash: String? = null,
|
||||
val inactiveReason: String? = null,
|
||||
val balance: String? = null,
|
||||
val bankLogoRes: Int? = null
|
||||
val bankLogoRes: Int? = null,
|
||||
/** The saved contact's category, for rows that are contacts; null otherwise. */
|
||||
val categoryId: String? = null
|
||||
) : PickerItem()
|
||||
}
|
||||
|
||||
@@ -125,7 +127,7 @@ class ContactPickerAdapter(
|
||||
Toast.makeText(binding.root.context, item.inactiveReason, Toast.LENGTH_SHORT).show()
|
||||
item.isSameAsFrom ->
|
||||
Toast.makeText(binding.root.context, R.string.transfer_same_account, Toast.LENGTH_SHORT).show()
|
||||
else -> onItemClick(item.accountNumber, item.displayName)
|
||||
else -> onItemClick(item.accountNumber, item.displayName, item.categoryId)
|
||||
}
|
||||
}
|
||||
binding.root.setOnLongClickListener { view ->
|
||||
|
||||
@@ -54,7 +54,7 @@ class ContactPickerSheetFragment : BottomSheetDialogFragment() {
|
||||
val pageAdapters: List<ContactPickerAdapter> = pages.mapIndexed { i, page ->
|
||||
ContactPickerAdapter(
|
||||
imageCache = sharedImageCache,
|
||||
onItemClick = { accountNumber, label -> handlePickerSelection(accountNumber, label) },
|
||||
onItemClick = { accountNumber, label, categoryId -> handlePickerSelection(accountNumber, label, categoryId) },
|
||||
onSameAsFrom = {},
|
||||
onImageNeeded = { hash -> fetchImage(hash) },
|
||||
onItemLongClick = { accountNumber, anchor ->
|
||||
@@ -161,12 +161,13 @@ class ContactPickerSheetFragment : BottomSheetDialogFragment() {
|
||||
}.also { it.attach() }
|
||||
}
|
||||
|
||||
private fun handlePickerSelection(accountNumber: String, label: String) {
|
||||
private fun handlePickerSelection(accountNumber: String, label: String, categoryId: String?) {
|
||||
val contacts = viewModel.contacts.value ?: emptyList()
|
||||
val accounts = viewModel.accounts.value ?: emptyList()
|
||||
val contact = contacts.firstOrNull { it.benefAccount == accountNumber }
|
||||
val account = accounts.firstOrNull { it.accountNumber == accountNumber }
|
||||
val bundle = bundleOf(KEY_ACCOUNT_NUMBER to accountNumber, KEY_LABEL to label)
|
||||
categoryId?.let { bundle.putString(KEY_CATEGORY, it) }
|
||||
when {
|
||||
accountNumber.startsWith("bmlqr:") -> {
|
||||
bundle.putString(KEY_SUBTITLE, "BML QR Merchant")
|
||||
@@ -226,7 +227,8 @@ class ContactPickerSheetFragment : BottomSheetDialogFragment() {
|
||||
imageHash = r.imageHash,
|
||||
// A MFAISA-tagged recent is itself a valid M-Faisa recipient — don't grey it out
|
||||
// when the source is M-Faisa.
|
||||
inactiveReason = if (r.bank == "MFAISA") null else mfaisaInactive
|
||||
inactiveReason = if (r.bank == "MFAISA") null else mfaisaInactive,
|
||||
categoryId = r.contactCategory
|
||||
))
|
||||
}
|
||||
return items
|
||||
@@ -328,7 +330,8 @@ class ContactPickerSheetFragment : BottomSheetDialogFragment() {
|
||||
colorHex = contact.bankColor,
|
||||
isSameAsFrom = contact.benefAccount == fromAccountNumber,
|
||||
imageHash = contact.customerImgHash,
|
||||
inactiveReason = mfaisaInactive ?: currencyMismatchReason(fromCurrency, contact.transferCyDesc)
|
||||
inactiveReason = mfaisaInactive ?: currencyMismatchReason(fromCurrency, contact.transferCyDesc),
|
||||
categoryId = contact.benefCategoryId
|
||||
))
|
||||
}
|
||||
return items
|
||||
@@ -396,6 +399,8 @@ class ContactPickerSheetFragment : BottomSheetDialogFragment() {
|
||||
const val KEY_SUBTITLE = "subtitle"
|
||||
const val KEY_COLOR = "color"
|
||||
const val KEY_IMAGE_HASH = "imageHash"
|
||||
/** The picked contact's category (`BankContact.benefCategoryId`); absent for non-contacts. */
|
||||
const val KEY_CATEGORY = "category"
|
||||
private const val ARG_FROM_ACCOUNT = "fromAccount"
|
||||
private const val RECENTS_TAG = "__recents__"
|
||||
private const val MY_ACCOUNTS_TAG = "__my_accounts__"
|
||||
|
||||
@@ -188,7 +188,8 @@ class ContactsFragment : Fragment() {
|
||||
displayName = contact.name,
|
||||
subtitle = contact.transferSubtitle,
|
||||
colorHex = contact.bankColor,
|
||||
imageHash = contact.imageHash
|
||||
imageHash = contact.imageHash,
|
||||
contactCategory = contact.categoryId
|
||||
)
|
||||
(requireActivity() as HomeActivity).showWithBackStack(fragment)
|
||||
}
|
||||
|
||||
@@ -51,6 +51,7 @@ import sh.sar.basedbank.databinding.ItemPickerRowBinding
|
||||
import sh.sar.basedbank.databinding.ItemPickerSectionHeaderBinding
|
||||
import sh.sar.basedbank.databinding.ItemTransferTypeBinding
|
||||
import sh.sar.basedbank.ui.home.transfer.BmlTransferHandler
|
||||
import sh.sar.basedbank.ui.home.transfer.FahipayService
|
||||
import sh.sar.basedbank.ui.home.transfer.FahipayTransferHandler
|
||||
import sh.sar.basedbank.ui.home.transfer.MfaisaTransferHandler
|
||||
import sh.sar.basedbank.ui.home.transfer.MibTransferHandler
|
||||
@@ -277,6 +278,7 @@ class TransferFragment : Fragment() {
|
||||
private const val ARG_SUBTITLE = "contact_subtitle"
|
||||
private const val ARG_COLOR = "contact_color"
|
||||
private const val ARG_IMAGE_HASH = "contact_image_hash"
|
||||
private const val ARG_CONTACT_CATEGORY = "contact_category"
|
||||
private const val ARG_FROM_ACCOUNT = "from_account"
|
||||
private const val ARG_AMOUNT_PREFILL = "amount_prefill"
|
||||
private const val ARG_REMARKS_PREFILL = "remarks_prefill"
|
||||
@@ -311,7 +313,9 @@ class TransferFragment : Fragment() {
|
||||
displayName: String,
|
||||
subtitle: String,
|
||||
colorHex: String,
|
||||
imageHash: String?
|
||||
imageHash: String?,
|
||||
/** The contact's category; a Fahipay favourites list opens as that payout service. */
|
||||
contactCategory: String? = null
|
||||
) = TransferFragment().apply {
|
||||
arguments = Bundle().apply {
|
||||
putString(ARG_ACCOUNT, accountNumber)
|
||||
@@ -319,6 +323,7 @@ class TransferFragment : Fragment() {
|
||||
putString(ARG_SUBTITLE, subtitle)
|
||||
putString(ARG_COLOR, colorHex)
|
||||
if (imageHash != null) putString(ARG_IMAGE_HASH, imageHash)
|
||||
if (contactCategory != null) putString(ARG_CONTACT_CATEGORY, contactCategory)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -393,6 +398,9 @@ class TransferFragment : Fragment() {
|
||||
return@setFragmentResultListener
|
||||
}
|
||||
val label = bundle.getString(ContactPickerSheetFragment.KEY_LABEL) ?: ""
|
||||
if (applyFahipayContact(accountNumber, bundle.getString(ContactPickerSheetFragment.KEY_CATEGORY), label)) {
|
||||
return@setFragmentResultListener
|
||||
}
|
||||
val subtitle = bundle.getString(ContactPickerSheetFragment.KEY_SUBTITLE) ?: accountNumber
|
||||
val colorHex = bundle.getString(ContactPickerSheetFragment.KEY_COLOR) ?: "#607D8B"
|
||||
val imageHash = bundle.getString(ContactPickerSheetFragment.KEY_IMAGE_HASH)
|
||||
@@ -435,9 +443,11 @@ class TransferFragment : Fragment() {
|
||||
private fun applyArguments() {
|
||||
// Pre-select contact if navigated from contacts page or QR scan
|
||||
arguments?.getString(ARG_ACCOUNT)?.let { account ->
|
||||
val name = arguments?.getString(ARG_NAME) ?: account
|
||||
if (applyFahipayContact(account, arguments?.getString(ARG_CONTACT_CATEGORY), name)) return@let
|
||||
prefillToDirectly(
|
||||
accountNumber = account,
|
||||
displayName = arguments?.getString(ARG_NAME) ?: account,
|
||||
displayName = name,
|
||||
subtitle = arguments?.getString(ARG_SUBTITLE) ?: account,
|
||||
colorHex = arguments?.getString(ARG_COLOR) ?: "#607D8B",
|
||||
imageHash = arguments?.getString(ARG_IMAGE_HASH)
|
||||
@@ -1090,6 +1100,9 @@ class TransferFragment : Fragment() {
|
||||
|
||||
binding.etTo.setOnItemClickListener { _, _, position, _ ->
|
||||
val contact = adapter.getContact(position) ?: return@setOnItemClickListener
|
||||
if (applyFahipayContact(contact.benefAccount, contact.benefCategoryId, contact.benefNickName)) {
|
||||
return@setOnItemClickListener
|
||||
}
|
||||
prefillToDirectly(
|
||||
accountNumber = contact.benefAccount,
|
||||
displayName = contact.benefNickName,
|
||||
@@ -1102,6 +1115,19 @@ class TransferFragment : Fragment() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A saved Fahipay favourite: its list ([categoryId]) already says which service pays it, so
|
||||
* that service is applied as the only transfer type, with no carrier lookup. That switches
|
||||
* the source to the Fahipay wallet and brings in the service's amount rules, the same as a
|
||||
* searched number. Returns false, doing nothing, when [categoryId] isn't a Fahipay list.
|
||||
*/
|
||||
private fun applyFahipayContact(number: String, categoryId: String?, name: String): Boolean {
|
||||
val service = FahipayService.fromContactCategory(categoryId) ?: return false
|
||||
clearRecipient()
|
||||
offerTransferTypes(number, listOf(TransferType.Fahipay(service, name.takeIf { it.isNotBlank() && it != number })))
|
||||
return true
|
||||
}
|
||||
|
||||
/** Picks the user's default account as the source when a recipient is chosen before any source. */
|
||||
private fun selectDefaultSourceIfNone() {
|
||||
if (selectedAccount != null) return
|
||||
@@ -1312,7 +1338,9 @@ class TransferFragment : Fragment() {
|
||||
displayName: String,
|
||||
subtitle: String,
|
||||
colorHex: String,
|
||||
imageHash: String?
|
||||
imageHash: String?,
|
||||
/** Saved with the recent, so picking it again pays the same way (Fahipay services). */
|
||||
contactCategory: String? = null
|
||||
) {
|
||||
resolvedAccountNumber = accountNumber
|
||||
resolvedRecipientName = displayName
|
||||
@@ -1363,7 +1391,8 @@ class TransferFragment : Fragment() {
|
||||
subtitle = subtitle,
|
||||
colorHex = colorHex,
|
||||
imageHash = recentImageHash,
|
||||
isProfileImage = recentIsProfileImage
|
||||
isProfileImage = recentIsProfileImage,
|
||||
contactCategory = contactCategory
|
||||
))
|
||||
if (contact != null && imageHash != null) loadToPhoto(imageHash, isProfile = false)
|
||||
}
|
||||
@@ -1890,10 +1919,15 @@ class TransferFragment : Fragment() {
|
||||
|
||||
|
||||
private fun updateTransferButton() {
|
||||
// The handler is dropped with the view, but a picked Fahipay service lives on in the
|
||||
// draft — build it so the amount rules still apply after the view is recreated.
|
||||
val fahipay = if (draft.fahipayService != null) fahipayHandler() else null
|
||||
fahipay?.syncAmountField()
|
||||
if (bmlHandler().isOtpFlowActive) return
|
||||
val amount = binding.etAmount.text?.toString()?.trim()?.toDoubleOrNull() ?: 0.0
|
||||
val recipientReady = bmlHandler().hasQrMerchant || bmlHandler().hasCardMerchant || mfaisaHandler().hasQrMerchant || resolvedAccountNumber.isNotBlank()
|
||||
val hasAll = selectedAccount != null && recipientReady && amount > 0 && !transferTypePending
|
||||
val amountOk = amount > 0 && fahipay?.amountProblem == null
|
||||
val hasAll = selectedAccount != null && recipientReady && amountOk && !transferTypePending
|
||||
if (!hasAll) { binding.btnTransfer.isEnabled = false; return }
|
||||
val errors = viewModel.connectivityErrors.value ?: emptySet()
|
||||
val bankOffline = "NO_INTERNET" in errors ||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package sh.sar.basedbank.ui.home.transfer
|
||||
|
||||
import android.text.InputType
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -12,6 +13,8 @@ import sh.sar.basedbank.databinding.FragmentTransferBinding
|
||||
import sh.sar.basedbank.ui.home.HomeViewModel
|
||||
import sh.sar.basedbank.ui.home.TransferFragment
|
||||
import sh.sar.basedbank.util.AccountInputParser
|
||||
import java.math.BigDecimal
|
||||
import java.math.RoundingMode
|
||||
|
||||
/**
|
||||
* A service a Fahipay wallet can pay out to. The carrier lookup decides which of these apply to
|
||||
@@ -26,11 +29,41 @@ enum class FahipayService(
|
||||
val label: String,
|
||||
val destinationLabel: String,
|
||||
@param:DrawableRes val iconRes: Int,
|
||||
/** Smallest amount the service accepts, in MVR. */
|
||||
val minAmount: Int,
|
||||
/** Largest amount the service accepts, in MVR, or null for no limit. */
|
||||
val maxAmount: Int?,
|
||||
/** Whether the amount may have a fractional part (up to 2 decimal places). */
|
||||
val decimalsAllowed: Boolean,
|
||||
/**
|
||||
* The contact category of this service's Fahipay favourites list (`BankContact.benefCategoryId`,
|
||||
* set by `FahipayContactsClient`). Recents of this service are tagged with it too.
|
||||
*/
|
||||
val contactCategory: String,
|
||||
/** GST the carrier takes out of the amount before crediting it, in percent, or null for none. */
|
||||
val gstPercent: Int? = null,
|
||||
) {
|
||||
RAASTAS("Raastas", "Ooredoo · Raastas", R.drawable.ooredoo_logo),
|
||||
OOREDOO_BILL("Ooredoo Bill Pay", "Ooredoo · Bill Pay", R.drawable.ooredoo_logo),
|
||||
DHIRAAGU_RELOAD("Dhiraagu Reload", "Dhiraagu · Reload", R.drawable.dhiraagu_logo),
|
||||
DHIRAAGU_BILL("Dhiraagu Bill Pay", "Dhiraagu · Bill Pay", R.drawable.dhiraagu_logo),
|
||||
RAASTAS("Raastas", "Ooredoo · Raastas", R.drawable.ooredoo_logo,
|
||||
minAmount = 11, maxAmount = null, decimalsAllowed = false,
|
||||
contactCategory = "FAHIPAY_RAASTAS", gstPercent = 8),
|
||||
OOREDOO_BILL("Ooredoo Bill Pay", "Ooredoo · Bill Pay", R.drawable.ooredoo_logo,
|
||||
minAmount = 10, maxAmount = 50000, decimalsAllowed = true,
|
||||
contactCategory = "FAHIPAY_OOREDOO_BILL"),
|
||||
DHIRAAGU_RELOAD("Dhiraagu Reload", "Dhiraagu · Reload", R.drawable.dhiraagu_logo,
|
||||
minAmount = 8, maxAmount = 1000, decimalsAllowed = false,
|
||||
contactCategory = "FAHIPAY_RELOAD"),
|
||||
DHIRAAGU_BILL("Dhiraagu Bill Pay", "Dhiraagu · Bill Pay", R.drawable.dhiraagu_logo,
|
||||
minAmount = 10, maxAmount = 5000, decimalsAllowed = false,
|
||||
contactCategory = "FAHIPAY_DHIRAAGU_BILL");
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* The service a saved Fahipay favourite or recent pays with, from its [contactCategory],
|
||||
* or null when [categoryId] isn't one of them.
|
||||
*/
|
||||
fun fromContactCategory(categoryId: String?): FahipayService? =
|
||||
entries.firstOrNull { it.contactCategory == categoryId }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -79,9 +112,71 @@ class FahipayTransferHandler(
|
||||
lookupCarrier(rawInput)
|
||||
}
|
||||
|
||||
/** Forgets the selected service. The picker itself is the fragment's to reset. */
|
||||
/**
|
||||
* Forgets the selected service and gives back the amount and reference fields it took over.
|
||||
* The picker itself is the fragment's to reset.
|
||||
*/
|
||||
fun clearState() {
|
||||
if (service == null) return
|
||||
service = null
|
||||
binding.tilAmount.error = null
|
||||
binding.tilAmount.helperText = null
|
||||
binding.etAmount.inputType = DECIMAL_INPUT
|
||||
binding.tilRemarks.isEnabled = true
|
||||
binding.tilRemarks.alpha = 1f
|
||||
}
|
||||
|
||||
/**
|
||||
* Why the typed amount can't be sent with the selected service, or null when it can (or the
|
||||
* field is empty, or no service is selected). Checks the service's minimum, maximum and
|
||||
* whether it takes decimals.
|
||||
*/
|
||||
val amountProblem: String?
|
||||
get() {
|
||||
val svc = service ?: return null
|
||||
val text = binding.etAmount.text?.toString()?.trim().orEmpty()
|
||||
if (text.isEmpty()) return null
|
||||
val amount = text.toBigDecimalOrNull() ?: return ctx.getString(R.string.transfer_fahipay_amount_invalid)
|
||||
val fraction = amount.stripTrailingZeros().scale()
|
||||
return when {
|
||||
!svc.decimalsAllowed && fraction > 0 -> ctx.getString(R.string.transfer_fahipay_amount_whole)
|
||||
fraction > 2 -> ctx.getString(R.string.transfer_fahipay_amount_decimals)
|
||||
amount < BigDecimal(svc.minAmount) -> ctx.getString(R.string.transfer_fahipay_amount_min, svc.minAmount)
|
||||
svc.maxAmount != null && amount > BigDecimal(svc.maxAmount) ->
|
||||
ctx.getString(R.string.transfer_fahipay_amount_max, "%,d".format(svc.maxAmount))
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Keeps the amount and reference fields in step with the selected service: the amount
|
||||
* error, the GST note under the amount, and the reference box disabled (none of the
|
||||
* payouts take one). Idempotent — the fragment calls it on every form change. A no-op
|
||||
* when no service is selected; [clearState] puts the fields back.
|
||||
*/
|
||||
fun syncAmountField() {
|
||||
val svc = service ?: return
|
||||
// Whole-number services get a keypad without a decimal point. Only set on change:
|
||||
// setting inputType restarts the keyboard, and this runs on every keystroke.
|
||||
val inputType = if (svc.decimalsAllowed) DECIMAL_INPUT else InputType.TYPE_CLASS_NUMBER
|
||||
if (binding.etAmount.inputType != inputType) binding.etAmount.inputType = inputType
|
||||
binding.tilRemarks.isEnabled = false
|
||||
binding.tilRemarks.alpha = 0.4f
|
||||
val problem = amountProblem
|
||||
binding.tilAmount.error = problem
|
||||
binding.tilAmount.helperText = if (problem == null) gstNote(svc) else null
|
||||
}
|
||||
|
||||
/**
|
||||
* What the recipient is credited once GST comes out, for services that charge it. The
|
||||
* amount paid is GST-inclusive, so the credit is amount / (1 + rate), rounded down.
|
||||
*/
|
||||
private fun gstNote(svc: FahipayService): String? {
|
||||
val gst = svc.gstPercent ?: return null
|
||||
val amount = binding.etAmount.text?.toString()?.trim()?.toBigDecimalOrNull()
|
||||
if (amount == null || amount.signum() <= 0) return ctx.getString(R.string.transfer_fahipay_gst_hint, gst)
|
||||
val credited = amount.divide(BigDecimal.ONE + BigDecimal(gst).movePointLeft(2), 2, RoundingMode.DOWN)
|
||||
return ctx.getString(R.string.transfer_fahipay_gst_receive, "%,.2f".format(credited), gst)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -90,6 +185,8 @@ class FahipayTransferHandler(
|
||||
*/
|
||||
fun applyService(type: TransferType.Fahipay, number: String) {
|
||||
service = type.service
|
||||
// None of the payouts take a reference; syncAmountField() disables the box
|
||||
binding.etRemarks.setText("")
|
||||
val contacts = viewModel.contacts.value ?: emptyList()
|
||||
val displayName = type.ownerName
|
||||
?: contacts.firstOrNull { it.benefAccount == number }?.benefNickName
|
||||
@@ -99,7 +196,8 @@ class FahipayTransferHandler(
|
||||
displayName = displayName,
|
||||
subtitle = "${type.label} · $number",
|
||||
colorHex = "#FF6B00",
|
||||
imageHash = null
|
||||
imageHash = null,
|
||||
contactCategory = type.service.contactCategory
|
||||
)
|
||||
fragment.focusAmount()
|
||||
}
|
||||
@@ -165,4 +263,9 @@ class FahipayTransferHandler(
|
||||
if (result.ooredoo == OoredooClient.CustType.PRE || result.ooredoo == OoredooClient.CustType.HYBRID) add(FahipayService.RAASTAS)
|
||||
if (result.ooredoo == OoredooClient.CustType.POST || result.ooredoo == OoredooClient.CustType.HYBRID) add(FahipayService.OOREDOO_BILL)
|
||||
}
|
||||
|
||||
private companion object {
|
||||
/** The amount field's input type from `fragment_transfer.xml` (`numberDecimal`). */
|
||||
const val DECIMAL_INPUT = InputType.TYPE_CLASS_NUMBER or InputType.TYPE_NUMBER_FLAG_DECIMAL
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,10 @@ data class RecentPick(
|
||||
val isProfileImage: Boolean,
|
||||
/** Source bank tag for the recent — e.g. "MFAISA". Used by the picker to decide
|
||||
* per-bank selectability. Null for legacy entries; treated as unspecified. */
|
||||
val bank: String? = null
|
||||
val bank: String? = null,
|
||||
/** The contact category the pick was paid as — set for Fahipay payouts (e.g.
|
||||
* "FAHIPAY_RAASTAS") so picking the recent again applies the same service. */
|
||||
val contactCategory: String? = null
|
||||
)
|
||||
|
||||
object RecentsCache {
|
||||
@@ -38,6 +41,7 @@ object RecentsCache {
|
||||
if (r.imageHash != null) put("imageHash", r.imageHash)
|
||||
put("isProfileImage", r.isProfileImage)
|
||||
if (r.bank != null) put("bank", r.bank)
|
||||
if (r.contactCategory != null) put("contactCategory", r.contactCategory)
|
||||
})
|
||||
}
|
||||
context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
|
||||
@@ -56,6 +60,7 @@ object RecentsCache {
|
||||
if (r.imageHash != null) put("imageHash", r.imageHash)
|
||||
put("isProfileImage", r.isProfileImage)
|
||||
if (r.bank != null) put("bank", r.bank)
|
||||
if (r.contactCategory != null) put("contactCategory", r.contactCategory)
|
||||
})
|
||||
}
|
||||
context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
|
||||
@@ -81,7 +86,8 @@ object RecentsCache {
|
||||
colorHex = o.getString("colorHex"),
|
||||
imageHash = o.optString("imageHash").takeIf { it.isNotBlank() },
|
||||
isProfileImage = o.optBoolean("isProfileImage", false),
|
||||
bank = o.optString("bank").takeIf { it.isNotBlank() }
|
||||
bank = o.optString("bank").takeIf { it.isNotBlank() },
|
||||
contactCategory = o.optString("contactCategory").takeIf { it.isNotBlank() }
|
||||
)
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package sh.sar.basedbank.util.fahipayapi
|
||||
|
||||
import sh.sar.basedbank.api.models.BankContact
|
||||
import sh.sar.basedbank.ui.home.transfer.FahipayService
|
||||
import sh.sar.basedbank.util.ContactDisplay
|
||||
import sh.sar.basedbank.util.TransferNetwork
|
||||
|
||||
@@ -20,7 +21,8 @@ object FahipayContactParser {
|
||||
imageHash = contact.customerImgHash,
|
||||
profileId = contact.profileId,
|
||||
transferSubtitle = contact.benefAccount,
|
||||
canTransfer = false,
|
||||
// Each favourites list is one payout service; the Transfer screen picks it from categoryId
|
||||
canTransfer = FahipayService.fromContactCategory(contact.benefCategoryId) != null,
|
||||
canEdit = false,
|
||||
canDelete = false
|
||||
)
|
||||
|
||||
@@ -262,6 +262,13 @@
|
||||
<string name="transfer_label_from">From</string>
|
||||
<string name="transfer_label_to">To</string>
|
||||
<string name="transfer_type_for">Transfer type for %1$s</string>
|
||||
<string name="transfer_fahipay_amount_invalid">Enter a valid amount</string>
|
||||
<string name="transfer_fahipay_amount_whole">Whole amounts only, no decimals</string>
|
||||
<string name="transfer_fahipay_amount_decimals">Up to 2 decimal places</string>
|
||||
<string name="transfer_fahipay_amount_min">Minimum is MVR %1$d</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_receive">Recipient receives MVR %1$s after %2$d%% GST</string>
|
||||
<string name="transfer_fahipay_phone_only">Fahipay transfers require a 7-digit phone number</string>
|
||||
<string name="transfer_my_accounts">My Accounts</string>
|
||||
<string name="transfer_same_as_from">This is the same account as the sender</string>
|
||||
|
||||
@@ -10,7 +10,7 @@ The transfer screen (`TransferFragment`) handles all outgoing payments across MI
|
||||
|
||||
| Factory method | Behaviour |
|
||||
|---|---|
|
||||
| `newInstance(accountNumber, displayName, subtitle, colorHex, imageHash)` | Pre-fills the "To" card from a contact, recents pick, or About → Donate |
|
||||
| `newInstance(accountNumber, displayName, subtitle, colorHex, imageHash, contactCategory?)` | Pre-fills the "To" card from a contact, recents pick, or About → Donate. A Fahipay favourite's `contactCategory` opens it as that payout service instead (see [Saved Fahipay favourites](#saved-fahipay-favourites)) |
|
||||
| `newInstanceFrom(account: BankAccount)` | Pre-selects the given account in the "From" dropdown |
|
||||
| `newInstanceFromQr(accountNumber, displayName, amount, remarks, fromAccountNumber?)` | Pre-fills recipient + optional amount/remarks from a PayMV QR scan |
|
||||
| `newInstanceFromBmlQr(qrUrl, fromAccountNumber?)` | BML card/gateway/POS QR merchant payment mode — locks recipient, may pre-fill amount |
|
||||
@@ -90,6 +90,51 @@ The result maps to one or more Fahipay services:
|
||||
|
||||
The matching services are offered in the Transfer Type picker (see above).
|
||||
|
||||
#### Saved Fahipay favourites
|
||||
|
||||
Each Fahipay favourites list is one payout service (`FahipayService.contactCategory` / `fromContactCategory`):
|
||||
|
||||
| Contact category | Service |
|
||||
|---|---|
|
||||
| `FAHIPAY_RAASTAS` | Raastas |
|
||||
| `FAHIPAY_RELOAD` | Dhiraagu Reload |
|
||||
| `FAHIPAY_OOREDOO_BILL` | Ooredoo 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:
|
||||
|
||||
- the contact picker sheet (the row's category goes back as `ContactPickerSheetFragment.KEY_CATEGORY`)
|
||||
- the "To" field's search-as-you-type dropdown
|
||||
- the Contacts page: the row's transfer button and the contact details sheet's Transfer action. Fahipay favourites have `canTransfer` set when their category maps to a service.
|
||||
|
||||
Recents work the same way. Paying a number as a Fahipay service saves the recent with that service's category (`RecentPick.contactCategory`). The picker passes it back like a favourite's, so picking the recent pays with the same service. Recents saved before this was added have no category and are still filled in directly.
|
||||
|
||||
#### Amount rules
|
||||
|
||||
Each service has its own limits on the amount (`FahipayService.minAmount` / `maxAmount` / `decimalsAllowed`):
|
||||
|
||||
| Service | Min (MVR) | Max (MVR) | Decimals |
|
||||
|---|---|---|---|
|
||||
| Raastas | 11 | no limit | no |
|
||||
| Ooredoo Bill Pay | 10 | 50,000 | yes, up to 2 places |
|
||||
| Dhiraagu Reload | 8 | 1,000 | no |
|
||||
| Dhiraagu Bill Pay | 10 | 5,000 | no |
|
||||
|
||||
The amount is checked as the user types (`FahipayTransferHandler.amountProblem`). An amount that breaks a rule shows an error on the amount field ("Minimum is MVR 11", "Maximum is MVR 5,000", "Whole amounts only, no decimals", "Up to 2 decimal places"), and Send stays disabled. A trailing `.00` counts as a whole number. Services that don't take decimals switch the amount field to a number-only keypad.
|
||||
|
||||
#### GST (Raastas)
|
||||
|
||||
Raastas charges 8% GST out of the amount paid (`FahipayService.gstPercent`), so the number credited is less than the amount deducted from the wallet. The amount is GST-inclusive, so the credit is `amount / 1.08`, rounded down to 2 decimal places. The amount field's helper text says so:
|
||||
|
||||
- Empty field: "8% GST is deducted from this amount"
|
||||
- With an amount: "Recipient receives MVR 92.59 after 8% GST" (for MVR 100)
|
||||
|
||||
When the amount breaks a rule, the error replaces the helper text.
|
||||
|
||||
#### Reference
|
||||
|
||||
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.
|
||||
|
||||
### BML source
|
||||
|
||||
1. If the input type is `MIB_ACCOUNT`, calls `BmlValidateClient.verifyMibAccount()`.
|
||||
@@ -281,6 +326,7 @@ The transfer button is only enabled when all of the following are true:
|
||||
- A recipient is resolved (`resolvedAccountNumber` not blank, or the BML handler's `qrInfo` is set)
|
||||
- Amount is greater than `0`
|
||||
- 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))
|
||||
- No connectivity error for `NO_INTERNET` or for the source bank
|
||||
|
||||
---
|
||||
|
||||
Reference in New Issue
Block a user