diff --git a/feature/merchants/.gitignore b/feature/merchants/.gitignore
new file mode 100644
index 00000000..42afabfd
--- /dev/null
+++ b/feature/merchants/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/feature/merchants/build.gradle.kts b/feature/merchants/build.gradle.kts
new file mode 100644
index 00000000..5ea5696e
--- /dev/null
+++ b/feature/merchants/build.gradle.kts
@@ -0,0 +1,19 @@
+plugins {
+ alias(libs.plugins.mifospay.android.feature)
+ alias(libs.plugins.mifospay.android.library.compose)
+}
+
+android {
+ namespace = "org.mifospay.feature.merchants"
+}
+
+dependencies {
+ implementation(projects.core.data)
+ implementation(libs.compose.material)
+
+ //Todo: Remove these after migration of MerchantTransferActivity
+ implementation("com.jakewharton:butterknife-annotations:10.2.3")
+ implementation("com.jakewharton:butterknife:10.2.3@aar")
+ implementation("com.mifos.mobile:mifos-passcode:0.3.0@aar")
+ implementation(project(":mifospay"))
+}
\ No newline at end of file
diff --git a/feature/merchants/consumer-rules.pro b/feature/merchants/consumer-rules.pro
new file mode 100644
index 00000000..e69de29b
diff --git a/feature/merchants/proguard-rules.pro b/feature/merchants/proguard-rules.pro
new file mode 100644
index 00000000..481bb434
--- /dev/null
+++ b/feature/merchants/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
\ No newline at end of file
diff --git a/feature/merchants/src/androidTest/java/org/mifospay/feature/merchants/ExampleInstrumentedTest.kt b/feature/merchants/src/androidTest/java/org/mifospay/feature/merchants/ExampleInstrumentedTest.kt
new file mode 100644
index 00000000..d6ab736a
--- /dev/null
+++ b/feature/merchants/src/androidTest/java/org/mifospay/feature/merchants/ExampleInstrumentedTest.kt
@@ -0,0 +1,24 @@
+package org.mifospay.feature.merchants
+
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.ext.junit.runners.AndroidJUnit4
+
+import org.junit.Test
+import org.junit.runner.RunWith
+
+import org.junit.Assert.*
+
+/**
+ * Instrumented test, which will execute on an Android device.
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+@RunWith(AndroidJUnit4::class)
+class ExampleInstrumentedTest {
+ @Test
+ fun useAppContext() {
+ // Context of the app under test.
+ val appContext = InstrumentationRegistry.getInstrumentation().targetContext
+ assertEquals("org.mifospay.feature.merchants.test", appContext.packageName)
+ }
+}
\ No newline at end of file
diff --git a/feature/merchants/src/main/AndroidManifest.xml b/feature/merchants/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..a5918e68
--- /dev/null
+++ b/feature/merchants/src/main/AndroidManifest.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/feature/merchants/src/main/kotlin/org/mifospay/feature/merchants/MerchantScreen.kt b/feature/merchants/src/main/kotlin/org/mifospay/feature/merchants/MerchantScreen.kt
new file mode 100644
index 00000000..e4daad70
--- /dev/null
+++ b/feature/merchants/src/main/kotlin/org/mifospay/feature/merchants/MerchantScreen.kt
@@ -0,0 +1,284 @@
+package org.mifospay.feature.merchants
+
+import android.content.Intent
+import android.widget.Toast
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.material.ExperimentalMaterialApi
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Close
+import androidx.compose.material.icons.filled.Search
+import androidx.compose.material.icons.rounded.Info
+import androidx.compose.material.pullrefresh.PullRefreshIndicator
+import androidx.compose.material.pullrefresh.pullRefresh
+import androidx.compose.material.pullrefresh.rememberPullRefreshState
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.SearchBar
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalClipboardManager
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import com.mifospay.core.model.entity.accounts.savings.SavingsWithAssociations
+import org.mifospay.R
+import org.mifospay.common.Constants
+import org.mifospay.core.designsystem.component.MfLoadingWheel
+import org.mifospay.core.ui.EmptyContentScreen
+import org.mifospay.theme.MifosTheme
+
+@Composable
+fun MerchantScreen(
+ viewModel: MerchantViewModel = hiltViewModel()
+) {
+ val merchantUiState by viewModel.merchantUiState.collectAsStateWithLifecycle()
+ val merchantsListUiState by viewModel.merchantsListUiState.collectAsStateWithLifecycle()
+ val isRefreshing by viewModel.isRefreshing.collectAsStateWithLifecycle()
+
+ MerchantScreen(
+ merchantUiState = merchantUiState,
+ merchantListUiState = merchantsListUiState,
+ updateQuery = { viewModel.updateSearchQuery(it) },
+ isRefreshing = isRefreshing,
+ onRefresh = { viewModel.refresh() },
+ )
+}
+
+@OptIn(ExperimentalMaterialApi::class)
+@Composable
+fun MerchantScreen(
+ merchantUiState: MerchantUiState,
+ merchantListUiState: MerchantUiState,
+ updateQuery: (String) -> Unit,
+ isRefreshing: Boolean,
+ onRefresh: () -> Unit,
+) {
+ val pullRefreshState = rememberPullRefreshState(isRefreshing, onRefresh)
+ Box(Modifier.pullRefresh(pullRefreshState)) {
+ Column(modifier = Modifier.fillMaxSize()) {
+ when (merchantUiState) {
+ MerchantUiState.Empty -> {
+ EmptyContentScreen(
+ modifier = Modifier,
+ title = stringResource(id = R.string.empty_no_merchants_title),
+ subTitle = stringResource(id = R.string.empty_no_merchants_subtitle),
+ iconTint = Color.Black,
+ iconImageVector = Icons.Rounded.Info
+ )
+ }
+
+ is MerchantUiState.Error -> {
+ EmptyContentScreen(
+ modifier = Modifier,
+ title = stringResource(id = R.string.error_oops),
+ subTitle = stringResource(id = R.string.unexpected_error_subtitle),
+ iconTint = Color.Black,
+ iconImageVector = Icons.Rounded.Info
+ )
+ }
+
+ MerchantUiState.Loading -> {
+ MfLoadingWheel(
+ contentDesc = stringResource(R.string.loading),
+ backgroundColor = Color.White
+ )
+ }
+
+ is MerchantUiState.ShowMerchants -> {
+ MerchantScreenContent(
+ merchantList = (merchantListUiState as MerchantUiState.ShowMerchants).merchants,
+ updateQuery = updateQuery
+ )
+ }
+ }
+ }
+ PullRefreshIndicator(
+ refreshing = isRefreshing,
+ state = pullRefreshState,
+ modifier = Modifier.align(Alignment.TopCenter)
+ )
+ }
+}
+
+@Composable
+fun MerchantScreenContent(
+ merchantList: List,
+ updateQuery: (String) -> Unit
+) {
+ val query by rememberSaveable { mutableStateOf("") }
+ Box(modifier = Modifier.fillMaxSize()) {
+ Column {
+ SearchBarScreen(
+ query = query,
+ onQueryChange = { q ->
+ updateQuery(q)
+ },
+ onSearch = {},
+ onClearQuery = { updateQuery("") }
+ )
+ MerchantList(merchantList = merchantList)
+ }
+ }
+}
+
+@Composable
+fun MerchantList(
+ merchantList: List
+) {
+ val context = LocalContext.current
+ val clipboardManager = LocalClipboardManager.current
+ LazyColumn(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 16.dp)
+ ) {
+ items(merchantList.size) { index ->
+ MerchantsItem(savingsWithAssociations = merchantList[index],
+ onMerchantClicked = {
+ val intent = Intent(context, MerchantTransferActivity::class.java)
+ intent.putExtra(Constants.MERCHANT_NAME, merchantList[index].clientName)
+ intent.putExtra(Constants.MERCHANT_VPA, merchantList[index].externalId)
+ intent.putExtra(Constants.MERCHANT_ACCOUNT_NO, merchantList[index].accountNo)
+ context.startActivity(intent)
+ },
+ onMerchantLongPressed = {
+ clipboardManager.setText(AnnotatedString(it ?: ""))
+ Toast.makeText(context, R.string.vpa_copy_success, Toast.LENGTH_LONG).show()
+ }
+ )
+ }
+ }
+}
+
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun SearchBarScreen(
+ query: String,
+ onQueryChange: (String) -> Unit,
+ onSearch: (String) -> Unit,
+ onClearQuery: () -> Unit
+) {
+ SearchBar(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(vertical = 16.dp, horizontal = 16.dp),
+ query = query,
+ onQueryChange = onQueryChange,
+ onSearch = onSearch,
+ active = false,
+ onActiveChange = { },
+ placeholder = {
+ Text(text = stringResource(R.string.search))
+ },
+ leadingIcon = {
+ Icon(
+ imageVector = Icons.Filled.Search,
+ contentDescription = stringResource(R.string.search)
+ )
+ },
+ trailingIcon = {
+ IconButton(
+ onClick = onClearQuery
+ ) {
+ Icon(
+ imageVector = Icons.Filled.Close,
+ contentDescription = stringResource(R.string.close)
+ )
+ }
+ }
+ ) {}
+}
+
+@Preview(showBackground = true)
+@Composable
+private fun MerchantLoadingPreview() {
+ MifosTheme {
+ MerchantScreen(merchantUiState = MerchantUiState.Loading,
+ merchantListUiState = MerchantUiState.ShowMerchants(sampleMerchantList),
+ updateQuery = {},
+ false, {}
+ )
+ }
+}
+
+@Preview(showBackground = true)
+@Composable
+private fun MerchantListPreview() {
+ MifosTheme {
+ MerchantScreen(
+ merchantUiState = MerchantUiState.ShowMerchants(sampleMerchantList),
+ merchantListUiState = MerchantUiState.ShowMerchants(sampleMerchantList),
+ updateQuery = {}, false, {}
+ )
+ }
+}
+
+@Preview(showBackground = true)
+@Composable
+private fun MerchantErrorPreview() {
+ MifosTheme {
+ MerchantScreen(
+ merchantUiState = MerchantUiState.Error("Error Screen"),
+ merchantListUiState = MerchantUiState.ShowMerchants(sampleMerchantList),
+ updateQuery = {}, true, {}
+ )
+ }
+}
+
+@Preview(showBackground = true)
+@Composable
+private fun MerchantEmptyPreview() {
+ MifosTheme {
+ MerchantScreen(
+ merchantUiState = MerchantUiState.Empty,
+ merchantListUiState = MerchantUiState.ShowMerchants(sampleMerchantList),
+ updateQuery = {}, false, {}
+ )
+ }
+}
+
+
+val sampleMerchantList = List(10) {
+ SavingsWithAssociations(
+ id = 1L,
+ accountNo = "123456789",
+ depositType = null,
+ externalId = "EXT987654",
+ clientId = 101,
+ clientName = "Alice Bob",
+ savingsProductId = 2001,
+ savingsProductName = "Premium Savings Account",
+ fieldOfficerId = 501,
+ status = null,
+ timeline = null,
+ currency = null,
+ nominalAnnualInterestRate = 3.5,
+ minRequiredOpeningBalance = 500.0,
+ lockinPeriodFrequency = 12.0,
+ withdrawalFeeForTransfers = true,
+ allowOverdraft = false,
+ enforceMinRequiredBalance = false,
+ withHoldTax = true,
+ lastActiveTransactionDate = listOf(2024, 3, 24),
+ dormancyTrackingActive = true,
+ summary = null,
+ transactions = listOf()
+ )
+}
\ No newline at end of file
diff --git a/feature/merchants/src/main/kotlin/org/mifospay/feature/merchants/MerchantTransferActivity.kt b/feature/merchants/src/main/kotlin/org/mifospay/feature/merchants/MerchantTransferActivity.kt
new file mode 100644
index 00000000..233c99a1
--- /dev/null
+++ b/feature/merchants/src/main/kotlin/org/mifospay/feature/merchants/MerchantTransferActivity.kt
@@ -0,0 +1,198 @@
+package org.mifospay.feature.merchants
+
+import android.os.Bundle
+import com.google.android.material.bottomsheet.BottomSheetBehavior
+import com.google.android.material.bottomsheet.BottomSheetBehavior.BottomSheetCallback
+import com.google.android.material.textfield.TextInputEditText
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import android.view.View
+import android.widget.Button
+import android.widget.ImageView
+import android.widget.TextView
+import butterknife.BindView
+import butterknife.ButterKnife
+import butterknife.OnClick
+import dagger.hilt.android.AndroidEntryPoint
+import com.mifospay.core.model.domain.Transaction
+import org.mifospay.MifosPayApp
+import org.mifospay.R
+import org.mifospay.base.BaseActivity
+import org.mifospay.common.ui.MakeTransferFragment
+import org.mifospay.history.ui.adapter.SpecificTransactionsAdapter
+import org.mifospay.merchants.presenter.MerchantTransferPresenter
+import org.mifospay.common.Constants
+import org.mifospay.home.BaseHomeContract
+import org.mifospay.utils.TextDrawable
+import org.mifospay.utils.Toaster
+import javax.inject.Inject
+
+/**
+ * Created by Shivansh Tiwari on 06/07/19.
+ */
+@AndroidEntryPoint
+class MerchantTransferActivity : BaseActivity(), BaseHomeContract.MerchantTransferView {
+ private var mBottomSheetBehavior: BottomSheetBehavior<*>? = null
+
+ @JvmField
+ @BindView(R.id.nsv_merchant_bottom_sheet_dialog)
+ var vMerchantBottomSheetDialog: View? = null
+
+ @JvmField
+ @BindView(R.id.iv_merchant_image)
+ var ivMerchantImage: ImageView? = null
+
+ @JvmField
+ @BindView(R.id.tv_pay_to_name)
+ var tvMerchantName: TextView? = null
+
+ @JvmField
+ @BindView(R.id.tv_pay_to_vpa)
+ var tvMerchantVPA: TextView? = null
+
+ @JvmField
+ @BindView(R.id.et_merchant_amount)
+ var etAmount: TextInputEditText? = null
+
+ @JvmField
+ @BindView(R.id.btn_submit)
+ var btnSubmit: Button? = null
+
+ @JvmField
+ @BindView(R.id.rv_merchant_history)
+ var rvMerchantHistory: RecyclerView? = null
+
+ @JvmField
+ @BindView(R.id.inc_empty_transactions_state_view)
+ var vEmptyState: View? = null
+
+ @JvmField
+ @BindView(R.id.iv_empty_no_transaction_history)
+ var ivTransactionsStateIcon: ImageView? = null
+
+ @JvmField
+ @BindView(R.id.tv_empty_no_transaction_history_title)
+ var tvTransactionsStateTitle: TextView? = null
+
+ @JvmField
+ @BindView(R.id.tv_empty_no_transaction_history_subtitle)
+ var tvTransactionsStateSubtitle: TextView? = null
+
+ @JvmField
+ @Inject
+ var mPresenter: MerchantTransferPresenter? = null
+ private var mTransferPresenter: BaseHomeContract.MerchantTransferPresenter? = null
+ private var merchantAccountNumber: String? = null
+
+ @JvmField
+ @Inject
+ var mMerchantHistoryAdapter: SpecificTransactionsAdapter? = null
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_merchant_transaction)
+ ButterKnife.bind(this)
+ setToolbarTitle("Merchant Transaction")
+ showColoredBackButton(R.drawable.ic_arrow_back_black_24dp)
+ setupUI()
+ mPresenter?.attachView(this)
+ mPresenter?.fetchMerchantTransfers(merchantAccountNumber)
+ }
+
+ private fun setupUI() {
+ setupBottomSheet()
+ merchantAccountNumber = intent.getStringExtra(Constants.MERCHANT_ACCOUNT_NO)
+ tvMerchantName?.text =
+ intent.getStringExtra(Constants.MERCHANT_NAME)
+ tvMerchantVPA?.text = intent.getStringExtra(Constants.MERCHANT_VPA)
+ val drawable = intent.getStringExtra(Constants.MERCHANT_NAME)
+ ?.substring(0, 1)?.let {
+ TextDrawable.builder().beginConfig()
+ .width(resources.getDimension(R.dimen.user_profile_image_size).toInt())
+ .height(resources.getDimension(R.dimen.user_profile_image_size).toInt())
+ .endConfig().buildRound(
+ it, R.color.colorPrimary
+ )
+ }
+ ivMerchantImage?.setImageDrawable(drawable)
+ showTransactionFetching()
+ setUpRecycleView()
+ }
+
+ private fun setUpRecycleView() {
+ mMerchantHistoryAdapter?.setContext(this)
+ rvMerchantHistory?.layoutManager =
+ LinearLayoutManager(MifosPayApp.context)
+ rvMerchantHistory?.adapter = mMerchantHistoryAdapter
+ }
+
+ override fun setPresenter(presenter: BaseHomeContract.MerchantTransferPresenter?) {
+ mTransferPresenter = presenter
+ }
+
+ private fun setupBottomSheet() {
+ mBottomSheetBehavior = BottomSheetBehavior.from(vMerchantBottomSheetDialog!!)
+ mBottomSheetBehavior?.setBottomSheetCallback(object : BottomSheetCallback() {
+ override fun onStateChanged(view: View, newState: Int) {
+ when (newState) {
+ BottomSheetBehavior.STATE_COLLAPSED -> {}
+ else -> {}
+ }
+ }
+
+ override fun onSlide(view: View, v: Float) {}
+ })
+ }
+
+ @OnClick(R.id.btn_submit)
+ fun makeTransaction() {
+ val externalId = tvMerchantVPA?.text.toString().trim { it <= ' ' }
+ val amount = etAmount?.text.toString().trim { it <= ' ' }
+ if (amount.isEmpty()) {
+ showToast(Constants.PLEASE_ENTER_ALL_THE_FIELDS)
+ return
+ } else if (amount.toDouble() <= 0) {
+ showToast(Constants.PLEASE_ENTER_VALID_AMOUNT)
+ return
+ }
+ mTransferPresenter?.checkBalanceAvailability(externalId, amount.toDouble())
+ }
+
+ override fun onBackPressed() {
+ if (mBottomSheetBehavior?.state != BottomSheetBehavior.STATE_COLLAPSED) {
+ mBottomSheetBehavior?.state = BottomSheetBehavior.STATE_COLLAPSED
+ return
+ }
+ super.onBackPressed()
+ }
+
+ override fun showToast(message: String?) {
+ Toaster.showToast(MifosPayApp.context, message)
+ }
+
+ override fun showPaymentDetails(externalId: String?, amount: Double) {
+ val fragment = MakeTransferFragment.newInstance(externalId, amount)
+ fragment.show(supportFragmentManager, "tag")
+ }
+
+ override fun showTransactionFetching() {
+ rvMerchantHistory?.visibility = View.GONE
+ tvTransactionsStateTitle?.text = resources.getString(R.string.fetching)
+ tvTransactionsStateSubtitle?.visibility = View.GONE
+ ivTransactionsStateIcon?.visibility = View.GONE
+ }
+
+ override fun showTransactions(transactions: List?) {
+ vEmptyState?.visibility = View.GONE
+ rvMerchantHistory?.visibility = View.VISIBLE
+ mMerchantHistoryAdapter?.setData(transactions as List)
+ }
+
+ override fun showSpecificView(drawable: Int, title: Int, subtitle: Int) {
+ rvMerchantHistory?.visibility = View.GONE
+ tvTransactionsStateSubtitle?.visibility = View.VISIBLE
+ ivTransactionsStateIcon?.visibility = View.VISIBLE
+ tvTransactionsStateTitle?.setText(title)
+ tvTransactionsStateSubtitle?.setText(subtitle)
+ ivTransactionsStateIcon?.setImageDrawable(resources.getDrawable(drawable))
+ }
+}
\ No newline at end of file
diff --git a/feature/merchants/src/main/kotlin/org/mifospay/feature/merchants/MerchantViewModel.kt b/feature/merchants/src/main/kotlin/org/mifospay/feature/merchants/MerchantViewModel.kt
new file mode 100644
index 00000000..5059b98d
--- /dev/null
+++ b/feature/merchants/src/main/kotlin/org/mifospay/feature/merchants/MerchantViewModel.kt
@@ -0,0 +1,144 @@
+package org.mifospay.feature.merchants
+
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.mifospay.core.model.entity.accounts.savings.SavingsWithAssociations
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.launch
+import org.mifospay.core.data.base.TaskLooper
+import org.mifospay.core.data.base.UseCase
+import org.mifospay.core.data.base.UseCaseFactory
+import org.mifospay.core.data.base.UseCaseHandler
+import org.mifospay.core.data.domain.usecase.account.FetchMerchants
+import org.mifospay.core.data.domain.usecase.client.FetchClientDetails
+import org.mifospay.core.data.util.Constants
+import javax.inject.Inject
+
+@HiltViewModel
+class MerchantViewModel @Inject constructor(
+ private val mUseCaseHandler: UseCaseHandler,
+ private val mFetchMerchantsUseCase: FetchMerchants,
+ private val mUseCaseFactory: UseCaseFactory
+) : ViewModel() {
+
+ @Inject
+ lateinit var mTaskLooper: TaskLooper
+
+ private val _searchQuery = MutableStateFlow("")
+ private val searchQuery: StateFlow = _searchQuery.asStateFlow()
+
+ private val _merchantUiState = MutableStateFlow(MerchantUiState.Loading)
+ val merchantUiState: StateFlow = _merchantUiState
+
+ init {
+ fetchMerchants()
+ }
+
+ private val _isRefreshing = MutableStateFlow(false)
+ val isRefreshing: StateFlow get() = _isRefreshing.asStateFlow()
+
+ fun refresh() {
+ viewModelScope.launch {
+ _isRefreshing.emit(true)
+ fetchMerchants()
+ _isRefreshing.emit(false)
+ }
+ }
+
+
+ val merchantsListUiState: StateFlow = searchQuery
+ .map { q ->
+ when (_merchantUiState.value) {
+ is MerchantUiState.ShowMerchants -> {
+ val merchantList =
+ (merchantUiState.value as MerchantUiState.ShowMerchants).merchants
+ val filterCards = merchantList.filter {
+ it.externalId.lowercase().contains(q.lowercase())
+ it.savingsProductName?.lowercase()?.contains(q.lowercase())
+ it.accountNo?.lowercase()?.contains(q.lowercase())
+ it.clientName.lowercase().contains(q.lowercase())
+ }
+ MerchantUiState.ShowMerchants(filterCards)
+ }
+
+ else -> MerchantUiState.ShowMerchants(arrayListOf())
+ }
+ }
+ .stateIn(
+ scope = viewModelScope,
+ started = SharingStarted.WhileSubscribed(5_000),
+ initialValue = MerchantUiState.ShowMerchants(arrayListOf())
+ )
+
+ fun updateSearchQuery(query: String) {
+ _searchQuery.update { query }
+ }
+
+
+ private fun fetchMerchants() {
+ _merchantUiState.value = MerchantUiState.Loading
+ mUseCaseHandler.execute(mFetchMerchantsUseCase,
+ FetchMerchants.RequestValues(),
+ object : UseCase.UseCaseCallback {
+ override fun onSuccess(response: FetchMerchants.ResponseValue) {
+ retrieveMerchantsData(response.savingsWithAssociationsList)
+ }
+
+ override fun onError(message: String) {
+ _merchantUiState.value = MerchantUiState.Error(message)
+ }
+ })
+ }
+
+ fun retrieveMerchantsData(
+ savingsWithAssociationsList: List
+ ) {
+ for (i in savingsWithAssociationsList.indices) {
+ mTaskLooper.addTask(
+ useCase = mUseCaseFactory.getUseCase(Constants.FETCH_CLIENT_DETAILS_USE_CASE)
+ as UseCase,
+ values = FetchClientDetails.RequestValues(
+ savingsWithAssociationsList[i].clientId.toLong()
+ ),
+ taskData = TaskLooper.TaskData("Client data", i)
+ )
+ }
+ mTaskLooper.listen(object : TaskLooper.Listener {
+ override fun onTaskSuccess(
+ taskData: TaskLooper.TaskData,
+ response: R
+ ) {
+ val responseValue = response as FetchClientDetails.ResponseValue
+ savingsWithAssociationsList[taskData.taskId].externalId =
+ responseValue.client.externalId
+ }
+
+ override fun onComplete() {
+ if (savingsWithAssociationsList.isEmpty()) {
+ _merchantUiState.value = MerchantUiState.Empty
+ } else {
+ _merchantUiState.value =
+ MerchantUiState.ShowMerchants(savingsWithAssociationsList)
+ }
+ }
+
+ override fun onFailure(message: String?) {
+ _merchantUiState.value = MerchantUiState.Error(message.toString())
+ }
+ })
+ }
+}
+
+sealed class MerchantUiState {
+ data object Loading : MerchantUiState()
+ data object Empty : MerchantUiState()
+ data class Error(val message: String) : MerchantUiState()
+ data class ShowMerchants(val merchants: List) : MerchantUiState()
+}
\ No newline at end of file
diff --git a/feature/merchants/src/main/kotlin/org/mifospay/feature/merchants/MerchantsItem.kt b/feature/merchants/src/main/kotlin/org/mifospay/feature/merchants/MerchantsItem.kt
new file mode 100644
index 00000000..776ead9b
--- /dev/null
+++ b/feature/merchants/src/main/kotlin/org/mifospay/feature/merchants/MerchantsItem.kt
@@ -0,0 +1,87 @@
+package org.mifospay.feature.merchants
+
+import androidx.compose.foundation.gestures.detectTapGestures
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.material3.CardDefaults
+import androidx.compose.material3.HorizontalDivider
+import androidx.compose.material3.Icon
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.input.pointer.pointerInput
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import com.mifospay.core.model.entity.accounts.savings.SavingsWithAssociations
+import org.mifospay.R
+import org.mifospay.core.designsystem.component.MifosCard
+import org.mifospay.core.designsystem.theme.mifosText
+import org.mifospay.core.designsystem.theme.styleMedium16sp
+
+@Composable
+fun MerchantsItem(
+ savingsWithAssociations: SavingsWithAssociations,
+ onMerchantClicked: () -> Unit,
+ onMerchantLongPressed: (String?) -> Unit
+) {
+ MifosCard(
+ modifier = Modifier.pointerInput(Unit) {
+ detectTapGestures(
+ onLongPress = {
+ onMerchantLongPressed(savingsWithAssociations.externalId)
+ }
+ )
+ },
+ onClick = { onMerchantClicked.invoke() },
+ colors = CardDefaults.cardColors(Color.White)
+ ) {
+ Column {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(top = 16.dp),
+ ) {
+ Icon(
+ painter = painterResource(id = R.drawable.ic_bank),
+ contentDescription = null,
+ modifier = Modifier
+ .align(Alignment.CenterVertically)
+ .padding(start = 16.dp, end = 16.dp)
+ .size(39.dp)
+ )
+
+ Column {
+ Text(
+ text = savingsWithAssociations.clientName,
+ color = mifosText,
+ )
+ Text(
+ text = savingsWithAssociations.externalId,
+ modifier = Modifier.padding(top = 4.dp),
+ style = styleMedium16sp.copy(mifosText)
+ )
+ }
+ }
+ }
+ HorizontalDivider(
+ thickness = 1.dp,
+ modifier = Modifier.padding(8.dp)
+ )
+ }
+}
+
+@Preview(showBackground = true)
+@Composable
+private fun AccountsItemPreview() {
+ MerchantsItem(
+ savingsWithAssociations = SavingsWithAssociations(),
+ onMerchantClicked = {},
+ onMerchantLongPressed = {}
+ )
+}
\ No newline at end of file
diff --git a/feature/merchants/src/test/java/org/mifospay/feature/merchants/ExampleUnitTest.kt b/feature/merchants/src/test/java/org/mifospay/feature/merchants/ExampleUnitTest.kt
new file mode 100644
index 00000000..3a6a0cc5
--- /dev/null
+++ b/feature/merchants/src/test/java/org/mifospay/feature/merchants/ExampleUnitTest.kt
@@ -0,0 +1,17 @@
+package org.mifospay.feature.merchants
+
+import org.junit.Test
+
+import org.junit.Assert.*
+
+/**
+ * Example local unit test, which will execute on the development machine (host).
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+class ExampleUnitTest {
+ @Test
+ fun addition_isCorrect() {
+ assertEquals(4, 2 + 2)
+ }
+}
\ No newline at end of file
diff --git a/settings.gradle.kts b/settings.gradle.kts
index b8e5231a..9e77d79d 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -48,6 +48,7 @@ include(":feature:invoices")
include(":feature:invoices")
include(":feature:settings")
include(":feature:profile")
+include(":feature:merchants")
include(":feature:accounts")
include(":feature:standing-instruction")
include(":feature:payments")