Compare commits

...

6 Commits

Author SHA1 Message Date
moon
0234c05e63 review detail changed 2026-09-28 10:48:40 +06:30
moon
1d2585f548 transaction info page fixed 2026-09-28 10:22:36 +06:30
moon
3e6a0707d8 settlement's unsettle data fixed 2026-09-21 17:26:20 +06:30
moon
4dee4bb6f9 mini preview box fixed 2026-09-21 17:10:43 +06:30
moon
a2e2829649 MPU to MPU/JCB/UPI 2026-09-21 16:37:42 +06:30
moon
d59933f00a phone number fixed 2026-09-21 15:02:50 +06:30
18 changed files with 750 additions and 213 deletions

View File

@ -40,8 +40,8 @@ android {
dimension = "env"
applicationIdSuffix = ".uat"
versionNameSuffix = "-UAT"
versionCode = 21
versionName = "3.9"
versionCode = 29
versionName = "4.7"
buildConfigField("String", "ENV", "\"UAT\"")
resValue("string", "app_name", "MOB-UAT")
}

View File

@ -10,21 +10,28 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Backspace
import androidx.compose.material.icons.automirrored.rounded.Backspace
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.unit.sp
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.mob.utsmyanmar.ui.theme.Color
import com.mob.utsmyanmar.ui.theme.MOBPOSTheme
import com.mob.utsmyanmar.utils.convertDateString
import com.utsmyanmar.paylibs.model.PayDetail
import com.utsmyanmar.paylibs.utils.core_utils.SystemParamsOperation
@Composable
internal fun HistoryTransactionCard(
@ -44,13 +51,32 @@ internal fun HistoryTransactionCard(
colors = CardDefaults.cardColors(containerColor = Color.White)
) {
Column(modifier = Modifier.padding(16.dp)) {
Text(
text = displayTransactionTitle(payDetail),
color = Color.LegacyRed,
style = MaterialTheme.typography.titleSmall
)
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(
text = displayTransactionTitle(payDetail),
color = Color.LegacyRed,
text = "Date & Time",
style = MaterialTheme.typography.titleSmall
)
Text(
"${convertDateString(payDetail.transDate.orEmpty())} ${payDetail.transTime.orEmpty()}".trim(),
style = MaterialTheme.typography.titleSmall
)
}
Spacer(modifier = Modifier.height(4.dp))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(
text = "Amount",
style = MaterialTheme.typography.titleSmall
)
Text(
@ -59,55 +85,172 @@ internal fun HistoryTransactionCard(
)
}
Spacer(modifier = Modifier.height(4.dp))
Text("Trace ${payDetail.voucherNo.ifBlank { "-" }}", style = MaterialTheme.typography.bodySmall)
Text("RRN ${payDetail.referNo.ifBlank { "-" }}", style = MaterialTheme.typography.bodySmall, color = Color.Gray)
Text(
"${convertDateString(payDetail.transDate)} ${payDetail.transTime}",
style = MaterialTheme.typography.bodySmall,
color = Color.Gray
)
}
}
}
@Composable
internal fun HistoryTransactionDetailCard(payDetail: PayDetail) {
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(containerColor = Color.White)
) {
Column(
modifier = Modifier.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
Text(
text = displayTransactionTitle(payDetail),
color = Color.LegacyRed,
style = MaterialTheme.typography.titleMedium
)
SummaryLine("Date", payDetail.tradeDate)
SummaryLine("Time", payDetail.tradeTime)
SummaryLine("Amount", displayAmountWithSign(payDetail))
SummaryLine("Trace No", payDetail.voucherNo)
// SummaryLine("Card No", payDetail.cardNo)
SummaryLine("Reference No", payDetail.referNo)
SummaryLine("Approval Code", payDetail.approvalCode.orEmpty())
SummaryLine("Status", displayTransactionStatus(payDetail))
}
}
}
@Composable
internal fun SummaryLine(label: String, value: String) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(label, color = Color.Gray, style = MaterialTheme.typography.bodyMedium)
Text(
value.ifBlank { "-" },
text = "Trace No",
style = MaterialTheme.typography.titleSmall
)
Text(
text = payDetail.voucherNo.orEmpty().ifBlank { "-" },
style = MaterialTheme.typography.titleSmall
)
}
Spacer(modifier = Modifier.height(4.dp))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(
text = "Reference No",
style = MaterialTheme.typography.titleSmall
)
Text(
text = payDetail.referNo.orEmpty().ifBlank { "-" },
style = MaterialTheme.typography.titleSmall
)
}
Spacer(modifier = Modifier.height(4.dp))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(
text = "Approval Code",
style = MaterialTheme.typography.titleSmall
)
Text(
text = (payDetail.approvalCode ?: payDetail.authNo).orEmpty().ifBlank { "-" },
style = MaterialTheme.typography.titleSmall
)
}
}
}
}
@Composable
internal fun HistoryTransactionDetailCard(
payDetail: PayDetail,
modifier: Modifier = Modifier,
) {
val dateStr = if (!payDetail.transDate.isNullOrBlank()) {
convertDateString(payDetail.transDate)
} else {
payDetail.tradeDate.orEmpty().ifBlank { "-" }
}
val timeStr = payDetail.transTime.orEmpty().ifBlank { payDetail.tradeTime.orEmpty() }.ifBlank { "-" }
val merchantNameStr = payDetail.merchantName?.takeIf { it.isNotBlank() }
?: SystemParamsOperation.getInstance().merchantName?.takeIf { it.isNotBlank() }
?: "-"
val midStr = payDetail.merchantNo?.takeIf { it.isNotBlank() }
?: SystemParamsOperation.getInstance().merchantId?.takeIf { it.isNotBlank() }
?: "-"
val tidStr = payDetail.terminalNo?.takeIf { it.isNotBlank() }
?: SystemParamsOperation.getInstance().terminalId?.takeIf { it.isNotBlank() }
?: "-"
val traceNoStr = payDetail.voucherNo.orEmpty().ifBlank { "-" }
val refNoStr = payDetail.referNo.orEmpty().ifBlank { "-" }
val aprCodeStr = payDetail.approvalCode?.takeIf { it.isNotBlank() }
?: payDetail.authNo?.takeIf { it.isNotBlank() }
?: "-"
val transTypeStr = displayTransactionTitle(payDetail)
val amountStr = displayAmountWithSign(payDetail)
val cardTypeStr = payDetail.accountType.orEmpty().ifBlank { "-" }
val cleanPan = payDetail.cardNo.orEmpty().replace(" ", "")
val cardNoStr = if (cleanPan.length > 10) {
cleanPan.take(6) + "*".repeat(cleanPan.length - 10) + cleanPan.takeLast(4)
} else if (cleanPan.isNotBlank()) {
cleanPan
} else {
"-"
}
val statusStr = displayTransactionStatus(payDetail)
Card(
modifier = modifier.fillMaxWidth(),
shape = RoundedCornerShape(12.dp),
colors = CardDefaults.cardColors(containerColor = Color.White),
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
) {
Column(
modifier = Modifier.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(10.dp)
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = transTypeStr,
color = Color.LegacyRed,
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold
)
// Text(
// text = amountStr,
// color = Color.Black,
// style = MaterialTheme.typography.titleMedium,
// fontWeight = FontWeight.Bold
// )
}
HorizontalDivider(
modifier = Modifier.padding(vertical = 2.dp),
color = Color.Gray.copy(alpha = 0.2f)
)
SummaryLine("Date", dateStr)
SummaryLine("Time", timeStr)
// SummaryLine("Merchant Name", merchantNameStr)
SummaryLine("Mid", midStr)
SummaryLine("Tid", tidStr)
SummaryLine("Trace No", traceNoStr)
SummaryLine("Ref No", refNoStr)
SummaryLine("APR Code", aprCodeStr)
SummaryLine("Transaction Type", transTypeStr)
SummaryLine("Amount", amountStr)
SummaryLine("Card Type", cardTypeStr)
SummaryLine("Card No", cardNoStr)
SummaryLine(
label = "Status",
value = statusStr,
valueColor = when {
statusStr.contains("SUCCESS", ignoreCase = true) ||
statusStr.contains("Approved", ignoreCase = true) -> Color.Success
statusStr.contains("FAIL", ignoreCase = true) ||
statusStr.contains("DECLINE", ignoreCase = true) -> Color.Error
else -> Color.Black
}
)
}
}
}
@Composable
internal fun SummaryLine(
label: String,
value: String,
valueColor: androidx.compose.ui.graphics.Color = Color.Black,
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = label,
color = Color.Gray,
style = MaterialTheme.typography.bodyMedium
)
Text(
text = value.ifBlank { "-" },
color = valueColor,
style = MaterialTheme.typography.bodyMedium,
fontWeight = FontWeight.Medium
)
}
}
@ -174,7 +317,7 @@ internal fun HistoryNumericKeypad(
)
) {
if (key == "⌫") {
Icon(Icons.Rounded.Backspace, contentDescription = "Backspace")
Icon(Icons.AutoMirrored.Rounded.Backspace, contentDescription = "Backspace")
} else {
Text(key, style = MaterialTheme.typography.headlineMedium)
}
@ -184,3 +327,89 @@ internal fun HistoryNumericKeypad(
}
}
}
internal val searchKeys = listOf(
listOf("1", "2", "3"),
listOf("4", "5", "6"),
listOf("7", "8", "9"),
listOf("C", "0", "\u232B")
)
@Composable
internal fun SearchInputBox(
query: String,
isFocused: Boolean,
onFocus: () -> Unit,
label: String = if (isFocused) "Trace or RRN" else "Tap to search by Trace or RRN",
onDelete: (() -> Unit)? = null,
) {
Card(
modifier = Modifier
.fillMaxWidth()
.clickable(onClick = onFocus),
colors = CardDefaults.cardColors(containerColor = Color.White),
shape = RoundedCornerShape(12.dp),
elevation = CardDefaults.cardElevation(defaultElevation = if (isFocused) 6.dp else 2.dp)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 14.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Column(modifier = Modifier.weight(1f)) {
Text(
text = label,
style = MaterialTheme.typography.bodySmall,
color = if (isFocused) Color.LegacyRed else Color.Gray
)
Spacer(modifier = Modifier.height(4.dp))
Text(
text = query.ifEmpty { "_" },
color = Color.LegacyRed,
style = MaterialTheme.typography.headlineSmall.copy(
fontWeight = FontWeight.Bold,
letterSpacing = 2.sp
)
)
}
if (query.isNotEmpty()) {
Card(
shape = RoundedCornerShape(18.dp),
colors = CardDefaults.cardColors(containerColor = Color.White),
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp),
modifier = Modifier
.shadow(2.dp, RoundedCornerShape(18.dp))
.clickable(enabled = onDelete != null) { onDelete?.invoke() }
) {
Icon(
imageVector = Icons.AutoMirrored.Rounded.Backspace,
contentDescription = "Delete",
tint = Color.LegacyRed,
modifier = Modifier.padding(horizontal = 14.dp, vertical = 12.dp)
)
}
}
}
}
}
@Preview(showBackground = true)
@Composable
private fun HistoryTransactionCardPreview() {
MOBPOSTheme {
HistoryTransactionCard(
payDetail = PayDetail().apply {
transDate = "21/09/26"
transTime = "14:04:15"
voucherNo = "000001"
referNo = "123456789012"
transType = "SALE"
amount = 1500000L
},
onClick = {}
)
}
}

