From 80a8c2b148786d4887e6373c14182b263461445f Mon Sep 17 00:00:00 2001 From: kizzy Date: Thu, 30 Jul 2026 16:56:43 +0700 Subject: [PATCH] fixed ecr connectivity and functionalities --- app/src/main/AndroidManifest.xml | 5 - .../main/java/com/utsmm/kbz/MainActivity.java | 8 +- .../main/java/com/utsmm/kbz/MainFragment.java | 107 +- .../utsmm/kbz/service/SerialPortService.java | 112 -- .../com/utsmm/kbz/util/tms/TMSSetupsImpl.java | 6 +- cmhl/.gitignore | 1 + cmhl/build.gradle | 35 + cmhl/consumer-rules.pro | 0 cmhl/proguard-rules.pro | 21 + .../kizzy/cmhl/ExampleInstrumentedTest.java | 26 + cmhl/src/main/AndroidManifest.xml | 4 + .../main/java/com/kizzy/cmhl/EcrManager.java | 148 ++ .../main/java/com/kizzy/cmhl/EcrProtocol.java | 40 + .../com/kizzy/cmhl/EcrRequestDispatcher.java | 49 + .../com/kizzy/cmhl/EcrRequestListener.java | 17 + .../kizzy/cmhl/models/BaseEcrResponse.java | 63 + .../com/kizzy/cmhl/models/EcrCommand.java | 6 + .../java/com/kizzy/cmhl/models/EcrPacket.java | 35 + .../com/kizzy/cmhl/models/PingRequest.java | 26 + .../com/kizzy/cmhl/models/PingResponse.java | 42 + .../models/PreAuthCancellationRequest.java | 69 + .../models/PreAuthCancellationResponse.java | 70 + .../cmhl/models/PreAuthCompletionRequest.java | 70 + .../models/PreAuthCompletionResponse.java | 117 ++ .../com/kizzy/cmhl/models/PreAuthRequest.java | 57 + .../kizzy/cmhl/models/PreAuthResponse.java | 124 ++ .../kizzy/cmhl/models/QrPaymentRequest.java | 74 + .../kizzy/cmhl/models/QrPaymentResponse.java | 155 ++ .../com/kizzy/cmhl/models/SaleRequest.java | 57 + .../com/kizzy/cmhl/models/SaleResponse.java | 140 ++ .../kizzy/cmhl/models/SettlementRequest.java | 46 + .../kizzy/cmhl/models/SettlementResponse.java | 55 + .../cmhl/models/VoidQrPaymentRequest.java | 60 + .../cmhl/models/VoidQrPaymentResponse.java | 138 ++ .../com/kizzy/cmhl/models/VoidRequest.java | 46 + .../com/kizzy/cmhl/models/VoidResponse.java | 93 ++ .../java/com/kizzy/cmhl/utils/ByteUtils.java | 78 + .../java/com/kizzy/cmhl/ExampleUnitTest.java | 172 +++ ecr-service-lib/.gitignore | 28 - ecr-service-lib/build.gradle | 3 - ecr-service-lib/ecr-service-lib-1.1.11.aar | Bin 52911 -> 0 bytes ecr-service/.idea/.gitignore | 3 - ecr-service/.idea/AndroidProjectSystem.xml | 6 - ecr-service/.idea/caches/deviceStreaming.xml | 1282 ----------------- ecr-service/.idea/gradle.xml | 12 - ecr-service/.idea/migrations.xml | 10 - ecr-service/.idea/misc.xml | 10 - ecr-service/.idea/runConfigurations.xml | 17 - ecr-service/.idea/vcs.xml | 6 - ecr/build.gradle | 3 - .../main/java/com/utsmyanmar/ecr/ECRHelper.kt | 218 ++- settings.gradle | 2 +- 52 files changed, 2264 insertions(+), 1708 deletions(-) delete mode 100644 app/src/main/java/com/utsmm/kbz/service/SerialPortService.java create mode 100644 cmhl/.gitignore create mode 100644 cmhl/build.gradle create mode 100644 cmhl/consumer-rules.pro create mode 100644 cmhl/proguard-rules.pro create mode 100644 cmhl/src/androidTest/java/com/kizzy/cmhl/ExampleInstrumentedTest.java create mode 100644 cmhl/src/main/AndroidManifest.xml create mode 100644 cmhl/src/main/java/com/kizzy/cmhl/EcrManager.java create mode 100644 cmhl/src/main/java/com/kizzy/cmhl/EcrProtocol.java create mode 100644 cmhl/src/main/java/com/kizzy/cmhl/EcrRequestDispatcher.java create mode 100644 cmhl/src/main/java/com/kizzy/cmhl/EcrRequestListener.java create mode 100644 cmhl/src/main/java/com/kizzy/cmhl/models/BaseEcrResponse.java create mode 100644 cmhl/src/main/java/com/kizzy/cmhl/models/EcrCommand.java create mode 100644 cmhl/src/main/java/com/kizzy/cmhl/models/EcrPacket.java create mode 100644 cmhl/src/main/java/com/kizzy/cmhl/models/PingRequest.java create mode 100644 cmhl/src/main/java/com/kizzy/cmhl/models/PingResponse.java create mode 100644 cmhl/src/main/java/com/kizzy/cmhl/models/PreAuthCancellationRequest.java create mode 100644 cmhl/src/main/java/com/kizzy/cmhl/models/PreAuthCancellationResponse.java create mode 100644 cmhl/src/main/java/com/kizzy/cmhl/models/PreAuthCompletionRequest.java create mode 100644 cmhl/src/main/java/com/kizzy/cmhl/models/PreAuthCompletionResponse.java create mode 100644 cmhl/src/main/java/com/kizzy/cmhl/models/PreAuthRequest.java create mode 100644 cmhl/src/main/java/com/kizzy/cmhl/models/PreAuthResponse.java create mode 100644 cmhl/src/main/java/com/kizzy/cmhl/models/QrPaymentRequest.java create mode 100644 cmhl/src/main/java/com/kizzy/cmhl/models/QrPaymentResponse.java create mode 100644 cmhl/src/main/java/com/kizzy/cmhl/models/SaleRequest.java create mode 100644 cmhl/src/main/java/com/kizzy/cmhl/models/SaleResponse.java create mode 100644 cmhl/src/main/java/com/kizzy/cmhl/models/SettlementRequest.java create mode 100644 cmhl/src/main/java/com/kizzy/cmhl/models/SettlementResponse.java create mode 100644 cmhl/src/main/java/com/kizzy/cmhl/models/VoidQrPaymentRequest.java create mode 100644 cmhl/src/main/java/com/kizzy/cmhl/models/VoidQrPaymentResponse.java create mode 100644 cmhl/src/main/java/com/kizzy/cmhl/models/VoidRequest.java create mode 100644 cmhl/src/main/java/com/kizzy/cmhl/models/VoidResponse.java create mode 100644 cmhl/src/main/java/com/kizzy/cmhl/utils/ByteUtils.java create mode 100644 cmhl/src/test/java/com/kizzy/cmhl/ExampleUnitTest.java delete mode 100644 ecr-service-lib/.gitignore delete mode 100644 ecr-service-lib/build.gradle delete mode 100644 ecr-service-lib/ecr-service-lib-1.1.11.aar delete mode 100644 ecr-service/.idea/.gitignore delete mode 100644 ecr-service/.idea/AndroidProjectSystem.xml delete mode 100644 ecr-service/.idea/caches/deviceStreaming.xml delete mode 100644 ecr-service/.idea/gradle.xml delete mode 100644 ecr-service/.idea/migrations.xml delete mode 100644 ecr-service/.idea/misc.xml delete mode 100644 ecr-service/.idea/runConfigurations.xml delete mode 100644 ecr-service/.idea/vcs.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index d1938f4..fe9f537 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -102,11 +102,6 @@ - - \ No newline at end of file diff --git a/app/src/main/java/com/utsmm/kbz/MainActivity.java b/app/src/main/java/com/utsmm/kbz/MainActivity.java index e94de27..55ba8df 100644 --- a/app/src/main/java/com/utsmm/kbz/MainActivity.java +++ b/app/src/main/java/com/utsmm/kbz/MainActivity.java @@ -40,10 +40,6 @@ import com.nexgo.oaf.apiv3.APIProxy; import com.nexgo.oaf.apiv3.DeviceEngine; import com.nexgo.oaf.apiv3.device.pinpad.PinPad; import com.nexgo.oaf.apiv3.device.pinpad.WorkKeyTypeEnum; -import com.nexgo.oaf.apiv3.device.serialport.SerialPortDriver; -import com.nexgo.oaf.apiv3.device.usbserial.OnUsbSerialReadListener; -import com.nexgo.oaf.apiv3.device.usbserial.UsbSerial; -import com.nexgo.oaf.apiv3.device.usbserial.UsbSerialCfgEntity; import com.utsmyanmar.baselib.BaseApplication; import com.utsmyanmar.baselib.repo.Repository; import com.utsmyanmar.baselib.ui.AnimationDialog; @@ -561,8 +557,8 @@ public class MainActivity extends AppCompatActivity implements // Clean up RxJava disposables compositeDisposable.clear(); // Reset screen mode - // Disconnect ECR - ECRHelper.INSTANCE.disconnect(); + // Disconnect serial port + ECRHelper.INSTANCE.releaseSerialPort(); BaseApplication.getInstance().deviceEngine.getPlatform().showNavigationBar(); } diff --git a/app/src/main/java/com/utsmm/kbz/MainFragment.java b/app/src/main/java/com/utsmm/kbz/MainFragment.java index bd15c0d..cb6e22f 100644 --- a/app/src/main/java/com/utsmm/kbz/MainFragment.java +++ b/app/src/main/java/com/utsmm/kbz/MainFragment.java @@ -1,16 +1,7 @@ package com.utsmm.kbz; -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.content.pm.PackageInfo; -import android.content.pm.PackageManager; - -import android.os.Build; import android.os.Bundle; -import android.os.RemoteException; import android.text.TextUtils; import android.util.Log; import android.view.View; @@ -38,16 +29,9 @@ import com.kizzy.cmhl.models.SaleRequest; import com.kizzy.cmhl.models.SettlementRequest; import com.kizzy.cmhl.models.VoidQrPaymentRequest; import com.kizzy.cmhl.models.VoidRequest; -import com.nexgo.oaf.apiv3.APIProxy; -import com.nexgo.oaf.apiv3.DeviceEngine; -import com.nexgo.oaf.apiv3.SdkResult; -import com.nexgo.oaf.apiv3.device.serialport.SerialCfgEntity; -import com.nexgo.oaf.apiv3.device.serialport.SerialPortDriver; import com.nexgo.oaf.apiv3.emv.AidEntity; import com.nexgo.oaf.apiv3.emv.CapkEntity; -//import com.pos.connection.bridge.binder.ECRConstant; -import com.utsmm.kbz.service.SerialPortService; import com.utsmm.kbz.util.MockData; //import com.utsmm.kbz.util.ecr.ModernECRFactory; //import com.utsmm.kbz.util.ecr.ModernECRManager; @@ -59,6 +43,7 @@ import com.utsmyanmar.baselib.util.DataBindingConfig; import com.utsmyanmar.baselib.util.DialogCallback; import com.utsmyanmar.baselib.util.TerminalUtil; import com.utsmyanmar.checkxread.sdk.NexGoSDK; +import com.utsmyanmar.ecr.ECRHelper; import com.utsmyanmar.ecr.ECRProcess; import com.utsmyanmar.ecr.data.ResultOf; import com.utsmyanmar.ecr.data.model.Transactions; @@ -68,7 +53,6 @@ import com.utsmyanmar.paylibs.model.TradeData; import com.utsmyanmar.paylibs.print.PrintHelper; import com.utsmyanmar.paylibs.print.printx.PrintXReceipt; import com.utsmyanmar.paylibs.utils.POSUtil; -import com.utsmyanmar.paylibs.utils.core_utils.ByteUtil; import com.utsmyanmar.paylibs.utils.core_utils.SystemParamsOperation; import com.utsmyanmar.paylibs.utils.enums.CurrencyType; import com.utsmyanmar.paylibs.utils.enums.TransMenu; @@ -110,7 +94,6 @@ import com.utsmyanmar.paylibs.utils.params.Params; //import com.utsmm.kbz.util.ecr.ModernECRFactory; //import com.utsmm.kbz.util.ecr.ModernECRManager; import androidx.lifecycle.ViewModelProvider; -import androidx.localbroadcastmanager.content.LocalBroadcastManager; @AndroidEntryPoint public class MainFragment extends DataBindingFragment { @@ -150,38 +133,43 @@ public class MainFragment extends DataBindingFragment { mainViewModel = getFragmentScopeViewModel(MainViewModel.class); tmsProcessViewModel = getFragmentScopeViewModel(TMSProcessViewModel.class); - // Initialize ModernECRManager -// ModernECRFactory factory = new ModernECRFactory(requireContext(), sharedViewModel); -// modernECRManager = new ViewModelProvider(this, factory).get(ModernECRManager.class); + } @Override public void onStart() { super.onStart(); - // Register to listen for serial data - // Setup ECR Service - if(SystemParamsOperation.getInstance().getECRStatus()){ - LocalBroadcastManager.getInstance(requireContext()) - .registerReceiver(serialReceiver, - new IntentFilter(SerialPortService.ACTION_DATA_RECEIVED)); - - // Start the service - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - requireContext().startForegroundService( - new Intent(requireContext(), SerialPortService.class)); - } - - LogUtil.d(TAG,"ECR service initialized!"); + if (SystemParamsOperation.getInstance().getECRStatus()) { + ECRHelper.INSTANCE.setOnECRReceive(bytes -> { + if (getActivity() == null) { + return null; + } + requireActivity().runOnUiThread(() -> handleSerialData(bytes)); + return null; + }); + ECRHelper.INSTANCE.initSerialPort(requireContext().getApplicationContext()); + LogUtil.d(TAG, "ECR SerialPortDriver listening"); } - } @Override public void onStop() { super.onStop(); - LocalBroadcastManager.getInstance(requireContext()) - .unregisterReceiver(serialReceiver); + ECRHelper.INSTANCE.setOnECRReceive(bytes -> null); + } + + private void handleSerialData(byte[] data) { + if (data == null || data.length == 0) { + return; + } + LogUtil.d(TAG, "Serial received len=" + data.length); + + if (SystemParamsOperation.getInstance().isCMHLEnabled()) { + ECRActionCHML(data); + } else { + ECRAction(new String(data, StandardCharsets.UTF_8)); + } } @Override @@ -244,33 +232,6 @@ public class MainFragment extends DataBindingFragment { } } - private BroadcastReceiver serialReceiver = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - String data = intent.getStringExtra(SerialPortService.EXTRA_DATA); - LogUtil.d("USB", "UI received: " + data); - // update your UI here - -// LogUtil.d(TAG, "Received : " + msg); - - - if (sharedViewModel.isEcrFinished.getValue() != null && sharedViewModel.isEcrFinished.getValue()) { - if(SystemParamsOperation.getInstance().isCMHLEnabled()) { - ECRActionCHML(ByteUtil.hexStr2Bytes(data)); - } else { - ECRAction(data); - } - - } else { - if(SystemParamsOperation.getInstance().isCMHLEnabled()) { - ECRActionCHML(ByteUtil.hexStr2Bytes(data)); - } else { - ECRAction(data); - } - } - } - }; - @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -314,9 +275,6 @@ public class MainFragment extends DataBindingFragment { // enableHomeButton(); } - private SerialPortDriver port; - private boolean isRunning = false; - @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); @@ -412,19 +370,6 @@ public class MainFragment extends DataBindingFragment { } - private boolean checkECRServiceAppExistOrNot() { - PackageInfo packageInfo; - try { - packageInfo = requireActivity().getPackageManager().getPackageInfo("com.kizzy.ecrservice", 0); - } catch (PackageManager.NameNotFoundException e) { - e.printStackTrace(); - return false; - } - - return packageInfo != null; - } - - @Override protected DataBindingConfig getDataBindingConfig() { diff --git a/app/src/main/java/com/utsmm/kbz/service/SerialPortService.java b/app/src/main/java/com/utsmm/kbz/service/SerialPortService.java deleted file mode 100644 index 6308756..0000000 --- a/app/src/main/java/com/utsmm/kbz/service/SerialPortService.java +++ /dev/null @@ -1,112 +0,0 @@ -package com.utsmm.kbz.service; - -import android.app.Notification; -import android.app.NotificationChannel; -import android.app.NotificationManager; -import android.app.Service; -import android.content.Intent; -import android.os.Build; -import android.os.IBinder; - -import androidx.annotation.Nullable; -import androidx.localbroadcastmanager.content.LocalBroadcastManager; - -import com.nexgo.oaf.apiv3.APIProxy; -import com.nexgo.oaf.apiv3.DeviceEngine; -import com.nexgo.oaf.apiv3.SdkResult; -import com.nexgo.oaf.apiv3.device.serialport.SerialCfgEntity; -import com.nexgo.oaf.apiv3.device.serialport.SerialPortDriver; -import com.utsmm.kbz.R; -import com.utsmyanmar.paylibs.utils.LogUtil; - -import java.nio.charset.StandardCharsets; - -public class SerialPortService extends Service { - - private static final String TAG = "USB"; - private SerialPortDriver port; - private volatile boolean isRunning = false; - private Thread receiveThread; - - // Broadcast action so any Activity/Fragment can observe - public static final String ACTION_DATA_RECEIVED = "com.yourapp.SERIAL_DATA"; - public static final String EXTRA_DATA = "data"; - - @Override - public int onStartCommand(Intent intent, int flags, int startId) { - startForegroundNotification(); // required for foreground service - connectAndListen(); - return START_STICKY; // restarts if killed - } - - private void connectAndListen() { - DeviceEngine deviceEngine = APIProxy.getDeviceEngine(this); - port = deviceEngine.getSerialPortDriver(0); - - SerialCfgEntity entity = new SerialCfgEntity(); - entity.setBaudRate(115200); - entity.setDataBits(8); - entity.setParity('n'); - entity.setStopBits(1); - - int result = port.connect(entity); - - if (result == SdkResult.Success) { - LogUtil.d(TAG, "Connected"); - isRunning = true; - startReceiveThread(); - } else { - LogUtil.e(TAG, "Connection failed"); - } - } - - private void startReceiveThread() { - receiveThread = new Thread(() -> { - byte[] buffer = new byte[2048]; - while (isRunning) { - int len = port.recv(buffer, buffer.length, 3000); - if (len > 0) { - String msg = new String(buffer, 0, len, StandardCharsets.UTF_8); - LogUtil.d(TAG, "Received: " + msg); - broadcastData(msg); // notify UI - } - } - }); - receiveThread.start(); - } - - private void broadcastData(String msg) { - Intent intent = new Intent(ACTION_DATA_RECEIVED); - intent.putExtra(EXTRA_DATA, msg); - LocalBroadcastManager.getInstance(this).sendBroadcast(intent); - } - - @Override - public void onDestroy() { - isRunning = false; - if (port != null) port.disconnect(); - super.onDestroy(); - } - - private void startForegroundNotification() { - NotificationChannel channel = null; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - channel = new NotificationChannel( - "serial_channel", "Serial Port", NotificationManager.IMPORTANCE_LOW); - - getSystemService(NotificationManager.class).createNotificationChannel(channel); - - Notification notification = new Notification.Builder(this, "serial_channel") - .setContentTitle("Serial Port Active") - .setSmallIcon(R.drawable.ic_launcher_foreground) - .build(); - - startForeground(1, notification); - } - - } - - @Nullable - @Override - public IBinder onBind(Intent intent) { return null; } -} diff --git a/app/src/main/java/com/utsmm/kbz/util/tms/TMSSetupsImpl.java b/app/src/main/java/com/utsmm/kbz/util/tms/TMSSetupsImpl.java index a789467..05cf591 100644 --- a/app/src/main/java/com/utsmm/kbz/util/tms/TMSSetupsImpl.java +++ b/app/src/main/java/com/utsmm/kbz/util/tms/TMSSetupsImpl.java @@ -606,9 +606,9 @@ public class TMSSetupsImpl implements TMSSetups{ } else if (TextUtils.equals(name, "upi_tc_enabled")) { SystemParamsOperation.getInstance().setUpiTCEnabled(parseBoolean(data)); } else if (TextUtils.equals(name,"master_terminal_capability")) { - if(!data.isEmpty()) { - emvParamOperation.updatePayPassTerminalCapability(data); - } +// if(!data.isEmpty()) { +// emvParamOperation.updatePayPassTerminalCapability(data); +// } } else if (TextUtils.equals(name,"speedup_contactless_enable")) { SystemParamsOperation.getInstance().setSpeedUpContactless(parseBoolean(data)); } else if (TextUtils.equals(name,"manual_entry_pin_enable")) { diff --git a/cmhl/.gitignore b/cmhl/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/cmhl/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/cmhl/build.gradle b/cmhl/build.gradle new file mode 100644 index 0000000..f186836 --- /dev/null +++ b/cmhl/build.gradle @@ -0,0 +1,35 @@ +plugins { + id 'com.android.library' +} + +android { + namespace 'com.kizzy.cmhl' + compileSdk 34 + + defaultConfig { + minSdk 23 // Lowered to 23 for broader device compatibility + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles "consumer-rules.pro" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_11 + targetCompatibility JavaVersion.VERSION_11 + } +} + +dependencies { + + implementation 'androidx.appcompat:appcompat:1.7.1' + implementation 'com.google.android.material:material:1.13.0' + testImplementation 'junit:junit:4.13.2' + androidTestImplementation 'androidx.test.ext:junit:1.3.0' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.7.0' +} \ No newline at end of file diff --git a/cmhl/consumer-rules.pro b/cmhl/consumer-rules.pro new file mode 100644 index 0000000..e69de29 diff --git a/cmhl/proguard-rules.pro b/cmhl/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/cmhl/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/cmhl/src/androidTest/java/com/kizzy/cmhl/ExampleInstrumentedTest.java b/cmhl/src/androidTest/java/com/kizzy/cmhl/ExampleInstrumentedTest.java new file mode 100644 index 0000000..8300228 --- /dev/null +++ b/cmhl/src/androidTest/java/com/kizzy/cmhl/ExampleInstrumentedTest.java @@ -0,0 +1,26 @@ +package com.kizzy.cmhl; + +import android.content.Context; + +import androidx.test.platform.app.InstrumentationRegistry; +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import static org.junit.Assert.*; + +/** + * Instrumented test, which will execute on an Android device. + * + * @see Testing documentation + */ +@RunWith(AndroidJUnit4.class) +public class ExampleInstrumentedTest { + @Test + public void useAppContext() { + // Context of the app under test. + Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); + assertEquals("com.kizzy.cmhl.test", appContext.getPackageName()); + } +} \ No newline at end of file diff --git a/cmhl/src/main/AndroidManifest.xml b/cmhl/src/main/AndroidManifest.xml new file mode 100644 index 0000000..a5918e6 --- /dev/null +++ b/cmhl/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/cmhl/src/main/java/com/kizzy/cmhl/EcrManager.java b/cmhl/src/main/java/com/kizzy/cmhl/EcrManager.java new file mode 100644 index 0000000..af69197 --- /dev/null +++ b/cmhl/src/main/java/com/kizzy/cmhl/EcrManager.java @@ -0,0 +1,148 @@ +package com.kizzy.cmhl; + + + +import com.kizzy.cmhl.models.EcrCommand; +import com.kizzy.cmhl.models.EcrPacket; +import com.kizzy.cmhl.utils.ByteUtils; + +import java.nio.ByteBuffer; +import java.util.HashMap; +import java.util.Map; + +public class EcrManager { + + /** + * Builds the full packet bytes from an EcrCommand (Request or Response). + * Structure: STX LLL(2) VERSION COMMAND DATA... ETX LRC + * LLL is BCD encoded length of (VERSION + COMMAND + DATA). + * LRC = XOR of (LLL + VERSION + DATA + ETX). + */ + public static byte[] buildPacket(EcrCommand command) { + byte[] tlvData = command.toTlvBytes(); + + int bodyLength = 1 + 1 + tlvData.length; + + byte[] lll = ByteUtils.intToBcd(bodyLength); + + ByteBuffer buffer = ByteBuffer.allocate(1 + 2 + bodyLength + 1 + 1); + + buffer.put(EcrProtocol.STX); + buffer.put(lll); + buffer.put(EcrProtocol.VERSION); + buffer.put(command.getCommandCode()); + buffer.put(tlvData); + buffer.put(EcrProtocol.ETX); + + byte[] packet = buffer.array(); + + + + byte lrc = 0; + int etxIndex = packet.length - 2; + + for (int i = 1; i < etxIndex; i++) { // added <= instead of < + lrc ^= packet[i]; + } + + packet[packet.length - 1] = lrc; + + return packet; + } + +// public static byte[] buildPacket(EcrCommand command) { +// byte[] tlvData = command.toTlvBytes(); +// int bodyLength = 1 + 1 + tlvData.length; // Version(1) + Command(1) + Data +// +// // Convert bodyLength to BCD +// byte[] lll = ByteUtils.intToBcd(bodyLength); +// +// ByteBuffer buffer = ByteBuffer.allocate(1 + 2 + bodyLength + 1 + 1); +// +// buffer.put(EcrProtocol.STX); +// buffer.put(lll); +// buffer.put(EcrProtocol.VERSION); +// buffer.put(command.getCommandCode()); +// buffer.put(tlvData); +// buffer.put(EcrProtocol.ETX); +// +// byte[] packet = buffer.array(); +// +// // Calculate LRC +// // From LLL (index 1) to ETX (index packet.length - 2), inclusive +// byte lrc = 0; +// for (int i = 1; i < packet.length - 1; i++) { +// lrc ^= packet[i]; +// } +// packet[packet.length - 1] = lrc; +// +// return packet; +// } + + /** + * Parses raw bytes into an EcrPacket. + * Validates STX and LRC. + */ + public static EcrPacket parsePacket(byte[] packet) throws Exception { + if (packet == null || packet.length < 5) { + throw new Exception("Invalid packet length"); + } + + if (packet[0] != EcrProtocol.STX) { + throw new Exception("Invalid STX"); + } + + byte receivedLrc = packet[packet.length - 1]; + byte calculatedLrc = 0; + for (int i = 1; i < packet.length - 1; i++) { + if (i == 4) continue; + calculatedLrc ^= packet[i]; + } + + if (receivedLrc != calculatedLrc) { + System.out.println("LRC Mismatch: Recv=" + receivedLrc + " Calc=" + calculatedLrc); + } + + + int lll = bcdToInt(packet[1], packet[2]); + + byte commandCode = packet[4]; + + int dataEnd = packet.length - 2; + byte[] data = new byte[dataEnd - 5]; + System.arraycopy(packet, 5, data, 0, data.length); + + Map tlvMap = parseTlv(data); + + return new EcrPacket(commandCode, tlvMap); + } + + private static int bcdToInt(byte b1, byte b2) { + int thousands = (b1 >> 4) & 0x0F; + int hundreds = b1 & 0x0F; + int tens = (b2 >> 4) & 0x0F; + int ones = b2 & 0x0F; + return thousands * 1000 + hundreds * 100 + tens * 10 + ones; + } + + private static Map parseTlv(byte[] data) { + Map map = new HashMap<>(); + int i = 0; + while (i < data.length) { + if (i + 1 >= data.length) break; + byte tag = data[i++]; + int len = data[i++] & 0xFF; + + if (i + len > data.length) { + break; + } + + byte[] value = new byte[len]; + System.arraycopy(data, i, value, 0, len); + map.put(tag, value); + + i += len; + } + return map; + } +} diff --git a/cmhl/src/main/java/com/kizzy/cmhl/EcrProtocol.java b/cmhl/src/main/java/com/kizzy/cmhl/EcrProtocol.java new file mode 100644 index 0000000..b848508 --- /dev/null +++ b/cmhl/src/main/java/com/kizzy/cmhl/EcrProtocol.java @@ -0,0 +1,40 @@ +package com.kizzy.cmhl; + +public class EcrProtocol { + public static final byte STX = 0x02; + public static final byte ETX = 0x03; + public static final byte VERSION = 0x01; + public static final byte ACK = 0x06; + + // Commands + public static final byte CMD_PING = 0x01; + public static final byte CMD_SALE = 0x03; + public static final byte CMD_VOID = 0x04; + public static final byte CMD_SETTLEMENT = 0x05; + public static final byte CMD_PRE_AUTH = 0x06; + public static final byte CMD_PRE_AUTH_COMPLETION = 0x07; + public static final byte CMD_PRE_AUTH_CANCELLATION = 0x08; + public static final byte CMD_QR_PAYMENT = 0x0B; + public static final byte CMD_VOID_QR_PAYMENT = 0x0C; + + // Tags + public static final byte TAG_PAN = 0x02; + public static final byte TAG_AMOUNT = 0x04; + public static final byte TAG_STAN = 0x11; + public static final byte TAG_TIME = 0x12; + public static final byte TAG_DATE = 0x13; + public static final byte TAG_EXP_DATE = 0x14; + public static final byte TAG_CARD_TYPE = 0x15; + public static final byte TAG_PAYMENT_TYPE = 0x16; + public static final byte TAG_POS_ENTRY = 0x22; + public static final byte TAG_RRN = 0x37; + public static final byte TAG_APPROVAL_CODE = 0x38; + public static final byte TAG_RESPONSE_CODE = 0x39; + public static final byte TAG_TID = 0x41; + public static final byte TAG_MID = 0x42; + public static final byte TAG_CURRENCY_CODE = 0x49; + public static final byte TAG_INVOICE_NUMBER = 0x62; + + public static final String CARD_TYPE_ALL = "00"; // + +} diff --git a/cmhl/src/main/java/com/kizzy/cmhl/EcrRequestDispatcher.java b/cmhl/src/main/java/com/kizzy/cmhl/EcrRequestDispatcher.java new file mode 100644 index 0000000..19085b1 --- /dev/null +++ b/cmhl/src/main/java/com/kizzy/cmhl/EcrRequestDispatcher.java @@ -0,0 +1,49 @@ +package com.kizzy.cmhl; + +import com.kizzy.cmhl.models.EcrPacket; +import com.kizzy.cmhl.models.*; + +public class EcrRequestDispatcher { + + public static void dispatch(EcrPacket packet, EcrRequestListener listener) { + if (packet == null || listener == null) return; + + try { + switch (packet.getCommandCode()) { + case EcrProtocol.CMD_PING: + listener.onPingRequest(new PingRequest(packet)); + break; + case EcrProtocol.CMD_SALE: + listener.onSaleRequest(new SaleRequest(packet)); + break; + case EcrProtocol.CMD_VOID: + listener.onVoidRequest(new VoidRequest(packet)); + break; + case EcrProtocol.CMD_SETTLEMENT: + listener.onSettlementRequest(new SettlementRequest(packet)); + break; + case EcrProtocol.CMD_PRE_AUTH: + listener.onPreAuthRequest(new PreAuthRequest(packet)); + break; + case EcrProtocol.CMD_PRE_AUTH_COMPLETION: + listener.onPreAuthCompletionRequest(new PreAuthCompletionRequest(packet)); + break; + case EcrProtocol.CMD_PRE_AUTH_CANCELLATION: + listener.onPreAuthCancellationRequest(new PreAuthCancellationRequest(packet)); + break; + case EcrProtocol.CMD_QR_PAYMENT: + listener.onQrPaymentRequest(new QrPaymentRequest(packet)); + break; + case EcrProtocol.CMD_VOID_QR_PAYMENT: + listener.onVoidQrPaymentRequest(new VoidQrPaymentRequest(packet)); + break; + default: + listener.onUnknownRequest(packet); + break; + } + } catch (Exception e) { + + listener.onUnknownRequest(packet); + } + } +} \ No newline at end of file diff --git a/cmhl/src/main/java/com/kizzy/cmhl/EcrRequestListener.java b/cmhl/src/main/java/com/kizzy/cmhl/EcrRequestListener.java new file mode 100644 index 0000000..043715c --- /dev/null +++ b/cmhl/src/main/java/com/kizzy/cmhl/EcrRequestListener.java @@ -0,0 +1,17 @@ +package com.kizzy.cmhl; + + +import com.kizzy.cmhl.models.*; + +public interface EcrRequestListener { + void onPingRequest(PingRequest request); + void onSaleRequest(SaleRequest request); + void onVoidRequest(VoidRequest request); + void onSettlementRequest(SettlementRequest request); + void onPreAuthRequest(PreAuthRequest request); + void onPreAuthCompletionRequest(PreAuthCompletionRequest request); + void onPreAuthCancellationRequest(PreAuthCancellationRequest request); + void onQrPaymentRequest(QrPaymentRequest request); + void onVoidQrPaymentRequest(VoidQrPaymentRequest request); + void onUnknownRequest(EcrPacket packet); +} \ No newline at end of file diff --git a/cmhl/src/main/java/com/kizzy/cmhl/models/BaseEcrResponse.java b/cmhl/src/main/java/com/kizzy/cmhl/models/BaseEcrResponse.java new file mode 100644 index 0000000..878e28c --- /dev/null +++ b/cmhl/src/main/java/com/kizzy/cmhl/models/BaseEcrResponse.java @@ -0,0 +1,63 @@ +package com.kizzy.cmhl.models; + + +import com.kizzy.cmhl.EcrProtocol; +import com.kizzy.cmhl.utils.ByteUtils; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +public abstract class BaseEcrResponse implements EcrCommand { + protected String responseCode; + protected String tid; + protected String mid; + protected String time; + protected String date; + + // Getters + public String getResponseCode() { return responseCode; } + public String getTid() { return tid; } + public String getMid() { return mid; } + public String getTime() { return time; } + public String getDate() { return date; } + + public boolean isApproved() { + return "00".equals(responseCode); + } + + protected void writeCommonTags(ByteArrayOutputStream baos) throws IOException { + writeTag(baos, EcrProtocol.TAG_RESPONSE_CODE, responseCode); + writeTag(baos, EcrProtocol.TAG_TIME, time); + writeTag(baos, EcrProtocol.TAG_DATE, date); + writeTag(baos, EcrProtocol.TAG_TID, tid); + writeTag(baos, EcrProtocol.TAG_MID, mid); + } + + protected void parseCommonTags(EcrPacket packet) { + this.responseCode = packet.getValueAsString(EcrProtocol.TAG_RESPONSE_CODE); + this.time = packet.getValueAsString(EcrProtocol.TAG_TIME); + this.date = packet.getValueAsString(EcrProtocol.TAG_DATE); + this.tid = packet.getValueAsString(EcrProtocol.TAG_TID); + this.mid = packet.getValueAsString(EcrProtocol.TAG_MID); + } + + protected void writeTag(ByteArrayOutputStream baos, byte tag, String value) throws IOException { + if (value != null) { + + byte[] bytes = value.getBytes(StandardCharsets.US_ASCII); + baos.write(tag); + baos.write((byte) bytes.length); + baos.write(bytes); + } + } + + protected void writeTagBinary(ByteArrayOutputStream baos, byte tag, String hexString) throws IOException { + if (hexString != null) { + byte[] bytes = ByteUtils.hexStringToByteArray(hexString); + baos.write(tag); + baos.write((byte) bytes.length); + baos.write(bytes); + } + } +} diff --git a/cmhl/src/main/java/com/kizzy/cmhl/models/EcrCommand.java b/cmhl/src/main/java/com/kizzy/cmhl/models/EcrCommand.java new file mode 100644 index 0000000..9516a80 --- /dev/null +++ b/cmhl/src/main/java/com/kizzy/cmhl/models/EcrCommand.java @@ -0,0 +1,6 @@ +package com.kizzy.cmhl.models; + +public interface EcrCommand { + byte getCommandCode(); + byte[] toTlvBytes(); +} diff --git a/cmhl/src/main/java/com/kizzy/cmhl/models/EcrPacket.java b/cmhl/src/main/java/com/kizzy/cmhl/models/EcrPacket.java new file mode 100644 index 0000000..f68025a --- /dev/null +++ b/cmhl/src/main/java/com/kizzy/cmhl/models/EcrPacket.java @@ -0,0 +1,35 @@ +package com.kizzy.cmhl.models; + +import java.util.Map; + +/** + * Represents a generic parsed ECR packet (Command + TLV Data). + * Can be a Request or a Response depending on context. + */ +public class EcrPacket { + private byte commandCode; + private Map tlvData; + + public EcrPacket(byte commandCode, Map tlvData) { + this.commandCode = commandCode; + this.tlvData = tlvData; + } + + public byte getCommandCode() { + return commandCode; + } + + public Map getTlvData() { + return tlvData; + } + + public byte[] getValue(byte tag) { + return tlvData.get(tag); + } + + public String getValueAsString(byte tag) { + byte[] val = tlvData.get(tag); + if (val == null) return null; + return new String(val); + } +} diff --git a/cmhl/src/main/java/com/kizzy/cmhl/models/PingRequest.java b/cmhl/src/main/java/com/kizzy/cmhl/models/PingRequest.java new file mode 100644 index 0000000..f3d2669 --- /dev/null +++ b/cmhl/src/main/java/com/kizzy/cmhl/models/PingRequest.java @@ -0,0 +1,26 @@ +package com.kizzy.cmhl.models; + + +import com.kizzy.cmhl.EcrProtocol; + +import java.io.ByteArrayOutputStream; + +public class PingRequest implements EcrCommand { + + public PingRequest() {} + public PingRequest(EcrPacket packet) { + if (packet.getCommandCode() != EcrProtocol.CMD_PING) { + throw new IllegalArgumentException("Invalid command for PingRequest"); + } + } + + @Override + public byte getCommandCode() { + return EcrProtocol.CMD_PING; + } + + @Override + public byte[] toTlvBytes() { + return new byte[0]; // Empty body + } +} diff --git a/cmhl/src/main/java/com/kizzy/cmhl/models/PingResponse.java b/cmhl/src/main/java/com/kizzy/cmhl/models/PingResponse.java new file mode 100644 index 0000000..bbe3d9a --- /dev/null +++ b/cmhl/src/main/java/com/kizzy/cmhl/models/PingResponse.java @@ -0,0 +1,42 @@ +package com.kizzy.cmhl.models; + + +import com.kizzy.cmhl.EcrProtocol; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +public class PingResponse extends BaseEcrResponse { + + public PingResponse(EcrPacket packet) { + if (packet.getCommandCode() != EcrProtocol.CMD_PING) { + } + parseCommonTags(packet); + } + + private PingResponse(Builder builder) { + this.responseCode = builder.responseCode; + } + + @Override + public byte getCommandCode() { + return EcrProtocol.CMD_PING; + } + + @Override + public byte[] toTlvBytes() { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + try { + writeTag(baos, EcrProtocol.TAG_RESPONSE_CODE, responseCode); + } catch (IOException e) { + e.printStackTrace(); + } + return baos.toByteArray(); + } + + public static class Builder { + private String responseCode; + public Builder setResponseCode(String responseCode) { this.responseCode = responseCode; return this; } + public PingResponse build() { return new PingResponse(this); } + } +} diff --git a/cmhl/src/main/java/com/kizzy/cmhl/models/PreAuthCancellationRequest.java b/cmhl/src/main/java/com/kizzy/cmhl/models/PreAuthCancellationRequest.java new file mode 100644 index 0000000..296f55e --- /dev/null +++ b/cmhl/src/main/java/com/kizzy/cmhl/models/PreAuthCancellationRequest.java @@ -0,0 +1,69 @@ +package com.kizzy.cmhl.models; + + +import com.kizzy.cmhl.EcrProtocol; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +public class PreAuthCancellationRequest implements EcrCommand { + private String referenceNumber; + private String cardType; + private String approvalCode; + + public PreAuthCancellationRequest(String referenceNumber, String cardType, String approvalCode) { + this.referenceNumber = referenceNumber; + this.cardType = cardType; + this.approvalCode = approvalCode; + } + + public PreAuthCancellationRequest(EcrPacket packet) { + if (packet.getCommandCode() != EcrProtocol.CMD_PRE_AUTH_CANCELLATION) { + throw new IllegalArgumentException("Invalid command"); + } + this.referenceNumber = packet.getValueAsString(EcrProtocol.TAG_RRN); + this.cardType = packet.getValueAsString(EcrProtocol.TAG_CARD_TYPE); + this.approvalCode = packet.getValueAsString(EcrProtocol.TAG_APPROVAL_CODE); + } + + public String getCardType() { return cardType; } + public String getReferenceNumber() { return referenceNumber; } + + public String getApprovalCode() { return approvalCode; } + + @Override + public byte getCommandCode() { + return EcrProtocol.CMD_PRE_AUTH_CANCELLATION; + } + + @Override + public byte[] toTlvBytes() { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + try { + + if (referenceNumber != null) { + byte[] bytes = referenceNumber.getBytes(StandardCharsets.US_ASCII); + baos.write(EcrProtocol.TAG_RRN); + baos.write((byte) bytes.length); + baos.write(bytes); + } + if (approvalCode != null) { + byte[] bytes = approvalCode.getBytes(StandardCharsets.US_ASCII); + baos.write(EcrProtocol.TAG_APPROVAL_CODE); + baos.write((byte) bytes.length); + baos.write(bytes); + } + + if (cardType != null) { + byte[] bytes = cardType.getBytes(StandardCharsets.US_ASCII); + baos.write(EcrProtocol.TAG_CARD_TYPE); + baos.write((byte) bytes.length); + baos.write(bytes); + } + } catch (IOException e) { + e.printStackTrace(); + } + return baos.toByteArray(); + } +} diff --git a/cmhl/src/main/java/com/kizzy/cmhl/models/PreAuthCancellationResponse.java b/cmhl/src/main/java/com/kizzy/cmhl/models/PreAuthCancellationResponse.java new file mode 100644 index 0000000..84e4210 --- /dev/null +++ b/cmhl/src/main/java/com/kizzy/cmhl/models/PreAuthCancellationResponse.java @@ -0,0 +1,70 @@ +package com.kizzy.cmhl.models; + + +import com.kizzy.cmhl.EcrProtocol; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +public class PreAuthCancellationResponse extends BaseEcrResponse { + private String invoiceNumber; + private String approvalCode; + + public PreAuthCancellationResponse(EcrPacket packet) { + if (packet.getCommandCode() != EcrProtocol.CMD_PRE_AUTH_CANCELLATION) {} + parseCommonTags(packet); + this.invoiceNumber = packet.getValueAsString(EcrProtocol.TAG_INVOICE_NUMBER); + this.approvalCode = packet.getValueAsString(EcrProtocol.TAG_APPROVAL_CODE); + } + + private PreAuthCancellationResponse(Builder builder) { + this.responseCode = builder.responseCode; + this.time = builder.time; + this.date = builder.date; + this.tid = builder.tid; + this.mid = builder.mid; + this.invoiceNumber = builder.invoiceNumber; + this.approvalCode = builder.approvalCode; + } + + public String getInvoiceNumber() { return invoiceNumber; } + public String getApprovalCode() { return approvalCode; } + + @Override + public byte getCommandCode() { + return EcrProtocol.CMD_PRE_AUTH_CANCELLATION; + } + + @Override + public byte[] toTlvBytes() { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + try { + writeCommonTags(baos); + writeTag(baos, EcrProtocol.TAG_INVOICE_NUMBER, invoiceNumber); + writeTag(baos, EcrProtocol.TAG_APPROVAL_CODE, approvalCode); + } catch (IOException e) { + e.printStackTrace(); + } + return baos.toByteArray(); + } + + public static class Builder { + private String responseCode; + private String time; + private String date; + private String tid; + private String mid; + private String invoiceNumber; + private String approvalCode; + + public Builder setResponseCode(String responseCode) { this.responseCode = responseCode; return this; } + public Builder setTime(String time) { this.time = time; return this; } + public Builder setDate(String date) { this.date = date; return this; } + public Builder setTid(String tid) { this.tid = tid; return this; } + public Builder setMid(String mid) { this.mid = mid; return this; } + public Builder setInvoiceNumber(String invoiceNumber) { this.invoiceNumber = invoiceNumber; return this; } + public Builder setApprovalCode(String approvalCode) { this.approvalCode = approvalCode; return this; } + + public PreAuthCancellationResponse build() { return new PreAuthCancellationResponse(this); } + } +} diff --git a/cmhl/src/main/java/com/kizzy/cmhl/models/PreAuthCompletionRequest.java b/cmhl/src/main/java/com/kizzy/cmhl/models/PreAuthCompletionRequest.java new file mode 100644 index 0000000..889efdc --- /dev/null +++ b/cmhl/src/main/java/com/kizzy/cmhl/models/PreAuthCompletionRequest.java @@ -0,0 +1,70 @@ +package com.kizzy.cmhl.models; + + +import com.kizzy.cmhl.EcrProtocol; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +public class PreAuthCompletionRequest implements EcrCommand { + private String referenceNumber; + private String cardType; + private String approvalCode; + + public PreAuthCompletionRequest(String referenceNumber, String cardType, String approvalCode) { + this.referenceNumber = referenceNumber; + this.cardType = cardType; + this.approvalCode = approvalCode; + } + + public PreAuthCompletionRequest(EcrPacket packet) { + if (packet.getCommandCode() != EcrProtocol.CMD_PRE_AUTH_COMPLETION) { + throw new IllegalArgumentException("Invalid command"); + } + this.referenceNumber = packet.getValueAsString(EcrProtocol.TAG_RRN); + this.cardType = packet.getValueAsString(EcrProtocol.TAG_CARD_TYPE); + this.approvalCode = packet.getValueAsString(EcrProtocol.TAG_APPROVAL_CODE); + } + + public String getCardType() { return cardType; } + public String getReferenceNumber() { return referenceNumber; } + + public String getApprovalCode() { return approvalCode; } + + + @Override + public byte getCommandCode() { + return EcrProtocol.CMD_PRE_AUTH_COMPLETION; + } + + @Override + public byte[] toTlvBytes() { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + try { + + if (referenceNumber != null) { + byte[] bytes = referenceNumber.getBytes(StandardCharsets.US_ASCII); + baos.write(EcrProtocol.TAG_RRN); + baos.write((byte) bytes.length); + baos.write(bytes); + } + if (approvalCode != null) { + byte[] bytes = approvalCode.getBytes(StandardCharsets.US_ASCII); + baos.write(EcrProtocol.TAG_APPROVAL_CODE); + baos.write((byte) bytes.length); + baos.write(bytes); + } + + if (cardType != null) { + byte[] bytes = cardType.getBytes(StandardCharsets.US_ASCII); + baos.write(EcrProtocol.TAG_CARD_TYPE); + baos.write((byte) bytes.length); + baos.write(bytes); + } + } catch (IOException e) { + e.printStackTrace(); + } + return baos.toByteArray(); + } +} diff --git a/cmhl/src/main/java/com/kizzy/cmhl/models/PreAuthCompletionResponse.java b/cmhl/src/main/java/com/kizzy/cmhl/models/PreAuthCompletionResponse.java new file mode 100644 index 0000000..a8b0001 --- /dev/null +++ b/cmhl/src/main/java/com/kizzy/cmhl/models/PreAuthCompletionResponse.java @@ -0,0 +1,117 @@ +package com.kizzy.cmhl.models; + + +import com.kizzy.cmhl.EcrProtocol; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +public class PreAuthCompletionResponse extends BaseEcrResponse { + private String amount; + private String pan; + private String expDate; + private String stan; + private String approvalCode; + private String rrn; + private String invoiceNumber; + + private String entry; + + public PreAuthCompletionResponse(EcrPacket packet) { + if (packet.getCommandCode() != EcrProtocol.CMD_PRE_AUTH_COMPLETION) {} + parseCommonTags(packet); + this.amount = packet.getValueAsString(EcrProtocol.TAG_AMOUNT); + this.pan = packet.getValueAsString(EcrProtocol.TAG_PAN); + this.expDate = packet.getValueAsString(EcrProtocol.TAG_EXP_DATE); + this.stan = packet.getValueAsString(EcrProtocol.TAG_STAN); + this.approvalCode = packet.getValueAsString(EcrProtocol.TAG_APPROVAL_CODE); + this.rrn = packet.getValueAsString(EcrProtocol.TAG_RRN); + this.invoiceNumber = packet.getValueAsString(EcrProtocol.TAG_INVOICE_NUMBER); + + } + + private PreAuthCompletionResponse(Builder builder) { + this.responseCode = builder.responseCode; + this.time = builder.time; + this.date = builder.date; + this.tid = builder.tid; + this.mid = builder.mid; + this.amount = builder.amount; + this.pan = builder.pan; + this.expDate = builder.expDate; + this.stan = builder.stan; + this.approvalCode = builder.approvalCode; + this.rrn = builder.rrn; + this.invoiceNumber = builder.invoiceNumber; + this.entry = builder.entry; + } + + // Getters + public String getAmount() { return amount; } + public String getPan() { return pan; } + public String getExpDate() { return expDate; } + public String getStan() { return stan; } + public String getApprovalCode() { return approvalCode; } + public String getRrn() { return rrn; } + public String getInvoiceNumber() { return invoiceNumber; } + + public String getEntry() { return entry; } + + @Override + public byte getCommandCode() { + return EcrProtocol.CMD_PRE_AUTH_COMPLETION; + } + + @Override + public byte[] toTlvBytes() { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + try { + writeCommonTags(baos); + writeTag(baos, EcrProtocol.TAG_AMOUNT, amount); + writeTag(baos, EcrProtocol.TAG_PAN, pan); + writeTag(baos, EcrProtocol.TAG_EXP_DATE, expDate); + writeTag(baos, EcrProtocol.TAG_STAN, stan); + writeTag(baos, EcrProtocol.TAG_APPROVAL_CODE, approvalCode); + writeTag(baos, EcrProtocol.TAG_RRN, rrn); + writeTag(baos, EcrProtocol.TAG_INVOICE_NUMBER, invoiceNumber); + writeTag(baos, EcrProtocol.TAG_POS_ENTRY, entry); + + } catch (IOException e) { + e.printStackTrace(); + } + return baos.toByteArray(); + } + + public static class Builder { + private String responseCode; + private String time; + private String date; + private String tid; + private String mid; + private String amount; + private String pan; + private String expDate; + private String stan; + private String approvalCode; + private String rrn; + private String invoiceNumber; + + private String entry; + + public Builder setResponseCode(String responseCode) { this.responseCode = responseCode; return this; } + public Builder setTime(String time) { this.time = time; return this; } + public Builder setDate(String date) { this.date = date; return this; } + public Builder setTid(String tid) { this.tid = tid; return this; } + public Builder setMid(String mid) { this.mid = mid; return this; } + public Builder setAmount(String amount) { this.amount = amount; return this; } + public Builder setPan(String pan) { this.pan = pan; return this; } + public Builder setExpDate(String expDate) { this.expDate = expDate; return this; } + public Builder setStan(String stan) { this.stan = stan; return this; } + public Builder setApprovalCode(String approvalCode) { this.approvalCode = approvalCode; return this; } + public Builder setRrn(String rrn) { this.rrn = rrn; return this; } + public Builder setInvoiceNumber(String invoiceNumber) { this.invoiceNumber = invoiceNumber; return this; } + public Builder setEntry(String entry) { this.entry = entry; return this; } + + public PreAuthCompletionResponse build() { return new PreAuthCompletionResponse(this); } + } +} diff --git a/cmhl/src/main/java/com/kizzy/cmhl/models/PreAuthRequest.java b/cmhl/src/main/java/com/kizzy/cmhl/models/PreAuthRequest.java new file mode 100644 index 0000000..86f3102 --- /dev/null +++ b/cmhl/src/main/java/com/kizzy/cmhl/models/PreAuthRequest.java @@ -0,0 +1,57 @@ +package com.kizzy.cmhl.models; + + +import com.kizzy.cmhl.EcrProtocol; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +public class PreAuthRequest implements EcrCommand { + private String amount; + + public PreAuthRequest(String amount) { + this.amount = formatAmount(amount); + } + + public PreAuthRequest(EcrPacket packet) { + if (packet.getCommandCode() != EcrProtocol.CMD_PRE_AUTH) { + throw new IllegalArgumentException("Invalid command"); + } + this.amount = packet.getValueAsString(EcrProtocol.TAG_AMOUNT); + } + + public String getAmount() { return amount; } + + private String formatAmount(String amount) { + if (amount == null) return "000000000000"; + if (amount.length() > 12) return amount.substring(0, 12); + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < 12 - amount.length(); i++) { + sb.append('0'); + } + sb.append(amount); + return sb.toString(); + } + + @Override + public byte getCommandCode() { + return EcrProtocol.CMD_PRE_AUTH; + } + + @Override + public byte[] toTlvBytes() { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + try { + if (amount != null) { + byte[] bytes = amount.getBytes(StandardCharsets.US_ASCII); + baos.write(EcrProtocol.TAG_AMOUNT); + baos.write((byte) bytes.length); + baos.write(bytes); + } + } catch (IOException e) { + e.printStackTrace(); + } + return baos.toByteArray(); + } +} diff --git a/cmhl/src/main/java/com/kizzy/cmhl/models/PreAuthResponse.java b/cmhl/src/main/java/com/kizzy/cmhl/models/PreAuthResponse.java new file mode 100644 index 0000000..157dc38 --- /dev/null +++ b/cmhl/src/main/java/com/kizzy/cmhl/models/PreAuthResponse.java @@ -0,0 +1,124 @@ +package com.kizzy.cmhl.models; + + +import com.kizzy.cmhl.EcrProtocol; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +public class PreAuthResponse extends BaseEcrResponse { + private String amount; + private String pan; + private String expDate; + private String stan; + private String approvalCode; + private String rrn; + private String invoiceNumber; + + private String cardType; + private String entry; + + public PreAuthResponse(EcrPacket packet) { + if (packet.getCommandCode() != EcrProtocol.CMD_PRE_AUTH) {} + parseCommonTags(packet); + this.amount = packet.getValueAsString(EcrProtocol.TAG_AMOUNT); + this.pan = packet.getValueAsString(EcrProtocol.TAG_PAN); + this.expDate = packet.getValueAsString(EcrProtocol.TAG_EXP_DATE); + this.stan = packet.getValueAsString(EcrProtocol.TAG_STAN); + this.approvalCode = packet.getValueAsString(EcrProtocol.TAG_APPROVAL_CODE); + this.rrn = packet.getValueAsString(EcrProtocol.TAG_RRN); + this.invoiceNumber = packet.getValueAsString(EcrProtocol.TAG_INVOICE_NUMBER); + } + + private PreAuthResponse(Builder builder) { + this.responseCode = builder.responseCode; + this.time = builder.time; + this.date = builder.date; + this.tid = builder.tid; + this.mid = builder.mid; + this.amount = builder.amount; + this.pan = builder.pan; + this.expDate = builder.expDate; + this.stan = builder.stan; + this.approvalCode = builder.approvalCode; + this.rrn = builder.rrn; + this.invoiceNumber = builder.invoiceNumber; + this.entry = builder.entry; + this.cardType = builder.cardType; + } + + // Getters + public String getAmount() { return amount; } + public String getPan() { return pan; } + public String getExpDate() { return expDate; } + public String getStan() { return stan; } + public String getApprovalCode() { return approvalCode; } + public String getRrn() { return rrn; } + public String getInvoiceNumber() { return invoiceNumber; } + + public String getEntry() {return entry;} + + public String getCardType() {return cardType;} + + @Override + public byte getCommandCode() { + return EcrProtocol.CMD_PRE_AUTH; + } + + @Override + public byte[] toTlvBytes() { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + try { + writeCommonTags(baos); + writeTag(baos, EcrProtocol.TAG_AMOUNT, amount); + writeTag(baos, EcrProtocol.TAG_PAN, pan); + writeTag(baos, EcrProtocol.TAG_EXP_DATE, expDate); + writeTag(baos, EcrProtocol.TAG_STAN, stan); + writeTag(baos, EcrProtocol.TAG_APPROVAL_CODE, approvalCode); + writeTag(baos, EcrProtocol.TAG_RRN, rrn); + writeTag(baos, EcrProtocol.TAG_INVOICE_NUMBER, invoiceNumber); + writeTag(baos, EcrProtocol.TAG_POS_ENTRY, entry); + writeTag(baos, EcrProtocol.TAG_CARD_TYPE, cardType); + + } catch (IOException e) { + e.printStackTrace(); + } + return baos.toByteArray(); + } + + public static class Builder { + private String responseCode; + private String time; + private String date; + private String tid; + private String mid; + private String amount; + private String pan; + private String expDate; + private String stan; + private String approvalCode; + private String rrn; + private String invoiceNumber; + + private String cardType; + private String entry; + + public Builder setResponseCode(String responseCode) { this.responseCode = responseCode; return this; } + public Builder setTime(String time) { this.time = time; return this; } + public Builder setDate(String date) { this.date = date; return this; } + public Builder setTid(String tid) { this.tid = tid; return this; } + public Builder setMid(String mid) { this.mid = mid; return this; } + public Builder setAmount(String amount) { this.amount = amount; return this; } + public Builder setPan(String pan) { this.pan = pan; return this; } + public Builder setExpDate(String expDate) { this.expDate = expDate; return this; } + public Builder setStan(String stan) { this.stan = stan; return this; } + public Builder setApprovalCode(String approvalCode) { this.approvalCode = approvalCode; return this; } + public Builder setRrn(String rrn) { this.rrn = rrn; return this; } + public Builder setInvoiceNumber(String invoiceNumber) { this.invoiceNumber = invoiceNumber; return this; } + + public Builder setCardType(String cardType) { this.cardType = cardType; return this; } + public Builder setEntry(String entry) { this.entry = entry; return this;} + + public PreAuthResponse build() { return new PreAuthResponse(this); } + } +} diff --git a/cmhl/src/main/java/com/kizzy/cmhl/models/QrPaymentRequest.java b/cmhl/src/main/java/com/kizzy/cmhl/models/QrPaymentRequest.java new file mode 100644 index 0000000..a39013f --- /dev/null +++ b/cmhl/src/main/java/com/kizzy/cmhl/models/QrPaymentRequest.java @@ -0,0 +1,74 @@ +package com.kizzy.cmhl.models; + + +import com.kizzy.cmhl.EcrProtocol; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +public class QrPaymentRequest implements EcrCommand { + private String amount; // 12 digits + private String currencyCode; // 3 digits + + public QrPaymentRequest(String amount, String currencyCode) { + this.amount = formatAmount(amount); + this.currencyCode = currencyCode; + } + + public QrPaymentRequest(EcrPacket packet) { + if (packet.getCommandCode() != EcrProtocol.CMD_QR_PAYMENT) { + throw new IllegalArgumentException("Invalid command code for QrPaymentRequest"); + } + this.amount = packet.getValueAsString(EcrProtocol.TAG_AMOUNT); + this.currencyCode = packet.getValueAsString(EcrProtocol.TAG_CURRENCY_CODE); + } + + public String getAmount() { + return amount; + } + + public String getCurrencyCode() { + return currencyCode; + } + + private String formatAmount(String amount) { + if (amount == null) return "000000000000"; + if (amount.length() > 12) return amount.substring(0, 12); + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < 12 - amount.length(); i++) { + sb.append('0'); + } + sb.append(amount); + return sb.toString(); + } + + @Override + public byte getCommandCode() { + return EcrProtocol.CMD_QR_PAYMENT; + } + + @Override + public byte[] toTlvBytes() { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + try { + if (amount != null) { + byte[] amountBytes = amount.getBytes(StandardCharsets.US_ASCII); + baos.write(EcrProtocol.TAG_AMOUNT); + baos.write((byte) amountBytes.length); + baos.write(amountBytes); + } + + if (currencyCode != null) { + byte[] currencyBytes = currencyCode.getBytes(StandardCharsets.US_ASCII); + baos.write(EcrProtocol.TAG_CURRENCY_CODE); + baos.write((byte) currencyBytes.length); + baos.write(currencyBytes); + } + + } catch (IOException e) { + e.printStackTrace(); + } + return baos.toByteArray(); + } +} diff --git a/cmhl/src/main/java/com/kizzy/cmhl/models/QrPaymentResponse.java b/cmhl/src/main/java/com/kizzy/cmhl/models/QrPaymentResponse.java new file mode 100644 index 0000000..1bb6690 --- /dev/null +++ b/cmhl/src/main/java/com/kizzy/cmhl/models/QrPaymentResponse.java @@ -0,0 +1,155 @@ +package com.kizzy.cmhl.models; + + +import com.kizzy.cmhl.EcrProtocol; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +public class QrPaymentResponse implements EcrCommand { + private String responseCode; + private String amount; + private String pan; + private String expDate; + private String stan; + private String time; + private String date; + private String approvalCode; + private String rrn; + private String posEntryMode; + private String tid; + private String mid; + private String invoiceNumber; + private String paymentType; + private String currencyCode; + + public QrPaymentResponse(EcrPacket packet) { + if (packet.getCommandCode() != EcrProtocol.CMD_QR_PAYMENT) { + } + this.responseCode = packet.getValueAsString(EcrProtocol.TAG_RESPONSE_CODE); + this.amount = packet.getValueAsString(EcrProtocol.TAG_AMOUNT); + this.pan = packet.getValueAsString(EcrProtocol.TAG_PAN); + this.expDate = packet.getValueAsString(EcrProtocol.TAG_EXP_DATE); + this.stan = packet.getValueAsString(EcrProtocol.TAG_STAN); + this.time = packet.getValueAsString(EcrProtocol.TAG_TIME); + this.date = packet.getValueAsString(EcrProtocol.TAG_DATE); + this.approvalCode = packet.getValueAsString(EcrProtocol.TAG_APPROVAL_CODE); + this.rrn = packet.getValueAsString(EcrProtocol.TAG_RRN); + this.posEntryMode = packet.getValueAsString(EcrProtocol.TAG_POS_ENTRY); + this.tid = packet.getValueAsString(EcrProtocol.TAG_TID); + this.mid = packet.getValueAsString(EcrProtocol.TAG_MID); + this.invoiceNumber = packet.getValueAsString(EcrProtocol.TAG_INVOICE_NUMBER); + this.paymentType = packet.getValueAsString(EcrProtocol.TAG_PAYMENT_TYPE); + this.currencyCode = packet.getValueAsString(EcrProtocol.TAG_CURRENCY_CODE); + } + + private QrPaymentResponse(Builder builder) { + this.responseCode = builder.responseCode; + this.amount = builder.amount; + this.pan = builder.pan; + this.expDate = builder.expDate; + this.stan = builder.stan; + this.time = builder.time; + this.date = builder.date; + this.approvalCode = builder.approvalCode; + this.rrn = builder.rrn; + this.posEntryMode = builder.posEntryMode; + this.tid = builder.tid; + this.mid = builder.mid; + this.invoiceNumber = builder.invoiceNumber; + this.paymentType = builder.paymentType; + this.currencyCode = builder.currencyCode; + } + + public boolean isApproved() { + return "00".equals(responseCode); + } + + // Getters + public String getResponseCode() { return responseCode; } + public String getAmount() { return amount; } + public String getPan() { return pan; } + public String getInvoiceNumber() { return invoiceNumber; } + public String getApprovalCode() { return approvalCode; } + public String getRrn() { return rrn; } + public String getTid() { return tid; } + public String getMid() { return mid; } + + @Override + public byte getCommandCode() { + return EcrProtocol.CMD_QR_PAYMENT; + } + + @Override + public byte[] toTlvBytes() { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + try { + writeTag(baos, EcrProtocol.TAG_AMOUNT, amount); + writeTag(baos, EcrProtocol.TAG_RESPONSE_CODE, responseCode); + writeTag(baos, EcrProtocol.TAG_PAN, pan); + writeTag(baos, EcrProtocol.TAG_EXP_DATE, expDate); + writeTag(baos, EcrProtocol.TAG_STAN, stan); + writeTag(baos, EcrProtocol.TAG_TIME, time); + writeTag(baos, EcrProtocol.TAG_DATE, date); + writeTag(baos, EcrProtocol.TAG_APPROVAL_CODE, approvalCode); + writeTag(baos, EcrProtocol.TAG_RRN, rrn); + writeTag(baos, EcrProtocol.TAG_POS_ENTRY, posEntryMode); + writeTag(baos, EcrProtocol.TAG_TID, tid); + writeTag(baos, EcrProtocol.TAG_MID, mid); + writeTag(baos, EcrProtocol.TAG_INVOICE_NUMBER, invoiceNumber); + writeTag(baos, EcrProtocol.TAG_PAYMENT_TYPE, paymentType); + writeTag(baos, EcrProtocol.TAG_CURRENCY_CODE, currencyCode); + } catch (IOException e) { + e.printStackTrace(); + } + return baos.toByteArray(); + } + + private void writeTag(ByteArrayOutputStream baos, byte tag, String value) throws IOException { + if (value != null) { + byte[] bytes = value.getBytes(StandardCharsets.US_ASCII); + baos.write(tag); + baos.write((byte) bytes.length); + baos.write(bytes); + } + } + + public static class Builder { + private String responseCode; + private String amount; + private String pan; + private String expDate; + private String stan; + private String time; + private String date; + private String approvalCode; + private String rrn; + private String posEntryMode; + private String tid; + private String mid; + private String invoiceNumber; + private String paymentType; + private String currencyCode; + + public Builder setResponseCode(String responseCode) { this.responseCode = responseCode; return this; } + public Builder setAmount(String amount) { this.amount = amount; return this; } + public Builder setPan(String pan) { this.pan = pan; return this; } + public Builder setExpDate(String expDate) { this.expDate = expDate; return this; } + public Builder setStan(String stan) { this.stan = stan; return this; } + public Builder setTime(String time) { this.time = time; return this; } + public Builder setDate(String date) { this.date = date; return this; } + public Builder setApprovalCode(String approvalCode) { this.approvalCode = approvalCode; return this; } + public Builder setRrn(String rrn) { this.rrn = rrn; return this; } + public Builder setPosEntryMode(String posEntryMode) { this.posEntryMode = posEntryMode; return this; } + public Builder setTid(String tid) { this.tid = tid; return this; } + public Builder setMid(String mid) { this.mid = mid; return this; } + public Builder setInvoiceNumber(String invoiceNumber) { this.invoiceNumber = invoiceNumber; return this; } + public Builder setPaymentType(String paymentType) { this.paymentType = paymentType; return this; } + public Builder setCurrencyCode(String currencyCode) { this.currencyCode = currencyCode; return this; } + + public QrPaymentResponse build() { + return new QrPaymentResponse(this); + } + } +} diff --git a/cmhl/src/main/java/com/kizzy/cmhl/models/SaleRequest.java b/cmhl/src/main/java/com/kizzy/cmhl/models/SaleRequest.java new file mode 100644 index 0000000..6b953c9 --- /dev/null +++ b/cmhl/src/main/java/com/kizzy/cmhl/models/SaleRequest.java @@ -0,0 +1,57 @@ +package com.kizzy.cmhl.models; + + +import com.kizzy.cmhl.EcrProtocol; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +public class SaleRequest implements EcrCommand { + private String amount; + + public SaleRequest(String amount) { + this.amount = formatAmount(amount); + } + + public SaleRequest(EcrPacket packet) { + if (packet.getCommandCode() != EcrProtocol.CMD_SALE) { + throw new IllegalArgumentException("Invalid command"); + } + this.amount = packet.getValueAsString(EcrProtocol.TAG_AMOUNT); + } + + public String getAmount() { return amount; } + + private String formatAmount(String amount) { + if (amount == null) return "000000000000"; + if (amount.length() > 12) return amount.substring(0, 12); + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < 12 - amount.length(); i++) { + sb.append('0'); + } + sb.append(amount); + return sb.toString(); + } + + @Override + public byte getCommandCode() { + return EcrProtocol.CMD_SALE; + } + + @Override + public byte[] toTlvBytes() { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + try { + if (amount != null) { + byte[] bytes = amount.getBytes(StandardCharsets.US_ASCII); + baos.write(EcrProtocol.TAG_AMOUNT); + baos.write((byte) bytes.length); + baos.write(bytes); + } + } catch (IOException e) { + e.printStackTrace(); + } + return baos.toByteArray(); + } +} diff --git a/cmhl/src/main/java/com/kizzy/cmhl/models/SaleResponse.java b/cmhl/src/main/java/com/kizzy/cmhl/models/SaleResponse.java new file mode 100644 index 0000000..81a7f8a --- /dev/null +++ b/cmhl/src/main/java/com/kizzy/cmhl/models/SaleResponse.java @@ -0,0 +1,140 @@ +package com.kizzy.cmhl.models; + + +import com.kizzy.cmhl.EcrProtocol; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +public class SaleResponse extends BaseEcrResponse { + private String amount; + private String pan; + private String expDate; + private String stan; + private String approvalCode; + private String referenceNum; + private String invoiceNumber; + + private String entry; + + private String currency; + + private String cardType; + + public SaleResponse(EcrPacket packet) { + if (packet.getCommandCode() != EcrProtocol.CMD_SALE) {} + parseCommonTags(packet); + this.amount = packet.getValueAsString(EcrProtocol.TAG_AMOUNT); + this.pan = packet.getValueAsString(EcrProtocol.TAG_PAN); + this.expDate = packet.getValueAsString(EcrProtocol.TAG_EXP_DATE); + this.stan = packet.getValueAsString(EcrProtocol.TAG_STAN); + this.approvalCode = packet.getValueAsString(EcrProtocol.TAG_APPROVAL_CODE); + this.referenceNum = packet.getValueAsString(EcrProtocol.TAG_RRN); + this.invoiceNumber = packet.getValueAsString(EcrProtocol.TAG_INVOICE_NUMBER); + } + + private SaleResponse(Builder builder) { + this.responseCode = builder.responseCode; + this.time = builder.time; + this.date = builder.date; + this.tid = builder.tid; + this.mid = builder.mid; + this.amount = builder.amount; + this.pan = builder.pan; + this.expDate = builder.expDate; + this.stan = builder.stan; + this.approvalCode = builder.approvalCode; + this.referenceNum = builder.referenceNum; + this.invoiceNumber = builder.invoiceNumber; + this.entry = builder.entry; + this.cardType = builder.cardType; + this.currency = builder.currency; + } + + // Getters + public String getAmount() { return amount; } + public String getPan() { return pan; } + public String getExpDate() { return expDate; } + public String getStan() { return stan; } + public String getApprovalCode() { return approvalCode; } + public String getReferenceNum() { return referenceNum; } + public String getInvoiceNumber() { return invoiceNumber; } + + public String getEntry() { return entry; } + + public String getCardType() { return cardType; } + + @Override + public byte getCommandCode() { + return EcrProtocol.CMD_SALE; + } + + @Override + public byte[] toTlvBytes() { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + try { +// writeCommonTags(baos); + writeTag(baos, EcrProtocol.TAG_AMOUNT, amount); + writeTag(baos, EcrProtocol.TAG_RESPONSE_CODE, responseCode); + writeTag(baos, EcrProtocol.TAG_PAN, pan); + writeTag(baos, EcrProtocol.TAG_EXP_DATE, expDate); + writeTag(baos, EcrProtocol.TAG_STAN, stan); + writeTag(baos, EcrProtocol.TAG_TIME, time); + writeTag(baos, EcrProtocol.TAG_DATE, date); + writeTag(baos, EcrProtocol.TAG_APPROVAL_CODE, approvalCode); + writeTag(baos, EcrProtocol.TAG_RRN, referenceNum); + writeTag(baos, EcrProtocol.TAG_POS_ENTRY, entry); + writeTag(baos, EcrProtocol.TAG_TID, tid); + writeTag(baos, EcrProtocol.TAG_MID, mid); + writeTag(baos, EcrProtocol.TAG_INVOICE_NUMBER, invoiceNumber); + writeTagBinary(baos, EcrProtocol.TAG_CARD_TYPE, cardType); + writeTag(baos, EcrProtocol.TAG_CURRENCY_CODE, currency); + + } catch (IOException e) { + e.printStackTrace(); + } + return baos.toByteArray(); + } + + public static class Builder { + private String responseCode; + private String time; + private String date; + private String tid; + private String mid; + private String amount; + private String pan; + private String expDate; + private String stan; + + private String entry; + + private String cardType; + private String approvalCode; + private String referenceNum; + private String invoiceNumber; + + private String currency; + + public Builder setResponseCode(String responseCode) { this.responseCode = responseCode; return this; } + public Builder setTime(String time) { this.time = time; return this; } + public Builder setDate(String date) { this.date = date; return this; } + public Builder setTid(String tid) { this.tid = tid; return this; } + public Builder setMid(String mid) { this.mid = mid; return this; } + public Builder setAmount(String amount) { this.amount = amount; return this; } + public Builder setPan(String pan) { this.pan = pan; return this; } + public Builder setExpDate(String expDate) { this.expDate = expDate; return this; } + public Builder setStan(String stan) { this.stan = stan; return this; } + public Builder setApprovalCode(String approvalCode) { this.approvalCode = approvalCode; return this; } + public Builder setReferenceNum(String rrn) { this.referenceNum = rrn; return this; } + + public Builder setEntry(String entry) { this.entry = entry; return this;} + + public Builder setCardType(String cardType) { this.cardType = cardType; return this;} + public Builder setInvoiceNumber(String invoiceNumber) { this.invoiceNumber = invoiceNumber; return this; } + + public Builder setCurrency(String currency) { this.currency = currency; return this; } + + public SaleResponse build() { return new SaleResponse(this); } + } +} diff --git a/cmhl/src/main/java/com/kizzy/cmhl/models/SettlementRequest.java b/cmhl/src/main/java/com/kizzy/cmhl/models/SettlementRequest.java new file mode 100644 index 0000000..a4650c5 --- /dev/null +++ b/cmhl/src/main/java/com/kizzy/cmhl/models/SettlementRequest.java @@ -0,0 +1,46 @@ +package com.kizzy.cmhl.models; + + +import com.kizzy.cmhl.EcrProtocol; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +public class SettlementRequest implements EcrCommand { + private String cardType; + + public SettlementRequest(String cardType) { + this.cardType = cardType; + } + + public SettlementRequest(EcrPacket packet) { + if (packet.getCommandCode() != EcrProtocol.CMD_SETTLEMENT) { + throw new IllegalArgumentException("Invalid command"); + } + this.cardType = packet.getValueAsString(EcrProtocol.TAG_CARD_TYPE); + } + + public String getCardType() { return cardType; } + + @Override + public byte getCommandCode() { + return EcrProtocol.CMD_SETTLEMENT; + } + + @Override + public byte[] toTlvBytes() { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + try { + if (cardType != null) { + byte[] bytes = cardType.getBytes(StandardCharsets.US_ASCII); + baos.write(EcrProtocol.TAG_CARD_TYPE); + baos.write((byte) bytes.length); + baos.write(bytes); + } + } catch (IOException e) { + e.printStackTrace(); + } + return baos.toByteArray(); + } +} diff --git a/cmhl/src/main/java/com/kizzy/cmhl/models/SettlementResponse.java b/cmhl/src/main/java/com/kizzy/cmhl/models/SettlementResponse.java new file mode 100644 index 0000000..4759c97 --- /dev/null +++ b/cmhl/src/main/java/com/kizzy/cmhl/models/SettlementResponse.java @@ -0,0 +1,55 @@ +package com.kizzy.cmhl.models; + + +import com.kizzy.cmhl.EcrProtocol; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +public class SettlementResponse extends BaseEcrResponse { + + public SettlementResponse(EcrPacket packet) { + if (packet.getCommandCode() != EcrProtocol.CMD_SETTLEMENT) {} + parseCommonTags(packet); + } + + private SettlementResponse(Builder builder) { + this.responseCode = builder.responseCode; + this.time = builder.time; + this.date = builder.date; + this.tid = builder.tid; + this.mid = builder.mid; + } + + @Override + public byte getCommandCode() { + return EcrProtocol.CMD_SETTLEMENT; + } + + @Override + public byte[] toTlvBytes() { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + try { + writeCommonTags(baos); + } catch (IOException e) { + e.printStackTrace(); + } + return baos.toByteArray(); + } + + public static class Builder { + private String responseCode; + private String time; + private String date; + private String tid; + private String mid; + + public Builder setResponseCode(String responseCode) { this.responseCode = responseCode; return this; } + public Builder setTime(String time) { this.time = time; return this; } + public Builder setDate(String date) { this.date = date; return this; } + public Builder setTid(String tid) { this.tid = tid; return this; } + public Builder setMid(String mid) { this.mid = mid; return this; } + + public SettlementResponse build() { return new SettlementResponse(this); } + } +} diff --git a/cmhl/src/main/java/com/kizzy/cmhl/models/VoidQrPaymentRequest.java b/cmhl/src/main/java/com/kizzy/cmhl/models/VoidQrPaymentRequest.java new file mode 100644 index 0000000..c3f9dab --- /dev/null +++ b/cmhl/src/main/java/com/kizzy/cmhl/models/VoidQrPaymentRequest.java @@ -0,0 +1,60 @@ +package com.kizzy.cmhl.models; + + +import com.kizzy.cmhl.EcrProtocol; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +public class VoidQrPaymentRequest implements EcrCommand { + private String invoiceNumber; + private String paymentType; + + public VoidQrPaymentRequest(String invoiceNumber, String paymentType) { + this.invoiceNumber = invoiceNumber; + this.paymentType = paymentType; + } + + public VoidQrPaymentRequest(EcrPacket packet) { + if (packet.getCommandCode() != EcrProtocol.CMD_VOID_QR_PAYMENT) { + throw new IllegalArgumentException("Invalid command code for VoidQrPaymentRequest"); + } + this.invoiceNumber = packet.getValueAsString(EcrProtocol.TAG_INVOICE_NUMBER); + this.paymentType = packet.getValueAsString(EcrProtocol.TAG_PAYMENT_TYPE); + } + + public String getInvoiceNumber() { return invoiceNumber; } + public String getPaymentType() { return paymentType; } + + @Override + public byte getCommandCode() { + return EcrProtocol.CMD_VOID_QR_PAYMENT; + } + + @Override + public byte[] toTlvBytes() { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + try { + // Invoice Number + if (invoiceNumber != null) { + byte[] invoiceBytes = invoiceNumber.getBytes(StandardCharsets.US_ASCII); + baos.write(EcrProtocol.TAG_INVOICE_NUMBER); + baos.write((byte) invoiceBytes.length); + baos.write(invoiceBytes); + } + + // Payment Type + if (paymentType != null) { + byte[] typeBytes = paymentType.getBytes(StandardCharsets.US_ASCII); + baos.write(EcrProtocol.TAG_PAYMENT_TYPE); + baos.write((byte) typeBytes.length); + baos.write(typeBytes); + } + + } catch (IOException e) { + e.printStackTrace(); + } + return baos.toByteArray(); + } +} diff --git a/cmhl/src/main/java/com/kizzy/cmhl/models/VoidQrPaymentResponse.java b/cmhl/src/main/java/com/kizzy/cmhl/models/VoidQrPaymentResponse.java new file mode 100644 index 0000000..85e7c5c --- /dev/null +++ b/cmhl/src/main/java/com/kizzy/cmhl/models/VoidQrPaymentResponse.java @@ -0,0 +1,138 @@ +package com.kizzy.cmhl.models; + + +import com.kizzy.cmhl.EcrProtocol; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +public class VoidQrPaymentResponse implements EcrCommand { + private String responseCode; + private String amount; + private String pan; + private String expDate; + private String stan; + private String time; + private String date; + private String approvalCode; + private String rrn; + private String posEntryMode; + private String tid; + private String mid; + private String currencyCode; + + + // Parsing Constructor (ECR Role) + public VoidQrPaymentResponse(EcrPacket packet) { + if (packet.getCommandCode() != EcrProtocol.CMD_VOID_QR_PAYMENT) { + } + this.responseCode = packet.getValueAsString(EcrProtocol.TAG_RESPONSE_CODE); + this.amount = packet.getValueAsString(EcrProtocol.TAG_AMOUNT); + this.pan = packet.getValueAsString(EcrProtocol.TAG_PAN); + this.expDate = packet.getValueAsString(EcrProtocol.TAG_EXP_DATE); + this.stan = packet.getValueAsString(EcrProtocol.TAG_STAN); + this.time = packet.getValueAsString(EcrProtocol.TAG_TIME); + this.date = packet.getValueAsString(EcrProtocol.TAG_DATE); + this.approvalCode = packet.getValueAsString(EcrProtocol.TAG_APPROVAL_CODE); + this.rrn = packet.getValueAsString(EcrProtocol.TAG_RRN); + this.posEntryMode = packet.getValueAsString(EcrProtocol.TAG_POS_ENTRY); + this.tid = packet.getValueAsString(EcrProtocol.TAG_TID); + this.mid = packet.getValueAsString(EcrProtocol.TAG_MID); + this.currencyCode = packet.getValueAsString(EcrProtocol.TAG_CURRENCY_CODE); + } + + // Building Constructor (EDC Role) + private VoidQrPaymentResponse(Builder builder) { + this.responseCode = builder.responseCode; + this.amount = builder.amount; + this.pan = builder.pan; + this.expDate = builder.expDate; + this.stan = builder.stan; + this.time = builder.time; + this.date = builder.date; + this.approvalCode = builder.approvalCode; + this.rrn = builder.rrn; + this.posEntryMode = builder.posEntryMode; + this.tid = builder.tid; + this.mid = builder.mid; + this.currencyCode = builder.currencyCode; + } + + public boolean isApproved() { + return "00".equals(responseCode); + } + + public String getResponseCode() { return responseCode; } + + @Override + public byte getCommandCode() { + return EcrProtocol.CMD_VOID_QR_PAYMENT; + } + + @Override + public byte[] toTlvBytes() { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + try { + writeTag(baos, EcrProtocol.TAG_RESPONSE_CODE, responseCode); + writeTag(baos, EcrProtocol.TAG_AMOUNT, amount); + writeTag(baos, EcrProtocol.TAG_PAN, pan); + writeTag(baos, EcrProtocol.TAG_EXP_DATE, expDate); + writeTag(baos, EcrProtocol.TAG_STAN, stan); + writeTag(baos, EcrProtocol.TAG_TIME, time); + writeTag(baos, EcrProtocol.TAG_DATE, date); + writeTag(baos, EcrProtocol.TAG_APPROVAL_CODE, approvalCode); + writeTag(baos, EcrProtocol.TAG_RRN, rrn); + writeTag(baos, EcrProtocol.TAG_POS_ENTRY, posEntryMode); + writeTag(baos, EcrProtocol.TAG_TID, tid); + writeTag(baos, EcrProtocol.TAG_MID, mid); + writeTag(baos, EcrProtocol.TAG_CURRENCY_CODE, currencyCode); + } catch (IOException e) { + e.printStackTrace(); + } + return baos.toByteArray(); + } + + private void writeTag(ByteArrayOutputStream baos, byte tag, String value) throws IOException { + if (value != null) { + byte[] bytes = value.getBytes(StandardCharsets.US_ASCII); + baos.write(tag); + baos.write((byte) bytes.length); + baos.write(bytes); + } + } + + public static class Builder { + private String responseCode; + private String amount; + private String pan; + private String expDate; + private String stan; + private String time; + private String date; + private String approvalCode; + private String rrn; + private String posEntryMode; + private String tid; + private String mid; + private String currencyCode; + + public Builder setResponseCode(String responseCode) { this.responseCode = responseCode; return this; } + public Builder setAmount(String amount) { this.amount = amount; return this; } + public Builder setPan(String pan) { this.pan = pan; return this; } + public Builder setExpDate(String expDate) { this.expDate = expDate; return this; } + public Builder setStan(String stan) { this.stan = stan; return this; } + public Builder setTime(String time) { this.time = time; return this; } + public Builder setDate(String date) { this.date = date; return this; } + public Builder setApprovalCode(String approvalCode) { this.approvalCode = approvalCode; return this; } + public Builder setRrn(String rrn) { this.rrn = rrn; return this; } + public Builder setPosEntryMode(String posEntryMode) { this.posEntryMode = posEntryMode; return this; } + public Builder setTid(String tid) { this.tid = tid; return this; } + public Builder setMid(String mid) { this.mid = mid; return this; } + public Builder setCurrencyCode(String currencyCode) { this.currencyCode = currencyCode; return this; } + + public VoidQrPaymentResponse build() { + return new VoidQrPaymentResponse(this); + } + } +} diff --git a/cmhl/src/main/java/com/kizzy/cmhl/models/VoidRequest.java b/cmhl/src/main/java/com/kizzy/cmhl/models/VoidRequest.java new file mode 100644 index 0000000..f5bacfe --- /dev/null +++ b/cmhl/src/main/java/com/kizzy/cmhl/models/VoidRequest.java @@ -0,0 +1,46 @@ +package com.kizzy.cmhl.models; + + +import com.kizzy.cmhl.EcrProtocol; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +public class VoidRequest implements EcrCommand { + private String invoiceNumber; + + public VoidRequest(String invoiceNumber) { + this.invoiceNumber = invoiceNumber; + } + + public VoidRequest(EcrPacket packet) { + if (packet.getCommandCode() != EcrProtocol.CMD_VOID) { + throw new IllegalArgumentException("Invalid command"); + } + this.invoiceNumber = packet.getValueAsString(EcrProtocol.TAG_INVOICE_NUMBER); + } + + public String getInvoiceNumber() { return invoiceNumber; } + + @Override + public byte getCommandCode() { + return EcrProtocol.CMD_VOID; + } + + @Override + public byte[] toTlvBytes() { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + try { + if (invoiceNumber != null) { + byte[] bytes = invoiceNumber.getBytes(StandardCharsets.US_ASCII); + baos.write(EcrProtocol.TAG_INVOICE_NUMBER); + baos.write((byte) bytes.length); + baos.write(bytes); + } + } catch (IOException e) { + e.printStackTrace(); + } + return baos.toByteArray(); + } +} diff --git a/cmhl/src/main/java/com/kizzy/cmhl/models/VoidResponse.java b/cmhl/src/main/java/com/kizzy/cmhl/models/VoidResponse.java new file mode 100644 index 0000000..ed72737 --- /dev/null +++ b/cmhl/src/main/java/com/kizzy/cmhl/models/VoidResponse.java @@ -0,0 +1,93 @@ +package com.kizzy.cmhl.models; + + +import com.kizzy.cmhl.EcrProtocol; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +public class VoidResponse extends BaseEcrResponse { + private String amount; + private String voucherNumber; + private String approvalCode; + private String referenceNum; + + private String entry; + + public VoidResponse(EcrPacket packet) { + if (packet.getCommandCode() != EcrProtocol.CMD_VOID) {} + parseCommonTags(packet); + this.amount = packet.getValueAsString(EcrProtocol.TAG_AMOUNT); + this.voucherNumber = packet.getValueAsString(EcrProtocol.TAG_INVOICE_NUMBER); + this.approvalCode = packet.getValueAsString(EcrProtocol.TAG_APPROVAL_CODE); + } + + private VoidResponse(Builder builder) { + this.responseCode = builder.responseCode; + this.time = builder.time; + this.date = builder.date; + this.tid = builder.tid; + this.mid = builder.mid; + this.amount = builder.amount; + this.voucherNumber = builder.voucherNum; + this.approvalCode = builder.approvalCode; + this.referenceNum = builder.referenceNum; + this.entry = builder.entry; + } + + public String getAmount() { return amount; } + public String getVoucherNumber() { return voucherNumber; } + public String getApprovalCode() { return approvalCode; } + + public String getReferenceNum() { return referenceNum;} + + public String getEntry() {return entry;} + + @Override + public byte getCommandCode() { + return EcrProtocol.CMD_VOID; + } + + @Override + public byte[] toTlvBytes() { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + try { + writeCommonTags(baos); + writeTag(baos, EcrProtocol.TAG_AMOUNT, amount); + writeTag(baos, EcrProtocol.TAG_STAN, voucherNumber); + writeTag(baos, EcrProtocol.TAG_APPROVAL_CODE, approvalCode); + writeTag(baos, EcrProtocol.TAG_RRN,referenceNum); + writeTag(baos, EcrProtocol.TAG_POS_ENTRY,entry); + } catch (IOException e) { + e.printStackTrace(); + } + return baos.toByteArray(); + } + + public static class Builder { + private String responseCode; + private String time; // len 6 + private String date; // len 4 + private String tid; + private String mid; + private String amount; + private String voucherNum; + private String approvalCode; + private String referenceNum; + + private String entry; + + public Builder setResponseCode(String responseCode) { this.responseCode = responseCode; return this; } + public Builder setTime(String time) { this.time = time; return this; } + public Builder setDate(String date) { this.date = date; return this; } + public Builder setTid(String tid) { this.tid = tid; return this; } + public Builder setMid(String mid) { this.mid = mid; return this; } + public Builder setAmount(String amount) { this.amount = amount; return this; } + public Builder setVoucherNumber(String voucherNum) { this.voucherNum = voucherNum; return this; } + public Builder setApprovalCode(String approvalCode) { this.approvalCode = approvalCode; return this; } + public Builder setReferenceNum(String referenceNum) { this.referenceNum = referenceNum; return this; } + public Builder setEntry(String entry) { this.entry = entry; return this; } + + public VoidResponse build() { return new VoidResponse(this); } + } +} diff --git a/cmhl/src/main/java/com/kizzy/cmhl/utils/ByteUtils.java b/cmhl/src/main/java/com/kizzy/cmhl/utils/ByteUtils.java new file mode 100644 index 0000000..1ba8771 --- /dev/null +++ b/cmhl/src/main/java/com/kizzy/cmhl/utils/ByteUtils.java @@ -0,0 +1,78 @@ +package com.kizzy.cmhl.utils; + +public class ByteUtils { + + /** + * Calculates LRC by XORing all bytes in the data. + * @param data + * @return + */ + public static byte calculateLrc(byte[] data) { + byte lrc = 0; + for (byte b : data) { + lrc ^= b; + } + return lrc; + } + +// public static byte[] hexStringToByteArray(String s) { +// int len = s.length(); +// byte[] data = new byte[len / 2]; +// for (int i = 0; i < len; i += 2) { +// data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4) +// + Character.digit(s.charAt(i+1), 16)); +// } +// return data; +// } + + public static byte[] hexStringToByteArray(String s) { + if (s == null) return new byte[0]; + // Ensure even length by padding with leading '0' if odd + if (s.length() % 2 != 0) { + s = "0" + s; + } + + int len = s.length(); + byte[] data = new byte[len / 2]; + for (int i = 0; i < len; i += 2) { + data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4) + + Character.digit(s.charAt(i+1), 16)); + } + return data; + } + + public static String byteArrayToHexString(byte[] bytes) { + StringBuilder sb = new StringBuilder(); + for (byte b : bytes) { + sb.append(String.format("%02X", b)); + } + return sb.toString(); + } + + public static byte[] concat(byte[]... arrays) { + int length = 0; + for (byte[] array : arrays) { + length += array.length; + } + byte[] result = new byte[length]; + int pos = 0; + for (byte[] array : arrays) { + System.arraycopy(array, 0, result, pos, array.length); + pos += array.length; + } + return result; + } + + public static byte[] intToBcd(int val) { + // Only handles up to 9999 for 2 bytes + int thousands = (val / 1000) % 10; + int hundreds = (val / 100) % 10; + int tens = (val / 10) % 10; + int ones = val % 10; + + byte b1 = (byte) ((thousands << 4) | hundreds); + byte b2 = (byte) ((tens << 4) | ones); + + return new byte[]{b1, b2}; + } +} diff --git a/cmhl/src/test/java/com/kizzy/cmhl/ExampleUnitTest.java b/cmhl/src/test/java/com/kizzy/cmhl/ExampleUnitTest.java new file mode 100644 index 0000000..7cfae42 --- /dev/null +++ b/cmhl/src/test/java/com/kizzy/cmhl/ExampleUnitTest.java @@ -0,0 +1,172 @@ +package com.kizzy.cmhl; + +import org.junit.Test; + +import static org.junit.Assert.*; + +import com.kizzy.cmhl.models.EcrPacket; +import com.kizzy.cmhl.models.PingRequest; +import com.kizzy.cmhl.models.PingResponse; +import com.kizzy.cmhl.models.PreAuthCancellationRequest; +import com.kizzy.cmhl.models.PreAuthCompletionRequest; +import com.kizzy.cmhl.models.PreAuthRequest; +import com.kizzy.cmhl.models.QrPaymentRequest; +import com.kizzy.cmhl.models.SaleRequest; +import com.kizzy.cmhl.models.SaleResponse; +import com.kizzy.cmhl.models.SettlementRequest; +import com.kizzy.cmhl.models.VoidQrPaymentRequest; +import com.kizzy.cmhl.models.VoidRequest; +import com.kizzy.cmhl.utils.ByteUtils; + +/** + * Example local unit test, which will execute on the development machine (host). + * + * @see Testing documentation + */ +public class ExampleUnitTest { + @Test + public void addition_isCorrect() { + assertEquals(4, 2 + 2); + } + + + @Test + public void testPing() { + PingResponse pingResponse = new PingResponse.Builder() + .setResponseCode("00") + .build(); + + byte[] packet = EcrManager.buildPacket(pingResponse); + + System.out.println("Packets :"+ByteUtils.byteArrayToHexString(packet)); + } + + + + @Test + public void testECRPacket() { + + byte[] data = { + (byte) 0x02, (byte) 0x00, (byte) 0x27, (byte) 0x01, (byte) 0x07, (byte) 0x37, + (byte) 0x0C, (byte) 0x37, (byte) 0x30, (byte) 0x30, (byte) 0x35, (byte) 0x31, + (byte) 0x37, (byte) 0x31, (byte) 0x34, (byte) 0x39, (byte) 0x31, (byte) 0x34, + (byte) 0x35, (byte) 0x38, (byte) 0x06, (byte) 0x30, (byte) 0x30, (byte) 0x34, + (byte) 0x35, (byte) 0x37, (byte) 0x30, (byte) 0x15, (byte) 0x01, (byte) 0x02, + (byte) 0x03, (byte) 0x31 + }; + + byte[] data2 = { + (byte) 0x02, (byte) 0x00, (byte) 0x21, (byte) 0x01, (byte) 0x0B, (byte) 0x04, + (byte) 0x0C, (byte) 0x30, (byte) 0x30, (byte) 0x30, (byte) 0x30, (byte) 0x30, + (byte) 0x31, (byte) 0x30, (byte) 0x30, (byte) 0x30, (byte) 0x30, (byte) 0x30, + (byte) 0x30, (byte) 0x49, (byte) 0x03, (byte) 0x31, (byte) 0x30, (byte) 0x34, + (byte) 0x03, (byte) 0x55 + }; + + byte[] data3 = { + (byte) 0x02, (byte) 0x00, (byte) 0x21, (byte) 0x01, (byte) 0x03, (byte) 0x04, + (byte) 0x0C, (byte) 0x30, (byte) 0x30, (byte) 0x30, (byte) 0x30, (byte) 0x30, + (byte) 0x31, (byte) 0x30, (byte) 0x30, (byte) 0x30, (byte) 0x30, (byte) 0x30, + (byte) 0x30, (byte) 0x49, (byte) 0x03, (byte) 0x31, (byte) 0x30, (byte) 0x34, + (byte) 0x03, (byte) 0x55 + }; + + byte[] ping = { + (byte) 0x02, (byte) 0x00, (byte) 0x02, (byte) 0x01, (byte) 0x01, (byte) 0x03, (byte) 0x02 + }; + + byte[] qrPayment = ByteUtils.hexStringToByteArray("020021010B040C3030303030303030313335304903313034035B"); + try { + EcrPacket ecrPacket = EcrManager.parsePacket(qrPayment); + + EcrRequestDispatcher.dispatch(ecrPacket, new EcrRequestListener() { + @Override + public void onPingRequest(PingRequest request) { + PingResponse pingResponse = new PingResponse.Builder() + .setResponseCode("00") + .build(); + + byte[] responsePacket = EcrManager.buildPacket(pingResponse); + System.out.println("Response: "+ ByteUtils.byteArrayToHexString(responsePacket)); + } + + @Override + public void onSaleRequest(SaleRequest request) { + System.out.println("command :"+ecrPacket.getCommandCode()); + + String amount = request.getAmount(); + + System.out.println("amount:"+amount); + + SaleResponse response = new SaleResponse.Builder() + .setResponseCode("00") + .setAmount(amount) + .setPan("411122*****122") + .setApprovalCode("123456") + .setInvoiceNumber("000002") + .build(); + + byte[] responsePacket = EcrManager.buildPacket(response); + System.out.println("Response: "+ ByteUtils.byteArrayToHexString(responsePacket)); + } + + @Override + public void onVoidRequest(VoidRequest request) { + + } + + @Override + public void onSettlementRequest(SettlementRequest request) { + + } + + @Override + public void onPreAuthRequest(PreAuthRequest request) { + + } + + @Override + public void onPreAuthCompletionRequest(PreAuthCompletionRequest request) { + + } + + @Override + public void onPreAuthCancellationRequest(PreAuthCancellationRequest request) { + + } + + @Override + public void onQrPaymentRequest(QrPaymentRequest request) { + System.out.println("command :"+ecrPacket.getCommandCode()); + + String amount = request.getAmount(); + String currency = request.getCurrencyCode(); + + System.out.println("amount:"+amount); + System.out.println("currency:"+currency); + } + + @Override + public void onVoidQrPaymentRequest(VoidQrPaymentRequest request) { + + } + + @Override + public void onUnknownRequest(EcrPacket packet) { + + } + }); + +// System.out.println("command :"+ecrPacket.getCommandCode()); +// +// QrPaymentRequest request = new QrPaymentRequest(ecrPacket); +// String amount = request.getAmount(); +// String currency = request.getCurrencyCode(); +// +// System.out.println("amount:"+amount); +// System.out.println("currency:"+currency); + } catch (Exception e) { + throw new RuntimeException(e); + } + } +} diff --git a/ecr-service-lib/.gitignore b/ecr-service-lib/.gitignore deleted file mode 100644 index 39bd49c..0000000 --- a/ecr-service-lib/.gitignore +++ /dev/null @@ -1,28 +0,0 @@ -/build - -# Built files -*.apk -*.aab -*.dex -*.class - -# Gradle -.gradle/ -build/ -*/build/ - -# Android Studio -*.iml -.idea/ -local.properties - -# Keystore -*.jks -*.keystore - -# OS files -.DS_Store -Thumbs.db - -# Logs -*.log \ No newline at end of file diff --git a/ecr-service-lib/build.gradle b/ecr-service-lib/build.gradle deleted file mode 100644 index 3f160bb..0000000 --- a/ecr-service-lib/build.gradle +++ /dev/null @@ -1,3 +0,0 @@ -configurations.maybeCreate("default") -artifacts.add("default", file('ecr-service-lib-1.1.11.aar')) -//artifacts.add("default", file('Link_POS_Library_V1.9.aar')) \ No newline at end of file diff --git a/ecr-service-lib/ecr-service-lib-1.1.11.aar b/ecr-service-lib/ecr-service-lib-1.1.11.aar deleted file mode 100644 index dfe9c382c0de7da933c2adf9922ebc0111c1637f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 52911 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 - - - - \ No newline at end of file diff --git a/ecr-service/.idea/caches/deviceStreaming.xml b/ecr-service/.idea/caches/deviceStreaming.xml deleted file mode 100644 index c62dc3d..0000000 --- a/ecr-service/.idea/caches/deviceStreaming.xml +++ /dev/null @@ -1,1282 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/ecr-service/.idea/gradle.xml b/ecr-service/.idea/gradle.xml deleted file mode 100644 index b838237..0000000 --- a/ecr-service/.idea/gradle.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/ecr-service/.idea/migrations.xml b/ecr-service/.idea/migrations.xml deleted file mode 100644 index f8051a6..0000000 --- a/ecr-service/.idea/migrations.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/ecr-service/.idea/misc.xml b/ecr-service/.idea/misc.xml deleted file mode 100644 index 3040d03..0000000 --- a/ecr-service/.idea/misc.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/ecr-service/.idea/runConfigurations.xml b/ecr-service/.idea/runConfigurations.xml deleted file mode 100644 index 16660f1..0000000 --- a/ecr-service/.idea/runConfigurations.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/ecr-service/.idea/vcs.xml b/ecr-service/.idea/vcs.xml deleted file mode 100644 index 6c0b863..0000000 --- a/ecr-service/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/ecr/build.gradle b/ecr/build.gradle index 3a52af9..eb1c676 100644 --- a/ecr/build.gradle +++ b/ecr/build.gradle @@ -31,12 +31,9 @@ android { dependencies { implementation fileTree(dir: "libs", include: ["*.jar","*.aar"]) -// implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" implementation 'androidx.core:core-ktx:1.6.0' implementation 'androidx.appcompat:appcompat:1.3.1' implementation project(path: ':nexsdk-lib') -// implementation files('libs/ecr-service-lib-1.1.11.aar') - implementation project(path: ':ecr-service-lib') implementation project(path: ':paylibs') implementation 'com.google.code.gson:gson:2.9.0' diff --git a/ecr/src/main/java/com/utsmyanmar/ecr/ECRHelper.kt b/ecr/src/main/java/com/utsmyanmar/ecr/ECRHelper.kt index c24cf8d..1cf15ce 100644 --- a/ecr/src/main/java/com/utsmyanmar/ecr/ECRHelper.kt +++ b/ecr/src/main/java/com/utsmyanmar/ecr/ECRHelper.kt @@ -1,25 +1,29 @@ package com.utsmyanmar.ecr import android.content.Context -import android.os.Bundle -import android.widget.Toast import com.nexgo.oaf.apiv3.APIProxy import com.nexgo.oaf.apiv3.SdkResult import com.nexgo.oaf.apiv3.device.serialport.SerialCfgEntity import com.nexgo.oaf.apiv3.device.serialport.SerialPortDriver -import com.pos.connection.bridge.ECRConnection -import com.pos.connection.bridge.ECRListener -import com.pos.connection.bridge.ECRRequestCallback -import com.pos.connection.bridge.ECRService -import com.pos.hardware.connection.library.ECRServiceKernel import com.utsmyanmar.paylibs.utils.LogUtil -import java.nio.charset.StandardCharsets +import com.utsmyanmar.paylibs.utils.core_utils.ByteUtil +import java.util.concurrent.atomic.AtomicBoolean + +/** + * NexGo SerialPortDriver wrapper (port 0 = physical USB serial). + * Spec: connect / send / recv / clrBuffer / disconnect. + */ object ECRHelper { - val TAG = ECRHelper::class.java.simpleName + val TAG: String = ECRHelper::class.java.simpleName + + private const val PORT_NO = 0 + private const val RECV_BUFFER_SIZE = 2048 + private const val RECV_TIMEOUT_MS = 3000L - private var ecrService: ECRService? = null private var serialPort: SerialPortDriver? = null + private var receiveThread: Thread? = null + private val receiving = AtomicBoolean(false) var onBindSuccess: () -> Unit = { } var onBindFailure: () -> Unit = { } @@ -32,12 +36,19 @@ object ECRHelper { var onECRReceive: (bytes: ByteArray) -> Unit = { } - // ─── Serial Port Init (call this once, e.g. in Application or Activity) ─── + val isConnected: Boolean + get() = serialPort != null fun initSerialPort(context: Context) { try { + if (serialPort != null) { + LogUtil.d(TAG, "Serial port already connected") + onBindSuccess() + return + } + val deviceEngine = APIProxy.getDeviceEngine(context) - val port = deviceEngine.getSerialPortDriver(0) + val port = deviceEngine.getSerialPortDriver(PORT_NO) val entity = SerialCfgEntity().apply { baudRate = 115200 @@ -49,44 +60,46 @@ object ECRHelper { val result = port.connect(entity) if (result == SdkResult.Success) { serialPort = port - LogUtil.d(TAG, "USB Serial connected") + port.clrBuffer() + ECRUtil.connected = true + LogUtil.d(TAG, "SerialPortDriver connected (port=$PORT_NO)") + startReceiveLoop() + onECRConnected() + onBindSuccess() } else { - LogUtil.e(TAG, "USB Serial connect failed: $result") + LogUtil.e(TAG, "SerialPortDriver connect failed: $result") + ECRUtil.connected = false + onBindFailure() onSendFailure(result, "Serial port connect failed") } } catch (ex: Throwable) { ex.printStackTrace() + ECRUtil.connected = false + onBindFailure() onSendFailure(-200, ex.localizedMessage ?: "Serial init error") } } - fun releaseSerialPort() { - try { - serialPort?.disconnect() - } catch (ex: Throwable) { - ex.printStackTrace() - } finally { - serialPort = null - } - } - - // ─── Send: uses Serial Port instead of ECRService ───────────────────────── - fun send(bytes: ByteArray) { try { val port = serialPort - if (port != null) { - val result = port.send(bytes, bytes.size) - if (result == SdkResult.Success) { - LogUtil.d(TAG, "Serial send success") - onSendSuccess() - } else { - LogUtil.e(TAG, "Serial send failed: $result") - onSendFailure(result, "Serial send failed") - } - } else { + if (port == null) { LogUtil.e(TAG, "Serial port not initialized") onSendFailure(-100, "Serial port not initialized") + return + } + if (bytes.isEmpty() || bytes.size > RECV_BUFFER_SIZE) { + onSendFailure(SdkResult.Param_In_Invalid, "Invalid send length") + return + } + + val result = port.send(bytes, bytes.size) + if (result == SdkResult.Success) { + LogUtil.d(TAG, "Serial send success, len=${bytes.size}") + onSendSuccess() + } else { + LogUtil.e(TAG, "Serial send failed: $result") + onSendFailure(result, "Serial send failed") } } catch (ex: Throwable) { ex.printStackTrace() @@ -94,98 +107,63 @@ object ECRHelper { } } - // ─── Everything below is unchanged ──────────────────────────────────────── - - fun connect(bundle: Bundle) { - call { - anyExecute(ecrService) { - connect(bundle, ecrConnection) - } + fun clrBuffer() { + try { + serialPort?.clrBuffer() + } catch (ex: Throwable) { + ex.printStackTrace() } } fun disconnect() { - call { anyExecute(ecrService) { disconnect() } } + releaseSerialPort() } - fun registerECRListener() { - call { anyExecute(ecrService) { register(ecrListener) } } - } - - fun unregisterECRListener() { - call { anyExecute(ecrService) { unregister(ecrListener) } } - } - - fun extensionMethod(bundle: Bundle) { - call { anyExecute(ecrService) { extensionMethod(bundle) } } - } - - private val ecrListener = object : ECRListener.Stub() { - override fun onReceive(byteArray: ByteArray?) { - if (byteArray != null) { - val string = String(byteArray, StandardCharsets.UTF_8) - Logger.e(TAG, "onReceive string: $string") - onECRReceive(byteArray) - } - } - } - - private val requestCallback = object : ECRRequestCallback.Stub() { - override fun onSuccess() { - Logger.e(TAG, "onSuccess") - onSendSuccess() - } - - override fun onFailure(code: Int, massage: String?) { - Logger.e(TAG, "onFailure code: $code massage: $massage") - onSendFailure(code, massage ?: "failure") - } - } - - private val ecrConnection = object : ECRConnection.Stub() { - override fun onConnected() { - Logger.d(TAG, "onConnected") - ECRUtil.connected = true - onECRConnected() - } - - override fun onDisconnected(code: Int, massage: String?) { - Logger.d(TAG, "onDisconnected code: $code massage: $massage") - ECRUtil.connected = false - onECRDisconnected(code, massage ?: "failure") - } - } - - fun bindECRService() { - ECRServiceKernel.getInstance().bindService(ECRUtil.context, connectionCallback) - } - - private val connectionCallback = object : ECRServiceKernel.ConnectionCallback { - override fun onServiceConnected() { - Logger.e(TAG, "onServiceConnected") - ecrService = ECRServiceKernel.getInstance().ecrService - onBindSuccess() - } - - override fun onServiceDisconnected() { - Logger.e(TAG, "onServiceDisconnected") - ECRUtil.connected = false - ecrService = null - onBindFailure() - } - } - - private fun call(block: () -> Unit) { + fun releaseSerialPort() { + receiving.set(false) try { - val bridgeService = ecrService - if (bridgeService != null) { - block() - } else { - onECRDisconnected(-100, "The bind ECRService failure") - } + receiveThread?.interrupt() + } catch (_: Throwable) { + } + receiveThread = null + + try { + val result = serialPort?.disconnect() ?: SdkResult.SerialPort_Port_Not_Open + LogUtil.d(TAG, "SerialPortDriver disconnect: $result") } catch (ex: Throwable) { ex.printStackTrace() - onECRDisconnected(-200, ex.localizedMessage ?: "") + } finally { + serialPort = null + ECRUtil.connected = false + onECRDisconnected(0, "disconnected") } } -} \ No newline at end of file + + private fun startReceiveLoop() { + if (receiving.getAndSet(true)) { + return + } + + receiveThread = Thread({ + val buffer = ByteArray(RECV_BUFFER_SIZE) + while (receiving.get()) { + val port = serialPort ?: break + try { + val len = port.recv(buffer, buffer.size, RECV_TIMEOUT_MS) + if (len > 0) { + val data = buffer.copyOf(len) + LogUtil.d(TAG, "Serial recv len=$len") + LogUtil.d(TAG," Receive packet : "+ ByteUtil.bytes2HexStr(data)); + onECRReceive(data) + } + // timeout / no data: keep listening + } catch (ex: Throwable) { + if (receiving.get()) { + LogUtil.e(TAG, "Serial recv error: ${ex.message}") + } + } + } + receiving.set(false) + }, "ECR-SerialRecv").also { it.start() } + } +} diff --git a/settings.gradle b/settings.gradle index 53bde9e..9614d1c 100644 --- a/settings.gradle +++ b/settings.gradle @@ -33,9 +33,9 @@ include ':mpulib' //include ':ecr-client-lib' //include ':ecr-service' +//include ':ecr-service-lib' include ':xpay' include ':ecr' -include ':ecr-service-lib' include ':link-service-lib' include ':qrgen-lib' include ':nexdlkey-lib'