Compare commits

...

15 Commits

Author SHA1 Message Date
f827e75b1e added manual entry and route to password for necessary transactions, integrated cz host 2026-06-26 00:26:17 +07:00
moon
6af4b79702 last sync caller 2026-06-25 16:30:46 +07:00
moon
fce4284b96 last sync animation 2026-06-25 16:30:26 +07:00
moon
fc3d599689 view model fix 2026-06-25 16:21:20 +07:00
moon
c66a1159c5 switch fixed 2026-06-25 16:21:14 +07:00
moon
5071f57ed7 notification 2026-06-25 16:21:11 +07:00
moon
cd1239e595 dashboard Ui changed 2026-06-25 16:19:54 +07:00
moon
5283bae462 pager in dashboard 2026-06-25 16:17:36 +07:00
moon
b7c04719c7 side bar modified 2026-06-25 16:15:38 +07:00
6998a270b9 cz_latest 2026-06-25 16:15:30 +07:00
430a8be526 only left, pre auth functions for cz host 2026-06-25 16:15:28 +07:00
01209196af integration with cz host 2026-06-25 16:15:11 +07:00
a565aa3f9b added signature for all MPU transactions 2026-06-25 16:14:41 +07:00
48e58722c9 MPU Certification latest 2026-06-25 16:14:34 +07:00
5c18853179 MPU still in progress only left preauth test cases 2026-06-25 16:14:16 +07:00
43 changed files with 1890 additions and 442 deletions

View File

@ -494,7 +494,7 @@ private fun AdvertisingArea(
modifier = modifier,
shape = RoundedCornerShape(0.dp),
colors = CardDefaults.cardColors(containerColor = Color.White),
) {
) {
HorizontalPager(
state = pageState, modifier = Modifier.fillMaxSize()
) { page ->

View File

@ -0,0 +1,928 @@
package com.mob.utsmyanmar.ui.dashboard
import android.os.Handler
import android.os.Looper
import androidx.compose.animation.core.tween
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
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.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.AccountBalanceWallet
import androidx.compose.material.icons.filled.BarChart
import androidx.compose.material.icons.filled.Check
import androidx.compose.material.icons.filled.ChevronRight
import androidx.compose.material.icons.filled.CreditCard
import androidx.compose.material.icons.filled.Lock
import androidx.compose.material.icons.filled.LockOpen
import androidx.compose.material.icons.filled.Menu
import androidx.compose.material.icons.filled.Notifications
import androidx.compose.material.icons.filled.Replay
import androidx.compose.material.icons.filled.SwapHoriz
import androidx.compose.material.icons.filled.Sync
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.DrawerValue
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalDrawerSheet
import androidx.compose.material3.ModalNavigationDrawer
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.VerticalDivider
import androidx.compose.material3.rememberDrawerState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import coil3.compose.AsyncImage
import com.mob.utsmyanmar.R
import com.mob.utsmyanmar.ui.components.appbar.AppBar
import com.mob.utsmyanmar.ui.device_info.DeviceInfoUiState
import com.mob.utsmyanmar.ui.preview.P2Preview
import com.mob.utsmyanmar.ui.preview.P3Preview
import com.mob.utsmyanmar.ui.theme.Color
import com.utsmyanmar.paylibs.sign_on.EchoTestProcess
import com.utsmyanmar.paylibs.sign_on.SignOnListener
import com.utsmyanmar.paylibs.utils.core_utils.SystemParamsOperation
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlin.time.Duration.Companion.milliseconds
@Composable
fun DashboardScreen2(
onNavigateAmount: (String) -> Unit = {},
onNavigateSignOn: () -> Unit = {},
onNavigateSettlement: () -> Unit = {},
onNavigateVersion: () -> Unit = {},
onNavigateFunctions: () -> Unit = {},
onNavigateAction: (String) -> Unit = {},
onNavigateNotifications: () -> Unit = {},
dashboardUiState: DashboardUiState = DashboardUiState(),
deviceInfo: DeviceInfoUiState = DeviceInfoUiState()
) {
val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed)
val scope = rememberCoroutineScope()
val mainHandler = remember { Handler(Looper.getMainLooper()) }
var showHostActionDialog by remember { mutableStateOf(false) }
var activeHostAction by remember { mutableStateOf("Log-On") }
var isHostActionRunning by remember { mutableStateOf(false) }
var dialogMessage by remember { mutableStateOf("") }
var reversalEnabled by remember { mutableStateOf(runCatching { SystemParamsOperation.getInstance().isReversalOn }.getOrDefault(false)) }
val isOnline = true
fun confirmationMessage(action: String) = "Do you want to start ${action.lowercase()}?"
fun processingMessage(action: String) = "Sending ${action.lowercase()} request to host..."
fun successMessage(action: String) = "$action success."
fun failureMessage(action: String, resultCode: Int?) =
"$action failed. Response code: ${resultCode ?: -1}"
fun networkFailureMessage(action: String) = "Network error during $action."
fun openHostActionDialog(action: String) {
activeHostAction = action
isHostActionRunning = false
dialogMessage = confirmationMessage(action)
showHostActionDialog = true
}
if (showHostActionDialog) {
AlertDialog(
onDismissRequest = {
if (!isHostActionRunning) {
showHostActionDialog = false
}
}, title = {
Text(
text = activeHostAction, color = Color.LegacyRed, fontWeight = FontWeight.Bold
)
}, text = {
Text(text = dialogMessage)
}, confirmButton = {
TextButton(
onClick = {
if (isHostActionRunning) return@TextButton
if (dialogMessage != confirmationMessage(activeHostAction)) {
showHostActionDialog = false
dialogMessage = confirmationMessage(activeHostAction)
return@TextButton
}
isHostActionRunning = true
dialogMessage = processingMessage(activeHostAction)
val signOnProcess = EchoTestProcess.getInstance()
val request = when (activeHostAction) {
"Echo Test" -> signOnProcess.enqueue(false)
"Log-Off" -> signOnProcess.enqueueLogOff()
else -> signOnProcess.enqueueLogOn()
}
request.startSignOn(object : SignOnListener {
override fun onSuccessSignOn() {
mainHandler.post {
isHostActionRunning = false
dialogMessage = successMessage(activeHostAction)
}
}
override fun onFailureSignOn(resultCode: Int?) {
mainHandler.post {
isHostActionRunning = false
mainHandler.post {
isHostActionRunning = false
dialogMessage = failureMessage(activeHostAction, resultCode)
}
}
}
override fun onNetworkFailSignOn(message: String?) {
mainHandler.post {
isHostActionRunning = false
dialogMessage = message?.takeIf { it.isNotBlank() }
?: networkFailureMessage(activeHostAction)
}
}
})
}) {
Text(
text = when {
isHostActionRunning -> "Processing"
dialogMessage == confirmationMessage(activeHostAction) -> "Start"
else -> "Close"
}, color = Color.LegacyRed
)
}
}, dismissButton = {
if (!isHostActionRunning) {
TextButton(
onClick = {
showHostActionDialog = false
dialogMessage = confirmationMessage(activeHostAction)
}) {
Text(text = "Cancel", color = Color.Gray)
}
}
}, containerColor = Color.White
)
}
ModalNavigationDrawer(
drawerState = drawerState, drawerContent = {
ModalDrawerSheet(
modifier = Modifier.fillMaxWidth(0.78f),
drawerContainerColor = Color.White,
drawerShape = RoundedCornerShape(topEnd = 24.dp, bottomEnd = 24.dp)
) {
Column(
modifier = Modifier
.fillMaxWidth()
.background(Color.IvoryBeige)
.padding(horizontal = 20.dp, vertical = 28.dp)
) {
Row {
Box(
modifier = Modifier
.size(80.dp)
.clip(CircleShape)
.background(Color.White), contentAlignment = Alignment.Center
) {
Image(
painter = painterResource(R.drawable.logo_mob),
contentDescription = "mob logo",
modifier = Modifier
.size(100.dp)
.padding(16.dp),
contentScale = ContentScale.Fit
)
}
Column {
Text(
text = "MOB Merchant",
color = Color.CrimsonRed,
fontSize = 12.sp,
fontWeight = FontWeight.Bold
)
Text(
text = "S/N:${deviceInfo.serialNumber}",
color = Color.Black,
fontSize = 12.sp,
fontWeight = FontWeight.Bold
)
Text(
text = if (isOnline) "Online" else "Offline",
color = if (isOnline) Color.Success else Color.Error,
fontSize = 12.sp,
fontWeight = FontWeight.Bold
)
}
}
}
Spacer(modifier = Modifier.height(10.dp))
Text(
text = "Connection Settings",
fontWeight = FontWeight.Medium,
modifier = Modifier.padding(horizontal = 16.dp)
)
Item(
title = "Echo Test", subTitle = "Test Connection Status",
onClick = {
scope.launch { drawerState.close() }
openHostActionDialog("Echo Test")
},
leadingIcon = {
Icon(
painterResource(R.drawable.ic_up_down_arrow),
contentDescription = "icon",
tint = Color.LegacyRed
)
},
trailingIcon = {},
)
Item(
title = "Log-On", subTitle = "Log on to System",
onClick = {
scope.launch { drawerState.close() }
openHostActionDialog("Log-On")
},
leadingIcon = {
Icon(
painterResource(R.drawable.ic_lock),
contentDescription = "icon",
tint = Color.LegacyRed
)
},
trailingIcon = {},
)
Item(
title = "Log-Off", subTitle = "Log off from System",
onClick = {
scope.launch { drawerState.close() }
openHostActionDialog("Log-Off")
},
leadingIcon = {
Icon(
painterResource(R.drawable.ic_cancel_circle),
contentDescription = "icon",
tint = Color.LegacyRed
)
},
trailingIcon = {},
)
Item(
title = "Reversal", subTitle = "Enable / Disable Reversal",
onClick = {
reversalEnabled = !reversalEnabled
runCatching { SystemParamsOperation.getInstance().setReversalFlag(reversalEnabled) }
},
leadingIcon = {
Icon(
Icons.Default.Sync,
contentDescription = "icon",
tint = Color.LegacyRed
)
},
trailingIcon = {
Switch(
checked = reversalEnabled,
onCheckedChange = { isChecked ->
reversalEnabled = isChecked
runCatching { SystemParamsOperation.getInstance().setReversalFlag(isChecked) }
}
)
},
)
Text(
text = "System Management",
fontWeight = FontWeight.Medium,
modifier = Modifier.padding(horizontal = 16.dp)
)
Item(
title = "Functions", subTitle = "System Function Settings",
onClick = {
scope.launch { drawerState.close() }
onNavigateFunctions()
},
leadingIcon = {
Icon(
painterResource(R.drawable.ic_four_boxes),
contentDescription = "icon",
tint = Color.LegacyRed,
modifier = Modifier.size(16.dp)
)
},
trailingIcon = {},
)
Item(
title = "Version", subTitle = "View App Version Info",
onClick = {
scope.launch { drawerState.close() }
onNavigateVersion()
},
leadingIcon = {
Icon(
painterResource(R.drawable.ic_version),
contentDescription = "icon",
tint = Color.LegacyRed
)
},
trailingIcon = {},
)
}
}) {
//body start
Scaffold(
containerColor = Color.IvoryBeige, topBar = {
AppBar(
title = "Dashboard",
icon = Icons.Default.Menu,
onIconClick = { scope.launch { drawerState.open() } },
actions = {
IconButton(onClick = onNavigateNotifications) {
Box {
Icon(
imageVector = Icons.Default.Notifications,
contentDescription = "Notifications",
tint = Color.White
)
Box(
modifier = Modifier
.size(8.dp)
.background(Color.GoldenGlow, CircleShape)
.align(Alignment.TopEnd)
.offset(x = 2.dp, y = (-2).dp)
)
}
}
}
)
}) { paddingValues ->
Column(
modifier = Modifier
.padding(paddingValues)
.fillMaxSize()
) {
//top section
Box(
modifier = Modifier
.weight(1f)
.fillMaxWidth(),
) {
AdvertisingArea()
}
//center section
Box(
modifier = Modifier
.weight(1f)
.fillMaxWidth(),
contentAlignment = Alignment.Center
) {
SummaryCard()
}
//pager section
Box(
modifier = Modifier
.weight(1.3f)
.fillMaxWidth(),
) {
MenuPager(
items = buildMenuItems(
onNavigateAmount = onNavigateAmount,
onNavigateSignOn = onNavigateSignOn,
onNavigateSettlement = onNavigateSettlement,
onNavigateAction = onNavigateAction
),
modifier = Modifier.fillMaxSize()
)
}
//transactions section
RecentTransactions(
transactions = dashboardUiState.recentTransactions,
modifier = Modifier
.weight(1.2f)
.fillMaxWidth()
)
}
}
}
}
@Composable
private fun AdvertisingArea() {
val imageArray = listOf(
"https://i.ytimg.com/vi/eRUVxGRp1Ms/maxresdefault.jpg",
"https://i.ytimg.com/vi/AwvmgTPd7qw/maxresdefault.jpg",
"https://mma.prnewswire.com/media/2080956/SUNMI_3rd_generation_products_T3_PRO_series_V3_MIX.jpg?p=facebook"
)
val pageState = rememberPagerState(pageCount = { imageArray.size })
LaunchedEffect(pageState) {
while (true) {
delay(10000.milliseconds)
val nextPage = (pageState.currentPage + 1) % imageArray.size
pageState.animateScrollToPage(
page = nextPage, animationSpec = tween(durationMillis = 700)
)
}
}
Card(
modifier = Modifier.fillMaxSize(),
shape = RoundedCornerShape(0.dp),
colors = CardDefaults.cardColors(containerColor = Color.White),
) {
HorizontalPager(
state = pageState, modifier = Modifier.fillMaxSize()
) { page ->
AsyncImage(
model = imageArray[page],
contentDescription = "ads images",
modifier = Modifier.fillMaxSize(),
contentScale = ContentScale.Crop
)
}
}
}
@Composable
private fun SummaryCard() {
Card(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
shape = RoundedCornerShape(16.dp),
colors = CardDefaults.cardColors(containerColor = Color.White),
elevation = CardDefaults.cardElevation(4.dp)
) {
Row(verticalAlignment = Alignment.CenterVertically) {
Column(modifier = Modifier.weight(1f)) {
SummaryItem(
title = "Sales Today",
value = "MMK 2,000,000",
subtitle = "47 Transactions",
icon = Icons.Default.BarChart,
iconBg = Color.CrimsonRed
)
HorizontalDivider(
color = Color.Gray,
modifier = Modifier.padding(vertical = 6.dp, horizontal = 6.dp)
)
SummaryItem(
title = "Settlement",
value = "Completed",
subtitle = "Today 11:00 PM",
icon = Icons.Default.Check,
iconBg = Color.CrimsonRed
)
}
VerticalDivider(color = Color.Gray, modifier = Modifier.height(160.dp))
Column(
modifier = Modifier
.weight(0.75f)
.padding(start = 18.dp),
horizontalAlignment = Alignment.Start
) {
Row(verticalAlignment = Alignment.Bottom) {
Text(
text = "Last Sync", fontSize = 12.sp
)
IconCircle(Icons.Default.Sync, Color.CrimsonRed)
}
Spacer(Modifier.height(6.dp))
Text(
text = "5 mins ago",
fontSize = 18.sp,
color = Color.LegacyRed,
fontWeight = FontWeight.SemiBold
)
}
}
}
}
@Composable
private fun SummaryItem(
title: String,
value: String,
subtitle: String,
icon: ImageVector,
iconBg: androidx.compose.ui.graphics.Color
) {
Row(
verticalAlignment = Alignment.Top, modifier = Modifier.padding(6.dp)
) {
Column(modifier = Modifier.weight(1f)) {
Text(title, fontSize = 12.sp)
Text(
value, fontSize = 14.sp, color = Color.LegacyRed, fontWeight = FontWeight.Bold
)
Text(subtitle, fontSize = 12.sp)
}
IconCircle(icon, iconBg)
}
}
@Composable
private fun IconCircle(
icon: ImageVector, color: androidx.compose.ui.graphics.Color
) {
Box(
modifier = Modifier
.size(38.dp)
.clip(CircleShape)
.background(color.copy(alpha = 0.1f)),
contentAlignment = Alignment.Center
) {
Icon(
imageVector = icon,
contentDescription = null,
tint = color,
modifier = Modifier.size(22.dp)
)
}
}
private class DashboardMenuItem(
val title: String,
val iconContent: @Composable () -> Unit,
val onClick: () -> Unit
)
@Composable
private fun buildMenuItems(
onNavigateAmount: (String) -> Unit,
onNavigateSignOn: () -> Unit,
onNavigateSettlement: () -> Unit,
onNavigateAction: (String) -> Unit
): List<DashboardMenuItem> = listOf(
DashboardMenuItem("Sale", { Icon(painterResource(R.drawable.ic_terminal), contentDescription = null, modifier = Modifier.size(40.dp), tint = Color.LegacyRed) }) { onNavigateAmount("Sale") },
DashboardMenuItem("MMQR", { Image(painter = painterResource(R.drawable.ic_mmqr_logo), contentDescription = null, modifier = Modifier.height(48.dp)) }) { },
DashboardMenuItem("History", { Icon(painterResource(R.drawable.ic_history), contentDescription = null, modifier = Modifier.size(32.dp), tint = Color.LegacyRed) }) { },
DashboardMenuItem("Sign On", { Icon(painterResource(R.drawable.ic_sign_on), contentDescription = null, modifier = Modifier.size(32.dp), tint = Color.LegacyRed) }) { onNavigateSignOn() },
DashboardMenuItem("Settlement", { Icon(painterResource(R.drawable.ic_settlement), contentDescription = null, modifier = Modifier.size(32.dp), tint = Color.LegacyRed) }) { onNavigateSettlement() },
DashboardMenuItem("Void", { Icon(Icons.Default.Lock, contentDescription = null, modifier = Modifier.size(32.dp), tint = Color.LegacyRed) }) { onNavigateAction("Void") },
DashboardMenuItem("Refund", { Icon(Icons.Default.Replay, contentDescription = null, modifier = Modifier.size(32.dp), tint = Color.LegacyRed) }) { onNavigateAction("Refund") },
DashboardMenuItem("Pre-Auth", { Icon(Icons.Default.Lock, contentDescription = null, modifier = Modifier.size(32.dp), tint = Color.LegacyRed) }) { onNavigateAction("Pre-Auth") },
DashboardMenuItem("Pre-Auth Void", { Icon(Icons.Default.LockOpen, contentDescription = null, modifier = Modifier.size(32.dp), tint = Color.LegacyRed) }) { onNavigateAction("Pre-Auth Void") },
DashboardMenuItem("Pre-Auth Complete", { Icon(Icons.Default.CreditCard, contentDescription = null, modifier = Modifier.size(32.dp), tint = Color.LegacyRed) }) { onNavigateAction("Pre-Auth Complete") },
DashboardMenuItem("Pre-Auth Complete Void", { Icon(Icons.Default.SwapHoriz, contentDescription = null, modifier = Modifier.size(32.dp), tint = Color.LegacyRed) }) { onNavigateAction("Pre-Auth Complete Void") },
DashboardMenuItem("Cash Out", { Icon(Icons.Default.AccountBalanceWallet, contentDescription = null, modifier = Modifier.size(32.dp), tint = Color.LegacyRed) }) { onNavigateAction("Cash Out") },
)
@Composable
private fun MenuPager(
items: List<DashboardMenuItem>,
modifier: Modifier = Modifier
) {
val pages = items.chunked(6)
val pagerState = rememberPagerState(pageCount = { pages.size })
Column(modifier = modifier) {
HorizontalPager(
state = pagerState,
modifier = Modifier.weight(1f)
) { pageIndex ->
MenuPage(items = pages[pageIndex])
}
Row(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 8.dp),
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically
) {
repeat(pages.size) { index ->
val selected = pagerState.currentPage == index
Box(
modifier = Modifier
.padding(horizontal = 4.dp)
.size(if (selected) 8.dp else 6.dp)
.background(
color = if (selected) Color.LegacyRed else Color.Gray.copy(alpha = 0.4f),
shape = CircleShape
)
)
}
}
}
}
@Composable
private fun MenuPage(items: List<DashboardMenuItem>) {
Column(
verticalArrangement = Arrangement.spacedBy(10.dp),
modifier = Modifier.padding(horizontal = 16.dp)
) {
Spacer(Modifier.height(8.dp))
items.chunked(3).forEach { rowItems ->
Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) {
rowItems.forEach { item ->
MenuCard(
title = item.title,
icon = item.iconContent,
modifier = Modifier.weight(1f),
onClick = item.onClick
)
}
repeat(3 - rowItems.size) {
Spacer(modifier = Modifier.weight(1f))
}
}
}
}
}
@Composable
private fun MenuCard(
title: String,
icon: @Composable () -> Unit,
modifier: Modifier = Modifier,
onClick: (() -> Unit)? = null
) {
Card(
modifier = modifier
.height(100.dp)
.then(
if (onClick != null) {
Modifier.clickable(onClick = onClick)
} else {
Modifier
}
),
shape = RoundedCornerShape(14.dp),
colors = CardDefaults.cardColors(containerColor = Color.White),
elevation = CardDefaults.cardElevation(5.dp)
) {
Box(
modifier = Modifier
.fillMaxSize()
.padding(6.dp)
) {
Column {
icon()
Spacer(modifier = Modifier.weight(1f))
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = title,
fontSize = 12.sp,
color = Color.Black,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
modifier = Modifier.weight(1f)
)
Icon(
imageVector = Icons.Default.ChevronRight,
contentDescription = null,
tint = Color.LegacyRed,
modifier = Modifier.size(28.dp)
)
}
}
}
}
}
@Composable
private fun RecentTransactions(
transactions: List<TrnxRecord>,
modifier: Modifier = Modifier
) {
Column(modifier = modifier) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 6.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = "Recent Transactions",
fontWeight = FontWeight.SemiBold,
fontSize = 14.sp
)
}
HorizontalDivider(
modifier = Modifier.padding(horizontal = 16.dp),
color = Color.Gray.copy(alpha = 0.2f)
)
LazyColumn(modifier = Modifier.fillMaxSize()) {
if (transactions.isEmpty()) {
item {
Text(
text = "No recent transactions",
color = Color.Gray,
fontSize = 13.sp,
textAlign = TextAlign.Center,
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 24.dp)
)
}
} else {
items(items = transactions, key = { it.pid }) { record ->
TrnxRow(record = record)
}
}
}
}
}
@Composable
private fun TrnxRow(record: TrnxRecord) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 12.dp, vertical = 4.dp)
.clip(RoundedCornerShape(10.dp))
.background(Color.White)
.padding(horizontal = 12.dp, vertical = 10.dp),
verticalAlignment = Alignment.CenterVertically
) {
Box(
modifier = Modifier
.size(36.dp)
.background(Color.LegacyRed.copy(alpha = 0.1f), CircleShape),
contentAlignment = Alignment.Center
) {
Text(
text = record.typeLabel.take(1),
color = Color.LegacyRed,
fontWeight = FontWeight.Bold,
fontSize = 15.sp
)
}
Spacer(Modifier.width(10.dp))
Column(modifier = Modifier.weight(1f)) {
Text(
text = if (record.isVoided) "${record.typeLabel} (Voided)" else record.typeLabel,
fontSize = 13.sp,
fontWeight = FontWeight.SemiBold,
color = if (record.isVoided) Color.Gray else Color.Black
)
Text(
text = record.maskedCard,
fontSize = 11.sp,
color = Color.Gray
)
}
Column(horizontalAlignment = Alignment.End) {
Text(
text = record.amountDisplay,
fontSize = 13.sp,
fontWeight = FontWeight.Bold,
color = if (record.isVoided) Color.Gray else Color.LegacyRed
)
Text(
text = record.dateTime,
fontSize = 10.sp,
color = Color.Gray
)
}
}
}
private val previewTransactions = listOf(
TrnxRecord(1L, "Sale", "MMK 10,000", "**** 1234", "06/10 14:32", isVoided = false, isApproved = true),
TrnxRecord(2L, "Void", "MMK 5,500", "**** 5678", "06/10 13:10", isVoided = true, isApproved = false),
TrnxRecord(3L, "Refund", "MMK 2,000", "**** 9012", "06/09 09:45", isVoided = false, isApproved = true),
TrnxRecord(4L, "Sale", "MMK 30,000", "**** 3456", "06/09 08:00", isVoided = false, isApproved = true),
TrnxRecord(5L, "Settlement", "MMK 0", "----", "06/08 18:00", isVoided = false, isApproved = true),
)
@P3Preview
@P2Preview
@Composable
fun PreviewDashboardScreen2() {
DashboardScreen2(dashboardUiState = DashboardUiState(previewTransactions))
}
@Preview
@Composable
fun PreviewItem() {
Item(
onClick = {},
title = "title",
subTitle = "sub-title"
)
}
@Composable
fun Item(
onClick: () -> Unit,
title: String,
subTitle: String,
leadingIcon: (@Composable () -> Unit)? = null,
trailingIcon: (@Composable () -> Unit)? = null,
) {
Button(
onClick = onClick,
colors = ButtonDefaults.buttonColors(
containerColor = Color.White,
contentColor = Color.Black
),
modifier = Modifier.fillMaxWidth()
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween,
modifier = Modifier
.fillMaxWidth()
) {
// Square icon background
leadingIcon?.let {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.size(40.dp)
.clip(RoundedCornerShape(16.dp)) // change to RectangleShape for sharp corners
.background(Color.LegacyRed.copy(alpha = 0.1f))
.padding(8.dp)
) {
it()
}
}
Spacer(modifier = Modifier.width(12.dp))
Column(
modifier = Modifier
.weight(1f)
.padding(horizontal = 8.dp)
) {
Text(text = title)
Text(
text = subTitle,
style = MaterialTheme.typography.bodySmall,
color = Color.Gray
)
}
trailingIcon?.invoke()
}
}
}