View File

@ -55,11 +55,17 @@ internal fun displayTransactionStatus(payDetail: PayDetail): String {
if (payDetail.transactionType == TransactionsType.REVERSAL.value) {
return if (payDetail.isNeedReversal) "REVERSAL FAILED" else "REVERSAL SUCCESS"
}
return POSUtil.getInstance().getResponse(payDetail.tradeAnswerCode.orEmpty().ifBlank { "-" })
val code = payDetail.tradeAnswerCode.orEmpty().ifBlank { "-" }
return try {
POSUtil.getInstance().getResponse(code)
} catch (_: Throwable) {
if (code == "00") "Approved" else code
}
}
internal fun displayTransactionTitle(payDetail: PayDetail): String {
val title = payDetail.transType
.orEmpty()
.ifBlank { "TRANSACTION" }
.replace("CASH_OUT", "CASH_ADV", ignoreCase = true)
return if (payDetail.transactionType == TransactionsType.REVERSAL.value) {

View File

@ -159,7 +159,8 @@ fun HistoryRoute(
is HistoryDestination.ReviewDetail -> ReviewDetailScreen(
viewModel = viewModel,
trace = current.trace,
onBack = { destination = HistoryDestination.ReviewTrace }
onBack = { destination = HistoryDestination.ReviewTrace },
onReprint = ::reprintTransaction
)
HistoryDestination.BatchTotal -> BatchTotalScreen(

View File

@ -21,6 +21,7 @@ class HistoryViewModel @Inject constructor(
fun getLastSettlement(): LiveData<List<PayDetail>> =
repository.getLastSettlement()
fun getReviewBatchTransaction(batchNo: String): LiveData<List<PayDetail>> =
repository.getReviewBatchTransaction(batchNo)
@ -28,5 +29,9 @@ class HistoryViewModel @Inject constructor(
repository.getSmileSummaryTrans(batchNo)
fun searchTransaction(voucherNo: String): LiveData<PayDetail> =
repository.searchTransaction(voucherNo)
if (voucherNo.length > 6) {
repository.searchPayDetail(voucherNo)
} else {
repository.searchTransaction(voucherNo.padStart(6, '0'))
}
}

View File

@ -39,25 +39,15 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.mob.utsmyanmar.ui.components.NumericKeypad
import com.mob.utsmyanmar.ui.preview.P2Preview
import com.mob.utsmyanmar.ui.preview.P3Preview
import com.mob.utsmyanmar.ui.theme.Color
import com.mob.utsmyanmar.ui.theme.MOBPOSTheme
import com.utsmyanmar.paylibs.model.PayDetail
import com.utsmyanmar.paylibs.utils.enums.HostType
private val searchKeys = listOf(
listOf("1", "2", "3"),
listOf("4", "5", "6"),
listOf("7", "8", "9"),
listOf("C", "0", "\u232B")
)
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ReprintAnyTransactionScreen(
viewModel: HistoryViewModel,
@ -66,6 +56,22 @@ fun ReprintAnyTransactionScreen(
onReprintTransaction: (PayDetail) -> Unit,
) {
val allTransactions by viewModel.getTransactionHistory().observeAsState(emptyList())
ReprintAnyTransactionContent(
allTransactions = allTransactions,
hostType = hostType,
onBack = onBack,
onReprintTransaction = onReprintTransaction
)
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ReprintAnyTransactionContent(
allTransactions: List<PayDetail>,
hostType: HostType,
onBack: () -> Unit,
onReprintTransaction: (PayDetail) -> Unit,
) {
var searchQuery by remember { mutableStateOf("") }
var isFocused by remember { mutableStateOf(false) }
@ -115,7 +121,8 @@ fun ReprintAnyTransactionScreen(
SearchInputBox(
query = searchQuery,
isFocused = isFocused,
onFocus = { isFocused = true }
onFocus = { isFocused = true },
onDelete = { searchQuery = searchQuery.dropLast(1) }
)
Spacer(modifier = Modifier.height(8.dp))
@ -178,86 +185,45 @@ fun ReprintAnyTransactionScreen(
}
}
@Composable
private fun SearchInputBox(
query: String,
isFocused: Boolean,
onFocus: () -> Unit,
) {
Card(
modifier = Modifier
.fillMaxWidth()
.clickable(onClick = onFocus),
colors = CardDefaults.cardColors(containerColor = Color.White),
shape = RoundedCornerShape(12.dp),
elevation = CardDefaults.cardElevation(defaultElevation = if (isFocused) 6.dp else 2.dp)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 14.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Column(modifier = Modifier.weight(1f)) {
Text(
text = if (isFocused) "Trace or RRN" else "Tap to search by Trace or RRN",
style = MaterialTheme.typography.bodySmall,
color = if (isFocused) Color.LegacyRed else Color.Gray
)
Spacer(modifier = Modifier.height(4.dp))
Text(
text = query.ifEmpty { "_" },
color = Color.LegacyRed,
style = MaterialTheme.typography.headlineSmall.copy(
fontWeight = FontWeight.Bold,
letterSpacing = 2.sp
)
)
}
if (query.isNotEmpty()) {
Card(
shape = RoundedCornerShape(18.dp),
colors = CardDefaults.cardColors(containerColor = Color.White),
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp),
modifier = Modifier
.shadow(2.dp, RoundedCornerShape(18.dp))
.clickable { }
) {
Icon(
imageVector = Icons.AutoMirrored.Rounded.Backspace,
contentDescription = "Delete",
tint = Color.LegacyRed,
modifier = Modifier.padding(horizontal = 14.dp, vertical = 12.dp)
)
}
}
}
}
}
@P2Preview
@P3Preview
@Preview(showBackground = true)
@Composable
private fun ReprintAnyTransactionScreenPreview() {
MOBPOSTheme {
Box(modifier = Modifier.height(600.dp)) {
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(containerColor = Color.White)
) {
Column(modifier = Modifier.padding(16.dp)) {
Text(
text = "SALE",
color = Color.LegacyRed,
style = MaterialTheme.typography.titleSmall
ReprintAnyTransactionContent(
allTransactions = listOf(
PayDetail().apply {
transDate = "21/09/26"
transTime = "14:04:15"
merchantName = "UTS Merchant"
merchantNo = "100089476209176"
terminalNo = "10420257"
voucherNo = "000001"
referNo = "123456789012"
approvalCode = "123456"
transType = "SALE"
amount = 1500000L
accountType = "MPU"
cardNo = "9503036867315548"
},
PayDetail().apply {
transDate = "21/09/26"
transTime = "15:20:30"
merchantName = "UTS Merchant"
merchantNo = "100089476209176"
terminalNo = "10420257"
voucherNo = "000002"
referNo = "123456789013"
approvalCode = "654321"
transType = "SALE"
amount = 250000L
accountType = "MPU"
cardNo = "9503036867319999"
}
),
hostType = HostType.MPU,
onBack = {},
onReprintTransaction = {}
)
Spacer(modifier = Modifier.height(4.dp))
Text("Trace 000001", style = MaterialTheme.typography.bodySmall)
Text("RRN 123456789012", style = MaterialTheme.typography.bodySmall, color = Color.Gray)
}
}
}
}
}

View File

@ -2,10 +2,19 @@ package com.mob.utsmyanmar.ui.history_functions
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@ -15,16 +24,34 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.mob.utsmyanmar.ui.components.appbar.AppBar
import com.mob.utsmyanmar.ui.preview.P2Preview
import com.mob.utsmyanmar.ui.preview.P3Preview
import com.mob.utsmyanmar.ui.theme.Color
import com.mob.utsmyanmar.ui.theme.MOBPOSTheme
import com.utsmyanmar.paylibs.model.PayDetail
@Composable
fun ReviewDetailScreen(
viewModel: HistoryViewModel,
trace: String,
onBack: () -> Unit,
onReprint: ((PayDetail) -> Unit)? = null,
) {
val transaction by viewModel.searchTransaction(trace).observeAsState()
ReviewDetailContent(
transaction = transaction,
onBack = onBack,
onReprint = onReprint
)
}
@Composable
fun ReviewDetailContent(
transaction: PayDetail?,
onBack: () -> Unit,
onReprint: ((PayDetail) -> Unit)? = null,
) {
Scaffold(
containerColor = Color.IvoryBeige,
topBar = {
@ -40,9 +67,28 @@ fun ReviewDetailScreen(
.fillMaxSize()
.padding(paddingValues)
.padding(16.dp)
.verticalScroll(rememberScrollState())
) {
transaction?.let { payDetail ->
HistoryTransactionDetailCard(payDetail)
if (onReprint != null && isValidForReprint(payDetail)) {
Spacer(modifier = Modifier.height(16.dp))
Button(
onClick = { onReprint(prepareReprintPayDetail(payDetail)) },
modifier = Modifier
.fillMaxWidth()
.height(48.dp),
shape = RoundedCornerShape(8.dp),
colors = ButtonDefaults.buttonColors(containerColor = Color.LegacyRed)
) {
Text(
text = "Reprint",
style = MaterialTheme.typography.titleMedium,
color = Color.White
)
}
}
} ?: Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
@ -52,3 +98,68 @@ fun ReviewDetailScreen(
}
}
}
@P2Preview
@P3Preview
@Composable
private fun ReviewDetailScreenPreview() {
MOBPOSTheme {
ReviewDetailContent(
transaction = PayDetail().apply {
transDate = "21/09/26"
transTime = "14:04:15"
merchantName = "UTS Merchant"
merchantNo = "100089476209176"
terminalNo = "10420257"
voucherNo = "000001"
referNo = "123456789012"
approvalCode = "123456"
transType = "SALE"
amount = 1500000L
accountType = "MPU"
cardNo = "9503036867315548"
tradeAnswerCode = "00"
},
onBack = {}
)
}
}
@P2Preview
@P3Preview
@Composable
private fun ReviewDetailScreenWithReprintPreview() {
MOBPOSTheme {
ReviewDetailContent(
transaction = PayDetail().apply {
transDate = "21/09/26"
transTime = "14:04:15"
merchantName = "UTS Merchant"
merchantNo = "100089476209176"
terminalNo = "10420257"
voucherNo = "000001"
referNo = "123456789012"
approvalCode = "123456"
transType = "SALE"
amount = 1500000L
accountType = "MPU"
cardNo = "9503036867315548"
tradeAnswerCode = "00"
},
onBack = {},
onReprint = {}
)
}
}
@P2Preview
@P3Preview
@Composable
private fun ReviewDetailScreenNotFoundPreview() {
MOBPOSTheme {
ReviewDetailContent(
transaction = null,
onBack = {}
)
}
}

View File

@ -1,18 +1,20 @@
package com.mob.utsmyanmar.ui.history_functions
import androidx.compose.foundation.layout.Box
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@ -22,16 +24,16 @@ import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.material3.MaterialTheme
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.mob.utsmyanmar.ui.components.NumericKeypad
import com.mob.utsmyanmar.ui.components.appbar.AppBar
import com.mob.utsmyanmar.ui.preview.P2Preview
import com.mob.utsmyanmar.ui.preview.P3Preview
import com.mob.utsmyanmar.ui.theme.Color
import com.mob.utsmyanmar.ui.theme.MOBPOSTheme
import com.utsmyanmar.paylibs.model.PayDetail
@Composable
@ -40,7 +42,6 @@ fun ReviewTraceScreen(
onBack: () -> Unit,
onNavigateReviewDetail: (String) -> Unit,
) {
var traceNumber by remember { mutableStateOf("") }
var searchedTrace by remember { mutableStateOf("") }
var lastNavigatedTrace by remember { mutableStateOf<String?>(null) }
@ -61,6 +62,24 @@ fun ReviewTraceScreen(
onNavigateReviewDetail(matchedTrace)
}
ReviewTraceContent(
searchedTrace = searchedTrace,
transactionFound = transaction != null,
onBack = onBack,
onSearch = { query -> searchedTrace = query }
)
}
@Composable
fun ReviewTraceContent(
searchedTrace: String,
transactionFound: Boolean,
onBack: () -> Unit,
onSearch: (String) -> Unit,
) {
var traceNumber by remember { mutableStateOf("") }
var isFocused by remember { mutableStateOf(true) }
Scaffold(
containerColor = Color.IvoryBeige,
topBar = {
@ -78,12 +97,12 @@ fun ReviewTraceScreen(
.padding(16.dp)
) {
Text(
text = if (searchedTrace.isNotBlank() && transaction == null) {
text = if (searchedTrace.isNotBlank() && !transactionFound) {
"No transaction found for trace $searchedTrace."
} else {
"Input trace number to review transaction."
},
color = if (searchedTrace.isNotBlank() && transaction == null) {
color = if (searchedTrace.isNotBlank() && !transactionFound) {
Color.LegacyRed
} else {
Color.Black
@ -93,49 +112,66 @@ fun ReviewTraceScreen(
Spacer(modifier = Modifier.height(16.dp))
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(containerColor = Color.White)
) {
Text(
text = traceNumber.padStart(6, '0').ifEmpty { "000000" },
modifier = Modifier
.fillMaxWidth()
.padding(24.dp),
textAlign = TextAlign.Center,
style = MaterialTheme.typography.displayMedium.copy(fontSize = 28.sp)
SearchInputBox(
query = traceNumber,
isFocused = isFocused,
onFocus = { isFocused = true },
onDelete = { traceNumber = traceNumber.dropLast(1) }
)
}
Spacer(modifier = Modifier.height(16.dp))
HistoryNumericKeypad(
onDigit = { digit ->
if (traceNumber.length < 6) traceNumber += digit
},
onBackspace = {
if (traceNumber.isNotEmpty()) {
traceNumber = traceNumber.dropLast(1)
AnimatedVisibility(
visible = isFocused,
enter = slideInVertically(initialOffsetY = { it }),
exit = slideOutVertically(targetOffsetY = { it })
) {
NumericKeypad(
keys = searchKeys,
modifier = Modifier
.fillMaxWidth()
.height(280.dp)
.padding(top = 8.dp),
onKeyClick = { key ->
when (key) {
"C" -> traceNumber = ""
"\u232B" -> traceNumber = traceNumber.dropLast(1)
else -> if (traceNumber.length < 12) traceNumber += key
}
}
},
onClear = { traceNumber = "" }
)
}
Spacer(modifier = Modifier.weight(1f))
Button(
onClick = { searchedTrace = traceNumber.padStart(6, '0') },
onClick = { onSearch(traceNumber.trim()) },
enabled = traceNumber.isNotBlank(),
modifier = Modifier
.fillMaxWidth()
.height(56.dp),
shape = RoundedCornerShape(8.dp),
colors = ButtonDefaults.buttonColors(
containerColor = Color.LegacyRed,
contentColor = Color.White
)
) {
Text("Search")
Text("Search", style = MaterialTheme.typography.titleMedium)
}
}
}
}
@P2Preview
@P3Preview
@Composable
private fun ReviewTraceScreenPreview() {
MOBPOSTheme {
ReviewTraceContent(
searchedTrace = "",
transactionFound = false,
onBack = {},
onSearch = {}
)
}
}

View File

@ -52,7 +52,7 @@ fun SelectHostScreen(
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
HostButton("MPU") { onSelectHost(HostType.MPU) }
HostButton("MPU / JCB / UPI") { onSelectHost(HostType.MPU) }
HostButton("Visa / Master") {
if (SystemParamsOperation.getInstance().isEmvEnabled) {
onSelectHost(HostType.VISA_MASTER)

View File

@ -669,8 +669,22 @@ fun AppNavGraph(
},
onNavigateInputAmount = { navController.popBackStack(Routes.Amount.route, false) },
onNavigateProcessing = {},
onNavigateEmvTransaction = {},
onNavigateError = {},
onNavigateEmvTransaction = {
navController.navigate(Routes.Processing.route) {
popUpTo(Routes.ProcessingCard.route) {
inclusive = true
}
launchSingleTop = true
}
},
onNavigateError = { message ->
navController.navigate(Routes.TransactionError.createRoute(false, message)) {
popUpTo(Routes.ProcessingCard.route) {
inclusive = true
}
launchSingleTop = true
}
},
onBack = { navController.popBackStack() },
onShowDecline = {}
)
@ -761,6 +775,7 @@ fun AppNavGraph(
sharedViewModel = sharedViewModel,
transProcessViewModel = transProcessViewModel,
settlementViewModel = settlementViewModel,
emvTransactionViewModel = hiltViewModel(activity),
onTransactionFinished = {
tmsSetupViewModel.downloadConfigs(dashboardViewModel::refreshLastSyncTime)
},

View File

@ -22,7 +22,6 @@ import com.utsmyanmar.paylibs.model.PayDetail
import com.utsmyanmar.paylibs.model.TradeData
import com.utsmyanmar.paylibs.model.enums.TransCVM
import com.utsmyanmar.paylibs.utils.POSUtil
import com.utsmyanmar.paylibs.utils.core_utils.SystemParamsOperation
import com.utsmyanmar.paylibs.utils.enums.TransMenu
import com.utsmyanmar.paylibs.utils.params.Params
import kotlinx.coroutines.channels.Channel
@ -250,7 +249,10 @@ class ProcessingCardViewModel(
emvTransactionViewModel.transType.value =
sharedViewModel.transactionsType.value
if (SystemParamsOperation.getInstance().isEmvEnabled) {
if (
// SystemParamsOperation.getInstance().isEmvEnabled
true
) {
prepareEmvTransaction()
sendUiEvent(ProcessingCardUiEvent.NavigateToEmvTransaction)
} else {

View File

@ -4,11 +4,13 @@ import android.R
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.mob.utsmyanmar.model.TransResultStatus
import com.mob.utsmyanmar.model.TransactionStatus
import com.mob.utsmyanmar.ui.settlement.SettlementViewModel
import com.mob.utsmyanmar.viewmodel.SharedViewModel
import com.mob.utsmyanmar.viewmodel.EmvTransactionProcessViewModel
import com.mob.utsmyanmar.viewmodel.TransProcessViewModel
import com.utsmyanmar.paylibs.utils.iso_utils.TransactionsType
@ -19,6 +21,7 @@ fun ProcessingRoute(
sharedViewModel: SharedViewModel,
transProcessViewModel: TransProcessViewModel,
settlementViewModel: SettlementViewModel,
emvTransactionViewModel: EmvTransactionProcessViewModel,
onNavigateTransactionResult: () -> Unit,
onNavigateSignature: () -> Unit,
onNavigateError: (Boolean, String) -> Unit,
@ -26,12 +29,47 @@ fun ProcessingRoute(
) {
val transStatus by transProcessViewModel.transResultStatus.collectAsStateWithLifecycle()
val emvStatus by emvTransactionViewModel.getTransStatus().observeAsState()
val state by transProcessViewModel.state.collectAsStateWithLifecycle()
val settlementStatus by settlementViewModel.uiState.collectAsStateWithLifecycle()
if(sharedViewModel.transactionsType.value == TransactionsType.SETTLEMENT) {
if (sharedViewModel.isEmvTrans().value == true) {
LaunchedEffect(Unit) {
emvTransactionViewModel.startProcess()
}
LaunchedEffect(emvStatus) {
when (emvStatus) {
TransResultStatus.SUCCESS,
TransResultStatus.OFFLINE_SUCCESS -> {
onTransactionFinished()
sharedViewModel.payDetail.value = emvTransactionViewModel.payDetailResult.value
onNavigateTransactionResult()
}
TransResultStatus.FAIL,
TransResultStatus.OFFLINE_FAILURE -> {
onTransactionFinished()
sharedViewModel.payDetail.value = emvTransactionViewModel.payDetailResult.value
onNavigateTransactionResult()
}
TransResultStatus.NETWORK_ERROR,
TransResultStatus.ERROR -> {
onTransactionFinished()
onNavigateError(
false,
emvTransactionViewModel.errorCodeMsg.value
?: "Something went wrong. Please try again."
)
}
else -> Unit
}
}
} else if(sharedViewModel.transactionsType.value == TransactionsType.SETTLEMENT) {
LaunchedEffect(Unit) {
settlementViewModel.startSettlementProcess()
}
@ -98,7 +136,9 @@ fun ProcessingRoute(
}
}
if(sharedViewModel.transactionsType.value == TransactionsType.SETTLEMENT) {
if (sharedViewModel.isEmvTrans().value == true) {
ProcessingScreen(state)
} else if(sharedViewModel.transactionsType.value == TransactionsType.SETTLEMENT) {
ProcessingScreen(settlementStatus.processingState)
} else {
ProcessingScreen(state)

View File

@ -25,10 +25,14 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.mob.utsmyanmar.ui.components.appbar.AppBar
import com.mob.utsmyanmar.ui.history_functions.displayAmountWithSign
import com.mob.utsmyanmar.ui.theme.Color
import com.mob.utsmyanmar.ui.theme.MOBPOSTheme
import com.mob.utsmyanmar.utils.convertDateString
import com.mob.utsmyanmar.viewmodel.SharedViewModel
import com.utsmyanmar.paylibs.model.PayDetail
import com.utsmyanmar.paylibs.utils.POSUtil
@ -42,6 +46,22 @@ fun SettlementScreen(
autoStartSettlement: Boolean = false
) {
val records by settlementViewMode.getSettlementPOS().observeAsState(emptyList())
SettlementContent(
records = records,
onBack = onBack,
onStartSettlement = onStartSettlement,
autoStartSettlement = autoStartSettlement
)
}
@Composable
private fun SettlementContent(
records: List<PayDetail>,
onBack: () -> Unit,
onStartSettlement: () -> Unit,
autoStartSettlement: Boolean = false
) {
val totalAmount = records.sumOf { it.amount }
LaunchedEffect(autoStartSettlement, records) {
@ -116,7 +136,7 @@ fun SettlementScreen(
verticalArrangement = Arrangement.spacedBy(0.dp)
) {
items(records) { record ->
SettlementRecordRow(record = record)
SettlementRecordRow(payDetail = record)
}
}
}
@ -164,40 +184,87 @@ private fun SummaryRow(
}
@Composable
private fun SettlementRecordRow(record: PayDetail) {
private fun SettlementRecordRow(payDetail: PayDetail) {
Column(modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp)) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
Spacer(Modifier.height(4.dp))
Text(
text = record.transType
text = payDetail.transType
.orEmpty()
.ifBlank { "UNKNOWN" }
.replace("CASH_OUT", "CASH_ADV", ignoreCase = true),
color = Color.LegacyRed,
style = MaterialTheme.typography.titleSmall
)
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(
text = POSUtil.getInstance().getDecimalAmountSeparatorFormat(record.amount),
color = Color.Black,
text = "Date & Time",
style = MaterialTheme.typography.titleSmall
)
Text(
text = "${convertDateString(payDetail.transDate.orEmpty())} ${payDetail.transTime.orEmpty()}".trim().ifBlank { "-" },
style = MaterialTheme.typography.titleSmall
)
}
Spacer(modifier = Modifier.height(4.dp))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(
text = "Trace ${record.voucherNo.ifBlank { "-" }}",
color = Color.Black,
style = MaterialTheme.typography.bodySmall
text = "Amount",
style = MaterialTheme.typography.titleSmall
)
Text(
text = "Card ${maskCardNumber(record.cardNo)}",
color = Color.Gray,
style = MaterialTheme.typography.bodySmall
text = displayAmountWithSign(payDetail),
style = MaterialTheme.typography.titleSmall
)
Spacer(modifier = Modifier.height(12.dp))
}
Spacer(modifier = Modifier.height(4.dp))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(
text = "Trace No",
style = MaterialTheme.typography.titleSmall
)
Text(
text = payDetail.voucherNo.orEmpty().ifBlank { "-" },
style = MaterialTheme.typography.titleSmall
)
}
Spacer(modifier = Modifier.height(4.dp))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(
text = "Reference No",
style = MaterialTheme.typography.titleSmall
)
Text(
text = payDetail.referNo.orEmpty().ifBlank { "-" },
style = MaterialTheme.typography.titleSmall
)
}
Spacer(modifier = Modifier.height(4.dp))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(
text = "Approval Code",
style = MaterialTheme.typography.titleSmall
)
Text(
text = payDetail.approvalCode.orEmpty().ifBlank { "-" },
style = MaterialTheme.typography.titleSmall
)
}
Spacer(Modifier.height(4.dp))
HorizontalDivider(color = Color.Gray.copy(alpha = 0.25f))
}
}
@ -209,3 +276,29 @@ private fun maskCardNumber(cardNo: String?): String {
val stars = "*".repeat(pan.length - 8)
return pan.take(4) + stars + pan.takeLast(4)
}
@Preview(showBackground = true)
@Composable
private fun SettlementScreenPreview() {
val sampleRecords = listOf(
PayDetail().apply {
transType = "SALE"
amount = 150000L
voucherNo = "000001"
cardNo = "9503036867315548"
},
PayDetail().apply {
transType = "CASH_OUT"
amount = 50000L
voucherNo = "000002"
cardNo = "9503036867311234"
}
)
MOBPOSTheme {
SettlementContent(
records = sampleRecords,
onBack = {},
onStartSettlement = {}
)
}
}

View File

@ -20,6 +20,7 @@ fun String?.maskCardNumber(): String {
return maskedPan.toString()
}
fun convertDateString(dateStr: String): String {
fun convertDateString(dateStr: String?): String {
if (dateStr.isNullOrEmpty()) return ""
return dateStr.replace(Regex("/(\\d{2})$"), "/20$1")
}

View File

@ -88,7 +88,7 @@ class TMSSetupsImpl : TMSSetups {
ops.merchantName = m.name
ops.merchantAddress = m.address
ops.merchantPhoneNo = m.phone
ops.merchantMobileNo = m.mobile_phone
ops.merchantMobileNo = m.mobilePhone.orEmpty()
}
siriusResponse.address.takeIf { it.isNotEmpty() }?.let { ops.merchantAddress = it }

View File

@ -1,5 +1,7 @@
package com.utsmyanmar.baselib.network.model.sirius;
import com.google.gson.annotations.SerializedName;
public class SiriusMerchant {
private String id;
@ -16,7 +18,11 @@ public class SiriusMerchant {
private String mobile;
private String mobile_phone;
@SerializedName(value = "mobilePhone", alternate = {"mobile_phone"})
private String mobilePhone;
public SiriusMerchant() {
}
public SiriusMerchant(String id, String name, String description, String address,String address2,String phone,String mobile) {
this.id = id;
@ -28,6 +34,17 @@ public class SiriusMerchant {
this.mobile = mobile;
}
public SiriusMerchant(String id, String name, String description, String address, String address2, String phone, String mobile, String mobilePhone) {
this.id = id;
this.name = name;
this.description = description;
this.address = address;
this.address2 = address2;
this.phone = phone;
this.mobile = mobile;
this.mobilePhone = mobilePhone;
}
public void setAddress2(String address2) {
this.address2 = address2;
}
@ -52,12 +69,20 @@ public class SiriusMerchant {
return mobile;
}
public String getMobilePhone() {
return mobilePhone != null ? mobilePhone : mobile;
}
public void setMobilePhone(String mobilePhone) {
this.mobilePhone = mobilePhone;
}
public String getMobile_phone() {
return mobile_phone;
return getMobilePhone();
}
public void setMobile_phone(String mobile_phone) {
this.mobile_phone = mobile_phone;
this.mobilePhone = mobile_phone;
}
public void setId(String id) {

View File

@ -88,23 +88,33 @@ public class BaseErrorCode {
}
public static String getCode(String code) {
if (instance == null) instance = new BaseErrorCode();
if (instance == null || (instance.errorCode.isEmpty() && PayLibsUtils.getInstance().context != null)) {
instance = new BaseErrorCode();
}
String errorMsg = instance.errorCode.get(code);
// return (errorMsg != null ? "RESPONSE CODE"+"(" + code + ")\n ("+errorMsg+")" : getString(R.string.errorOthers));
if(code.length() == 2) {
return (errorMsg != null ? errorMsg : getString(R.string.errorOthers));
if (errorMsg != null) return errorMsg;
String defaultError = getString(R.string.errorOthers);
return defaultError != null ? defaultError : ("00".equals(code) ? "Approved" : "Others");
} else if(code.isEmpty()) {
return "NO RESPONSE";
} else {
return (errorMsg != null ? code + " - "+errorMsg : getString(R.string.errorOthers));
if (errorMsg != null) return code + " - "+errorMsg;
String defaultError = getString(R.string.errorOthers);
return defaultError != null ? code + " - " + defaultError : code + " - Others";
}
}
public static String getErrorMessage(String code) {
if (instance == null) instance = new BaseErrorCode();
if (instance == null || (instance.errorCode.isEmpty() && PayLibsUtils.getInstance().context != null)) {
instance = new BaseErrorCode();
}
String errorMsg = instance.errorCode.get(code);
return (errorMsg != null ?" "+ errorMsg+" " :getString(R.string.errorOthers));
if (errorMsg != null) return " " + errorMsg + " ";
String defaultError = getString(R.string.errorOthers);
return " " + (defaultError != null ? defaultError : "Others") + " ";
}
private static BaseErrorCode instance;
@ -117,14 +127,21 @@ public class BaseErrorCode {
if (KeyValueSet == null) return;
int size = KeyValueSet.length;
if (size % 2 == 0)
for (int i = 0; i < size; i += 2)
errorCode.put((String) KeyValueSet[i],getString((Integer)KeyValueSet[i + 1]));
for (int i = 0; i < size; i += 2) {
String val = getString((Integer)KeyValueSet[i + 1]);
if (val != null) {
errorCode.put((String) KeyValueSet[i], val);
}
}
else throw new RuntimeException("Error code key value pairs must be even numbers");
}
private Map<String, String> errorCode = new HashMap<>();
private static String getString(int id) {
if (PayLibsUtils.getInstance() == null || PayLibsUtils.getInstance().context == null) {
return null;
}
return PayLibsUtils.getInstance().context.getString(id);
}

View File

@ -3,6 +3,7 @@ package com.utsmyanmar.paylibs.utils;
import android.text.TextUtils;
import sunmi.sunmiui.utils.LogUtil;
import com.utsmyanmar.paylibs.utils.enums.CardScheme;
public class CardSchemeUtils {
private static final String TAG = "CardSchemeUtil";
@ -25,19 +26,8 @@ public class CardSchemeUtils {
} else if ("A000000524".equals(group)) {
accountType = AccountType.RPC.getValue();
} else if ("A000000333".equals(group)) {
if ("A000000333010101".equals(aid)) {
accountType = AccountType.DD.getValue();
} else if ("A000000333010102".equals(aid)) {
accountType = AccountType.CC.getValue();
} else if ("A000000333010103".equals(aid)) {
accountType = AccountType.SCC.getValue();
} else if ("A000000333010101".equals(aid)) {
accountType = AccountType.EC.getValue();
}
// Keep the scheme value used by host routing and receipt logic.
accountType = CardScheme.UPI.name;
}
LogUtil.d(TAG, "Card accountType = " + accountType);
return accountType;