Compare commits

..

2 Commits

Author SHA1 Message Date
moon
752693c8f3 refresh last sync 2026-08-20 23:44:33 +06:30
moon
d011b46345 auto settlement 2026-08-20 23:18:59 +06:30
18 changed files with 407 additions and 93 deletions

View File

@ -1,6 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" /> <component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="jbr-21" project-jdk-type="JavaSDK"> <component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="temurin-21" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" /> <output url="file://$PROJECT_DIR$/build/classes" />
</component> </component>
<component name="ProjectType"> <component name="ProjectType">

View File

@ -11,10 +11,9 @@
### version 3.4 ### version 3.4
* disable display always on * disable display always on
* auto-settlement * auto-settlement
* show expiry date as "`**`/`**`" * show expiry date as "`**`/`**`"
* align the customer name, in paper slip * align the customer name, in paper slip
* settlement fixed in both UI and system * settlement fixed in both UI and system
* amount, date, time fixed in Void Preview * amount, date, time fixed in Void Preview
* show reversal status in trans records * show reversal status in trans records

View File

@ -19,7 +19,10 @@
<uses-permission android:name="com.sunmi.perm.SECURITY" /> <uses-permission android:name="com.sunmi.perm.SECURITY" />
<uses-permission android:name="com.sunmi.perm.CONTACTLESS_CARD" /> <uses-permission android:name="com.sunmi.perm.CONTACTLESS_CARD" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
<uses-permission android:name="android.permission.CAMERA" /> <uses-permission android:name="android.permission.CAMERA" />

View File