View File

@ -14,6 +14,8 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import java.math.BigDecimal
import java.text.DecimalFormat
import javax.inject.Inject
@HiltViewModel
@ -57,7 +59,7 @@ class DashboardViewModel @Inject constructor(
private fun PayDetail.toRecord(): TrnxRecord {
val type = transType?.takeIf { it.isNotBlank() }?.let { formatTypeLabel(it) } ?: "Transaction"
val amount = "MMK %,d".format(amount)
val amount = "MMK ${formatAmount(amount)}"
val card = CardNo?.takeIf { it.length >= 4 }?.let { "**** ${it.takeLast(4)}" } ?: "----"
val dt = buildDateTime(TradeDate, TradeTime)
val approved = tradeAnswerCode == "00" || approvalCode?.isNotBlank() == true
@ -73,6 +75,11 @@ private fun PayDetail.toRecord(): TrnxRecord {
)
}
fun formatAmount(amount: Long): String {
val scaled = BigDecimal(amount).movePointLeft(2)
return DecimalFormat("#,###.00").format(scaled)
}
private fun formatTypeLabel(raw: String): String = when {
raw.contains("SALE", ignoreCase = true) -> "Sale"
raw.contains("VOID", ignoreCase = true) -> "Void"

View File

@ -0,0 +1,438 @@
package com.mob.utsmyanmar.ui.manual_entry
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
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.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.rounded.ArrowBack
import androidx.compose.material.icons.outlined.CreditCard
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.mob.utsmyanmar.ui.components.appbar.AppBar
import com.mob.utsmyanmar.ui.pinpad.PinPadViewModel
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.utils.TransactionUtil
import com.mob.utsmyanmar.viewmodel.SharedViewModel
import com.mob.utsmyanmar.viewmodel.TransProcessViewModel
import com.utsmyanmar.checkxread.model.CardDataX
import com.utsmyanmar.checkxread.util.CardTypeX
import com.utsmyanmar.paylibs.model.TradeData
import com.utsmyanmar.paylibs.utils.POSUtil
import com.utsmyanmar.paylibs.utils.iso_utils.TransactionsType
import sunmi.sunmiui.utils.LogUtil
private const val PAN_RAW_MAX = 16
private const val EXPIRY_RAW_MAX = 4
@Composable
fun InputManualCardRoute(
transProcessViewModel: TransProcessViewModel,
sharedViewModel: SharedViewModel,
pinPadViewModel: PinPadViewModel,
onBack: () -> Unit,
onNavigatePinPad: () -> Unit
) {
var pan by rememberSaveable { mutableStateOf("") }
var expiry by rememberSaveable { mutableStateOf("") }
var panError by rememberSaveable { mutableStateOf<String?>(null) }
var expiryError by rememberSaveable { mutableStateOf<String?>(null) }
val focusManager = LocalFocusManager.current
LaunchedEffect(Unit) {
pan = ""; expiry = ""
panError = null; expiryError = null
}
fun validateAndConfirm() {
panError = null; expiryError = null
val rawPan = pan.filter { it.isDigit() }
val rawExp = expiry.filter { it.isDigit() }
if (rawPan.length < 13 || rawPan.length > PAN_RAW_MAX) {
panError = "Invalid card number"; return
}
if (!luhnCheck(rawPan)) {
panError = "Invalid card number"; return
}
if (rawExp.length != EXPIRY_RAW_MAX) {
expiryError = "Enter MM/YY"; return
}
val month = rawExp.take(2).toIntOrNull() ?: 0
if (month < 1 || month > 12) {
expiryError = "Invalid month"; return
}
focusManager.clearFocus()
LogUtil.d("Tag","exp date : $rawExp")
val cardNo = rawPan.replace("\\s","")
val expDate =
POSUtil.getInstance().getISOFormatExpDate(rawExp)
val mpuCardData = CardDataX()
mpuCardData.setPan(cardNo)
mpuCardData.setExp(expDate)
mpuCardData.setCardHolderName("MANUAL ENTRY")
val tradeData =
TransactionUtil.initMPUTransaction(mpuCardData, CardTypeX.MANUAL)
transProcessViewModel.setTransType(sharedViewModel.transactionsType.value)
transProcessViewModel.setTradeData(tradeData)
pinPadViewModel.setTradeData(tradeData)
onNavigatePinPad()
}
ManualCardEntryScreen(
pan = pan,
expiry = expiry,
panError = panError,
expiryError = expiryError,
onPanChange = { input ->
val digits = input.filter { it.isDigit() }.take(PAN_RAW_MAX)
pan = digits
panError = null
},
onExpiryChange = { input ->
val digits = input.filter { it.isDigit() }.take(EXPIRY_RAW_MAX)
expiry = digits
expiryError = null
},
onBackClick = onBack,
onCancelClick = onBack,
onConfirmClick = ::validateAndConfirm
)
}
@Composable
private fun ManualCardEntryScreen(
pan: String,
expiry: String,
panError: String?,
expiryError: String?,
onPanChange: (String) -> Unit,
onExpiryChange: (String) -> Unit,
onBackClick: () -> Unit,
onCancelClick: () -> Unit,
onConfirmClick: () -> Unit
) {
val panDisplay = remember(pan) {
formatPan(pan)
}
val expiryDisplay = remember(expiry) {
formatExpiry(expiry)
}
val cardScheme = detectCardScheme(pan)
val confirmEnabled = pan.length >= 13 && expiry.length == 4
val expiryFocusRequester = remember { FocusRequester() }
LaunchedEffect(pan) {
if (pan.length == PAN_RAW_MAX) expiryFocusRequester.requestFocus()
}
Scaffold(
topBar = { AppBar(title = "Manual Entry", icon = Icons.AutoMirrored.Rounded.ArrowBack, onIconClick = onBackClick) },
) { innerPadding ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(innerPadding)
.padding(horizontal = 20.dp, vertical = 16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
Surface(
shape = MaterialTheme.shapes.medium,
tonalElevation = 1.dp,
modifier = Modifier.fillMaxWidth()
) {
Column(modifier = Modifier.padding(16.dp)) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(bottom = 16.dp)
) {
Icon(
imageVector = Icons.Outlined.CreditCard,
contentDescription = null,
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier.size(20.dp)
)
Spacer(Modifier.width(8.dp))
Text(
"Card details",
style = MaterialTheme.typography.labelLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
if (cardScheme != null) {
Spacer(Modifier.weight(1f))
SchemeBadge(cardScheme)
}
}
OutlinedTextField(
value = panDisplay,
onValueChange = {
onPanChange(it.text)
},
label = { Text("Card number") },
placeholder = { Text("0000 0000 0000 0000") },
isError = panError != null,
supportingText = {
Text(panError ?: " ") // reserve space to avoid layout jump
},
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Number,
imeAction = ImeAction.Next
),
keyboardActions = KeyboardActions(
onNext = { expiryFocusRequester.requestFocus() }
),
singleLine = true,
modifier = Modifier.fillMaxWidth(),
textStyle = MaterialTheme.typography.bodyLarge.copy(
letterSpacing = 2.sp,
fontFamily = FontFamily.Monospace
)
)
Spacer(Modifier.height(4.dp))
OutlinedTextField(
value = expiryDisplay,
onValueChange = {
onExpiryChange(it.text)
},
label = { Text("Expiry date") },
placeholder = { Text("MM/YY") },
isError = expiryError != null,
supportingText = {
Text(expiryError ?: " ")
},
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Number,
imeAction = ImeAction.Done
),
keyboardActions = KeyboardActions(
onDone = { onConfirmClick() }
),
singleLine = true,
modifier = Modifier
.width(160.dp)
.focusRequester(expiryFocusRequester),
textStyle = MaterialTheme.typography.bodyLarge.copy(
letterSpacing = 2.sp,
fontFamily = FontFamily.Monospace
)
)
}
}
AnimatedVisibility(visible = pan.length >= 4) {
Surface(
shape = MaterialTheme.shapes.small,
color = MaterialTheme.colorScheme.surfaceVariant,
modifier = Modifier.fillMaxWidth()
) {
Row(
modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = "**** **** **** ${pan.takeLast(4)}",
style = MaterialTheme.typography.bodyMedium,
fontFamily = FontFamily.Monospace,
letterSpacing = 1.5.sp
)
if (expiry.length == 4) {
Text(
text = "Exp ${expiryDisplay.text}",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
}
}
Spacer(Modifier.weight(1f))
Box(
modifier = Modifier
.fillMaxWidth()
){
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
Button(
onClick = onBackClick,
modifier = Modifier
.weight(1f)
.height(56.dp),
shape = RoundedCornerShape(8.dp),
colors = ButtonDefaults.buttonColors(
containerColor = Color.White,
contentColor = Color.LegacyRed
)
) {
Text("Cancel")
}
Button(
onClick = onConfirmClick,
modifier = Modifier
.weight(1f)
.height(56.dp),
shape = RoundedCornerShape(8.dp),
colors = ButtonDefaults.buttonColors(
containerColor = Color.LegacyRed,
contentColor = Color.White,
disabledContainerColor = Color.LegacyRed.copy(alpha = 0.5f),
disabledContentColor = Color.White
)
) {
Text(
text = "Confirm",
fontSize = 14.sp,
fontWeight = FontWeight.Medium
)
}
}
}
}
}
}
@Composable
private fun SchemeBadge(scheme: String) {
Surface(
color = MaterialTheme.colorScheme.secondaryContainer,
shape = MaterialTheme.shapes.small
) {
Text(
text = scheme,
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSecondaryContainer,
modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp)
)
}
}
private fun formatPan(raw: String): TextFieldValue {
val formatted = raw.chunked(4).joinToString(" ")
val cursorPos = buildString {
raw.forEachIndexed { index, c ->
append(c)
if ((index + 1) % 4 == 0 && index != raw.lastIndex) {
append(" ")
}
}
}.length
return TextFieldValue(
text = formatted,
selection = TextRange(cursorPos)
)
}
private fun formatExpiry(raw: String): TextFieldValue {
val formatted = when {
raw.length > 2 -> "${raw.take(2)}/${raw.drop(2)}"
else -> raw
}
val cursorPos = when {
raw.length <= 2 -> raw.length
else -> raw.length + 1 // account for '/'
}
return TextFieldValue(
text = formatted,
selection = TextRange(cursorPos)
)
}
private fun detectCardScheme(pan: String): String? = when {
pan.startsWith("4") -> "Visa"
pan.length >= 2 && (pan.take(2).toInt() in 51..55 || pan.take(4).toIntOrNull() in 2221..2720) -> "Mastercard"
pan.length >= 2 && pan.take(2) in listOf("34", "37") -> "Amex"
pan.startsWith("6") -> "UnionPay"
else -> null
}
private fun luhnCheck(pan: String): Boolean {
var sum = 0; var alt = false
for (i in pan.length - 1 downTo 0) {
var n = pan[i].digitToIntOrNull() ?: return false
if (alt) { n *= 2; if (n > 9) n -= 9 }
sum += n; alt = !alt
}
return sum % 10 == 0
}
@P2Preview
@P3Preview
@Composable
private fun ManualCardEntryScreenPreview() {
MaterialTheme {
ManualCardEntryScreen(
pan = "4111111111111111",
expiry = "1228",
panError = null,
expiryError = null,
onPanChange = {},
onExpiryChange = {},
onBackClick = {},
onCancelClick = {},
onConfirmClick = {}
)
}
}

View File

@ -23,6 +23,7 @@ import com.mob.utsmyanmar.ui.device_info.DeviceInfoViewModel
import com.mob.utsmyanmar.ui.disable.DisableScreen
import com.mob.utsmyanmar.ui.functions.FunctionsScreen
import com.mob.utsmyanmar.ui.input_amount.AmountRoute
import com.mob.utsmyanmar.ui.manual_entry.InputManualCardRoute
import com.mob.utsmyanmar.ui.notification.NotificationDetailScreen
import com.mob.utsmyanmar.ui.notification.NotificationListScreen
import com.mob.utsmyanmar.ui.notification.NotificationViewModel
@ -34,6 +35,7 @@ import com.mob.utsmyanmar.ui.print_receipt.PrintReceiptScreen
import com.mob.utsmyanmar.ui.processing_card.ProcessingCardRoute
import com.mob.utsmyanmar.ui.processing_card.ProcessingCardViewModel
import com.mob.utsmyanmar.ui.refund_rrn.InputRrnRoute
import com.mob.utsmyanmar.ui.refund_rrn.RRNViewModel
import com.mob.utsmyanmar.ui.sale_void.TranDetailPage
import com.mob.utsmyanmar.ui.sale_void.VoidTraceScreen
import com.mob.utsmyanmar.ui.sale_void.VoidViewModel
@ -52,7 +54,9 @@ import com.mob.utsmyanmar.viewmodel.CardReaderViewModel
import com.mob.utsmyanmar.viewmodel.EmvTransactionProcessViewModel
import com.mob.utsmyanmar.viewmodel.SharedViewModel
import com.mob.utsmyanmar.viewmodel.TransProcessViewModel
import com.utsmyanmar.paylibs.model.TradeData
import com.utsmyanmar.paylibs.utils.iso_utils.TransactionsType
import sunmi.sunmiui.utils.LogUtil
@SuppressLint("ContextCastToActivity")
@Composable
@ -156,7 +160,24 @@ fun AppNavGraph(
},
onNavigateAction = { action ->
when (action) {
"Void" -> navController.navigate(Routes.VoidTrace.route) { launchSingleTop = true }
"Void" -> {
sharedViewModel.transactionsType.value = TransactionsType.VOID
navController.navigate(Routes.Password.createRoute(Routes.VoidTrace.route, PasswordType.SYSTEM)) {
launchSingleTop = true
}
}
"Pre-Auth Void" -> navController.navigate(Routes.Password.createRoute(Routes.Amount.createRoute(action), PasswordType.SYSTEM)) {
launchSingleTop = true
}
"Pre-Auth Complete Void" -> {
sharedViewModel.transactionsType.value = TransactionsType.PRE_AUTH_COMPLETE_VOID
navController.navigate(Routes.Password.createRoute(Routes.VoidTrace.route, PasswordType.SYSTEM)) {
launchSingleTop = true
}
}
"Refund" -> navController.navigate(Routes.Password.createRoute(Routes.Amount.createRoute(action), PasswordType.SYSTEM)) {
launchSingleTop = true
}
else -> navController.navigate(Routes.Amount.createRoute(action)) { launchSingleTop = true }
}
},
@ -206,9 +227,10 @@ fun AppNavGraph(
composable(Routes.VoidTrace.route) {
val voidViewModel: VoidViewModel = hiltViewModel()
val sharedViewModel: SharedViewModel = hiltViewModel(activity)
VoidTraceScreen(
voidViewModel = voidViewModel,
sharedViewModel = sharedViewModel,
onNavigateTranDetail = { trace ->
navController.navigate(Routes.VoidTranDetail.createRoute(trace)) {
launchSingleTop = true
@ -256,8 +278,18 @@ fun AppNavGraph(
trace = trace,
onBack = { navController.popBackStack() },
onProceedVoid = { payDetail ->
sharedViewModel.transactionsType.value = TransactionsType.VOID
transProcessViewModel.setTransType(TransactionsType.VOID)
if(sharedViewModel.transactionsType.value == TransactionsType.VOID) {
transProcessViewModel.setTransType(TransactionsType.VOID)
payDetail.transType = TransactionsType.VOID.name
payDetail.transactionType = TransactionsType.VOID.value
} else {
transProcessViewModel.setTransType(TransactionsType.PRE_AUTH_COMPLETE_VOID)
payDetail.transType = TransactionsType.PRE_AUTH_COMPLETE_VOID.name
payDetail.transactionType = TransactionsType.PRE_AUTH_COMPLETE_VOID.value
}
val tradeData = TradeData()
tradeData.payDetail = payDetail
transProcessViewModel.setTradeData(tradeData)
sharedViewModel.payDetail.value = payDetail
navController.navigate(Routes.Processing.route) {
popUpTo(Routes.VoidTranDetail.route) {
@ -358,7 +390,14 @@ fun AppNavGraph(
CardWaitingScreen(
viewModel = cardWaitingViewModel,
amount = formatAmountForDisplay(sharedViewModel.amount.value),
onManualEntry = {},
onManualEntry = {
navController.navigate(Routes.ManualEntry.route) {
popUpTo(Routes.CardWaiting.route) {
inclusive = true
}
launchSingleTop = true
}
},
onProcessingCard = {
navController.navigate(Routes.ProcessingCard.route) {
popUpTo(Routes.CardWaiting.route) {
@ -454,8 +493,11 @@ fun AppNavGraph(
composable(Routes.InputRrn.route) {
val sharedViewModel: SharedViewModel = hiltViewModel(activity)
val transProcessViewModel: TransProcessViewModel = hiltViewModel(activity)
val rrnViewModel: RRNViewModel = hiltViewModel(activity)
InputRrnRoute(
rrnViewModel = rrnViewModel,
transProcessViewModel = transProcessViewModel,
sharedViewModel = sharedViewModel,
onBack = { navController.popBackStack() },
onNavigateProcessing = {
@ -469,6 +511,27 @@ fun AppNavGraph(
)
}
composable(Routes.ManualEntry.route) {
val sharedViewModel: SharedViewModel = hiltViewModel(activity)
val transProcessViewModel: TransProcessViewModel = hiltViewModel(activity)
val pinPadViewModel: PinPadViewModel = hiltViewModel(activity)
InputManualCardRoute(
transProcessViewModel = transProcessViewModel,
sharedViewModel = sharedViewModel,
pinPadViewModel = pinPadViewModel,
onBack = { navController.popBackStack() },
onNavigatePinPad = {
pinPadViewModel.resetSessionState()
navController.navigate(Routes.PinPad.route) {
popUpTo(Routes.ProcessingCard.route) {
inclusive = true
}
launchSingleTop = true
}
},
)
}
composable(Routes.Processing.route) {
val sharedViewModel: SharedViewModel = hiltViewModel(activity)
val transProcessViewModel: TransProcessViewModel = hiltViewModel(activity)

View File

@ -25,6 +25,9 @@ sealed class Routes(val route: String) {
data object PinPad : Routes("pin_pad")
data object InputRrn : Routes("input_rrn")
data object Processing : Routes("processing")
data object ManualEntry : Routes("manual_entry")
data object TransactionResult : Routes("transaction_result")
data object PrintReceipt : Routes("print_receipt")
data object Version : Routes("version")

View File

@ -8,6 +8,7 @@ import android.view.View
import android.view.ViewTreeObserver
import android.widget.TextView
import androidx.lifecycle.ViewModel
import com.kizzy.cmhl.utils.ByteUtils
import com.mob.utsmyanmar.config.Constants
import com.mob.utsmyanmar.config.SunmiPayManager
import com.mob.utsmyanmar.model.PinPadStatus
@ -405,8 +406,19 @@ class PinPadViewModel @Inject constructor(
ON_CONFIRM_CLICK -> {
LogUtil.d(TAG, "ON CLICK CONFIRM")
isTerminalStateReached = true
_pinStatus.value = PinPadStatus.ON_CONFIRM
if (
transType.value == TransactionsType.PRE_AUTH_COMPLETE ||
transType.value == TransactionsType.PRE_AUTH_VOID ||
transType.value == TransactionsType.REFUND
) {
isTerminalStateReached = true
_pinStatus.value = PinPadStatus.ON_NEXT_SCREEN
} else {
isTerminalStateReached = true
_pinStatus.value = PinPadStatus.ON_CONFIRM
}
}
ON_CANCEL_CLICK -> {
@ -488,18 +500,11 @@ class PinPadViewModel @Inject constructor(
payDetail?.pinCipher = ""
}
if (
transType.value == TransactionsType.PRE_AUTH_COMPLETE ||
transType.value == TransactionsType.PRE_AUTH_VOID ||
transType.value == TransactionsType.REFUND
) {
isTerminalStateReached = true
_pinStatus.value = PinPadStatus.ON_NEXT_SCREEN
} else {
handler.obtainMessage(
ON_CONFIRM_CLICK
).sendToTarget()
}
}
override fun onCancel() {
@ -550,10 +555,13 @@ class PinPadViewModel @Inject constructor(
private fun getPanBytes(pan: String): ByteArray {
LogUtil.d(TAG,"pan : $pan")
if (pan.length < 13) {
return ByteArray(0)
}
LogUtil.d(TAG,"pan bytes : ${ByteUtils.byteArrayToHexString(pan
.substring(pan.length - 13, pan.length - 1)
.toByteArray(StandardCharsets.US_ASCII))}")
return pan
.substring(pan.length - 13, pan.length - 1)
.toByteArray(StandardCharsets.US_ASCII)

View File

@ -200,6 +200,7 @@ fun ReceiptPreview(
ReceiptDivider()
ReceiptRow("Merchant", state.payDetail?.merchantName.toString() )
ReceiptRow("Terminal ID", state.payDetail?.terminalNo.toString() )
ReceiptRow("Trace No", state.payDetail?.voucherNo.toString() )
ReceiptRow("Transaction", state.payDetail?.transType.toString())
ReceiptRow("Amount", POSUtil.getInstance().getDecimalAmountSeparatorFormat(state.payDetail?.amount?:0))
ReceiptRow("Card Type", state.payDetail?.accountType.toString())

View File

@ -6,14 +6,22 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.mob.utsmyanmar.ui.components.NumericEntryScreen
import com.mob.utsmyanmar.viewmodel.SharedViewModel
import com.mob.utsmyanmar.viewmodel.TransProcessViewModel
import com.utsmyanmar.paylibs.model.PayDetail
import com.utsmyanmar.paylibs.model.TradeData
import com.utsmyanmar.paylibs.utils.POSUtil
import com.utsmyanmar.paylibs.utils.iso_utils.TransactionsType
import sunmi.sunmiui.utils.LogUtil
private const val RRN_MAX_LENGTH = 12
@Composable
fun InputRrnRoute(
transProcessViewModel: TransProcessViewModel,
rrnViewModel: RRNViewModel,
sharedViewModel: SharedViewModel,
onBack: () -> Unit,
onNavigateProcessing: () -> Unit
@ -28,6 +36,75 @@ fun InputRrnRoute(
errorMessage = null
}
val payDetailState by rrnViewModel.payDetailState.collectAsStateWithLifecycle()
fun processPreAuthComp(payDetail: PayDetail) {
val newPay = transProcessViewModel.payDetail
newPay?.setReferNo(payDetail.referNo)
newPay?.setApprovalCode(payDetail.getApprovalCode())
newPay?.setTradeTime(payDetail.getTradeTime())
newPay?.setTradeDate(payDetail.getTradeDate())
val tradeData = TradeData()
tradeData.payDetail = newPay
transProcessViewModel.setOldTransPayDetail(payDetail)
transProcessViewModel.setTradeData(tradeData)
rrnViewModel.resetState()
onNavigateProcessing()
}
fun processPreAuthVoid(payDetail: PayDetail) {
val newPay = transProcessViewModel.payDetail
newPay?.setReferNo(payDetail.referNo)
val tradeData = TradeData()
tradeData.payDetail = newPay
transProcessViewModel.setOldTransPayDetail(payDetail)
transProcessViewModel.setTradeData(tradeData)
rrnViewModel.resetState()
onNavigateProcessing()
}
LaunchedEffect(payDetailState) {
LogUtil.d("RRN","paydetail state: $payDetailState")
when (val state = payDetailState) {
is RRNViewModel.PayDetailState.Success -> {
sharedViewModel.payDetail.value = state.data
sharedViewModel.rrNNo.value = state.data.referNo
if(sharedViewModel.transactionsType.value == TransactionsType.PRE_AUTH_COMPLETE) {
processPreAuthComp(state.data)
} else if(sharedViewModel.transactionsType.value == TransactionsType.PRE_AUTH_VOID) {
processPreAuthVoid(state.data)
}
}
is RRNViewModel.PayDetailState.Error -> {
errorMessage = state.message
rrnViewModel.resetState()
// show dialog
// navigate to home
}
else -> Unit
}
}
NumericEntryScreen(
title = "Input RRN",
prompt = "Enter RRN",
@ -38,17 +115,32 @@ fun InputRrnRoute(
onCancelClick = onBack,
onConfirmClick = {
val trimmedRrn = rrn.trim()
if (POSUtil.getInstance().checkNumberField(trimmedRrn)) {
errorMessage = "Invalid RRN"
return@NumericEntryScreen
}
sharedViewModel.rrNNo.value = trimmedRrn
sharedViewModel.payDetail.value?.let { payDetail ->
payDetail.referNo = trimmedRrn
sharedViewModel.payDetail.value = payDetail
if(sharedViewModel.transactionsType.value == TransactionsType.REFUND ) {
val newPay = transProcessViewModel.payDetail
newPay?.setReferNo(trimmedRrn)
val tradeData = TradeData()
tradeData.payDetail = newPay
transProcessViewModel.setTradeData(tradeData)
rrnViewModel.resetState()
onNavigateProcessing()
} else{
rrnViewModel.searchPayDetail(
trimmedRrn,
sharedViewModel.payDetail.value?.cardNo?:""
)
}
onNavigateProcessing()
},
onKeyClick = { value ->
rrn = appendRrnValue(rrn, value)

View File

@ -0,0 +1,56 @@
package com.mob.utsmyanmar.ui.refund_rrn
import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModel
import androidx.lifecycle.asFlow
import androidx.lifecycle.viewModelScope
import com.utsmyanmar.baselib.repo.Repository
import com.utsmyanmar.paylibs.model.PayDetail
import com.utsmyanmar.paylibs.system.SingleLiveEvent
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
class RRNViewModel @Inject constructor(
private val repository: Repository
) : ViewModel() {
sealed class PayDetailState {
object Idle : PayDetailState()
object Loading : PayDetailState()
data class Success(val data: PayDetail) : PayDetailState()
data class Error(val message: String) : PayDetailState()
}
private val _payDetailState = MutableStateFlow<PayDetailState>(PayDetailState.Idle)
val payDetailState: StateFlow<PayDetailState> = _payDetailState
fun searchPayDetail(refNo: String, cardNo: String) {
_payDetailState.value = PayDetailState.Loading
viewModelScope.launch {
try {
repository.searchPayDetail(cardNo, refNo)
.asFlow()
.filterNotNull()
.first()
.let { result ->
_payDetailState.value = PayDetailState.Success(result)
}
} catch (e: Exception) {
_payDetailState.value = PayDetailState.Error(e.message ?: "Search failed")
}
}
}
fun resetState() {
_payDetailState.value = PayDetailState.Idle
}
}

View File

@ -40,11 +40,14 @@ import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.mob.utsmyanmar.ui.components.appbar.AppBar
import com.mob.utsmyanmar.ui.theme.Color
import com.mob.utsmyanmar.viewmodel.SharedViewModel
import com.utsmyanmar.paylibs.model.PayDetail
import com.utsmyanmar.paylibs.utils.iso_utils.TransactionsType
import sunmi.sunmiui.utils.LogUtil
@Composable
fun VoidTraceScreen(
sharedViewModel: SharedViewModel,
voidViewModel: VoidViewModel,
onNavigateTranDetail: (String) -> Unit,
onBack: () -> Unit = {}
@ -57,12 +60,18 @@ fun VoidTraceScreen(
val recentTransactions by voidViewModel.getLastThreeTransactions().observeAsState(emptyList())
val transactionSource: LiveData<PayDetail?> = remember(searchedTrace) {
val transType = if(sharedViewModel.transactionsType.value == TransactionsType.VOID) {
TransactionsType.SALE.value
} else {
TransactionsType.PRE_AUTH_COMPLETE.value;
}
LogUtil.d("void screen","transaction type : $transType and trace number : $searchedTrace")
if (searchedTrace.isBlank()) {
MutableLiveData<PayDetail?>(null)
} else {
@Suppress("UNCHECKED_CAST")
voidViewModel.getVoidTrans(
TransactionsType.SALE.value,
transType,
searchedTrace
) as LiveData<PayDetail?>
}

View File

@ -1,5 +1,9 @@
package com.mob.utsmyanmar.ui.sending_to_host
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.lifecycle.compose.collectAsStateWithLifecycle
@ -13,9 +17,14 @@ import com.mob.utsmyanmar.viewmodel.ProcessingTransaction
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Modifier
import com.mob.utsmyanmar.model.TransactionStatus
import com.mob.utsmyanmar.ui.settlement.SettlementViewModel
import com.utsmyanmar.paylibs.utils.iso_utils.TransactionsType
import kotlinx.coroutines.channels.Channel
private const val MOCK_HOST_DELAY_MS = 2000L
@ -34,19 +43,30 @@ fun ProcessingRoute(
val settlementStatus by settlementViewModel.uiState.collectAsStateWithLifecycle()
var showReversalDialog by rememberSaveable { mutableStateOf(false) }
var reversalMessage by rememberSaveable { mutableStateOf("") }
if(sharedViewModel.transactionsType.value == TransactionsType.SETTLEMENT) {
LaunchedEffect(Unit) {
settlementViewModel.startSettlementProcess()
}
LaunchedEffect(settlementStatus.status) {
when(settlementStatus.status) {
TransactionStatus.ON_ERROR,
TransactionStatus.ON_SUCCESS -> {
sharedViewModel.payDetail.value = settlementViewModel.getPayDetail()
onNavigateTransactionResult()
}
else -> {}
}
}
} else {
LaunchedEffect(Unit) {
transProcessViewModel.resetTransactionStatus()
transProcessViewModel.startOnlineProcess()
}
}
LaunchedEffect(transStatus) {
LaunchedEffect(transStatus) {
when(transStatus) {
TransResultStatus.SUCCESS -> {
sharedViewModel.payDetail.value = transProcessViewModel.payDetailResult.value
@ -57,21 +77,23 @@ fun ProcessingRoute(
sharedViewModel.payDetail.value = transProcessViewModel.payDetailResult.value
onNavigateTransactionResult()
}
TransResultStatus.REVERSAL_SUCCESS -> onNavigateMain()
TransResultStatus.REVERSAL_FAIL -> onNavigateMain()
TransResultStatus.REVERSAL_SUCCESS ,
TransResultStatus.REVERSAL_FAIL -> {
delay(2500)
onNavigateMain()
}
else -> {}
}
}
LaunchedEffect(settlementStatus.status) {
when(settlementStatus.status) {
TransactionStatus.ON_SUCCESS -> onNavigateTransactionResult()
else -> {}
}
}
if(sharedViewModel.transactionsType.value == TransactionsType.SETTLEMENT) {
ProcessingScreen(settlementStatus.processingState)
} else {

View File

@ -9,8 +9,13 @@ 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.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Check
import androidx.compose.material.icons.rounded.Close
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Icon
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@ -20,8 +25,12 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.mob.utsmyanmar.ui.input_amount.InputAmount
import com.mob.utsmyanmar.ui.preview.P2Preview
import com.mob.utsmyanmar.ui.preview.P3Preview
import com.mob.utsmyanmar.ui.processing_card.ProcessingCardState
import com.mob.utsmyanmar.ui.theme.Black
import com.mob.utsmyanmar.ui.theme.Color
import com.mob.utsmyanmar.ui.theme.Primary
import com.mob.utsmyanmar.ui.theme.White
@ -53,7 +62,28 @@ fun ProcessingScreen(state: ProcessingState) {
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
CircularProgressIndicator(color = White)
when (state.title) {
"Reversal Failure" -> {
Icon(
imageVector = Icons.Rounded.Close,
contentDescription = "Failed",
tint = Color.White,
modifier = Modifier.size(64.dp)
)
}
"Reversal Success" -> {
Icon(
imageVector = Icons.Rounded.Check,
contentDescription = "Success",
tint = Color.White,
modifier = Modifier.size(64.dp)
)
}
else -> {
CircularProgressIndicator(color = White)
}
}
Spacer(modifier = Modifier.height(20.dp))
@ -86,3 +116,10 @@ fun ProcessingScreen(state: ProcessingState) {
}
}
}
@P2Preview
@P3Preview
@Composable
fun PreviewSendingToHostScreen(){
ProcessingScreen(ProcessingState("Processing"))
}

View File

@ -205,6 +205,7 @@ class SettlementViewModel @Inject constructor(
}
}
fun getPayDetail() = payDetail
fun setSettlementType(type: SettlementType) {
_uiState.update {
it.copy(settlementType = type)
@ -232,6 +233,7 @@ class SettlementViewModel @Inject constructor(
deleteTrans.clear()
}
fun startSettlementProcess() {
_uiState.update {
it.copy(isLoading = true)
@ -339,6 +341,11 @@ class SettlementViewModel @Inject constructor(
currentPayDetail.settleData = settlementData
currentPayDetail.amount = totalAmount
} else {
LogUtil.d(TAG,"sale count :$totalSaleCount")
LogUtil.d(TAG,"sale amount :$totalSaleAmount")
LogUtil.d(TAG,"refund count :$totalRefundCount")
LogUtil.d(TAG,"refund amount :$totalRefundAmount")
currentPayDetail.settleData =
totalSaleCount +
totalSaleAmount +
@ -350,6 +357,7 @@ class SettlementViewModel @Inject constructor(
totalERefundAmount
}
payDetail = currentPayDetail
tradeData.payDetail = currentPayDetail
tradeData.field60 = field60

View File

@ -66,6 +66,7 @@ class TmsSetupViewModel @Inject constructor(
init {
viewModelScope.launch {
waitForHardware()
TMSUtil.getInstance().generateFinalVersion()
downloadConfigs()
}
}
@ -75,6 +76,8 @@ class TmsSetupViewModel @Inject constructor(
it.copy(isLoading = true, isError = false, statusText = "Connecting to TMS server...")
}
val disposable = repository.getParams(buildRequest())
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
@ -131,6 +134,7 @@ class TmsSetupViewModel @Inject constructor(
delay(500.milliseconds)
elapsed += 500
}
}
private fun onConfigApplied() {

View File

@ -13,14 +13,40 @@ data class MockCardData(
object MockData {
// private val mockCardData: MockCardData = MockCardData(
// cardNo="9503712156912514",
// expDate="2912",
// cardScheme="MPU",
// cardHolderName="Htin Kyaw Win",
// iccData="9503712156912514=29121010000000000000"
// )
// production card
// private val mockCardData: MockCardData = MockCardData(
// cardNo="9503220100322798",
// expDate="2912",
// cardScheme="MPU",
// cardHolderName="Htin Kyaw Win",
// iccData="9503220100322798=300110100000942"
// )
//debit
private val mockCardData: MockCardData = MockCardData(
cardNo="9503712156912514",
expDate="2912",
cardNo="9503032443884321",
expDate="3102",
cardScheme="MPU",
cardHolderName="Htin Kyaw Win",
iccData="9503712156912514=29121010000000000000"
iccData="9503032443884321=310220100"
)
// private val mockCardData: MockCardData = MockCardData(
// cardNo="6244310039574344",
// expDate="3011",
// cardScheme="MPU",
// cardHolderName="Htin Kyaw Win",
// iccData="6244310039574344=30112011558125900000"
// )
fun generateMPUCard(): CardDataX = CardDataX().apply {
pan = mockCardData.cardNo
exp = mockCardData.expDate

View File

@ -267,6 +267,7 @@ class TMSSetupsImpl : TMSSetups {
} catch (e: Exception) {
"?"
}
SystemParamsOperation.getInstance().setFinalVersion("PHV$phv-RV$rv-SV$sv")
return "PHV$phv-RV$rv-SV$sv"
}
private fun parseBoolean(data: String): Boolean =

View File

@ -3,7 +3,6 @@ package com.mob.utsmyanmar.viewmodel
import android.text.TextUtils
import androidx.lifecycle.ViewModel
import com.mob.utsmyanmar.model.TransResultStatus
import com.mob.utsmyanmar.ui.processing_card.ProcessingCardState
import com.mob.utsmyanmar.ui.sending_to_host.ProcessingState
import com.utsmyanmar.baselib.repo.Repository
import com.utsmyanmar.paylibs.model.PayDetail
@ -109,6 +108,12 @@ class TransProcessViewModel @Inject constructor(
fun setOldTransPayDetail(payDetail: PayDetail) {
oldTransPayDetail = payDetail
}
fun setNewPayDetail(payDetail: PayDetail) {
this.payDetail = payDetail
tradeData = TradeData()
tradeData.payDetail = payDetail
}
private val _state = MutableStateFlow(ProcessingState())
val state = _state.asStateFlow()
@ -263,9 +268,36 @@ class TransProcessViewModel @Inject constructor(
)
}
override fun processPreVoidDb(payResult: PayDetail) {}
override fun processPreCompDb(payResult: PayDetail) {}
override fun processPreCompVoidDb(payResult: PayDetail) {}
override fun processPreVoidDb(payResult: PayDetail) {
oldTransPayDetail?.apply {
isCanceled = true
repository.updatePayDetail(oldTransPayDetail!!)
}
repository.insertPayDetail(updateCurrentDateAndTime(payResult))
}
override fun processPreCompDb(payResult: PayDetail) {
if (oldTransPayDetail?.getAmount() == payResult.getAmount()) {
oldTransPayDetail?.apply {
isCanceled = true
repository.updatePayDetail(this)
}
} else {
oldTransPayDetail?.apply {
isCanceled = false
repository.updatePayDetail(this)
}
}
repository.insertPayDetail(updateCurrentDateAndTime(payResult))
}
override fun processPreCompVoidDb(payResult: PayDetail) {
payDetail?.apply {
isCanceled = true
repository.updatePayDetail(this)
}
repository.insertPayDetail(updateCurrentDateAndTime(payResult))
}
/*
* Reversal
@ -285,7 +317,7 @@ class TransProcessViewModel @Inject constructor(
_transResultStatus.value =
TransResultStatus.REVERSAL_SUCCESS
_state.value = _state.value.copy("Reversal Success")
payDetail?.let {
repository.insertPayDetail(it)
}
@ -319,12 +351,14 @@ class TransProcessViewModel @Inject constructor(
_transResultStatus.value =
TransResultStatus.REVERSAL_FAIL
_state.value = _state.value.copy("Reversal Failure")
}
} else {
_transResultStatus.value =
TransResultStatus.REVERSAL_FAIL
_state.value = _state.value.copy("Reversal Failure")
}
}
@ -332,6 +366,7 @@ class TransProcessViewModel @Inject constructor(
_transResultStatus.value =
TransResultStatus.REVERSAL_FAIL
_state.value = _state.value.copy("Reversal Failure")
}
})
}

View File

@ -3,10 +3,15 @@ package com.utsmyanmar.baselib;
import android.content.ContentValues;
import android.os.RemoteException;
import com.sunmi.pay.hardware.aidl.AidlConstants;
import com.sunmi.pay.hardware.aidlv2.AidlConstantsV2;
import com.sunmi.pay.hardware.aidlv2.security.SecurityOptV2;
import com.utsmyanmar.paylibs.Constant;
import com.utsmyanmar.paylibs.utils.core_utils.ByteUtil;
import com.utsmyanmar.paylibs.utils.core_utils.SystemParamsOperation;
import com.utsmyanmar.paylibs.utils.secure.TriDes;
import java.security.GeneralSecurityException;
import sunmi.sunmiui.utils.LogUtil;
@ -23,7 +28,9 @@ public final class TerminalKeyUtil {
SecurityOptV2 mSecurityOptV2 = BaseApplication.getInstance().mSecurityOptV2;
byte[] cvByte = ByteUtil.hexStr2Bytes("B7B520");
byte[] dataByte = ByteUtil.hexStr2Bytes("05FFB893726A5F1E59692D24E72E36AC".toUpperCase());
// byte[] dataByte = ByteUtil.hexStr2Bytes("e121249099a677e8b7d4f6a9d49fe8d1".toUpperCase()); // mpu
// byte[] dataByte = ByteUtil.hexStr2Bytes("05FFB893726A5F1E59692D24E72E36AC");
byte[] dataByte = ByteUtil.hexStr2Bytes("DB7887664E3A06873342B753171896B1");
byte[] makBytes = ByteUtil.hexStr2Bytes("250738083EC15BD3BA67D66B8A7AA13B");
// byte[] makCvBytes = ByteUtil.hexStr2Bytes("204E449B97");
@ -41,10 +48,11 @@ public final class TerminalKeyUtil {
int result = 0;
SystemParamsOperation.getInstance().setTMKIndex("9");
SystemParamsOperation.getInstance().setInjectOnce(false);
if(!SystemParamsOperation.getInstance().isInjectOnce()) {
try {
// result = mSecurityOptV2.saveKeyDukpt(AidlConstantsV2.Security.KEY_TYPE_DUPKT_IPEK,IPEKByte,IPEKKCVByte,KSNByte,AidlConstantsV2.Security.KEY_ALG_TYPE_3DES,9);
result = mSecurityOptV2.savePlaintextKey(AidlConstantsV2.Security.KEY_TYPE_TMK,dataByte,cvByte,AidlConstantsV2.Security.KEY_ALG_TYPE_3DES,9);
result = mSecurityOptV2.savePlaintextKey(AidlConstantsV2.Security.KEY_TYPE_TMK,dataByte,null,AidlConstantsV2.Security.KEY_ALG_TYPE_3DES,9);
LogUtil.d(ContentValues.TAG, "save TMK result:" + result);
if (result != 0) {
LogUtil.d(TAG, "save TMK fail");

View File

@ -42,7 +42,7 @@ public class BatchUploadProcess {
}
private BatchUploadProcess() {
isoMsgX = new ISOMsgX.ISOMsgXBuilder(ISOVersion.VERSION_1993, ISOMode.BOTH_HEADER_TPDU,HostName.BPC)
isoMsgX = new ISOMsgX.ISOMsgXBuilder(ISOVersion.VERSION_1993, ISOMode.ONLY_HEADER,HostName.CARDZONE)
.build();
}
@ -78,7 +78,7 @@ public class BatchUploadProcess {
public void startBatchUpload(BatchListener batchListener) {
HostName hostName = HostName.BPC;
LogUtil.d(TAG, "Starting Batch Upload");
byte[] sendBytes = isoMsgX.buildISOPackets(tradeData, BitmapConfig.BPC_BATCH_UPLOAD, MessageType.BATCH_UPLOAD);
byte[] sendBytes = isoMsgX.buildISOPackets(tradeData, BitmapConfig.CZ_BATCH_UPLOAD, MessageType.BATCH_UPLOAD);
ISOSocket.getInstance().enqueue(sendBytes, sendBytes.length,false, new ISOCallback() {
@Override
public void onReceive(byte[] bytes, int length) {

View File

@ -409,7 +409,8 @@ public class DecodePackage {
if(hostName == HostName.BPC) {
System.arraycopy(body, tmpLen, nextData, 0, datLen);
} else {
System.arraycopy(body, tmpLen, nextData, 0, body.length-tmpLen);
// System.arraycopy(body, tmpLen, nextData, 0, body.length-tmpLen);
System.arraycopy(body, tmpLen, nextData, 0, datLen);
}
//

View File

@ -280,6 +280,7 @@ public class EncodePackage {
System.arraycopy(fieldBody.getBytes(StandardCharsets.ISO_8859_1), 0, body, index, fieldBody.length());
index += fieldBody.length();
} else {
// MPU
String fieldHead = msgField.getDataStr().substring(0,4);
int bcdLen = (fieldHead.length() / 2) + (fieldHead.length() % 2);
byte dat[] = new byte[bcdLen];

View File

@ -106,6 +106,9 @@ public class ISOMsgX extends BaseISOMsgX{
MTI = "0400";
// MTI = "0420";
break;
case CZ_REVERSAL:
MTI = "0420";
break;
case NETWORK_MANAGEMENT:
MTI = "0800";
break;
@ -124,6 +127,7 @@ public class ISOMsgX extends BaseISOMsgX{
Map<String, MsgField> map = FieldUtils.fillEachFieldValue(rm, tradeData);
tradeData.payDetail.setMsgType(MTI);
return packByte(map, MTI,hostName,isoMode);
}

View File

@ -26,14 +26,11 @@ import java.security.KeyManagementException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.regex.Matcher;
@ -67,7 +64,6 @@ import sunmi.sunmiui.utils.LogUtil;
public class ISOSocket {
private static final String TAG = ISOSocket.class.getSimpleName();
private static final int DEFAULT_TIMEOUT = 30 * 1000;
private String serverIP;
private int serverPort;
@ -152,7 +148,7 @@ public class ISOSocket {
private SSLSocketFactory getSSLSocketFactory()
throws CertificateException, KeyStoreException, IOException, NoSuchAlgorithmException, KeyManagementException {
CertificateFactory cf = CertificateFactory.getInstance("X.509");
InputStream caInput = context.getResources().openRawResource(R.raw.mpu_crt_2026); // this cert file stored in \app\src\main\res\raw folder path
InputStream caInput = context.getResources().openRawResource(R.raw.mob); // this cert file stored in \app\src\main\res\raw folder path
Certificate ca = cf.generateCertificate(caInput);
caInput.close();
@ -179,7 +175,7 @@ public class ISOSocket {
private OkHttpClient getClient()
throws CertificateException, KeyStoreException, IOException, NoSuchAlgorithmException, KeyManagementException {
CertificateFactory cf = CertificateFactory.getInstance("X.509");
InputStream caInput = context.getResources().openRawResource(R.raw.mpu_crt_2026); // this cert file stored in \app\src\main\res\raw folder path
InputStream caInput = context.getResources().openRawResource(R.raw.mob); // this cert file stored in \app\src\main\res\raw folder path
Certificate ca = cf.generateCertificate(caInput);
caInput.close();
@ -330,16 +326,9 @@ public class ISOSocket {
}
return port;
}
private String formatDate(Date d) { return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(d); }
public void switchIp() {
isSwitchIp = true;
// serverIP = "posuat.myanmarorientalbank.com";
// serverPort = 5033;
// serverIP = "192.168.0.100";
// serverPort = 5001;
// serverIP = "103.84.101.82";
// serverPort = 60147;
serverIP = getSecondaryIp();
serverPort = getSecondaryPort();
}
@ -360,13 +349,6 @@ public class ISOSocket {
if (!isSwitchIp) {
serverIP = getIp();
serverPort = getPort();
// serverIP = "192.168.0.100";
// serverPort = 5001;
// serverIP = "103.84.101.82";
// serverPort = 60147;
// SystemParamsOperation.getInstance().setSslSwitchStatus(true);
// serverIP = "posuat.myanmarorientalbank.com";
// serverPort = 5033;
} else {
isSwitchIp = false;
}
@ -374,11 +356,7 @@ public class ISOSocket {
reversalSwitchFlag = SystemParamsOperation.getInstance().isReversalOn();
if (reversalSwitchFlag) {
if (reversalTrans) {
reversalTestCaseFlag = true;
} else {
reversalTestCaseFlag = false;
}
reversalTestCaseFlag = reversalTrans;
} else {
reversalTestCaseFlag = true;
}
@ -402,54 +380,8 @@ public class ISOSocket {
if (SystemParamsOperation.getInstance().isSslOn()) {
try {
TrustManager[] trustAll = new TrustManager[]{new X509TrustManager() {
public void checkClientTrusted(X509Certificate[] chain, String authType) {}
public void checkServerTrusted(X509Certificate[] chain, String authType) {}
public X509Certificate[] getAcceptedIssuers() { return new X509Certificate[0]; }
}};
SSLContext ctx = SSLContext.getInstance("TLS");
ctx.init(null, trustAll, new SecureRandom());
SSLSocketFactory factory = ctx.getSocketFactory();
// try (SSLSocket sslsock = (SSLSocket) factory.createSocket()) {
//
// sslsock.setEnabledProtocols(new String[]{"TLSv1.2"});
//// while (!isSSLOnline(timeout,subscriber)) {
//// Thread.sleep(150);
//// subscriber.onError(new Throwable("java.net.SocketTimeoutException: connect timed out"));
//// }
//
// sslsock.setSoTimeout(60000);
// sslsock.connect(new InetSocketAddress(serverIP, serverPort), connectTimeout);
//
// sslsock.startHandshake();
//
// SSLSession session = sslsock.getSession();
//
// LogUtil.d(TAG, "SSL handshake success to " + serverIP + ":" + serverPort + "\n");
// LogUtil.d(TAG, "TLS handshake success to " + serverIP + ":" + serverPort + "\n");
// LogUtil.d(TAG, "TLS version: " + session.getProtocol() + "\n");
// LogUtil.d(TAG,"Cipher suite: " + session.getCipherSuite() + "\n");
//
// try {
// java.security.cert.Certificate[] chain = session.getPeerCertificates();
// if (chain != null && chain.length > 0) {
// LogUtil.d(TAG,"Peer certificates:\n");
// for (int i = 0; i < chain.length; i++) {
// X509Certificate x = (X509Certificate) chain[i];
// LogUtil.d(TAG, " [" + i + "] Subject: " + x.getSubjectX500Principal().getName() + "\n");
// LogUtil.d(TAG," Issuer: " + x.getIssuerX500Principal().getName() + "\n");
// LogUtil.d(TAG," Valid: " + formatDate(x.getNotBefore()) + " - " + formatDate(x.getNotAfter()) + "\n");
// }
// }
// } catch (Exception ignored) { }
// }
// OkHttpClient client = getClient();
sslSocket = (SSLSocket) factory.createSocket();
sslSocket.setEnabledProtocols(new String[]{"TLSv1.2"});
// sslSocket.setEnabledProtocols(new String[]{"TLSv1", "TLSv1.1", "TLSv1.2", "TLSv1.3"});
OkHttpClient client = getClient();
sslSocket = (SSLSocket) client.sslSocketFactory().createSocket();
// while (!isSSLOnline(timeout,subscriber)) {
// Thread.sleep(150);
// subscriber.onError(new Throwable("java.net.SocketTimeoutException: connect timed out"));
@ -458,28 +390,6 @@ public class ISOSocket {
sslSocket.setSoTimeout(60000);
sslSocket.connect(new InetSocketAddress(serverIP, serverPort), connectTimeout);
sslSocket.startHandshake();
SSLSession session = sslSocket.getSession();
LogUtil.d(TAG, "SSL handshake success to " + serverIP + ":" + serverPort + "\n");
LogUtil.d(TAG, "TLS handshake success to " + serverIP + ":" + serverPort + "\n");
LogUtil.d(TAG, "TLS version: " + session.getProtocol() + "\n");
LogUtil.d(TAG,"Cipher suite: " + session.getCipherSuite() + "\n");
try {
java.security.cert.Certificate[] chain = session.getPeerCertificates();
if (chain != null && chain.length > 0) {
LogUtil.d(TAG,"Peer certificates:\n");
for (int i = 0; i < chain.length; i++) {
X509Certificate x = (X509Certificate) chain[i];
LogUtil.d(TAG, " [" + i + "] Subject: " + x.getSubjectX500Principal().getName() + "\n");
LogUtil.d(TAG," Issuer: " + x.getIssuerX500Principal().getName() + "\n");
LogUtil.d(TAG," Valid: " + formatDate(x.getNotBefore()) + " - " + formatDate(x.getNotAfter()) + "\n");
}
}
} catch (Exception ignored) { }
inputStream = sslSocket.getInputStream();
outputStream = sslSocket.getOutputStream();
@ -493,11 +403,6 @@ public class ISOSocket {
int len = -1;
if (reversalTestCaseFlag) {
len = inputStream.read(recBuff);
} else {
int lenn = inputStream.read(recBuff);
String received = ByteUtil.bytes2HexStr(recBuff);
LogUtil.d(TAG,"Received Response :"+received);
}
rLen = len;
@ -512,7 +417,7 @@ public class ISOSocket {
subscriber.onComplete();
} catch ( IOException |
} catch (CertificateException | KeyStoreException | IOException |
NoSuchAlgorithmException | KeyManagementException e) {
e.printStackTrace();
subscriber.onError(e);
@ -549,17 +454,12 @@ public class ISOSocket {
int len = -1;
if (reversalTestCaseFlag) {
len = inputStream.read(recBuff);
} else {
int lenn = inputStream.read(recBuff);
String received = ByteUtil.bytes2HexStr(recBuff);
LogUtil.d(TAG,"Received Response :"+received);
}
rLen = len;
if (rLen == -1) {
reversalTestCaseFlag = true;
reversalTestCaseFlag = true;
subscriber.onError(new Throwable("length -1"));
} else {
subscriber.onNext(recBuff);

View File

@ -206,7 +206,7 @@ public abstract class BaseXPrint {
String finalVersion = SystemParamsOperation.getInstance().getFinalVersion();
sunmiPrinterService.setFontSize(textSize, innerResultCallback);
sunmiPrinterService.printText("***** MERCHANT COPY *****" + "\n", innerResultCallback);
// sunmiPrinterService.printText("***** MERCHANT COPY *****" + "\n", innerResultCallback);
sunmiPrinterService.printText(finalVersion + "\n", innerResultCallback);
sunmiPrinterService.printText(receiptFooter + "\n", innerResultCallback);
} catch (RemoteException e) {

View File

@ -58,14 +58,14 @@ public class PrintXImpl extends BaseXPrint implements PrintX {
opts.inJustDecodeBounds = true;
// Step 1: Only read dimensions first
BitmapFactory.decodeResource(resources, R.drawable.print_logo_mpu, opts);
BitmapFactory.decodeResource(resources, R.drawable.mob_print_logo, opts);
// Step 2: Calculate sample size so width <= 384px
opts.inSampleSize = calculateInSampleSize(opts, 384, 384);
opts.inJustDecodeBounds = false;
// Step 3: Decode scaled-down bitmap
bitmap = BitmapFactory.decodeResource(resources, R.drawable.print_logo_mpu, opts);
bitmap = BitmapFactory.decodeResource(resources, R.drawable.mob_print_logo, opts);
}
@Override

View File

@ -50,14 +50,14 @@ public class ReversalAction {
}
private ReversalAction() {
isoMsgX = new ISOMsgX.ISOMsgXBuilder(ISOVersion.VERSION_1993, ISOMode.ONLY_HEADER,HostName.FINEXUS)
isoMsgX = new ISOMsgX.ISOMsgXBuilder(ISOVersion.VERSION_1993, ISOMode.ONLY_HEADER,HostName.CARDZONE)
.build();
}
public ReversalAction setData(TradeData tradeData) {
this.tradeData = tradeData;
payDetail = tradeData.getPayDetail();
payDetail.setTransactionType(TransactionType.REVERSAL);
payDetail.setTransactionType(TransactionsType.REVERSAL.value);
// payDetail.setTransType(REVERSAL);
if (TextUtils.equals(payDetail.getAccountType(), MPU)) {
BITMAP = getBitmapMPU(payDetail);
@ -129,7 +129,7 @@ public class ReversalAction {
public void startReversal(ReversalListener reversalListener) {
HostName hostName = HostName.BPC;
LogUtil.d(TAG, "Starting Reversal....");
byte[] sendBytes = isoMsgX.buildISOPackets(tradeData, BitmapConfig.MPU_NEW_REVERSAL, MessageType.REVERSAL);
byte[] sendBytes = isoMsgX.buildISOPackets(tradeData, BitmapConfig.CZ_REVERSAL, MessageType.CZ_REVERSAL);
// byte[] sendBytes = isoMsgX.buildISOPackets(tradeData, BitmapConfig.BPC_AUTOMATIC_REVERSAL, MessageType.REVERSAL);
ISOSocket.getInstance().enqueue(sendBytes, sendBytes.length,true, new ISOCallback() {
@Override

View File

@ -50,7 +50,7 @@ public class SignOnProcess {
private SignOnProcess() {
tradeData = Params.newTrade(true);
// payDetail = tradeData.getPayDetail();
isoMsgX = new ISOMsgX.ISOMsgXBuilder(ISOVersion.VERSION_1993, ISOMode.ONLY_HEADER,HostName.MPU)
isoMsgX = new ISOMsgX.ISOMsgXBuilder(ISOVersion.VERSION_1993, ISOMode.ONLY_HEADER,HostName.CARDZONE)
.build();
}
@ -59,7 +59,7 @@ public class SignOnProcess {
flag = false;
return listener -> {
// LogUtil.d(TAG, "Starting TX SignOn");
HostName hostName = HostName.MPU;
HostName hostName = HostName.CARDZONE;
PayDetail payDetail = tradeData.getPayDetail();
payDetail.setProcessCode(TransactionsType.SIGN_ON.processCode);
@ -67,7 +67,7 @@ public class SignOnProcess {
payDetail.setTransactionType(TransactionsType.SIGN_ON.value);
payDetail.setHostName(hostName.name);
byte[] sendBytes = isoMsgX.buildISOPackets(tradeData, BitmapConfig.MPU_NEW_SIGN_ON, MessageType.NETWORK_MANAGEMENT);
byte[] sendBytes = isoMsgX.buildISOPackets(tradeData, BitmapConfig.CZ_SIGN_ON, MessageType.NETWORK_MANAGEMENT);
ISOSocket.getInstance().enqueue(sendBytes, sendBytes.length,false, new ISOCallback() {
@Override
public void onReceive(byte[] bytes, int length) {
@ -94,6 +94,7 @@ public class SignOnProcess {
System.arraycopy(field62, 0, encryptedPIK, 0, 8);
System.arraycopy(field62, 0, encryptedPIK, 8, 8);
// System.arraycopy(field62, 0, encryptedPIK, 0, 16);
try {
byte[] kcv = ByteUtil.hexStr2Bytes(TriDes.getKcv(encryptedPIK));
LogUtil.d(TAG, "Encrypted PIK:" + ByteUtil.bytes2HexStr(encryptedPIK));
@ -122,6 +123,7 @@ public class SignOnProcess {
System.arraycopy(field62, 0, encryptedPIK, 0, 8);
System.arraycopy(field62, 0, encryptedPIK, 8, 8);
// System.arraycopy(field62, 0, encryptedPIK, 0, 16);
try {

View File

@ -174,8 +174,8 @@ public class TransactionsOperation {
if (responseMap.get("F038") != null) {
transPayDetail.setApprovalCode(Objects.requireNonNull(responseMap.get("F038")).getDataStr());
} else {
transPayDetail.setIsNeedReversal(true);
return;
// transPayDetail.setIsNeedReversal(true);
// return;
}
if (responseMap.get("F046") != null) {
@ -295,7 +295,7 @@ public class TransactionsOperation {
if(payDetail.getAccountType().equals(MPU)){
// payDetail.setIsFreeSign(true);
payDetail.setTransCVM(TransCVM.NO_CVM);
payDetail.setTransCVM(TransCVM.SIGNATURE);
}
return tradeData;
}
@ -376,7 +376,6 @@ public class TransactionsOperation {
newPay.setTransCVM(TransCVM.SIGNATURE);
if((oldPay.getAccountType().equals(CardScheme.MASTERCARD.name) || oldPay.getAccountType().equals(CardScheme.VISA.name))) {
newPay.setProcessCode(TransactionsType.PRE_AUTH_SALE.processCode);
}
// for manual entry reversal which need de 35
@ -391,12 +390,14 @@ public class TransactionsOperation {
newPay.setReferNo(oldPay.getReferNo());
newPay.setTempKSN(oldPay.getTempKSN());
newPay.setTransCVM(oldPay.getTransCVM()); //added on 26 Nov 2025
newPay.setApprovalCode(oldPay.getApprovalCode());
newPay.setTradeTime(oldPay.getTradeTime()); // mpu
newPay.setTradeDate(oldPay.getTradeDate()); // mpu
} else if (transactionsType == TransactionsType.PRE_AUTH_COMPLETE_VOID) {
newPay.setVoucherNo(oldPay.getVoucherNo());
newPay.setCustomOrderNo(oldPay.getVoucherNo());
newPay.setReferNo(oldPay.getReferNo());
newPay.setCardInfo(oldPay.getCardInfo());
/*
* new requirements */
newPay.setCardType(AidlConstants.CardType.IC.getValue());
@ -406,7 +407,7 @@ public class TransactionsOperation {
if(oldPay.getAccountType().equals(CardScheme.MPU.name) || oldPay.getAccountType().equals(CardScheme.MPU_CTLS.name)){
newPay.setTransCVM(TransCVM.NO_CVM);
newPay.setTransCVM(TransCVM.SIGNATURE);
}
newTrade.setPayDetail(newPay);

View File

@ -5,6 +5,7 @@ public enum MessageType {
AUTHORIZATION,
AUTHORIZATION_COMPLETE,
REVERSAL,
CZ_REVERSAL,
NETWORK_MANAGEMENT,
TC_ADVICE,
BATCH_UPLOAD,

View File

@ -5,7 +5,8 @@ public enum HostName {
FINEXUS("FINEXUS"),
CARDZONE("CARDZONE"),
MPU("MPU");
MPU("MPU"),
MOB("MOB");
public final String name;

View File

@ -44,7 +44,7 @@ public class HostUtils {
if (hostName == HostName.MPU) {
bitmap = BitmapConfig.MPU_NEW_VOID;
} else {
bitmap = BitmapConfig.FINEXUS_VOID;
bitmap = BitmapConfig.CZ_VOID;
}
break;
case SETTLEMENT:
@ -58,35 +58,35 @@ public class HostUtils {
if (hostName == HostName.MPU) {
bitmap = BitmapConfig.MPU_NEW_PRE_AUTH;
} else {
bitmap = BitmapConfig.FUND_TRANSFER_PRE_CHECK;
bitmap = BitmapConfig.CZ_PRE_AUTH;
}
break;
case PRE_AUTH_VOID:
if (hostName == HostName.MPU) {
bitmap = BitmapConfig.MPU_NEW_PRE_AUTH_CANCEL;
} else {
bitmap = BitmapConfig.CZ_SALE;
bitmap = BitmapConfig.CZ_PRE_AUTH_VOID;
}
break;
case PRE_AUTH_COMPLETE:
if (hostName == HostName.MPU) {
bitmap = BitmapConfig.MPU_NEW_PRE_AUTH_COMPLETE_BITMAP;
} else {
bitmap = BitmapConfig.CZ_SALE;
bitmap = BitmapConfig.CZ_PRE_AUTH_COMP;
}
break;
case PRE_AUTH_COMPLETE_VOID:
if (hostName == HostName.MPU) {
bitmap = BitmapConfig.MPU_NEW_PRE_AUTH_COMPLETE_CANCEL;
} else {
bitmap = BitmapConfig.FINEXUS_PRE_AUTH_VOID;
bitmap = BitmapConfig.CZ_PRE_AUTH_COMP_VOID;
}
break;
case REFUND:
if (hostName == HostName.MPU) {
bitmap = BitmapConfig.MPU_NEW_REFUND;
} else {
bitmap = BitmapConfig.CZ_SALE;
bitmap = BitmapConfig.CZ_REFUND;
}
break;
case TIP_ADJUSTMENT:
@ -130,7 +130,7 @@ public class HostUtils {
if (hostName == HostName.BPC) {
msgType = MessageType.REVERSAL;
} else {
msgType = MessageType.FINANCIAL;
msgType = MessageType.REVERSAL;
}
break;
case SETTLEMENT:

View File

@ -176,7 +176,7 @@ public class BitmapConfig {
// 703C058008C08000 7038058028C00000
public static final String MPU_NEW_REFUND ="703C058008C08000";
public static final String MPU_NEW_PRE_AUTH ="7020058020C08000";
public static final String MPU_NEW_PRE_AUTH ="7020058020C09000";
@ -193,6 +193,14 @@ public class BitmapConfig {
public static final String MPU_NEW_CASH_ADVANCE="7020058020C09000";
public static final String CZ_SALE="7020058020C09000";
public static final String CZ_VOID="7038058008C08010";
public static final String CZ_REFUND="703C058008C08000";
public static final String CZ_REVERSAL="7020078000C08000";
public static final String CZ_BATCH_UPLOAD="703805800CC00010";
public static final String CZ_PRE_AUTH="7020058020C09000";
public static final String CZ_PRE_AUTH_VOID="7038058028C08000";
public static final String CZ_PRE_AUTH_COMP="703805802CC08000";
public static final String CZ_PRE_AUTH_COMP_VOID="703805802CC08004";
public static final String UPI_SALE = "7020058020C09200";

View File

@ -247,7 +247,7 @@ public class FieldConfig {
// for TTIP MPU
// /* FLD 35 */ {2, SDK_8583_LEN_BCD, 37, SDK_8583_DATA_BCD, SDK_8583_ALIGN_L, '0'},
/* FLD 35 */ {2, SDK_8583_LEN_BCD, 37, SDK_8583_DATA_BCD, SDK_8583_ALIGN_R, '0'},
/* FLD 35 */ {2, SDK_8583_LEN_BCD, 37, SDK_8583_DATA_BCD, SDK_8583_ALIGN_R, ' '},
/* FLD 36 */ {3, SDK_8583_LEN_BCD, 999, SDK_8583_DATA_BIT, SDK_8583_ALIGN_L, '0'},
/* FLD 37 */ {0, SDK_8583_LEN_BCD, 12, SDK_8583_DATA_ASC, SDK_8583_ALIGN_R, '0'},

View File

@ -4,11 +4,13 @@ import android.text.TextUtils;
import com.sunmi.pay.hardware.aidl.AidlConstants;
import com.utsmyanmar.paylibs.Constant;
import com.utsmyanmar.paylibs.batch_upload.BatchUploadProcess;
import com.utsmyanmar.paylibs.model.MsgField;
import com.utsmyanmar.paylibs.model.PayDetail;
import com.utsmyanmar.paylibs.model.TradeData;
import com.utsmyanmar.paylibs.model.enums.TransCVM;
import com.utsmyanmar.paylibs.utils.enums.CardScheme;
import com.utsmyanmar.paylibs.utils.enums.HostName;
import java.io.UnsupportedEncodingException;
import java.util.Iterator;
@ -359,7 +361,7 @@ public class FieldUtils {
// } else {
// iterator.remove();
// }
field.setDataStr("001");
field.setDataStr("000");
break;
case 24: //Network International Identifier NII
@ -374,14 +376,12 @@ public class FieldUtils {
08 – mail order/telephone order
* */
// Service point condition code Fill in 00 for ordinary consumer transactions and mobile phone chip transactions, fill in 08 for order consumer transactions, fill in 64 for installment transactions, fill in 65 for points consumption transactions, fill in 67 for mobile phone card-free reservations
// if (payDetail.getTransactionType() == PRE_AUTH_VOID.value) {
// field.setDataStr("06");
// } else {
// field.setDataStr("00");
// }
if(payDetail.getTransactionType() == REFUND.value) {
field.setDataStr("00"); // MPU
if (payDetail.getTransactionType() == PRE_AUTH_SALE.value) {
field.setDataStr("06");
}
else if(payDetail.getTransactionType() == REFUND.value || payDetail.getTransactionType() == REVERSAL.value) {
// field.setDataStr("00"); // MPU
field.setDataStr("61"); // CZ
} else if(payDetail.getCardType() == -1) {
field.setDataStr("08");
}
@ -507,9 +507,16 @@ public class FieldUtils {
break;
case 60: //Private(oldMPU) //original Amount Void(new MPU)
//
String field60 = tradeData.getField60();
String lengthValue = String.format(Locale.getDefault(),"%04d", field60.length())+field60;
field.setDataStr(lengthValue);
// for CardZone
// String field60 = tradeData.getField60();
// String lengthValue = String.format(Locale.getDefault(),"%04d", field60.length())+field60;
// field.setDataStr(field60);
break;
case 61:
// TODO
@ -536,7 +543,9 @@ public class FieldUtils {
break;
case 63: //Private Use
String settleData = payDetail.getSettleData();
field.setDataStr(settleData);
String settleValue = String.format(Locale.getDefault(),"%04d", settleData.length())+settleData;
field.setDataStr(settleValue);
// field.setDataStr(settleData);
break;
case 64: //Message Authentication Code (MAC) Required when the 39 field in the response message is "00"
field.setDataStr("0000000000000000");

View File

@ -82,7 +82,9 @@ public class Params {
payDetail.setIsCanceled(false);
payDetail.transNum = System.currentTimeMillis() / 1000;
// need to add host logic in another place
payDetail.setHostName("MPU");
// payDetail.setHostName("MPU");
payDetail.setHostName("MOB");
// payDetail.setHostName("MOB");
payDetail.setOriginalTransDate(SystemDateTime.getMMDD()+SystemDateTime.getYYYY());

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

View File

@ -0,0 +1,21 @@
-----BEGIN CERTIFICATE-----
MIIDYTCCAkmgAwIBAgIETeycDjANBgkqhkiG9w0BAQsFADBhMQswCQYDVQQGEwJt
eTELMAkGA1UECBMCa2wxDDAKBgNVBAcTA2trbDERMA8GA1UEChMIY2FyZHpvbmUx
ETAPBgNVBAsTCGNhcmR6b25lMREwDwYDVQQDEwhjYXJkem9uZTAeFw0yMTA3MDcw
ODU5MzlaFw0zMTA3MDUwODU5MzlaMGExCzAJBgNVBAYTAm15MQswCQYDVQQIEwJr
bDEMMAoGA1UEBxMDa2tsMREwDwYDVQQKEwhjYXJkem9uZTERMA8GA1UECxMIY2Fy
ZHpvbmUxETAPBgNVBAMTCGNhcmR6b25lMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A
MIIBCgKCAQEA2v7WJjmr0b9WpbcsTtk6llJDA5XeReWc3in0sirutMQi+9jLUdgJ
0Kdr0Cro0vbJAcVWTBYnMqnqh7oJQYs6vpIFKJA175ZhclQFAcL2jth+Tsqx9Av1
DxbuuB7yxU7ErkhXe+/B0R8zOag866R3Xg7u09GG4mlQMx5zmo1Hn3lqMU0lxEXM
vhkzVXLUMLhVwu/oJk98u+6pPXoCzXPjBCgy3iR+BaGIR6aiflz8Bi1Rn3XgfnMi
PzOzxKRZu5mNBRijs7ztcjA8Pc2uNI0ux3+jUJTgu2JRIBJs+7BkNcJeLzpW5psz
qfFdwxTKkgfqU9gJwuPaEbdnP0j7Rv2MBwIDAQABoyEwHzAdBgNVHQ4EFgQUOF7s
btNCxmv9HSAkSAPVAHpDXY4wDQYJKoZIhvcNAQELBQADggEBADOJaUnRiVucIb8q
jxplpSE0gOHMng+BMq2gKcqqc+QRc/mgM3n0J8F1A3rzj3udbzen3/9TnOiH17uy
FGLH12sJ7dJWMGAJ2p15N307ckrCab3+3Y1w6OQJLZ8t3eeXFGPVWQJtiOUbyX73
3Fv+TpNl6X1uEdQ0zNDiAvWZQ/e6ynYGF/tSgZLJvgV+8UY5ShCA0Ypousm6sPqb
KQXPCs3kRn1eMgMy8PcmUlZK/5qhD+xZswWgNiQ6EyCWmwpB4O0N9wQbLBTFZkGA
bDke78LVS1nUam9czcNvSDYyjWsk9ud8mo9JfF21qmwCR68btqDgeU0hM9W6lEQo
ezrrz9k=
-----END CERTIFICATE-----

View File

@ -1,23 +0,0 @@
-----BEGIN CERTIFICATE-----
MIID3DCCAsSgAwIBAgIJAIuL4ypXInCoMA0GCSqGSIb3DQEBBQUAMGcxCzAJBgNV
BAYTAk1SMQswCQYDVQQIDAJNUjEMMAoGA1UECgwDTVBVMQwwCgYDVQQLDANNUFUx
DTALBgNVBAMMBHRlc3QxIDAeBgkqhkiG9w0BCQEWEXdhaXlhbkBmcHQuY29tLnZu
MB4XDTIxMDIwMzE3MTExMloXDTIyMDIwMzE3MTExMlowdzELMAkGA1UEBhMCTVIx
DzANBgNVBAgMBllhbmdvbjEPMA0GA1UEBwwGWWFuZ29uMQwwCgYDVQQKDANNUFUx
DDAKBgNVBAsMA01QVTENMAsGA1UEAwwEdGVzdDEbMBkGCSqGSIb3DQEJARYMdGVz
dEBtcHUuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAujLe6mWJ
GXliGUoLdJEA37VyBOQ7zLpge7u2QM2UMQXnKosdtZzMzXnVrnctJ6ehzLnhL0Lv
Oissr7qo53haXsJJccHeqcTH5cFkh96IswOrc8/1MMrb4zMjnNjOZaxmB4HeGHKk
KjNLKTTO8It3v/MziBhxUEEKVrN4npeUfsGVPfrvYnZXSWuRLhzYYLtEz/LFg4Yh
28pCXqk2vmGNV62oZ2+SDisoiYdIVEOMRqb/7cJg+cd0JRdvxHsTNuYRHGtJOM10
lEnoI+moE5OIUpp8Z+i4ZcvyThpNbRWf9sRzB8oc32fZ2UWnlN5+bVRLCjlk2jNO
SGdMuMaF0bs3GwIDAQABo3sweTAJBgNVHRMEAjAAMCwGCWCGSAGG+EIBDQQfFh1P
cGVuU1NMIEdlbmVyYXRlZCBDZXJ0aWZpY2F0ZTAdBgNVHQ4EFgQUipyzRYfqRc2P
QCJ0FJp6eAvI1wkwHwYDVR0jBBgwFoAUvYEAOtcvUt4CxCp+9zRG2oJqos8wDQYJ
KoZIhvcNAQEFBQADggEBAAjBQroy74ACyyJCZJ50KBTYQI3Sk82hO/+zZFqsJgWm
mNum31TtfwhvQmi94sXQSar0UsqLTmLC3UTucwEdSKOPzXF5BK4EEbkYgMm78hNK
2zqhGvcWlqun+KPxmnMRFoVmN3Ck2PkcgudvAeV0G3rm0OdLUwLoK2TcREVf1Cwg
C7jX5QtGOgVqnKDZBuoSvdbV30ka7pKkdJAy/Wus4N9hHH+bR2FNkLgTDoXnV8LL
E2dRKVYAh9TtuXiW/ejhPtEc14sMP+8JpCFX3DYx4yRW8ZeMl5jal4LhRFXdDbVl
Wm99KcWM+TNw/nzKwv0zUJXy0HlN82tvLX1/wbBUo8M=
-----END CERTIFICATE-----

Binary file not shown.

Binary file not shown.

View File

@ -1,113 +0,0 @@
-----BEGIN CERTIFICATE-----
MIIG8DCCBNigAwIBAgIQWgAhSkRBOifV9FjHQOJCuDANBgkqhkiG9w0BAQwFADBL
MQswCQYDVQQGEwJBVDEQMA4GA1UEChMHWmVyb1NTTDEqMCgGA1UEAxMhWmVyb1NT
TCBSU0EgRG9tYWluIFNlY3VyZSBTaXRlIENBMB4XDTI0MDkwMzAwMDAwMFoXDTI1
MDkwMzIzNTk1OVowGTEXMBUGA1UEAwwOKi55b21hYmFuay5uZXQwggEiMA0GCSqG
SIb3DQEBAQUAA4IBDwAwggEKAoIBAQCAqUfzHF/N4m3xxNr/noBpg9s8edFWghYn
sYEAOQmMD1tVsjFavds9eAO5JBhL2+yQ8dxd5P5yqAHuQmEcBpw0YdPEAsKJbrqG
4VXU40q1DbQipnHiTrQx0RG5aRFsP6OTrRU0MnQMaFGTSgMKYKg0YM24si/JyyiX
eJXpEIXzt10NWpQfJ7T3RrzkkEHsfPzYmiBecUVmCu2Mb0/jym+Hr5eBvEf/mmhR
re/0hofitx9OTwmZt85uPVwpP6miOLpby+/46v7k8PSZ7qJhCv34AnnldY3uA+Ws
ld+Ria5Kpt6dHiBPzer/8HNd2Xkdpo2Ny3jq8N9vo0UJ2H7rWgy7AgMBAAGjggMA
MIIC/DAfBgNVHSMEGDAWgBTI2XhootkZaNU9ct5fCj7ctYaGpjAdBgNVHQ4EFgQU
Iu3iisKOIvooYnMumJGFqW4LTEkwDgYDVR0PAQH/BAQDAgWgMAwGA1UdEwEB/wQC
MAAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMEkGA1UdIARCMEAwNAYL
KwYBBAGyMQECAk4wJTAjBggrBgEFBQcCARYXaHR0cHM6Ly9zZWN0aWdvLmNvbS9D
UFMwCAYGZ4EMAQIBMIGIBggrBgEFBQcBAQR8MHowSwYIKwYBBQUHMAKGP2h0dHA6
Ly96ZXJvc3NsLmNydC5zZWN0aWdvLmNvbS9aZXJvU1NMUlNBRG9tYWluU2VjdXJl
U2l0ZUNBLmNydDArBggrBgEFBQcwAYYfaHR0cDovL3plcm9zc2wub2NzcC5zZWN0
aWdvLmNvbTAnBgNVHREEIDAegg4qLnlvbWFiYW5rLm5ldIIMeW9tYWJhbmsubmV0
MIIBfAYKKwYBBAHWeQIEAgSCAWwEggFoAWYAdQDd3Mo0ldfhFgXnlTL6x5/4PRxQ
39sAOhQSdgosrLvIKgAAAZG2rpYwAAAEAwBGMEQCIAdAYV4eBpRPW8pCNmvriCr0
sOJZeBqBHoQ6fBSJHG1CAiAs841ZeuQRpFtLzZxxDXWbQYKJuT8iV6l2bZHsUdDh
owB1AA3h8jAr0w3BQGISCepVLvxHdHyx1+kw7w5CHrR+Tqo0AAABkbaulhAAAAQD
AEYwRAIgCLjpVhxG/w/Fsm+aAmE4t4PVAgASOgVMzoD/eOuor/MCIHN5h/YzzOzy
KbhEl5VkeSLbLUFz99Lq+Ci5Z0jRRTJ9AHYAEvFONL1TckyEBhnDjz96E/jntWKH
iJxtMAWE6+WGJjoAAAGRtq6WDwAABAMARzBFAiEA7uQaaiKBOBPRukDoHqvTzifk
1N8fcbf34xDC+BlelnUCIAZaPHlu2u+DJcxCAGgBoqVtZhdO64tcV6yy1fYqo2fq
MA0GCSqGSIb3DQEBDAUAA4ICAQBrxQPnmGA+CnhEq2ZaqZNQteRXb9+HbLT9Vq/I
6je5BtuYZrF1hd8V/NV3ug+vfWwNjUAILM068BnzfCvAaFxQ/kzmzPgttH2S5e0x
0fkG/I0pLtNj9ABcIcOKAnLK3Ce4ANgkpoHg8LtlBy2Tu9Aj+QgDCxz3Jhk6fIOi
dBlcveoRE3UpzaZ6G6XLz6CLHcFePhca+qlwWh9L3aw7Kr7ykgU62Y+z2R/KBmvg
6lPhumXSMffd3ikESgn+RyVXO6VRyszrIG5/B9NGLc1Of4g0+eE+znWIk4T7eVet
RnBMO5I4pLZb63Af9tuYBBBBOkoW+oWFeqxu0Y8G5EqReZiBvG8NvNi2Zj/wvDB+
aQN8u2oQnWHTqH5TS+3WSymWKsxHu3anYtRsNdLWb898HkYRPENXDLn8jETDyopN
cMBA4qou6di0HOaTmGfGn9cDvrJ/ciYZnT3msCdTSwAk19hacpEtgH3T12FMAtjk
aDj45VD1rRfmWj6j8xLVuOV6G6gjsloh3ruQv0j9va1vbmGufc9VbeUPCqFzL2UZ
vRr8xAggyjtYPpRwtA+SHAD27nnma3XeEBPwdJJs15wvZw2EH7fI4yYwZ3IrTtAq
ICt+BIXVlTQhfgtIC2XCuw3f5e5VlThZRKrJEB1TFAcmW9cCpTrKc5R4cs7XcTbt
m7MTbw==
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIG1TCCBL2gAwIBAgIQbFWr29AHksedBwzYEZ7WvzANBgkqhkiG9w0BAQwFADCB
iDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0pl
cnNleSBDaXR5MR4wHAYDVQQKExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNV
BAMTJVVTRVJUcnVzdCBSU0EgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMjAw
MTMwMDAwMDAwWhcNMzAwMTI5MjM1OTU5WjBLMQswCQYDVQQGEwJBVDEQMA4GA1UE
ChMHWmVyb1NTTDEqMCgGA1UEAxMhWmVyb1NTTCBSU0EgRG9tYWluIFNlY3VyZSBT
aXRlIENBMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAhmlzfqO1Mdgj
4W3dpBPTVBX1AuvcAyG1fl0dUnw/MeueCWzRWTheZ35LVo91kLI3DDVaZKW+TBAs
JBjEbYmMwcWSTWYCg5334SF0+ctDAsFxsX+rTDh9kSrG/4mp6OShubLaEIUJiZo4
t873TuSd0Wj5DWt3DtpAG8T35l/v+xrN8ub8PSSoX5Vkgw+jWf4KQtNvUFLDq8mF
WhUnPL6jHAADXpvs4lTNYwOtx9yQtbpxwSt7QJY1+ICrmRJB6BuKRt/jfDJF9Jsc
RQVlHIxQdKAJl7oaVnXgDkqtk2qddd3kCDXd74gv813G91z7CjsGyJ93oJIlNS3U
gFbD6V54JMgZ3rSmotYbz98oZxX7MKbtCm1aJ/q+hTv2YK1yMxrnfcieKmOYBbFD
hnW5O6RMA703dBK92j6XRN2EttLkQuujZgy+jXRKtaWMIlkNkWJmOiHmErQngHvt
iNkIcjJumq1ddFX4iaTI40a6zgvIBtxFeDs2RfcaH73er7ctNUUqgQT5rFgJhMmF
x76rQgB5OZUkodb5k2ex7P+Gu4J86bS15094UuYcV09hVeknmTh5Ex9CBKipLS2W
2wKBakf+aVYnNCU6S0nASqt2xrZpGC1v7v6DhuepyyJtn3qSV2PoBiU5Sql+aARp
wUibQMGm44gjyNDqDlVp+ShLQlUH9x8CAwEAAaOCAXUwggFxMB8GA1UdIwQYMBaA
FFN5v1qqK0rPVIDh2JvAnfKyA2bLMB0GA1UdDgQWBBTI2XhootkZaNU9ct5fCj7c
tYaGpjAOBgNVHQ8BAf8EBAMCAYYwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHSUE
FjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwIgYDVR0gBBswGTANBgsrBgEEAbIxAQIC
TjAIBgZngQwBAgEwUAYDVR0fBEkwRzBFoEOgQYY/aHR0cDovL2NybC51c2VydHJ1
c3QuY29tL1VTRVJUcnVzdFJTQUNlcnRpZmljYXRpb25BdXRob3JpdHkuY3JsMHYG
CCsGAQUFBwEBBGowaDA/BggrBgEFBQcwAoYzaHR0cDovL2NydC51c2VydHJ1c3Qu
Y29tL1VTRVJUcnVzdFJTQUFkZFRydXN0Q0EuY3J0MCUGCCsGAQUFBzABhhlodHRw
Oi8vb2NzcC51c2VydHJ1c3QuY29tMA0GCSqGSIb3DQEBDAUAA4ICAQAVDwoIzQDV
ercT0eYqZjBNJ8VNWwVFlQOtZERqn5iWnEVaLZZdzxlbvz2Fx0ExUNuUEgYkIVM4
YocKkCQ7hO5noicoq/DrEYH5IuNcuW1I8JJZ9DLuB1fYvIHlZ2JG46iNbVKA3ygA
Ez86RvDQlt2C494qqPVItRjrz9YlJEGT0DrttyApq0YLFDzf+Z1pkMhh7c+7fXeJ
qmIhfJpduKc8HEQkYQQShen426S3H0JrIAbKcBCiyYFuOhfyvuwVCFDfFvrjADjd
4jX1uQXd161IyFRbm89s2Oj5oU1wDYz5sx+hoCuh6lSs+/uPuWomIq3y1GDFNafW
+LsHBU16lQo5Q2yh25laQsKRgyPmMpHJ98edm6y2sHUabASmRHxvGiuwwE25aDU0
2SAeepyImJ2CzB80YG7WxlynHqNhpE7xfC7PzQlLgmfEHdU+tHFeQazRQnrFkW2W
kqRGIq7cKRnyypvjPMkjeiV9lRdAM9fSJvsB3svUuu1coIG1xxI1yegoGM4r5QP4
RGIVvYaiI76C0djoSbQ/dkIUUXQuB8AL5jyH34g3BZaaXyvpmnV4ilppMXVAnAYG
ON51WhJ6W0xNdNJwzYASZYH+tmCWI+N60Gv2NNMGHwMZ7e9bXgzUCZH5FaBFDGR5
S9VWqHB73Q+OyIVvIbKYcSc2w/aSuFKGSA==
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIF3jCCA8agAwIBAgIQAf1tMPyjylGoG7xkDjUDLTANBgkqhkiG9w0BAQwFADCB
iDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0pl
cnNleSBDaXR5MR4wHAYDVQQKExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNV
BAMTJVVTRVJUcnVzdCBSU0EgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTAw
MjAxMDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBiDELMAkGA1UEBhMCVVMxEzARBgNV
BAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNleSBDaXR5MR4wHAYDVQQKExVU
aGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMTJVVTRVJUcnVzdCBSU0EgQ2Vy
dGlmaWNhdGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIK
AoICAQCAEmUXNg7D2wiz0KxXDXbtzSfTTK1Qg2HiqiBNCS1kCdzOiZ/MPans9s/B
3PHTsdZ7NygRK0faOca8Ohm0X6a9fZ2jY0K2dvKpOyuR+OJv0OwWIJAJPuLodMkY
tJHUYmTbf6MG8YgYapAiPLz+E/CHFHv25B+O1ORRxhFnRghRy4YUVD+8M/5+bJz/
Fp0YvVGONaanZshyZ9shZrHUm3gDwFA66Mzw3LyeTP6vBZY1H1dat//O+T23LLb2
VN3I5xI6Ta5MirdcmrS3ID3KfyI0rn47aGYBROcBTkZTmzNg95S+UzeQc0PzMsNT
79uq/nROacdrjGCT3sTHDN/hMq7MkztReJVni+49Vv4M0GkPGw/zJSZrM233bkf6
c0Plfg6lZrEpfDKEY1WJxA3Bk1QwGROs0303p+tdOmw1XNtB1xLaqUkL39iAigmT
Yo61Zs8liM2EuLE/pDkP2QKe6xJMlXzzawWpXhaDzLhn4ugTncxbgtNMs+1b/97l
c6wjOy0AvzVVdAlJ2ElYGn+SNuZRkg7zJn0cTRe8yexDJtC/QV9AqURE9JnnV4ee
UB9XVKg+/XRjL7FQZQnmWEIuQxpMtPAlR1n6BB6T1CZGSlCBst6+eLf8ZxXhyVeE
Hg9j1uliutZfVS7qXMYoCAQlObgOK6nyTJccBz8NUvXt7y+CDwIDAQABo0IwQDAd
BgNVHQ4EFgQUU3m/WqorSs9UgOHYm8Cd8rIDZsswDgYDVR0PAQH/BAQDAgEGMA8G
A1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEMBQADggIBAFzUfA3P9wF9QZllDHPF
Up/L+M+ZBn8b2kMVn54CVVeWFPFSPCeHlCjtHzoBN6J2/FNQwISbxmtOuowhT6KO
VWKR82kV2LyI48SqC/3vqOlLVSoGIG1VeCkZ7l8wXEskEVX/JJpuXior7gtNn3/3
ATiUFJVDBwn7YKnuHKsSjKCaXqeYalltiz8I+8jRRa8YFWSQEg9zKC7F4iRO/Fjs
8PRF/iKz6y+O0tlFYQXBl2+odnKPi4w2r78NBc5xjeambx9spnFixdjQg3IM8WcR
iQycE0xyNN+81XHfqnHd4blsjDwSXWXavVcStkNr/+XeTWYRUc+ZruwXtuhxkYze
Sf7dNXGiFSeUHM9h4ya7b6NnJSFd5t0dCy5oGzuCr+yDZ4XUmFF0sbmZgIn/f3gZ
XHlKYC6SQK5MNyosycdiyA5d9zZbyuAlJQG03RoHnHcAP9Dc1ew91Pq7P8yF1m9/
qS3fuQL39ZeatTXaw2ewh0qpKJ4jjv9cJ2vhsE/zB+4ALtRZh8tSQZXq9EfX7mRB
VXyNWQKV3WKdwrnuWih0hKWbt5DHDAff9Yk2dDLWKMGwsAvgnEzDHNb842m1R0aB
L6KCq9NjRHDEjf8tM7qtj3u1cIiuPhnPQCjY/MiQu12ZIvVS5ljFH4gxQ+6IHdfG
jjxDah2nGN59PRbxYvnKkKj9
-----END CERTIFICATE-----

View File

@ -1,113 +0,0 @@
-----BEGIN CERTIFICATE-----
MIIG7zCCBNegAwIBAgIQX5oO6r1lg7MfeJb1v5aSwDANBgkqhkiG9w0BAQwFADBL
MQswCQYDVQQGEwJBVDEQMA4GA1UEChMHWmVyb1NTTDEqMCgGA1UEAxMhWmVyb1NT
TCBSU0EgRG9tYWluIFNlY3VyZSBTaXRlIENBMB4XDTIzMDkyOTAwMDAwMFoXDTI0
MDkyODIzNTk1OVowGTEXMBUGA1UEAwwOKi55b21hYmFuay5uZXQwggEiMA0GCSqG
SIb3DQEBAQUAA4IBDwAwggEKAoIBAQCAqUfzHF/N4m3xxNr/noBpg9s8edFWghYn
sYEAOQmMD1tVsjFavds9eAO5JBhL2+yQ8dxd5P5yqAHuQmEcBpw0YdPEAsKJbrqG
4VXU40q1DbQipnHiTrQx0RG5aRFsP6OTrRU0MnQMaFGTSgMKYKg0YM24si/JyyiX
eJXpEIXzt10NWpQfJ7T3RrzkkEHsfPzYmiBecUVmCu2Mb0/jym+Hr5eBvEf/mmhR
re/0hofitx9OTwmZt85uPVwpP6miOLpby+/46v7k8PSZ7qJhCv34AnnldY3uA+Ws
ld+Ria5Kpt6dHiBPzer/8HNd2Xkdpo2Ny3jq8N9vo0UJ2H7rWgy7AgMBAAGjggL/
MIIC+zAfBgNVHSMEGDAWgBTI2XhootkZaNU9ct5fCj7ctYaGpjAdBgNVHQ4EFgQU
Iu3iisKOIvooYnMumJGFqW4LTEkwDgYDVR0PAQH/BAQDAgWgMAwGA1UdEwEB/wQC
MAAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMEkGA1UdIARCMEAwNAYL
KwYBBAGyMQECAk4wJTAjBggrBgEFBQcCARYXaHR0cHM6Ly9zZWN0aWdvLmNvbS9D
UFMwCAYGZ4EMAQIBMIGIBggrBgEFBQcBAQR8MHowSwYIKwYBBQUHMAKGP2h0dHA6
Ly96ZXJvc3NsLmNydC5zZWN0aWdvLmNvbS9aZXJvU1NMUlNBRG9tYWluU2VjdXJl
U2l0ZUNBLmNydDArBggrBgEFBQcwAYYfaHR0cDovL3plcm9zc2wub2NzcC5zZWN0
aWdvLmNvbTAnBgNVHREEIDAegg4qLnlvbWFiYW5rLm5ldIIMeW9tYWJhbmsubmV0
MIIBewYKKwYBBAHWeQIEAgSCAWsEggFnAWUAdQB2/4g/Crb7lVHCYcz1h7o0tKTN
uyncaEIKn+ZnTFo6dAAAAYrfFnGoAAAEAwBGMEQCID23iGjCqSL5njxZ4dhGR6xC
lr6Wrn//LYaRhG3luDIHAiBLPvTSOFO9fz8LpL/QZ+49xE5WbvFYBPW6pVcXYvqA
/gB2ANq2v2s/tbYin5vCu1xr6HCRcWy7UYSFNL2kPTBI1/urAAABit8WcgQAAAQD
AEcwRQIgANdbNT0g+H+IUw2MxicwxabC/rsHw9/QwF6KRQW1bKMCIQCHPlGpPhow
CiXAGeBXMt/of2hI0ePkT+2Wq70vzYLa+QB0AO7N0GTV2xrOxVy3nbTNE6Iyh0Z8
vOzew1FIWUZxH7WbAAABit8WcdcAAAQDAEUwQwIfQypm/cC8h3QPtMaBJC5m72O0
rY6wDYEhQQQ2vlcehQIgV4tV4sv8dlrkMjMVXDhKAutb9neYewzaYKYgOAD95C8w
DQYJKoZIhvcNAQEMBQADggIBAAhDQXNDUdbWDhRVIeFnJuueUrpq6IbG3S+l8tt4
qxUEa3xqhUsujry4yNrJvY+rCSjS6QxHytctmLVaFwvKgELNUXqPtEBTYIw/O/v/
+RZnLF2wG4rdD7BzhyBGQZsbcY1nxJlJ1BB8m0+b8aNQesL731ARbn1Tf9Tl46jZ
C9pDE5+KbJHJRy9yE/cyik+W9ndUBgEHRDwtZaMsQVHTstg2pBg2Sw9XvDraYMQI
Qhr2R9hjMHJaZd9pnjXfDkvA2MZvhjGEEHOvPv0k/r+vf4p2eiHhzYZ0Bv0atMVe
RG1sFV7jPvgsi/XhWQgAPUPk8nh/zyImgzVfoiHi4hZkGE2Kv0Gri+lxGyFzZ3rk
SNkcgMRFYoLI993ypLNIkcWTs8k4w5RjbQXYml7lEZHS1GjBcc4yaxX25Viyto74
vVwSjoKj/PrtT6gVOcub1HrXEaWauJgPaeHNcyT7ZpQ0s3PlnLjrLe5YSrlX1Aji
acMq7vQKtGG/6NSShCyIcKAQrFy90tmBV7qs8dwuPh/sUeBPT0fkFQQ50GIswMZU
10f/D0GAxEJtdFdp8zHdYmuz0CfTTao6EOKsszQVdde1GRlo4bWNifjtQPpulhai
dVmhitRmgLHWz2dIgD0kxM5elRsL1U35bumg9ldSacBCthJYvzeHZkPsa7+g2xZD
Mrq3
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIG1TCCBL2gAwIBAgIQbFWr29AHksedBwzYEZ7WvzANBgkqhkiG9w0BAQwFADCB
iDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0pl
cnNleSBDaXR5MR4wHAYDVQQKExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNV
BAMTJVVTRVJUcnVzdCBSU0EgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMjAw
MTMwMDAwMDAwWhcNMzAwMTI5MjM1OTU5WjBLMQswCQYDVQQGEwJBVDEQMA4GA1UE
ChMHWmVyb1NTTDEqMCgGA1UEAxMhWmVyb1NTTCBSU0EgRG9tYWluIFNlY3VyZSBT
aXRlIENBMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAhmlzfqO1Mdgj
4W3dpBPTVBX1AuvcAyG1fl0dUnw/MeueCWzRWTheZ35LVo91kLI3DDVaZKW+TBAs
JBjEbYmMwcWSTWYCg5334SF0+ctDAsFxsX+rTDh9kSrG/4mp6OShubLaEIUJiZo4
t873TuSd0Wj5DWt3DtpAG8T35l/v+xrN8ub8PSSoX5Vkgw+jWf4KQtNvUFLDq8mF
WhUnPL6jHAADXpvs4lTNYwOtx9yQtbpxwSt7QJY1+ICrmRJB6BuKRt/jfDJF9Jsc
RQVlHIxQdKAJl7oaVnXgDkqtk2qddd3kCDXd74gv813G91z7CjsGyJ93oJIlNS3U
gFbD6V54JMgZ3rSmotYbz98oZxX7MKbtCm1aJ/q+hTv2YK1yMxrnfcieKmOYBbFD
hnW5O6RMA703dBK92j6XRN2EttLkQuujZgy+jXRKtaWMIlkNkWJmOiHmErQngHvt
iNkIcjJumq1ddFX4iaTI40a6zgvIBtxFeDs2RfcaH73er7ctNUUqgQT5rFgJhMmF
x76rQgB5OZUkodb5k2ex7P+Gu4J86bS15094UuYcV09hVeknmTh5Ex9CBKipLS2W
2wKBakf+aVYnNCU6S0nASqt2xrZpGC1v7v6DhuepyyJtn3qSV2PoBiU5Sql+aARp
wUibQMGm44gjyNDqDlVp+ShLQlUH9x8CAwEAAaOCAXUwggFxMB8GA1UdIwQYMBaA
FFN5v1qqK0rPVIDh2JvAnfKyA2bLMB0GA1UdDgQWBBTI2XhootkZaNU9ct5fCj7c
tYaGpjAOBgNVHQ8BAf8EBAMCAYYwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHSUE
FjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwIgYDVR0gBBswGTANBgsrBgEEAbIxAQIC
TjAIBgZngQwBAgEwUAYDVR0fBEkwRzBFoEOgQYY/aHR0cDovL2NybC51c2VydHJ1
c3QuY29tL1VTRVJUcnVzdFJTQUNlcnRpZmljYXRpb25BdXRob3JpdHkuY3JsMHYG
CCsGAQUFBwEBBGowaDA/BggrBgEFBQcwAoYzaHR0cDovL2NydC51c2VydHJ1c3Qu
Y29tL1VTRVJUcnVzdFJTQUFkZFRydXN0Q0EuY3J0MCUGCCsGAQUFBzABhhlodHRw
Oi8vb2NzcC51c2VydHJ1c3QuY29tMA0GCSqGSIb3DQEBDAUAA4ICAQAVDwoIzQDV
ercT0eYqZjBNJ8VNWwVFlQOtZERqn5iWnEVaLZZdzxlbvz2Fx0ExUNuUEgYkIVM4
YocKkCQ7hO5noicoq/DrEYH5IuNcuW1I8JJZ9DLuB1fYvIHlZ2JG46iNbVKA3ygA
Ez86RvDQlt2C494qqPVItRjrz9YlJEGT0DrttyApq0YLFDzf+Z1pkMhh7c+7fXeJ
qmIhfJpduKc8HEQkYQQShen426S3H0JrIAbKcBCiyYFuOhfyvuwVCFDfFvrjADjd
4jX1uQXd161IyFRbm89s2Oj5oU1wDYz5sx+hoCuh6lSs+/uPuWomIq3y1GDFNafW
+LsHBU16lQo5Q2yh25laQsKRgyPmMpHJ98edm6y2sHUabASmRHxvGiuwwE25aDU0
2SAeepyImJ2CzB80YG7WxlynHqNhpE7xfC7PzQlLgmfEHdU+tHFeQazRQnrFkW2W
kqRGIq7cKRnyypvjPMkjeiV9lRdAM9fSJvsB3svUuu1coIG1xxI1yegoGM4r5QP4
RGIVvYaiI76C0djoSbQ/dkIUUXQuB8AL5jyH34g3BZaaXyvpmnV4ilppMXVAnAYG
ON51WhJ6W0xNdNJwzYASZYH+tmCWI+N60Gv2NNMGHwMZ7e9bXgzUCZH5FaBFDGR5
S9VWqHB73Q+OyIVvIbKYcSc2w/aSuFKGSA==
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIF3jCCA8agAwIBAgIQAf1tMPyjylGoG7xkDjUDLTANBgkqhkiG9w0BAQwFADCB
iDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0pl
cnNleSBDaXR5MR4wHAYDVQQKExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNV
BAMTJVVTRVJUcnVzdCBSU0EgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTAw
MjAxMDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBiDELMAkGA1UEBhMCVVMxEzARBgNV
BAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNleSBDaXR5MR4wHAYDVQQKExVU
aGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMTJVVTRVJUcnVzdCBSU0EgQ2Vy
dGlmaWNhdGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIK
AoICAQCAEmUXNg7D2wiz0KxXDXbtzSfTTK1Qg2HiqiBNCS1kCdzOiZ/MPans9s/B
3PHTsdZ7NygRK0faOca8Ohm0X6a9fZ2jY0K2dvKpOyuR+OJv0OwWIJAJPuLodMkY
tJHUYmTbf6MG8YgYapAiPLz+E/CHFHv25B+O1ORRxhFnRghRy4YUVD+8M/5+bJz/
Fp0YvVGONaanZshyZ9shZrHUm3gDwFA66Mzw3LyeTP6vBZY1H1dat//O+T23LLb2
VN3I5xI6Ta5MirdcmrS3ID3KfyI0rn47aGYBROcBTkZTmzNg95S+UzeQc0PzMsNT
79uq/nROacdrjGCT3sTHDN/hMq7MkztReJVni+49Vv4M0GkPGw/zJSZrM233bkf6
c0Plfg6lZrEpfDKEY1WJxA3Bk1QwGROs0303p+tdOmw1XNtB1xLaqUkL39iAigmT
Yo61Zs8liM2EuLE/pDkP2QKe6xJMlXzzawWpXhaDzLhn4ugTncxbgtNMs+1b/97l
c6wjOy0AvzVVdAlJ2ElYGn+SNuZRkg7zJn0cTRe8yexDJtC/QV9AqURE9JnnV4ee
UB9XVKg+/XRjL7FQZQnmWEIuQxpMtPAlR1n6BB6T1CZGSlCBst6+eLf8ZxXhyVeE
Hg9j1uliutZfVS7qXMYoCAQlObgOK6nyTJccBz8NUvXt7y+CDwIDAQABo0IwQDAd
BgNVHQ4EFgQUU3m/WqorSs9UgOHYm8Cd8rIDZsswDgYDVR0PAQH/BAQDAgEGMA8G
A1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEMBQADggIBAFzUfA3P9wF9QZllDHPF
Up/L+M+ZBn8b2kMVn54CVVeWFPFSPCeHlCjtHzoBN6J2/FNQwISbxmtOuowhT6KO
VWKR82kV2LyI48SqC/3vqOlLVSoGIG1VeCkZ7l8wXEskEVX/JJpuXior7gtNn3/3
ATiUFJVDBwn7YKnuHKsSjKCaXqeYalltiz8I+8jRRa8YFWSQEg9zKC7F4iRO/Fjs
8PRF/iKz6y+O0tlFYQXBl2+odnKPi4w2r78NBc5xjeambx9spnFixdjQg3IM8WcR
iQycE0xyNN+81XHfqnHd4blsjDwSXWXavVcStkNr/+XeTWYRUc+ZruwXtuhxkYze
Sf7dNXGiFSeUHM9h4ya7b6NnJSFd5t0dCy5oGzuCr+yDZ4XUmFF0sbmZgIn/f3gZ
XHlKYC6SQK5MNyosycdiyA5d9zZbyuAlJQG03RoHnHcAP9Dc1ew91Pq7P8yF1m9/
qS3fuQL39ZeatTXaw2ewh0qpKJ4jjv9cJ2vhsE/zB+4ALtRZh8tSQZXq9EfX7mRB
VXyNWQKV3WKdwrnuWih0hKWbt5DHDAff9Yk2dDLWKMGwsAvgnEzDHNb842m1R0aB
L6KCq9NjRHDEjf8tM7qtj3u1cIiuPhnPQCjY/MiQu12ZIvVS5ljFH4gxQ+6IHdfG
jjxDah2nGN59PRbxYvnKkKj9
-----END CERTIFICATE-----