@ -1,41 +1,108 @@
package com.mob.utsmyanmar package com.mob.utsmyanmar
import android.Manifest import android.Manifest
import android.app.AlarmManager
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.provider.Settings
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge import androidx.activity.enableEdgeToEdge
import androidx.annotation.RequiresApi
import androidx.annotation.RequiresPermission
import androidx.core.view.WindowCompat import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat import androidx.core.view.WindowInsetsControllerCompat
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.navigation.compose.rememberNavController import androidx.navigation.compose.rememberNavController
import com.mob.utsmyanmar.auto_settlement.SettlementScheduler import com.mob.utsmyanmar.auto_settlement.SettlementScheduler
import com.mob.utsmyanmar.ui.navigation.AppNavGraph import com.mob.utsmyanmar.ui.navigation.AppNavGraph
import com.mob.utsmyanmar.ui.theme.MOBPOSTheme import com.mob.utsmyanmar.ui.theme.MOBPOSTheme
import com.utsmyanmar.paylibs.utils.core_utils.SystemParamsOperation
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint @AndroidEntryPoint
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
@RequiresPermission(Manifest.permission.SCHEDULE_EXACT_ALARM) private var autoSettlementRequested by mutableStateOf(false)
@RequiresApi(Build.VERSION_CODES.N) private var exactAlarmSettingsOpened = false
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
SettlementScheduler.schedule(this)
// installSplashScreen()
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
autoSettlementRequested = intent?.getBooleanExtra("AUTO_SETTLEMENT", false) == true
requestNotificationPermission()
scheduleSettlementAlarm()
enableEdgeToEdge() enableEdgeToEdge()
hideSystemBars() hideSystemBars()
// window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) // window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
setContent { setContent {
MOBPOSTheme { MOBPOSTheme {
val navController = rememberNavController() val navController = rememberNavController()
AppNavGraph(navController = navController) AppNavGraph(
navController = navController,
autoSettlementRequested = autoSettlementRequested,
onAutoSettlementHandled = { autoSettlementRequested = false }
)
} }
} }
} }
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
setIntent(intent)
if (intent.getBooleanExtra("AUTO_SETTLEMENT", false)) {
autoSettlementRequested = true
}
}
override fun onResume() {
super.onResume()
scheduleSettlementAlarm()
}
private fun requestNotificationPermission() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU &&
checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED
) {
requestPermissions(
arrayOf(Manifest.permission.POST_NOTIFICATIONS),
NOTIFICATION_PERMISSION_REQUEST_CODE
)
}
}
private fun scheduleSettlementAlarm() {
if (!SystemParamsOperation.getInstance().isAutoSettlement()) {
SettlementScheduler.schedule(this)
return
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
val alarmManager = getSystemService(AlarmManager::class.java)
if (!alarmManager.canScheduleExactAlarms()) {
if (!exactAlarmSettingsOpened) {
exactAlarmSettingsOpened = true
startActivity(
Intent(
Settings.ACTION_REQUEST_SCHEDULE_EXACT_ALARM,
Uri.parse("package:$packageName")
)
)
}
return
}
}
exactAlarmSettingsOpened = false
SettlementScheduler.schedule(this)
}
private companion object {
const val NOTIFICATION_PERMISSION_REQUEST_CODE = 1003
}
private fun hideSystemBars() { private fun hideSystemBars() {
val windowInsetsController = WindowCompat.getInsetsController(window, window.decorView) val windowInsetsController = WindowCompat.getInsetsController(window, window.decorView)
windowInsetsController.hide(WindowInsetsCompat.Type.navigationBars()) windowInsetsController.hide(WindowInsetsCompat.Type.navigationBars())

View File

@ -4,22 +4,32 @@ import android.content.BroadcastReceiver
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import com.utsmyanmar.paylibs.utils.core_utils.SystemParamsOperation
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import sunmi.sunmiui.utils.LogUtil import sunmi.sunmiui.utils.LogUtil
@AndroidEntryPoint @AndroidEntryPoint
class AlarmReceiver : BroadcastReceiver() { class AlarmReceiver : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) { override fun onReceive(context: Context?, intent: Intent?) {
val serviceIntent = Intent(context, AutoSettleService::class.java) val appContext = context ?: return
// Exact alarms are one-shot, so keep the next daily alarm alive before
// starting the foreground service.
if (!SettlementScheduler.schedule(appContext)) {
LogUtil.e(AlarmReceiver::class.java.simpleName, "Unable to reschedule settlement alarm")
}
val systemParams = SystemParamsOperation.getInstance()
if (!systemParams.isAutoSettlement() || !systemParams.settlementStatus) {
LogUtil.d(AlarmReceiver::class.java.simpleName, "Auto settlement is disabled")
return
}
val serviceIntent = Intent(appContext, AutoSettleService::class.java)
LogUtil.d(AlarmReceiver::class.java.simpleName, "Alert service") LogUtil.d(AlarmReceiver::class.java.simpleName, "Alert service")
if (context != null) { ContextCompat.startForegroundService(appContext, serviceIntent)
ContextCompat.startForegroundService(
context,
serviceIntent
)
}
LogUtil.d(AlarmReceiver::class.java.simpleName, "Foreground service") LogUtil.d(AlarmReceiver::class.java.simpleName, "Foreground service")
} }

View File

@ -3,13 +3,16 @@ package com.mob.utsmyanmar.auto_settlement
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.NotificationChannel import android.app.NotificationChannel
import android.app.NotificationManager import android.app.NotificationManager
import android.app.PendingIntent
import android.app.Service import android.app.Service
import android.content.Intent import android.content.Intent
import android.media.AudioAttributes import android.media.AudioAttributes
import android.media.Ringtone
import android.media.RingtoneManager import android.media.RingtoneManager
import android.os.Build import android.os.Build
import android.os.Handler
import android.os.IBinder import android.os.IBinder
import androidx.annotation.RequiresApi import android.os.Looper
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import com.mob.utsmyanmar.MainActivity import com.mob.utsmyanmar.MainActivity
import com.mob.utsmyanmar.R import com.mob.utsmyanmar.R
@ -21,8 +24,24 @@ import sunmi.sunmiui.utils.LogUtil
class AutoSettleService : Service() { class AutoSettleService : Service() {
companion object { companion object {
const val CHANNEL_ID = "auto_settlement_channel" private const val FOREGROUND_CHANNEL_ID = "auto_settlement_foreground_channel"
const val NOTIFICATION_ID = 1001 private const val ALERT_CHANNEL_ID = "auto_settlement_alert_channel"
private const val NOTIFICATION_ID = 1001
private const val ALERT_NOTIFICATION_ID = 1002
private const val ALERT_SOUND_DURATION_MS = 3_000L
}
private val mainHandler = Handler(Looper.getMainLooper())
private var alertRingtone: Ringtone? = null
private val stopServiceRunnable = Runnable {
alertRingtone?.stop()
alertRingtone = null
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
stopForeground(STOP_FOREGROUND_REMOVE)
} else {
stopForeground(true)
}
stopSelf()
} }
override fun onCreate() { override fun onCreate() {
@ -31,59 +50,129 @@ class AutoSettleService : Service() {
startForegroundNotification() startForegroundNotification()
} }
@RequiresApi(Build.VERSION_CODES.N)
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
if (!SystemParamsOperation.getInstance().settlementStatus) { val systemParams = SystemParamsOperation.getInstance()
if (!systemParams.isAutoSettlement() || !systemParams.settlementStatus) {
stopSelf() stopSelf()
return START_NOT_STICKY return START_NOT_STICKY
} }
showSettlementAlert()
playAlertSound()
launchSettlement() launchSettlement()
stopForeground(STOP_FOREGROUND_REMOVE) mainHandler.removeCallbacks(stopServiceRunnable)
stopSelf() mainHandler.postDelayed(stopServiceRunnable, ALERT_SOUND_DURATION_MS)
return START_NOT_STICKY return START_NOT_STICKY
} }
override fun onBind(intent: Intent?): IBinder? = null override fun onBind(intent: Intent?): IBinder? = null
override fun onDestroy() {
mainHandler.removeCallbacks(stopServiceRunnable)
alertRingtone?.stop()
alertRingtone = null
super.onDestroy()
}
@SuppressLint("ForegroundServiceType") @SuppressLint("ForegroundServiceType")
private fun startForegroundNotification() { private fun startForegroundNotification() {
LogUtil.d(AutoSettleService::class.java.simpleName, "foreground noti called") LogUtil.d(AutoSettleService::class.java.simpleName, "foreground noti called")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel( val channel = NotificationChannel(
CHANNEL_ID, FOREGROUND_CHANNEL_ID,
"AUTO SETTLEMENT", "AUTO SETTLEMENT",
NotificationManager.IMPORTANCE_HIGH NotificationManager.IMPORTANCE_LOW
)
channel.description = "Auto Settlement notification"
channel.enableVibration(true)
channel.setSound(
RingtoneManager.getDefaultUri(
RingtoneManager.TYPE_NOTIFICATION
), AudioAttributes.Builder().setUsage(AudioAttributes.USAGE_NOTIFICATION).build()
) )
channel.description = "Auto Settlement service status"
val manager = getSystemService(NotificationManager::class.java) val manager = getSystemService(NotificationManager::class.java)
manager?.createNotificationChannel(channel) manager?.createNotificationChannel(channel)
} }
val notification = NotificationCompat.Builder(this, CHANNEL_ID) val notification = NotificationCompat.Builder(this, FOREGROUND_CHANNEL_ID)
.setSmallIcon(R.drawable.logo_mob) .setSmallIcon(R.drawable.logo_mob)
.setContentTitle("Auto Settlement") .setContentTitle("Auto Settlement")
.setContentText("Processing...") .setContentText("Preparing settlement reminder...")
.setPriority(NotificationCompat.PRIORITY_HIGH) .setPriority(NotificationCompat.PRIORITY_LOW)
.setOngoing(true)
.build() .build()
startForeground(NOTIFICATION_ID, notification) startForeground(NOTIFICATION_ID, notification)
} }
private fun launchSettlement() { private fun showSettlementAlert() {
val intent = Intent(this, MainActivity::class.java).apply { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
addFlags( val channel = NotificationChannel(
Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP ALERT_CHANNEL_ID,
) "AUTO SETTLEMENT ALERT",
putExtra("AUTO_SETTLEMENT", true) NotificationManager.IMPORTANCE_HIGH
).apply {
description = "Daily settlement reminder"
enableVibration(true)
setSound(
RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION),
AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_NOTIFICATION)
.build()
)
}
getSystemService(NotificationManager::class.java)?.createNotificationChannel(channel)
} }
startActivity(intent)
val pendingIntent = PendingIntent.getActivity(
this,
ALERT_NOTIFICATION_ID,
settlementIntent(),
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
val notification = NotificationCompat.Builder(this, ALERT_CHANNEL_ID)
.setSmallIcon(R.drawable.logo_mob)
.setContentTitle("Settlement Reminder")
.setContentText("Daily settlement is due. Tap to open settlement.")
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setCategory(NotificationCompat.CATEGORY_REMINDER)
.setSound(RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION))
.setVibrate(longArrayOf(0, 250, 150, 250))
.setContentIntent(pendingIntent)
.setAutoCancel(true)
.build()
getSystemService(NotificationManager::class.java)
?.notify(ALERT_NOTIFICATION_ID, notification)
}
private fun playAlertSound() {
alertRingtone?.stop()
alertRingtone = RingtoneManager.getRingtone(
this,
RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION)
)?.apply {
audioAttributes = AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_NOTIFICATION)
.build()
play()
}
}
private fun launchSettlement() {
runCatching {
startActivity(settlementIntent())
}.onFailure {
LogUtil.e(
AutoSettleService::class.java.simpleName,
"Unable to open settlement screen: ${it.message}"
)
}
}
private fun settlementIntent(): Intent = Intent(this, MainActivity::class.java).apply {
addFlags(
Intent.FLAG_ACTIVITY_NEW_TASK or
Intent.FLAG_ACTIVITY_SINGLE_TOP or
Intent.FLAG_ACTIVITY_CLEAR_TOP
)
putExtra("AUTO_SETTLEMENT", true)
} }
} }

View File

@ -1,26 +1,42 @@
package com.mob.utsmyanmar.auto_settlement package com.mob.utsmyanmar.auto_settlement
import android.Manifest
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.AlarmManager import android.app.AlarmManager
import android.app.PendingIntent import android.app.PendingIntent
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import androidx.annotation.RequiresPermission import android.os.Build
import com.utsmyanmar.paylibs.utils.core_utils.SystemParamsOperation
import java.util.Calendar import java.util.Calendar
object SettlementScheduler { object SettlementScheduler {
private const val FALLBACK_ALERT_TIME = "01:10:00"
@RequiresPermission(Manifest.permission.SCHEDULE_EXACT_ALARM) fun schedule(context: Context): Boolean {
fun schedule(context: Context) { if (!SystemParamsOperation.getInstance().isAutoSettlement()) {
cancel(context)
return false
}
val alarmManager =
context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S &&
!alarmManager.canScheduleExactAlarms()
) {
return false
}
val configuredTime = parseTime(
// SystemParamsOperation.getInstance().clearBatchTime
"23:05:00"
) ?: parseTime(FALLBACK_ALERT_TIME)!!
val calendar = Calendar.getInstance().apply { val calendar = Calendar.getInstance().apply {
set(Calendar.HOUR_OF_DAY, configuredTime.first)
// Auto settlement time set(Calendar.MINUTE, configuredTime.second)
set(Calendar.HOUR_OF_DAY, 1) set(Calendar.SECOND, configuredTime.third)
set(Calendar.MINUTE, 10) set(Calendar.MILLISECOND, 0)
set(Calendar.SECOND, 0)
if (before(Calendar.getInstance())) { if (before(Calendar.getInstance())) {
add(Calendar.DAY_OF_MONTH, 1) add(Calendar.DAY_OF_MONTH, 1)
@ -28,11 +44,6 @@ object SettlementScheduler {
} }
val alarmManager =
context.getSystemService(Context.ALARM_SERVICE)
as AlarmManager
val intent = Intent( val intent = Intent(
context, context,
AlarmReceiver::class.java AlarmReceiver::class.java
@ -53,6 +64,20 @@ object SettlementScheduler {
calendar.timeInMillis, calendar.timeInMillis,
pendingIntent pendingIntent
) )
return true
}
private fun parseTime(value: String): Triple<Int, Int, Int>? {
val parts = value.trim().split(":")
if (parts.size !in 2..3) return null
val hour = parts[0].toIntOrNull() ?: return null
val minute = parts[1].toIntOrNull() ?: return null
val second = if (parts.size == 3) parts[2].toIntOrNull() ?: return null else 0
if (hour !in 0..23 || minute !in 0..59 || second !in 0..59) return null
return Triple(hour, minute, second)
} }

View File

@ -16,6 +16,7 @@ import androidx.navigation.NavHostController
import androidx.navigation.NavType import androidx.navigation.NavType
import androidx.navigation.compose.NavHost import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable import androidx.navigation.compose.composable
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.navArgument import androidx.navigation.navArgument
import com.mob.utsmyanmar.ui.cardwaiting.CardWaitingScreen import com.mob.utsmyanmar.ui.cardwaiting.CardWaitingScreen
import com.mob.utsmyanmar.ui.cardwaiting.CardWaitingViewModel import com.mob.utsmyanmar.ui.cardwaiting.CardWaitingViewModel
@ -65,9 +66,31 @@ import com.utsmyanmar.paylibs.utils.iso_utils.TransactionsType
@SuppressLint("ContextCastToActivity") @SuppressLint("ContextCastToActivity")
@Composable @Composable
fun AppNavGraph( fun AppNavGraph(
navController: NavHostController navController: NavHostController,
autoSettlementRequested: Boolean = false,
onAutoSettlementHandled: () -> Unit = {}
) { ) {
val activity = LocalContext.current as ComponentActivity val activity = LocalContext.current as ComponentActivity
val currentBackStackEntry by navController.currentBackStackEntryAsState()
val currentRoute = currentBackStackEntry?.destination?.route
LaunchedEffect(autoSettlementRequested, currentRoute) {
if (!autoSettlementRequested ||
currentRoute == null ||
currentRoute == Routes.TmsSetup.route
) return@LaunchedEffect
if (currentRoute == Routes.Settlement.route) {
return@LaunchedEffect
}
if (currentRoute == Routes.Password.route) {
onAutoSettlementHandled()
return@LaunchedEffect
}
navController.navigate(Routes.Settlement.route) {
launchSingleTop = true
}
}
NavHost( NavHost(
navController = navController, navController = navController,
@ -78,7 +101,9 @@ fun AppNavGraph(
TmsSetupRoute( TmsSetupRoute(
viewModel = tmsSetupViewModel, viewModel = tmsSetupViewModel,
onNavigateDashboard = { onNavigateDashboard = {
navController.navigate(Routes.Dashboard.route) { navController.navigate(
if (autoSettlementRequested) Routes.Settlement.route else Routes.Dashboard.route
) {
popUpTo(Routes.TmsSetup.route) { popUpTo(Routes.TmsSetup.route) {
inclusive = true inclusive = true
} }
@ -412,6 +437,7 @@ fun AppNavGraph(
sharedViewModel = sharedViewModel, sharedViewModel = sharedViewModel,
settlementViewMode = settlementViewMode, settlementViewMode = settlementViewMode,
onBack = { navController.popBackStack(Routes.Dashboard.route,false) }, onBack = { navController.popBackStack(Routes.Dashboard.route,false) },
autoStartSettlement = autoSettlementRequested,
onStartSettlement = { onStartSettlement = {
sharedViewModel.transactionsType.value = TransactionsType.SETTLEMENT sharedViewModel.transactionsType.value = TransactionsType.SETTLEMENT
navController.navigate(Routes.Processing.route) { navController.navigate(Routes.Processing.route) {
@ -420,6 +446,9 @@ fun AppNavGraph(
} }
launchSingleTop = true launchSingleTop = true
} }
if (autoSettlementRequested) {
onAutoSettlementHandled()
}
} }
) )
} }
@ -717,10 +746,15 @@ fun AppNavGraph(
val sharedViewModel: SharedViewModel = hiltViewModel(activity) val sharedViewModel: SharedViewModel = hiltViewModel(activity)
val transProcessViewModel: TransProcessViewModel = hiltViewModel(activity) val transProcessViewModel: TransProcessViewModel = hiltViewModel(activity)
val settlementViewModel: SettlementViewModel = hiltViewModel(activity) val settlementViewModel: SettlementViewModel = hiltViewModel(activity)
val tmsSetupViewModel: TmsSetupViewModel = hiltViewModel(activity)
val dashboardViewModel: DashboardViewModel = hiltViewModel(activity)
ProcessingRoute( ProcessingRoute(
sharedViewModel = sharedViewModel, sharedViewModel = sharedViewModel,
transProcessViewModel = transProcessViewModel, transProcessViewModel = transProcessViewModel,
settlementViewModel = settlementViewModel, settlementViewModel = settlementViewModel,
onTransactionFinished = {
tmsSetupViewModel.downloadConfigs(dashboardViewModel::refreshLastSyncTime)
},
onNavigateTransactionResult = { onNavigateTransactionResult = {
navController.navigate(Routes.TransactionResult.route) { navController.navigate(Routes.TransactionResult.route) {
popUpTo(Routes.Processing.route) { popUpTo(Routes.Processing.route) {
@ -779,14 +813,10 @@ fun AppNavGraph(
composable(Routes.TransactionResult.route) { composable(Routes.TransactionResult.route) {
val sharedViewModel: SharedViewModel = hiltViewModel(activity) val sharedViewModel: SharedViewModel = hiltViewModel(activity)
val transResultViewModel: TransactionResultViewModel = hiltViewModel(activity) val transResultViewModel: TransactionResultViewModel = hiltViewModel(activity)
val tmsSetupViewModel: TmsSetupViewModel = hiltViewModel(activity)
val dashboardViewModel: DashboardViewModel = hiltViewModel(activity)
TransactionResultRoute( TransactionResultRoute(
viewModel = transResultViewModel, viewModel = transResultViewModel,
sharedViewModel = sharedViewModel, sharedViewModel = sharedViewModel,
tmsSetupViewModel = tmsSetupViewModel,
dashboardViewModel = dashboardViewModel,
onNavigateMain = { onNavigateMain = {
navController.navigate(Routes.Dashboard.route) { navController.navigate(Routes.Dashboard.route) {
popUpTo(Routes.Dashboard.route) { popUpTo(Routes.Dashboard.route) {

View File

@ -21,7 +21,8 @@ fun ProcessingRoute(
settlementViewModel: SettlementViewModel, settlementViewModel: SettlementViewModel,
onNavigateTransactionResult: () -> Unit, onNavigateTransactionResult: () -> Unit,
onNavigateSignature: () -> Unit, onNavigateSignature: () -> Unit,
onNavigateError: (Boolean, String) -> Unit onNavigateError: (Boolean, String) -> Unit,
onTransactionFinished: () -> Unit
) { ) {
val transStatus by transProcessViewModel.transResultStatus.collectAsStateWithLifecycle() val transStatus by transProcessViewModel.transResultStatus.collectAsStateWithLifecycle()
@ -39,6 +40,7 @@ fun ProcessingRoute(
when(settlementStatus.status) { when(settlementStatus.status) {
TransactionStatus.ON_ERROR, TransactionStatus.ON_ERROR,
TransactionStatus.ON_SUCCESS -> { TransactionStatus.ON_SUCCESS -> {
onTransactionFinished()
sharedViewModel.payDetail.value = settlementViewModel.getPayDetail() sharedViewModel.payDetail.value = settlementViewModel.getPayDetail()
onNavigateTransactionResult() onNavigateTransactionResult()
} }
@ -54,6 +56,7 @@ fun ProcessingRoute(
LaunchedEffect(transStatus) { LaunchedEffect(transStatus) {
when(transStatus) { when(transStatus) {
TransResultStatus.SUCCESS -> { TransResultStatus.SUCCESS -> {
onTransactionFinished()
sharedViewModel.payDetail.value = transProcessViewModel.payDetailResult.value sharedViewModel.payDetail.value = transProcessViewModel.payDetailResult.value
if (requiresSignature(sharedViewModel.transactionsType.value, sharedViewModel.payDetail.value)) { if (requiresSignature(sharedViewModel.transactionsType.value, sharedViewModel.payDetail.value)) {
onNavigateSignature() onNavigateSignature()
@ -63,18 +66,22 @@ fun ProcessingRoute(
} }
TransResultStatus.FAIL -> { TransResultStatus.FAIL -> {
onTransactionFinished()
sharedViewModel.payDetail.value = transProcessViewModel.payDetailResult.value sharedViewModel.payDetail.value = transProcessViewModel.payDetailResult.value
onNavigateTransactionResult() onNavigateTransactionResult()
} }
TransResultStatus.REVERSAL_SUCCESS -> { TransResultStatus.REVERSAL_SUCCESS -> {
onTransactionFinished()
onNavigateError(true,"Reversal success") onNavigateError(true,"Reversal success")
} }
TransResultStatus.REVERSAL_FAIL -> { TransResultStatus.REVERSAL_FAIL -> {
onTransactionFinished()
onNavigateError(false,"Reversal failed") onNavigateError(false,"Reversal failed")
} }
TransResultStatus.ERROR -> { TransResultStatus.ERROR -> {
onTransactionFinished()
onNavigateError(false, onNavigateError(false,
transProcessViewModel.errorMessage.value?.takeIf { it.isNotBlank() } transProcessViewModel.errorMessage.value?.takeIf { it.isNotBlank() }
?: "Something went wrong. Please try again." ?: "Something went wrong. Please try again."

View File

@ -38,11 +38,17 @@ fun SettlementScreen(
sharedViewModel: SharedViewModel, sharedViewModel: SharedViewModel,
settlementViewMode: SettlementViewModel, settlementViewMode: SettlementViewModel,
onBack: () -> Unit, onBack: () -> Unit,
onStartSettlement: () -> Unit onStartSettlement: () -> Unit,
autoStartSettlement: Boolean = false
) { ) {
val records by settlementViewMode.getSettlementPOS().observeAsState(emptyList()) val records by settlementViewMode.getSettlementPOS().observeAsState(emptyList())
val totalAmount = records.sumOf { it.amount } val totalAmount = records.sumOf { it.amount }
LaunchedEffect(autoStartSettlement, records) {
if (autoStartSettlement && records.isNotEmpty()) {
onStartSettlement()
}
}
Scaffold( Scaffold(
containerColor = Color.IvoryBeige, containerColor = Color.IvoryBeige,

View File

@ -74,7 +74,7 @@ class TmsSetupViewModel @Inject constructor(
} }
} }
fun downloadConfigs() { fun downloadConfigs(onSuccess: () -> Unit = {}) {
_uiState.update { _uiState.update {
it.copy(isLoading = true, isError = false, statusText = "Connecting to TMS server...") it.copy(isLoading = true, isError = false, statusText = "Connecting to TMS server...")
} }
@ -99,7 +99,7 @@ class TmsSetupViewModel @Inject constructor(
) )
tmsSetups.initParams(appResponse, TMSUpdate.UPDATE, emvParamOperation) tmsSetups.initParams(appResponse, TMSUpdate.UPDATE, emvParamOperation)
onConfigApplied() onConfigApplied(onSuccess)
}, },
{ error -> { error ->
val errorMessage = if (error is retrofit2.HttpException) { val errorMessage = if (error is retrofit2.HttpException) {
@ -141,7 +141,7 @@ class TmsSetupViewModel @Inject constructor(
} }
private fun onConfigApplied() { private fun onConfigApplied(onSuccess: () -> Unit) {
val ops = SystemParamsOperation.getInstance() val ops = SystemParamsOperation.getInstance()
if (!ops.isActive) { if (!ops.isActive) {
@ -166,6 +166,7 @@ class TmsSetupViewModel @Inject constructor(
.edit { .edit {
putLong("last_sync_time", System.currentTimeMillis()) putLong("last_sync_time", System.currentTimeMillis())
} }
onSuccess()
_uiState.update { it.copy(isLoading = false, statusText = "Ready.") } _uiState.update { it.copy(isLoading = false, statusText = "Ready.") }
viewModelScope.launch { viewModelScope.launch {
_onSyncCompleted.emit(Unit) _onSyncCompleted.emit(Unit)

View File

@ -5,4 +5,5 @@ sealed interface TransactionResultEvent {
data object BackClick: TransactionResultEvent data object BackClick: TransactionResultEvent
data object RetryPrint: TransactionResultEvent data object RetryPrint: TransactionResultEvent
data object PrintLater: TransactionResultEvent data object PrintLater: TransactionResultEvent
data object Continue: TransactionResultEvent
} }

View File

@ -5,8 +5,6 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.mob.utsmyanmar.ui.dashboard.DashboardViewModel
import com.mob.utsmyanmar.ui.tms_setup.TmsSetupViewModel
import com.mob.utsmyanmar.viewmodel.SharedViewModel import com.mob.utsmyanmar.viewmodel.SharedViewModel
import com.utsmyanmar.paylibs.utils.iso_utils.TransactionsType import com.utsmyanmar.paylibs.utils.iso_utils.TransactionsType
@ -18,9 +16,7 @@ fun TransactionResultRoute(
onNavigatePrintReceipt: () -> Unit, onNavigatePrintReceipt: () -> Unit,
onShowError: (String) -> Unit, onShowError: (String) -> Unit,
onShowSuccess: (String) -> Unit, onShowSuccess: (String) -> Unit,
onShowPrinterDialog: (String) -> Unit, onShowPrinterDialog: (String) -> Unit
tmsSetupViewModel: TmsSetupViewModel,
dashboardViewModel: DashboardViewModel,
) { ) {
val state by viewModel.state.collectAsStateWithLifecycle() val state by viewModel.state.collectAsStateWithLifecycle()
val canGoBack = sharedViewModel.transactionsType.value != TransactionsType.SALE val canGoBack = sharedViewModel.transactionsType.value != TransactionsType.SALE
@ -30,8 +26,6 @@ fun TransactionResultRoute(
TransactionResultEvent.Start, TransactionResultEvent.Start,
sharedViewModel sharedViewModel
) )
dashboardViewModel.refreshLastSyncTime()
tmsSetupViewModel.downloadConfigs()
} }
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
@ -51,6 +45,9 @@ fun TransactionResultRoute(
canGoBack = canGoBack, canGoBack = canGoBack,
onEvent = { onEvent = {
viewModel.onEvent(it, sharedViewModel) viewModel.onEvent(it, sharedViewModel)
},
onContinue = {
viewModel.onEvent(TransactionResultEvent.Continue, sharedViewModel)
} }
) )
} }

View File

@ -36,6 +36,7 @@ fun TransactionResultScreen(
state: TransactionResultState, state: TransactionResultState,
canGoBack: Boolean, canGoBack: Boolean,
onEvent: (TransactionResultEvent) -> Unit, onEvent: (TransactionResultEvent) -> Unit,
onContinue: () -> Unit,
) { ) {
BackHandler(enabled = canGoBack) { BackHandler(enabled = canGoBack) {
onEvent(TransactionResultEvent.BackClick) onEvent(TransactionResultEvent.BackClick)
@ -70,11 +71,8 @@ fun TransactionResultScreen(
Spacer(modifier = Modifier.height(6.dp)) Spacer(modifier = Modifier.height(6.dp))
Text( ReasonCard(
text = state.message.ifBlank { "Your payment has been processed successfully." }, reason = state.message.ifBlank { "Your payment has been processed successfully." }
color = Color.Gray,
style = MaterialTheme.typography.bodyMedium,
textAlign = TextAlign.Center
) )
Spacer(modifier = Modifier.height(28.dp)) Spacer(modifier = Modifier.height(28.dp))
@ -89,6 +87,50 @@ fun TransactionResultScreen(
transactionId = state.payDetail?.referNo.toString() transactionId = state.payDetail?.referNo.toString()
) )
Spacer(modifier = Modifier.height(24.dp))
Button(
onClick = onContinue,
modifier = Modifier
.fillMaxWidth()
.height(56.dp),
colors = ButtonDefaults.buttonColors(
containerColor = Color.LegacyRed,
contentColor = Color.White
)
) {
Text("Continue")
}
}
}
}
@Composable
private fun ReasonCard(reason: String) {
Card(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(16.dp),
colors = CardDefaults.cardColors(containerColor = Color.White)
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 18.dp, vertical = 14.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = "Reason",
color = Color.Gray,
style = MaterialTheme.typography.labelLarge
)
Spacer(modifier = Modifier.height(6.dp))
Text(
text = reason,
color = Color.Black,
style = MaterialTheme.typography.titleMedium,
textAlign = TextAlign.Center
)
} }
} }
} }
@ -323,5 +365,6 @@ fun PreviewTransactionResultScreen() {
), ),
canGoBack = true, canGoBack = true,
onEvent = {}, onEvent = {},
onContinue = {},
) )
} }

View File

@ -14,6 +14,7 @@ import com.utsmyanmar.paylibs.model.PayDetail
import com.utsmyanmar.paylibs.print.PaperRollStatusCallback import com.utsmyanmar.paylibs.print.PaperRollStatusCallback
import com.utsmyanmar.paylibs.print.PrintHelper import com.utsmyanmar.paylibs.print.PrintHelper
import com.utsmyanmar.paylibs.utils.PrintStatus import com.utsmyanmar.paylibs.utils.PrintStatus
import com.utsmyanmar.paylibs.utils.POSUtil
import com.utsmyanmar.paylibs.utils.core_utils.ByteUtil import com.utsmyanmar.paylibs.utils.core_utils.ByteUtil
import com.utsmyanmar.paylibs.utils.core_utils.SystemParamsOperation import com.utsmyanmar.paylibs.utils.core_utils.SystemParamsOperation
import com.utsmyanmar.paylibs.utils.iso_utils.TransactionsType import com.utsmyanmar.paylibs.utils.iso_utils.TransactionsType
@ -24,6 +25,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.Job
import sunmi.sunmiui.utils.LogUtil import sunmi.sunmiui.utils.LogUtil
import javax.inject.Inject import javax.inject.Inject
@ -44,6 +46,7 @@ class TransactionResultViewModel @Inject constructor(
val uiEvent = _uiEvent.receiveAsFlow() val uiEvent = _uiEvent.receiveAsFlow()
private var started = false private var started = false
private var resultTimeoutJob: Job? = null
fun onEvent( fun onEvent(
event: TransactionResultEvent, event: TransactionResultEvent,
@ -52,11 +55,16 @@ class TransactionResultViewModel @Inject constructor(
when (event) { when (event) {
TransactionResultEvent.Start -> { TransactionResultEvent.Start -> {
started = false started = false
resultTimeoutJob?.cancel()
start(sharedViewModel) start(sharedViewModel)
} }
TransactionResultEvent.BackClick -> isCardInside() TransactionResultEvent.BackClick -> isCardInside()
TransactionResultEvent.RetryPrint -> startPrintProcess(sharedViewModel, false) TransactionResultEvent.RetryPrint -> startPrintProcess(sharedViewModel, false)
TransactionResultEvent.PrintLater -> isCardInside() TransactionResultEvent.PrintLater -> isCardInside()
TransactionResultEvent.Continue -> {
resultTimeoutJob?.cancel()
navigateMain()
}
} }
} }
@ -99,7 +107,7 @@ class TransactionResultViewModel @Inject constructor(
val message = when { val message = when {
isSuccess -> "Transaction approved" isSuccess -> "Transaction approved"
payDetail?.tradeAnswerCode?.isNotBlank() == true -> payDetail.tradeAnswerCode payDetail != null -> failureReason(payDetail)
else -> "Transaction failed" else -> "Transaction failed"
} }
@ -109,8 +117,20 @@ class TransactionResultViewModel @Inject constructor(
) )
} }
private fun failureReason(payDetail: PayDetail): String {
val responseCode = payDetail.tradeAnswerCode.orEmpty().trim()
val resultDescription = payDetail.tradeResultDes.orEmpty().trim()
if (resultDescription.isNotBlank() && !resultDescription.equals(responseCode, ignoreCase = true)) {
return resultDescription
}
if (responseCode.isBlank()) return "Transaction failed"
return POSUtil.getInstance().getResponse(responseCode)
}
private fun startResultTimeout(sharedViewModel: SharedViewModel) { private fun startResultTimeout(sharedViewModel: SharedViewModel) {
viewModelScope.launch { resultTimeoutJob = viewModelScope.launch {
delay(RESULT_TIMEOUT) delay(RESULT_TIMEOUT)
val payDetail = sharedViewModel.payDetail.value val payDetail = sharedViewModel.payDetail.value
@ -128,7 +148,6 @@ class TransactionResultViewModel @Inject constructor(
if (SystemParamsOperation.getInstance().isPrintErrorSlip) { if (SystemParamsOperation.getInstance().isPrintErrorSlip) {
startPrintProcess(sharedViewModel, false) startPrintProcess(sharedViewModel, false)
} }
isCardInside()
} }
transactionType == TransactionsType.SETTLEMENT -> { transactionType == TransactionsType.SETTLEMENT -> {
@ -138,12 +157,10 @@ class TransactionResultViewModel @Inject constructor(
"Configs are updated" "Configs are updated"
) )
) )
navigateMain()
} }
isWavePayNonSuccessTransaction(transactionType, payDetail) -> { isWavePayNonSuccessTransaction(transactionType, payDetail) -> {
startPrintProcess(sharedViewModel, false) startPrintProcess(sharedViewModel, false)
navigateMain()
} }
else -> { else -> {

View File

@ -3,6 +3,7 @@ package com.mob.utsmyanmar.utils.tms
import com.google.gson.Gson import com.google.gson.Gson
import com.mob.utsmyanmar.model.sirius.SiriusResponse import com.mob.utsmyanmar.model.sirius.SiriusResponse
import com.mob.utsmyanmar.model.sirius.TMSUpdate import com.mob.utsmyanmar.model.sirius.TMSUpdate
import com.mob.utsmyanmar.auto_settlement.SettlementScheduler
import com.utsmyanmar.baselib.BaseApplication import com.utsmyanmar.baselib.BaseApplication
import com.utsmyanmar.baselib.emv.EmvParamOperation import com.utsmyanmar.baselib.emv.EmvParamOperation
import com.utsmyanmar.baselib.network.model.sirius.SiriusHost import com.utsmyanmar.baselib.network.model.sirius.SiriusHost
@ -157,9 +158,10 @@ class TMSSetupsImpl : TMSSetups {
"pre_auth_enable" -> ops.preAuthStatus = parseBoolean(data) "pre_auth_enable" -> ops.preAuthStatus = parseBoolean(data)
"void_enable" -> ops.voidStatus = parseBoolean(data) "void_enable" -> ops.voidStatus = parseBoolean(data)
"cash_advance_enable" -> ops.cashAdvanceStatus = parseBoolean(data) "cash_advance_enable" -> ops.cashAdvanceStatus = parseBoolean(data)
"refund_enable" -> ops.refundStatus = parseBoolean(data) "refund_enable" -> ops.refundStatus = parseBoolean(data)
"settlement_enable" -> ops.settlementStatus = parseBoolean(data) "settlement_enable" -> ops.settlementStatus = parseBoolean(data)
"system_password" -> ops.systemPassword = data.take(6).ifEmpty { data } "auto_settlement" -> ops.setAutoSettlement(parseBoolean(data))
"system_password" -> ops.systemPassword = data.take(6).ifEmpty { data }
"settlement_password" -> ops.settlementPassword = data.take(6).ifEmpty { data } "settlement_password" -> ops.settlementPassword = data.take(6).ifEmpty { data }
"setting_password" -> ops.settingPassword = data.take(6).ifEmpty { data } "setting_password" -> ops.settingPassword = data.take(6).ifEmpty { data }
"terminal_enable" -> ops.isActive = parseBoolean(data) "terminal_enable" -> ops.isActive = parseBoolean(data)
@ -226,6 +228,7 @@ class TMSSetupsImpl : TMSSetups {
} }
} }
ops.carouselUrls = imgUrls.joinToString(",") ops.carouselUrls = imgUrls.joinToString(",")
SettlementScheduler.schedule(BaseApplication.getInstance().applicationContext)
} }
override fun initParams(json: String) { override fun initParams(json: String) {

View File

@ -409,6 +409,17 @@ public class SystemParamsOperation {
return params.getSettlementStatus(); return params.getSettlementStatus();
} }
public boolean isAutoSettlement() {
SystemParamsSettings params = getSystemParamsSettings();
return params.isAutoSettlement();
}
public void setAutoSettlement(boolean enabled) {
SystemParamsSettings params = getSystemParamsSettings();
params.setAutoSettlement(enabled);
saveSystemParamsSettings(params);
}
public String getCarouselUrls() { public String getCarouselUrls() {
SystemParamsSettings params = getSystemParamsSettings(); SystemParamsSettings params = getSystemParamsSettings();
return params.getCarouselUrls(); return params.getCarouselUrls();

View File

@ -220,6 +220,8 @@ public class SystemParamsSettings implements Serializable {
private boolean settlementStatus = true; private boolean settlementStatus = true;
private boolean autoSettlement = false;
private boolean isNeedSettlement = false; private boolean isNeedSettlement = false;
private boolean manualEntryStatus = true; private boolean manualEntryStatus = true;
@ -566,6 +568,8 @@ public class SystemParamsSettings implements Serializable {
protected boolean getSettlementStatus() { return settlementStatus; } protected boolean getSettlementStatus() { return settlementStatus; }
protected void setSettlementStatus(boolean status) { this.settlementStatus = status; } protected void setSettlementStatus(boolean status) { this.settlementStatus = status; }
protected boolean isAutoSettlement() { return autoSettlement; }
protected void setAutoSettlement(boolean enabled) { this.autoSettlement = enabled; }
protected void setUserRole(int role){ this.user_role = role;} protected void setUserRole(int role){ this.user_role = role;}
protected int getUserRole(){ return user_role;} protected int getUserRole(){ return user_role;}