diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index d03d974..a17b804 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -20,6 +20,10 @@ simpleXml = "2.7.1" junit = "4.13.2" mockk = "1.13.8" kotest = "5.8.0" +robolectric = "4.14" +androidxCoreTest = "1.6.1" +kotestRobolectric = "0.4.0" +kotestExtensionsAndroid = "0.1.1" lifecycle = "2.7.0" activity-compose = "1.8.2" @@ -78,6 +82,10 @@ kotest-assertions = { group = "io.kotest", name = "kotest-assertions-core-jvm", kotest-property = { group = "io.kotest", name = "kotest-property-jvm", version.ref = "kotest" } kotest-api = { group = "io.kotest", name = "kotest-framework-api", version.ref = "kotest" } lifecycle-runtime-testing = { group = "androidx.lifecycle", name = "lifecycle-runtime-testing", version.ref = "lifecycle" } +robolectric = { group = "org.robolectric", name = "robolectric", version.ref = "robolectric" } +kotest-robolectric = { group = "io.kotest.extensions", name = "kotest-extensions-robolectric", version.ref = "kotestRobolectric" } +kotest-extensions-android = { group = "br.com.colman", name = "kotest-extensions-android", version.ref = "kotestExtensionsAndroid" } +androidx-test-core = { group = "androidx.test", name = "core-ktx", version.ref = "androidxCoreTest" } [plugins] androidApplication = { id = "com.android.application", version.ref = "agp" } @@ -114,6 +122,12 @@ testing = [ "kotest.assertions", "kotest.property", "kotest-api", - "lifecycle-runtime-testing" + "lifecycle-runtime-testing", +] +robolectric = [ + "robolectric", + "kotest-robolectric", + "androidx-test-core", + "kotest-extensions-android" ] diff --git a/payment-engine/build.gradle.kts b/payment-engine/build.gradle.kts index 874ef10..b2f930e 100644 --- a/payment-engine/build.gradle.kts +++ b/payment-engine/build.gradle.kts @@ -84,6 +84,7 @@ dependencies { // Unit tests testImplementation(libs.bundles.testing) + testImplementation(libs.bundles.robolectric) } afterEvaluate { @@ -92,7 +93,7 @@ afterEvaluate { create("payment-engine") { groupId = "de.tillhub.paymentengine" artifactId = "payment-engine" - version = "2.2.0" + version = "3.0.0" from(components.getByName("release")) } diff --git a/payment-engine/config/detekt.yml b/payment-engine/config/detekt.yml index 8b39eed..5f4f2bd 100644 --- a/payment-engine/config/detekt.yml +++ b/payment-engine/config/detekt.yml @@ -769,6 +769,7 @@ libraries: - '**/main/java/de/tillhub/paymentengine/ReconciliationManager.kt' - '**/main/java/de/tillhub/paymentengine/RefundManager.kt' - '**/main/java/de/tillhub/paymentengine/ReversalManager.kt' + - '**/main/java/de/tillhub/paymentengine/ConnectionManager.kt' - '**/main/java/de/tillhub/paymentengine/data/CardSaleConfig.kt' - '**/main/java/de/tillhub/paymentengine/data/ISOAlphaCurrency.kt' - '**/main/java/de/tillhub/paymentengine/data/Payment.kt' diff --git a/payment-engine/proguard-rules.pro b/payment-engine/proguard-rules.pro index 68754d5..5fdbc20 100644 --- a/payment-engine/proguard-rules.pro +++ b/payment-engine/proguard-rules.pro @@ -37,6 +37,7 @@ -keep class de.tillhub.paymentengine.ReconciliationManager { *; } -keep class de.tillhub.paymentengine.RefundManager { *; } -keep class de.tillhub.paymentengine.ReversalManager { *; } +-keep class de.tillhub.paymentengine.ConnectionManager { *; } -keep class de.tillhub.paymentengine.helper.SingletonHolder { *; } -keep class de.tillhub.paymentengine.analytics.PaymentAnalytics { *; } diff --git a/payment-engine/src/main/AndroidManifest.xml b/payment-engine/src/main/AndroidManifest.xml index 9d478f1..dc12f32 100644 --- a/payment-engine/src/main/AndroidManifest.xml +++ b/payment-engine/src/main/AndroidManifest.xml @@ -7,6 +7,10 @@ + + + + , + terminalState: MutableStateFlow, + resultCaller: ActivityResultCaller, + private val connectContract: ActivityResultLauncher = + resultCaller.registerForActivityResult(TerminalConnectContract()) { result -> + terminalState.tryEmit(result) + }, + private val disconnectContract: ActivityResultLauncher = + resultCaller.registerForActivityResult(TerminalDisconnectContract()) { result -> + terminalState.tryEmit(result) + } +) : CardManagerImpl(configs, terminalState), ConnectionManager { + + override fun startSPOSConnect() { + val configName = configs.values.firstOrNull()?.name.orEmpty() + startSPOSConnect(configName) + } + + override fun startSPOSConnect(configName: String) { + val terminalConfig = configs.getOrDefault(configName, defaultConfig) + startSPOSConnect(terminalConfig) + } + + override fun startSPOSConnect(config: Terminal) { + terminalState.tryEmit(TerminalOperationStatus.Pending.Connecting) + connectContract.launch(config) + } + + override fun startSPOSDisconnect() { + val configName = configs.values.firstOrNull()?.name.orEmpty() + startSPOSDisconnect(configName) + } + + override fun startSPOSDisconnect(configName: String) { + val terminalConfig = configs.getOrDefault(configName, defaultConfig) + startSPOSDisconnect(terminalConfig) + } + + override fun startSPOSDisconnect(config: Terminal) { + terminalState.tryEmit(TerminalOperationStatus.Pending.Disconnecting) + disconnectContract.launch(config) + } +} \ No newline at end of file diff --git a/payment-engine/src/main/java/de/tillhub/paymentengine/PaymentEngine.kt b/payment-engine/src/main/java/de/tillhub/paymentengine/PaymentEngine.kt index 2d9cafd..bb403ac 100644 --- a/payment-engine/src/main/java/de/tillhub/paymentengine/PaymentEngine.kt +++ b/payment-engine/src/main/java/de/tillhub/paymentengine/PaymentEngine.kt @@ -50,5 +50,11 @@ class PaymentEngine private constructor() { } } + fun newConnectionManager(registry: ActivityResultCaller): ConnectionManager { + return ConnectionManagerImpl(configs, terminalState, registry).also { + terminalState.tryEmit(TerminalOperationStatus.Waiting) + } + } + companion object : SingletonHolder(::PaymentEngine) } \ No newline at end of file diff --git a/payment-engine/src/main/java/de/tillhub/paymentengine/PaymentManager.kt b/payment-engine/src/main/java/de/tillhub/paymentengine/PaymentManager.kt index 9082bd5..e872aed 100644 --- a/payment-engine/src/main/java/de/tillhub/paymentengine/PaymentManager.kt +++ b/payment-engine/src/main/java/de/tillhub/paymentengine/PaymentManager.kt @@ -15,44 +15,71 @@ import java.math.BigDecimal * it sets up the manager so the data from the transaction is collected correctly. */ interface PaymentManager : CardManager { - fun startPaymentTransaction(amount: BigDecimal, currency: ISOAlphaCurrency) - fun startPaymentTransaction(amount: BigDecimal, currency: ISOAlphaCurrency, configName: String) - fun startPaymentTransaction(amount: BigDecimal, currency: ISOAlphaCurrency, config: Terminal) + fun startPaymentTransaction( + transactionId: String, + amount: BigDecimal, + tip: BigDecimal = BigDecimal.ZERO, + currency: ISOAlphaCurrency + ) + + fun startPaymentTransaction( + transactionId: String, + amount: BigDecimal, + tip: BigDecimal = BigDecimal.ZERO, + currency: ISOAlphaCurrency, + configName: String + ) + + fun startPaymentTransaction( + transactionId: String, + amount: BigDecimal, + tip: BigDecimal = BigDecimal.ZERO, + currency: ISOAlphaCurrency, + config: Terminal + ) } internal class PaymentManagerImpl( configs: MutableMap, terminalState: MutableStateFlow, - resultCaller: ActivityResultCaller -) : CardManagerImpl(configs, terminalState), PaymentManager { - + resultCaller: ActivityResultCaller, private val paymentResultContract: ActivityResultLauncher = resultCaller.registerForActivityResult(PaymentResultContract()) { result -> terminalState.tryEmit(result) } +) : CardManagerImpl(configs, terminalState), PaymentManager { - override fun startPaymentTransaction(amount: BigDecimal, currency: ISOAlphaCurrency) { + override fun startPaymentTransaction( + transactionId: String, + amount: BigDecimal, + tip: BigDecimal, + currency: ISOAlphaCurrency + ) { val configName = configs.values.firstOrNull()?.name.orEmpty() - startPaymentTransaction(amount, currency, configName) + startPaymentTransaction(transactionId, amount, tip, currency, configName) } override fun startPaymentTransaction( + transactionId: String, amount: BigDecimal, + tip: BigDecimal, currency: ISOAlphaCurrency, configName: String ) { val terminalConfig = configs.getOrDefault(configName, defaultConfig) - startPaymentTransaction(amount, currency, terminalConfig) + startPaymentTransaction(transactionId, amount, tip, currency, terminalConfig) } override fun startPaymentTransaction( + transactionId: String, amount: BigDecimal, + tip: BigDecimal, currency: ISOAlphaCurrency, config: Terminal ) { terminalState.tryEmit(TerminalOperationStatus.Pending.Payment(amount, currency)) paymentResultContract.launch( - PaymentRequest(config, amount, currency) + PaymentRequest(config, transactionId, amount, tip, currency) ) } } \ No newline at end of file diff --git a/payment-engine/src/main/java/de/tillhub/paymentengine/ReconciliationManager.kt b/payment-engine/src/main/java/de/tillhub/paymentengine/ReconciliationManager.kt index 577ccd5..23fe497 100644 --- a/payment-engine/src/main/java/de/tillhub/paymentengine/ReconciliationManager.kt +++ b/payment-engine/src/main/java/de/tillhub/paymentengine/ReconciliationManager.kt @@ -20,13 +20,12 @@ interface ReconciliationManager : CardManager { internal class ReconciliationManagerImpl( configs: MutableMap, terminalState: MutableStateFlow, - resultCaller: ActivityResultCaller -) : CardManagerImpl(configs, terminalState), ReconciliationManager { - + resultCaller: ActivityResultCaller, private val reconciliationContract: ActivityResultLauncher = resultCaller.registerForActivityResult(TerminalReconciliationContract()) { result -> terminalState.tryEmit(result) } +) : CardManagerImpl(configs, terminalState), ReconciliationManager { override fun startReconciliation() { val configName = configs.values.firstOrNull()?.name.orEmpty() diff --git a/payment-engine/src/main/java/de/tillhub/paymentengine/RefundManager.kt b/payment-engine/src/main/java/de/tillhub/paymentengine/RefundManager.kt index 430ac42..a153ad6 100644 --- a/payment-engine/src/main/java/de/tillhub/paymentengine/RefundManager.kt +++ b/payment-engine/src/main/java/de/tillhub/paymentengine/RefundManager.kt @@ -15,9 +15,25 @@ import java.math.BigDecimal * it sets up the manager so the data from the transaction is collected correctly. */ interface RefundManager : CardManager { - fun startRefundTransaction(amount: BigDecimal, currency: ISOAlphaCurrency) - fun startRefundTransaction(amount: BigDecimal, currency: ISOAlphaCurrency, configName: String) - fun startRefundTransaction(amount: BigDecimal, currency: ISOAlphaCurrency, config: Terminal) + fun startRefundTransaction( + transactionId: String, + amount: BigDecimal, + currency: ISOAlphaCurrency + ) + + fun startRefundTransaction( + transactionId: String, + amount: BigDecimal, + currency: ISOAlphaCurrency, + configName: String + ) + + fun startRefundTransaction( + transactionId: String, + amount: BigDecimal, + currency: ISOAlphaCurrency, + config: Terminal + ) } internal class RefundManagerImpl( @@ -31,28 +47,39 @@ internal class RefundManagerImpl( terminalState.tryEmit(result) } - override fun startRefundTransaction(amount: BigDecimal, currency: ISOAlphaCurrency) { + override fun startRefundTransaction( + transactionId: String, + amount: BigDecimal, + currency: ISOAlphaCurrency + ) { val configName = configs.values.firstOrNull()?.name.orEmpty() - startRefundTransaction(amount, currency, configName) + startRefundTransaction(transactionId, amount, currency, configName) } override fun startRefundTransaction( + transactionId: String, amount: BigDecimal, currency: ISOAlphaCurrency, configName: String ) { val terminalConfig = configs.getOrDefault(configName, defaultConfig) - startRefundTransaction(amount, currency, terminalConfig) + startRefundTransaction(transactionId, amount, currency, terminalConfig) } override fun startRefundTransaction( + transactionId: String, amount: BigDecimal, currency: ISOAlphaCurrency, config: Terminal ) { terminalState.tryEmit(TerminalOperationStatus.Pending.Refund(amount, currency)) refundContract.launch( - RefundRequest(config, amount, currency) + RefundRequest( + config = config, + transactionId = transactionId, + amount = amount, + currency = currency + ) ) } } \ No newline at end of file diff --git a/payment-engine/src/main/java/de/tillhub/paymentengine/ReversalManager.kt b/payment-engine/src/main/java/de/tillhub/paymentengine/ReversalManager.kt index 5002bd9..23340bd 100644 --- a/payment-engine/src/main/java/de/tillhub/paymentengine/ReversalManager.kt +++ b/payment-engine/src/main/java/de/tillhub/paymentengine/ReversalManager.kt @@ -4,45 +4,109 @@ import androidx.activity.result.ActivityResultCaller import androidx.activity.result.ActivityResultLauncher import de.tillhub.paymentengine.contract.PaymentReversalContract import de.tillhub.paymentengine.contract.ReversalRequest +import de.tillhub.paymentengine.data.ISOAlphaCurrency import de.tillhub.paymentengine.data.Terminal import de.tillhub.paymentengine.data.TerminalOperationStatus import kotlinx.coroutines.flow.MutableStateFlow +import java.math.BigDecimal /** * This is called to start of a card payment reversal, * it sets up the manager so the data from the transaction is collected correctly. */ interface ReversalManager : CardManager { - fun startReversalTransaction(receiptNo: String) - fun startReversalTransaction(receiptNo: String, configName: String) - fun startReversalTransaction(receiptNo: String, config: Terminal) + fun startReversalTransaction( + transactionId: String, + amount: BigDecimal, + tip: BigDecimal = BigDecimal.ZERO, + currency: ISOAlphaCurrency, + receiptNo: String + ) + + fun startReversalTransaction( + transactionId: String, + amount: BigDecimal, + tip: BigDecimal = BigDecimal.ZERO, + currency: ISOAlphaCurrency, + configName: String, + receiptNo: String, + ) + + fun startReversalTransaction( + transactionId: String, + amount: BigDecimal, + tip: BigDecimal = BigDecimal.ZERO, + currency: ISOAlphaCurrency, + config: Terminal, + receiptNo: String + ) } internal class ReversalManagerImpl( configs: MutableMap, - transactionState: MutableStateFlow, - resultCaller: ActivityResultCaller -) : CardManagerImpl(configs, transactionState), ReversalManager { - + terminalState: MutableStateFlow, + resultCaller: ActivityResultCaller, private val reversalContract: ActivityResultLauncher = resultCaller.registerForActivityResult(PaymentReversalContract()) { result -> terminalState.tryEmit(result) } +) : CardManagerImpl(configs, terminalState), ReversalManager { - override fun startReversalTransaction(receiptNo: String) { + override fun startReversalTransaction( + transactionId: String, + amount: BigDecimal, + tip: BigDecimal, + currency: ISOAlphaCurrency, + receiptNo: String + ) { val configName = configs.values.firstOrNull()?.name.orEmpty() - startReversalTransaction(receiptNo, configName) + startReversalTransaction( + transactionId = transactionId, + amount = amount, + tip = tip, + currency = currency, + configName = configName, + receiptNo = receiptNo + ) } - override fun startReversalTransaction(receiptNo: String, configName: String) { + override fun startReversalTransaction( + transactionId: String, + amount: BigDecimal, + tip: BigDecimal, + currency: ISOAlphaCurrency, + configName: String, + receiptNo: String + ) { val terminalConfig = configs.getOrDefault(configName, defaultConfig) - startReversalTransaction(receiptNo, terminalConfig) + startReversalTransaction( + transactionId = transactionId, + amount = amount, + tip = tip, + currency = currency, + config = terminalConfig, + receiptNo = receiptNo + ) } - override fun startReversalTransaction(receiptNo: String, config: Terminal) { + override fun startReversalTransaction( + transactionId: String, + amount: BigDecimal, + tip: BigDecimal, + currency: ISOAlphaCurrency, + config: Terminal, + receiptNo: String + ) { terminalState.tryEmit(TerminalOperationStatus.Pending.Reversal(receiptNo)) reversalContract.launch( - ReversalRequest(config, receiptNo) + ReversalRequest( + transactionId = transactionId, + amount = amount, + tip = tip, + currency = currency, + config = config, + receiptNo = receiptNo + ) ) } } \ No newline at end of file diff --git a/payment-engine/src/main/java/de/tillhub/paymentengine/contract/PaymentContract.kt b/payment-engine/src/main/java/de/tillhub/paymentengine/contract/PaymentContract.kt index 9dfb598..5e32fcc 100644 --- a/payment-engine/src/main/java/de/tillhub/paymentengine/contract/PaymentContract.kt +++ b/payment-engine/src/main/java/de/tillhub/paymentengine/contract/PaymentContract.kt @@ -5,48 +5,106 @@ import android.content.Context import android.content.Intent import androidx.activity.result.contract.ActivityResultContract import androidx.core.os.BundleCompat +import de.tillhub.paymentengine.PaymentEngine +import de.tillhub.paymentengine.analytics.PaymentAnalytics import de.tillhub.paymentengine.data.ExtraKeys import de.tillhub.paymentengine.data.ISOAlphaCurrency import de.tillhub.paymentengine.data.Terminal import de.tillhub.paymentengine.data.TerminalOperationStatus import de.tillhub.paymentengine.opi.ui.OPIPaymentActivity +import de.tillhub.paymentengine.spos.AnalyticsMessageFactory +import de.tillhub.paymentengine.spos.SPOSIntentFactory +import de.tillhub.paymentengine.spos.SPOSResponseHandler +import de.tillhub.paymentengine.spos.data.SPOSKey import de.tillhub.paymentengine.zvt.ui.CardPaymentActivity import java.math.BigDecimal import java.util.Objects -class PaymentResultContract : ActivityResultContract() { +class PaymentResultContract( + private val analytics: PaymentAnalytics? = PaymentEngine.getInstance().paymentAnalytics +) : ActivityResultContract() { override fun createIntent(context: Context, input: PaymentRequest): Intent { return when (input.config) { is Terminal.ZVT -> Intent(context, CardPaymentActivity::class.java).apply { putExtra(ExtraKeys.EXTRA_CONFIG, input.config) - putExtra(ExtraKeys.EXTRA_AMOUNT, input.amount) + putExtra(ExtraKeys.EXTRA_AMOUNT, input.amount + input.tip) putExtra(ExtraKeys.EXTRA_CURRENCY, input.currency) } is Terminal.OPI -> Intent(context, OPIPaymentActivity::class.java).apply { putExtra(ExtraKeys.EXTRA_CONFIG, input.config) - putExtra(ExtraKeys.EXTRA_AMOUNT, input.amount) + putExtra(ExtraKeys.EXTRA_AMOUNT, input.amount + input.tip) putExtra(ExtraKeys.EXTRA_CURRENCY, input.currency) } + is Terminal.SPOS -> SPOSIntentFactory.createPaymentIntent(input) + }.also { + analytics?.logOperation(AnalyticsMessageFactory.createPaymentOperation(input)) } } override fun parseResult(resultCode: Int, intent: Intent?): TerminalOperationStatus { - return intent.takeIf { resultCode == Activity.RESULT_OK }?.extras?.let { - BundleCompat.getParcelable(it, ExtraKeys.EXTRAS_RESULT, TerminalOperationStatus::class.java) - } ?: TerminalOperationStatus.Canceled + return if (resultCode == Activity.RESULT_OK) { + if (intent?.extras?.containsKey(ExtraKeys.EXTRAS_RESULT) == true) { + intent.extras?.let { + BundleCompat.getParcelable( + it, + ExtraKeys.EXTRAS_RESULT, + TerminalOperationStatus::class.java + ) + } ?: TerminalOperationStatus.Canceled + } else { + SPOSResponseHandler.handleTransactionResponse(resultCode, intent).also { + analytics?.logCommunication( + protocol = SPOS_PROTOCOL, + message = AnalyticsMessageFactory.createResultOk(intent?.extras) + ) + } + } + } else { + if (intent?.extras?.containsKey(SPOSKey.ResultExtra.ERROR) == true) { + SPOSResponseHandler.handleTransactionResponse(resultCode, intent).also { + analytics?.logCommunication( + protocol = SPOS_PROTOCOL, + message = AnalyticsMessageFactory.createResultCanceled(intent.extras) + ) + } + } else { + TerminalOperationStatus.Canceled + } + } + } + + companion object { + private const val SPOS_PROTOCOL = "SPOS" } } +/*** + * @param config - terminal config used for this request + * @param transactionId - id of the transaction that will be created by the request (S-POS only) + * @param amount - amount being charged in the created transaction + * @param tip - tip being charged in the created transaction, by default it will be set to 0.00 + * @param currency - currency the transaction will be made in + */ class PaymentRequest( val config: Terminal, + val transactionId: String, val amount: BigDecimal, + val tip: BigDecimal = BigDecimal.ZERO, val currency: ISOAlphaCurrency ) { - override fun toString() = "PaymentRequest(config=$config, amount=$amount, currency=$currency)" + override fun toString() = "PaymentRequest(" + + "config=$config, " + + "transactionId=$transactionId, " + + "amount=$amount, " + + "tip=$tip, " + + "currency=$currency" + + ")" override fun equals(other: Any?) = other is PaymentRequest && config == other.config && + transactionId == other.transactionId && amount == other.amount && + tip == other.tip && currency == other.currency - override fun hashCode() = Objects.hash(config, amount, currency) + override fun hashCode() = Objects.hash(config, transactionId, amount, tip, currency) } \ No newline at end of file diff --git a/payment-engine/src/main/java/de/tillhub/paymentengine/contract/PaymentRefundContract.kt b/payment-engine/src/main/java/de/tillhub/paymentengine/contract/PaymentRefundContract.kt index 268604c..fa1a89a 100644 --- a/payment-engine/src/main/java/de/tillhub/paymentengine/contract/PaymentRefundContract.kt +++ b/payment-engine/src/main/java/de/tillhub/paymentengine/contract/PaymentRefundContract.kt @@ -5,16 +5,24 @@ import android.content.Context import android.content.Intent import androidx.activity.result.contract.ActivityResultContract import androidx.core.os.BundleCompat +import de.tillhub.paymentengine.PaymentEngine +import de.tillhub.paymentengine.analytics.PaymentAnalytics import de.tillhub.paymentengine.data.ExtraKeys import de.tillhub.paymentengine.data.ISOAlphaCurrency import de.tillhub.paymentengine.data.Terminal import de.tillhub.paymentengine.data.TerminalOperationStatus import de.tillhub.paymentengine.opi.ui.OPIPartialRefundActivity +import de.tillhub.paymentengine.spos.AnalyticsMessageFactory +import de.tillhub.paymentengine.spos.SPOSIntentFactory +import de.tillhub.paymentengine.spos.SPOSResponseHandler +import de.tillhub.paymentengine.spos.data.SPOSKey import de.tillhub.paymentengine.zvt.ui.CardPaymentPartialRefundActivity import java.math.BigDecimal import java.util.Objects -class PaymentRefundContract : ActivityResultContract() { +class PaymentRefundContract( + private val analytics: PaymentAnalytics? = PaymentEngine.getInstance().paymentAnalytics +) : ActivityResultContract() { override fun createIntent(context: Context, input: RefundRequest): Intent { return when (input.config) { @@ -29,22 +37,59 @@ class PaymentRefundContract : ActivityResultContract SPOSIntentFactory.createPaymentRefundIntent(input) + }.also { + analytics?.logOperation(AnalyticsMessageFactory.createRefundOperation(input)) } } override fun parseResult(resultCode: Int, intent: Intent?): TerminalOperationStatus { - return intent.takeIf { resultCode == Activity.RESULT_OK }?.extras?.let { - BundleCompat.getParcelable( - it, - ExtraKeys.EXTRAS_RESULT, - TerminalOperationStatus::class.java - ) - } ?: TerminalOperationStatus.Canceled + return if (resultCode == Activity.RESULT_OK) { + if (intent?.extras?.containsKey(ExtraKeys.EXTRAS_RESULT) == true) { + intent.extras?.let { + BundleCompat.getParcelable( + it, + ExtraKeys.EXTRAS_RESULT, + TerminalOperationStatus::class.java + ) + } ?: TerminalOperationStatus.Canceled + } else { + SPOSResponseHandler.handleTransactionResponse(resultCode, intent).also { + analytics?.logCommunication( + protocol = SPOS_PROTOCOL, + message = AnalyticsMessageFactory.createResultOk(intent?.extras) + ) + } + } + } else { + if (intent?.extras?.containsKey(SPOSKey.ResultExtra.ERROR) == true) { + SPOSResponseHandler.handleTransactionResponse(resultCode, intent).also { + analytics?.logCommunication( + protocol = SPOS_PROTOCOL, + message = AnalyticsMessageFactory.createResultCanceled(intent.extras) + ) + } + } else { + TerminalOperationStatus.Canceled + } + } + } + + companion object { + private const val SPOS_PROTOCOL = "SPOS" } } +/*** + * @param config - terminal config used for this request + * @param transactionId - id of the transaction that will be created by the request (S-POS only) + * @param amount - amount being refunded in this transaction + * @param currency - currency the transaction will be made in + */ class RefundRequest( val config: Terminal, + val transactionId: String, val amount: BigDecimal, val currency: ISOAlphaCurrency ) { diff --git a/payment-engine/src/main/java/de/tillhub/paymentengine/contract/PaymentReversalContract.kt b/payment-engine/src/main/java/de/tillhub/paymentengine/contract/PaymentReversalContract.kt index 4e6b60d..cbe1c91 100644 --- a/payment-engine/src/main/java/de/tillhub/paymentengine/contract/PaymentReversalContract.kt +++ b/payment-engine/src/main/java/de/tillhub/paymentengine/contract/PaymentReversalContract.kt @@ -5,14 +5,24 @@ import android.content.Context import android.content.Intent import androidx.activity.result.contract.ActivityResultContract import androidx.core.os.BundleCompat +import de.tillhub.paymentengine.PaymentEngine +import de.tillhub.paymentengine.analytics.PaymentAnalytics import de.tillhub.paymentengine.data.ExtraKeys +import de.tillhub.paymentengine.data.ISOAlphaCurrency import de.tillhub.paymentengine.data.Terminal import de.tillhub.paymentengine.data.TerminalOperationStatus import de.tillhub.paymentengine.opi.ui.OPIPaymentReversalActivity +import de.tillhub.paymentengine.spos.AnalyticsMessageFactory +import de.tillhub.paymentengine.spos.SPOSIntentFactory +import de.tillhub.paymentengine.spos.SPOSResponseHandler +import de.tillhub.paymentengine.spos.data.SPOSKey import de.tillhub.paymentengine.zvt.ui.CardPaymentReversalActivity +import java.math.BigDecimal import java.util.Objects -class PaymentReversalContract : ActivityResultContract() { +class PaymentReversalContract( + private val analytics: PaymentAnalytics? = PaymentEngine.getInstance().paymentAnalytics +) : ActivityResultContract() { override fun createIntent(context: Context, input: ReversalRequest): Intent { return when (input.config) { @@ -25,23 +35,82 @@ class PaymentReversalContract : ActivityResultContract SPOSIntentFactory.createPaymentReversalIntent(input) + }.also { + analytics?.logOperation(AnalyticsMessageFactory.createReversalOperation(input)) } } override fun parseResult(resultCode: Int, intent: Intent?): TerminalOperationStatus { - return intent.takeIf { resultCode == Activity.RESULT_OK }?.extras?.let { - BundleCompat.getParcelable(it, ExtraKeys.EXTRAS_RESULT, TerminalOperationStatus::class.java) - } ?: TerminalOperationStatus.Canceled + return if (resultCode == Activity.RESULT_OK) { + if (intent?.extras?.containsKey(ExtraKeys.EXTRAS_RESULT) == true) { + intent.extras?.let { + BundleCompat.getParcelable( + it, + ExtraKeys.EXTRAS_RESULT, + TerminalOperationStatus::class.java + ) + } ?: TerminalOperationStatus.Canceled + } else { + SPOSResponseHandler.handleTransactionResponse(resultCode, intent).also { + analytics?.logCommunication( + protocol = SPOS_PROTOCOL, + message = AnalyticsMessageFactory.createResultOk(intent?.extras) + ) + } + } + } else { + if (intent?.extras?.containsKey(SPOSKey.ResultExtra.ERROR) == true) { + SPOSResponseHandler.handleTransactionResponse(resultCode, intent).also { + analytics?.logCommunication( + protocol = SPOS_PROTOCOL, + message = AnalyticsMessageFactory.createResultCanceled(intent.extras) + ) + } + } else { + TerminalOperationStatus.Canceled + } + } + } + + companion object { + private const val SPOS_PROTOCOL = "SPOS" } } +/*** + * @param config - terminal config used for this request + * @param transactionId - id of the transaction that will be created by the request (S-POS only) + * @param amount - amount of the original transaction being cancelled + * @param tip - tip of the original transaction being cancelled, by default it will be set to 0.00 + * @param currency - currency the transaction will be made in + * @param receiptNo - the receipt number of the original transaction being cancelled + */ class ReversalRequest( val config: Terminal, + val transactionId: String, + val amount: BigDecimal, + val tip: BigDecimal = BigDecimal.ZERO, + val currency: ISOAlphaCurrency, val receiptNo: String ) { - override fun toString() = "ReversalRequest(config=$config, receiptNo=$receiptNo)" + override fun toString() = "ReversalRequest(" + + "config=$config, " + + "transactionId=$transactionId, " + + "amount=$amount, " + + "tip=$tip, " + + "currency=$currency" + + "receiptNo=$receiptNo, " + + ")" + override fun equals(other: Any?) = other is ReversalRequest && config == other.config && + transactionId == other.transactionId && + amount == other.amount && + tip == other.tip && + currency == other.currency && receiptNo == other.receiptNo - override fun hashCode() = Objects.hash(config, receiptNo) + + override fun hashCode() = Objects.hash(config, transactionId, amount, tip, currency, receiptNo) } \ No newline at end of file diff --git a/payment-engine/src/main/java/de/tillhub/paymentengine/contract/TerminalConnectContract.kt b/payment-engine/src/main/java/de/tillhub/paymentengine/contract/TerminalConnectContract.kt new file mode 100644 index 0000000..b0bad85 --- /dev/null +++ b/payment-engine/src/main/java/de/tillhub/paymentengine/contract/TerminalConnectContract.kt @@ -0,0 +1,47 @@ +package de.tillhub.paymentengine.contract + +import android.app.Activity +import android.content.Context +import android.content.Intent +import androidx.activity.result.contract.ActivityResultContract +import de.tillhub.paymentengine.PaymentEngine +import de.tillhub.paymentengine.analytics.PaymentAnalytics +import de.tillhub.paymentengine.data.Terminal +import de.tillhub.paymentengine.data.TerminalOperationStatus +import de.tillhub.paymentengine.spos.AnalyticsMessageFactory +import de.tillhub.paymentengine.spos.SPOSIntentFactory +import de.tillhub.paymentengine.spos.SPOSResponseHandler + +class TerminalConnectContract( + private val analytics: PaymentAnalytics? = PaymentEngine.getInstance().paymentAnalytics +) : ActivityResultContract() { + override fun createIntent(context: Context, input: Terminal): Intent { + return if (input is Terminal.SPOS) { + SPOSIntentFactory.createConnectIntent(input) + } else { + throw UnsupportedOperationException("Connect only supported for S-POS terminals") + }.also { + analytics?.logOperation(AnalyticsMessageFactory.createConnectOperation(input)) + } + } + + override fun parseResult(resultCode: Int, intent: Intent?): TerminalOperationStatus { + return SPOSResponseHandler.handleTerminalConnectResponse(resultCode, intent).also { + if (resultCode == Activity.RESULT_OK) { + analytics?.logCommunication( + protocol = SPOS_PROTOCOL, + message = AnalyticsMessageFactory.RESPONSE_RESULT_OK + ) + } else { + analytics?.logCommunication( + protocol = SPOS_PROTOCOL, + message = AnalyticsMessageFactory.createResultCanceled(intent?.extras) + ) + } + } + } + + companion object { + private const val SPOS_PROTOCOL = "SPOS" + } +} \ No newline at end of file diff --git a/payment-engine/src/main/java/de/tillhub/paymentengine/contract/TerminalDisconnectContract.kt b/payment-engine/src/main/java/de/tillhub/paymentengine/contract/TerminalDisconnectContract.kt new file mode 100644 index 0000000..e746cfc --- /dev/null +++ b/payment-engine/src/main/java/de/tillhub/paymentengine/contract/TerminalDisconnectContract.kt @@ -0,0 +1,47 @@ +package de.tillhub.paymentengine.contract + +import android.app.Activity +import android.content.Context +import android.content.Intent +import androidx.activity.result.contract.ActivityResultContract +import de.tillhub.paymentengine.PaymentEngine +import de.tillhub.paymentengine.analytics.PaymentAnalytics +import de.tillhub.paymentengine.data.Terminal +import de.tillhub.paymentengine.data.TerminalOperationStatus +import de.tillhub.paymentengine.spos.AnalyticsMessageFactory +import de.tillhub.paymentengine.spos.SPOSIntentFactory +import de.tillhub.paymentengine.spos.SPOSResponseHandler + +class TerminalDisconnectContract( + private val analytics: PaymentAnalytics? = PaymentEngine.getInstance().paymentAnalytics +) : ActivityResultContract() { + override fun createIntent(context: Context, input: Terminal): Intent { + return if (input is Terminal.SPOS) { + SPOSIntentFactory.createDisconnectIntent(input) + } else { + throw UnsupportedOperationException("Disconnect only supported for S-POS terminals") + }.also { + analytics?.logOperation(AnalyticsMessageFactory.createDisconnectOperation(input)) + } + } + + override fun parseResult(resultCode: Int, intent: Intent?): TerminalOperationStatus { + return SPOSResponseHandler.handleTerminalDisconnectResponse(resultCode).also { + if (resultCode == Activity.RESULT_OK) { + analytics?.logCommunication( + protocol = SPOS_PROTOCOL, + message = AnalyticsMessageFactory.RESPONSE_RESULT_OK + ) + } else { + analytics?.logCommunication( + protocol = SPOS_PROTOCOL, + message = AnalyticsMessageFactory.RESPONSE_RESULT_CANCELED + ) + } + } + } + + companion object { + private const val SPOS_PROTOCOL = "SPOS" + } +} \ No newline at end of file diff --git a/payment-engine/src/main/java/de/tillhub/paymentengine/contract/TerminalReconciliationContract.kt b/payment-engine/src/main/java/de/tillhub/paymentengine/contract/TerminalReconciliationContract.kt index f4cc785..e15d5d9 100644 --- a/payment-engine/src/main/java/de/tillhub/paymentengine/contract/TerminalReconciliationContract.kt +++ b/payment-engine/src/main/java/de/tillhub/paymentengine/contract/TerminalReconciliationContract.kt @@ -5,13 +5,21 @@ import android.content.Context import android.content.Intent import androidx.activity.result.contract.ActivityResultContract import androidx.core.os.BundleCompat +import de.tillhub.paymentengine.PaymentEngine +import de.tillhub.paymentengine.analytics.PaymentAnalytics import de.tillhub.paymentengine.data.ExtraKeys import de.tillhub.paymentengine.data.Terminal import de.tillhub.paymentengine.data.TerminalOperationStatus import de.tillhub.paymentengine.opi.ui.OPIReconciliationActivity +import de.tillhub.paymentengine.spos.AnalyticsMessageFactory +import de.tillhub.paymentengine.spos.SPOSIntentFactory +import de.tillhub.paymentengine.spos.SPOSResponseHandler +import de.tillhub.paymentengine.spos.data.SPOSKey import de.tillhub.paymentengine.zvt.ui.TerminalReconciliationActivity -class TerminalReconciliationContract : ActivityResultContract() { +class TerminalReconciliationContract( + private val analytics: PaymentAnalytics? = PaymentEngine.getInstance().paymentAnalytics +) : ActivityResultContract() { override fun createIntent(context: Context, input: Terminal): Intent { return when (input) { is Terminal.ZVT -> Intent(context, TerminalReconciliationActivity::class.java).apply { @@ -21,12 +29,46 @@ class TerminalReconciliationContract : ActivityResultContract Intent(context, OPIReconciliationActivity::class.java).apply { putExtra(ExtraKeys.EXTRA_CONFIG, input) } + + is Terminal.SPOS -> SPOSIntentFactory.createReconciliationIntent() + }.also { + analytics?.logOperation(AnalyticsMessageFactory.createReconciliationOperation(input)) } } override fun parseResult(resultCode: Int, intent: Intent?): TerminalOperationStatus { - return intent.takeIf { resultCode == Activity.RESULT_OK }?.extras?.let { - BundleCompat.getParcelable(it, ExtraKeys.EXTRAS_RESULT, TerminalOperationStatus::class.java) - } ?: TerminalOperationStatus.Canceled + return if (resultCode == Activity.RESULT_OK) { + if (intent?.extras?.containsKey(ExtraKeys.EXTRAS_RESULT) == true) { + intent.extras?.let { + BundleCompat.getParcelable( + it, + ExtraKeys.EXTRAS_RESULT, + TerminalOperationStatus::class.java + ) + } ?: TerminalOperationStatus.Canceled + } else { + SPOSResponseHandler.handleTransactionResponse(resultCode, intent).also { + analytics?.logCommunication( + protocol = SPOS_PROTOCOL, + message = AnalyticsMessageFactory.createResultOk(intent?.extras) + ) + } + } + } else { + if (intent?.extras?.containsKey(SPOSKey.ResultExtra.ERROR) == true) { + SPOSResponseHandler.handleTransactionResponse(resultCode, intent).also { + analytics?.logCommunication( + protocol = SPOS_PROTOCOL, + message = AnalyticsMessageFactory.createResultCanceled(intent.extras) + ) + } + } else { + TerminalOperationStatus.Canceled + } + } + } + + companion object { + private const val SPOS_PROTOCOL = "SPOS" } } \ No newline at end of file diff --git a/payment-engine/src/main/java/de/tillhub/paymentengine/data/Terminal.kt b/payment-engine/src/main/java/de/tillhub/paymentengine/data/Terminal.kt index f81a966..4e075df 100644 --- a/payment-engine/src/main/java/de/tillhub/paymentengine/data/Terminal.kt +++ b/payment-engine/src/main/java/de/tillhub/paymentengine/data/Terminal.kt @@ -7,15 +7,14 @@ import java.util.Objects @Parcelize sealed class Terminal : Parcelable { abstract val name: String - abstract val ipAddress: String - abstract val port: Int + abstract val saleConfig: CardSaleConfig class ZVT( override val name: String = DEFAULT_NAME, - override val ipAddress: String = DEFAULT_IP_ADDRESS, - override val port: Int = DEFAULT_PORT, override val saleConfig: CardSaleConfig = CardSaleConfig(), + val ipAddress: String = DEFAULT_IP_ADDRESS, + val port: Int = DEFAULT_PORT, val terminalPrinterAvailable: Boolean = DEFAULT_PRINTER_AVAILABLE, val isoCurrencyNumber: String = DEFAULT_CURRENCY_CODE, ) : Terminal() { @@ -56,9 +55,9 @@ sealed class Terminal : Parcelable { class OPI( override val name: String = DEFAULT_NAME, - override val ipAddress: String = DEFAULT_IP_ADDRESS, - override val port: Int = DEFAULT_PORT_1, override val saleConfig: CardSaleConfig = CardSaleConfig(), + val ipAddress: String = DEFAULT_IP_ADDRESS, + val port: Int = DEFAULT_PORT_1, val port2: Int = DEFAULT_PORT_2, val currencyCode: String = DEFAULT_CURRENCY_CODE, ) : Terminal() { @@ -96,4 +95,39 @@ sealed class Terminal : Parcelable { const val DEFAULT_CURRENCY_CODE = "EUR" } } + + class SPOS( + override val name: String = DEFAULT_NAME, + override val saleConfig: CardSaleConfig = CardSaleConfig(), + val appId: String = DEFAULT_APP_ID, + val connected: Boolean = DEFAULT_CONNECTION, + val currencyCode: String = DEFAULT_CURRENCY_CODE, + ) : Terminal() { + override fun toString() = "Terminal.SPOS(" + + "name=$name, " + + "appId=$appId, " + + "saleConfig=$saleConfig, " + + "currencyCode=$currencyCode" + + ")" + + override fun equals(other: Any?) = other is SPOS && + name == other.name && + appId == other.appId && + saleConfig == other.saleConfig && + currencyCode == other.currencyCode + + override fun hashCode() = Objects.hash( + name, + appId, + saleConfig, + currencyCode + ) + + companion object { + private const val DEFAULT_NAME = "Default:SPOS" + private const val DEFAULT_APP_ID = "TESTCLIENT" + private const val DEFAULT_CONNECTION = false + const val DEFAULT_CURRENCY_CODE = "EUR" + } + } } \ No newline at end of file diff --git a/payment-engine/src/main/java/de/tillhub/paymentengine/data/TerminalOperationStatus.kt b/payment-engine/src/main/java/de/tillhub/paymentengine/data/TerminalOperationStatus.kt index 3ec6005..f8b07e4 100644 --- a/payment-engine/src/main/java/de/tillhub/paymentengine/data/TerminalOperationStatus.kt +++ b/payment-engine/src/main/java/de/tillhub/paymentengine/data/TerminalOperationStatus.kt @@ -40,6 +40,9 @@ sealed class TerminalOperationStatus : Parcelable { override fun toString() = "Pending.Refund(amount=$amount, currency=$currency)" } data object Reconciliation : Pending() + + data object Connecting : Pending() + data object Disconnecting : Pending() } @Parcelize @@ -66,6 +69,14 @@ sealed class TerminalOperationStatus : Parcelable { override val data: TransactionData? ) : Success() + class SPOS( + override val date: Instant, + override val customerReceipt: String, + override val merchantReceipt: String, + override val rawData: String, + override val data: TransactionData? + ) : Success() + override fun toString() = "Success(" + "date=$date, " + "customerReceipt=$customerReceipt, " + @@ -75,22 +86,9 @@ sealed class TerminalOperationStatus : Parcelable { ")" override fun equals(other: Any?) = when (this) { - is OPI -> { - other is OPI && - date == other.date && - customerReceipt == other.customerReceipt && - merchantReceipt == other.merchantReceipt && - rawData == other.rawData && - data == other.data - } - is ZVT -> { - other is ZVT && - date == other.date && - customerReceipt == other.customerReceipt && - merchantReceipt == other.merchantReceipt && - rawData == other.rawData && - data == other.data - } + is OPI -> other is OPI && equalsVals(other) + is ZVT -> other is ZVT && equalsVals(other) + is SPOS -> other is SPOS && equalsVals(other) } override fun hashCode() = Objects.hash( @@ -100,6 +98,13 @@ sealed class TerminalOperationStatus : Parcelable { rawData, data ) + + private fun equalsVals(other: Success) = + date == other.date && + customerReceipt == other.customerReceipt && + merchantReceipt == other.merchantReceipt && + rawData == other.rawData && + data == other.data } @Parcelize @@ -129,6 +134,15 @@ sealed class TerminalOperationStatus : Parcelable { override val resultCode: TransactionResultCode ) : Error() + class SPOS( + override val date: Instant, + override val customerReceipt: String, + override val merchantReceipt: String, + override val rawData: String, + override val data: TransactionData?, + override val resultCode: TransactionResultCode + ) : Error() + override fun equals(other: Any?) = other is Error && date == other.date && customerReceipt == other.customerReceipt && diff --git a/payment-engine/src/main/java/de/tillhub/paymentengine/data/TransactionResultCode.kt b/payment-engine/src/main/java/de/tillhub/paymentengine/data/TransactionResultCode.kt index c60b378..7035d7a 100644 --- a/payment-engine/src/main/java/de/tillhub/paymentengine/data/TransactionResultCode.kt +++ b/payment-engine/src/main/java/de/tillhub/paymentengine/data/TransactionResultCode.kt @@ -34,6 +34,7 @@ sealed class TransactionResultCode : Parcelable { class Unknown( val resultCode: Int, + val resultCodeString: String? = null, @StringRes override val errorMessage: Int, @StringRes @@ -60,8 +61,8 @@ sealed class TransactionResultCode : Parcelable { } @SuppressWarnings("MagicNumber") -internal sealed class ResultCodeSets(val mapping: Map) { - data object OpiResultCodes : ResultCodeSets( +internal sealed class ResultCodeSets(val mapping: Map) { + data object OpiResultCodes : ResultCodeSets( mapOf( Pair(0, TransactionResultCode.Known(R.string.opi_error_code_00)), Pair(1, TransactionResultCode.Known(R.string.opi_error_code_01)), @@ -232,7 +233,7 @@ internal sealed class ResultCodeSets(val mapping: Map( mapOf( Pair( 0, @@ -454,7 +455,7 @@ internal sealed class ResultCodeSets(val mapping: Map( mapOf( Pair(2, TransactionResultCode.Known(R.string.lavego_result_code_2_call_merchant)), Pair( @@ -561,6 +562,99 @@ internal sealed class ResultCodeSets(val mapping: Map( + mapOf( + Pair( + "S_SWITCH_NOT_CONNECTED", + TransactionResultCode.Known(R.string.spos_error_terminal_not_connected) + ), + Pair( + "CARD_PAYMENT_NOT_ONBOARDED", + TransactionResultCode.Known(R.string.spos_error_terminal_not_onboarded) + ), + Pair( + "Failure", + TransactionResultCode.Known(R.string.spos_error_failure) + ), + Pair( + "Aborted", + TransactionResultCode.Known(R.string.spos_error_aborted) + ), + Pair( + "Busy", + TransactionResultCode.Known(R.string.spos_error_busy) + ), + Pair( + "CommunicationError", + TransactionResultCode.Known(R.string.spos_error_communication_error) + ), + Pair( + "DeviceConfigurationFailure", + TransactionResultCode.Known(R.string.spos_error_configuration_failure) + ), + Pair( + "DeviceUnavailable", + TransactionResultCode.Known(R.string.spos_error_device_unavailable) + ), + Pair( + "FormatError", + TransactionResultCode.Known(R.string.spos_error_format_error) + ), + Pair( + "MissingMandatoryData", + TransactionResultCode.Known(R.string.spos_error_missing_mandatory_data) + ), + Pair( + "NoActivePayment", + TransactionResultCode.Known(R.string.spos_error_no_active_payment) + ), + Pair( + "ParsingError", + TransactionResultCode.Known(R.string.spos_error_parsing_error) + ), + Pair( + "PartialFailure", + TransactionResultCode.Known(R.string.spos_error_partial_failure) + ), + Pair( + "PaymentOnGoing", + TransactionResultCode.Known(R.string.spos_error_payment_ongoing) + ), + Pair( + "PcCommunicationFailed", + TransactionResultCode.Known(R.string.spos_error_pc_communication_failed) + ), + Pair( + "DeviceConfigurationFailed", + TransactionResultCode.Known(R.string.spos_error_configuration_failure) + ), + Pair( + "PrintLastTicket", + TransactionResultCode.Known(R.string.spos_error_print_last_ticket) + ), + Pair( + "TimedOut", + TransactionResultCode.Known(R.string.spos_error_timed_out) + ), + Pair( + "ReceiptCallFailed", + TransactionResultCode.Known(R.string.spos_error_receipt_call_failed) + ), + Pair( + "TerminalAlreadyActivated", + TransactionResultCode.Known(R.string.spos_error_terminal_already_activated) + ), + Pair( + "ValidationError", + TransactionResultCode.Known(R.string.spos_error_validation_error) + ), + Pair( + "Unknown", + TransactionResultCode.Known(R.string.spos_error_unknown) + ) + ) + ) + companion object { private const val UNKNOWN_RESULT_CODE = -1 @@ -586,5 +680,16 @@ internal sealed class ResultCodeSets(val mapping: Map + TerminalOperationStatus.Error.SPOS( + date = Instant.now(), + customerReceipt = "", + merchantReceipt = "", + rawData = "", + data = null, + resultCode = ResultCodeSets.getSPOSCode(errCode) + ) + } ?: TerminalOperationStatus.Canceled + } + + fun handleTerminalDisconnectResponse( + resultCode: Int, + ): TerminalOperationStatus = + if (resultCode == Activity.RESULT_OK) { + TerminalOperationStatus.Success.SPOS( + date = Instant.now(), + customerReceipt = "", + merchantReceipt = "", + rawData = "", + data = null + ) + } else { + TerminalOperationStatus.Canceled + } + + fun handleTransactionResponse( + resultCode: Int, + intent: Intent?, + converter: StringToReceiptDtoConverter = StringToReceiptDtoConverter() + ): TerminalOperationStatus = if (resultCode == Activity.RESULT_OK) { + intent?.extras?.let { extras -> + val merchantReceipt = extras.getReceipt(SPOSKey.ResultExtra.RECEIPT_MERCHANT, converter) + val customerReceipt = extras.getReceipt(SPOSKey.ResultExtra.RECEIPT_CUSTOMER, converter) + val resultState = SPOSResultState.find( + type = extras.getString(SPOSKey.ResultExtra.RESULT_STATE).orEmpty() + ) + val transactionResult = SPOSTransactionResult.find( + type = extras.getString(SPOSKey.ResultExtra.TRANSACTION_RESULT).orEmpty() + ) + + if (transactionResult == SPOSTransactionResult.ACCEPTED && + resultState == SPOSResultState.SUCCESS + ) { + TerminalOperationStatus.Success.SPOS( + date = Instant.now(), + customerReceipt = customerReceipt, + merchantReceipt = merchantReceipt, + rawData = extras.toRawData(), + data = extras.toTransactionData(), + ) + } else { + TerminalOperationStatus.Error.SPOS( + date = Instant.now(), + customerReceipt = customerReceipt, + merchantReceipt = merchantReceipt, + rawData = extras.toRawData(), + data = extras.toTransactionData(), + resultCode = ResultCodeSets.getSPOSCode(resultState.value) + ) + } + } ?: TerminalOperationStatus.Error.SPOS( + date = Instant.now(), + customerReceipt = "", + merchantReceipt = "", + rawData = "", + data = null, + resultCode = ResultCodeSets.getSPOSCode(null) + ) + } else { + intent?.extras?.getString(SPOSKey.ResultExtra.ERROR)?.let { errCode -> + TerminalOperationStatus.Error.SPOS( + date = Instant.now(), + customerReceipt = "", + merchantReceipt = "", + rawData = intent.extras?.toRawData().orEmpty(), + data = intent.extras?.toTransactionData(), + resultCode = ResultCodeSets.getSPOSCode(errCode) + ) + } ?: TerminalOperationStatus.Canceled + } + + internal fun Bundle.toRawData(): String { + val builder = StringBuilder() + builder.appendLine("Extras {") + + keySet().forEach { + builder.appendLine("$it = ${getString(it)}") + } + + builder.append("}") + return builder.toString() + } + + private fun Bundle.toTransactionData(): TransactionData = + TransactionData( + terminalId = getString(SPOSKey.ResultExtra.TERMINAL_ID).orEmpty(), + transactionId = getString(SPOSKey.ResultExtra.TRANSACTION_DATA).orEmpty(), + cardCircuit = getString(SPOSKey.ResultExtra.CARD_CIRCUIT).orEmpty(), + cardPan = getString(SPOSKey.ResultExtra.CARD_PAN).orEmpty(), + paymentProvider = getString(SPOSKey.ResultExtra.MERCHANT).orEmpty(), // TODO check if it is ok + ) + + private fun Bundle.getReceipt(key: String, converter: StringToReceiptDtoConverter): String = + getString(key)?.let { + converter.convert(it).toReceiptString() + }.orEmpty() +} \ No newline at end of file diff --git a/payment-engine/src/main/java/de/tillhub/paymentengine/spos/data/ReceiptDto.kt b/payment-engine/src/main/java/de/tillhub/paymentengine/spos/data/ReceiptDto.kt new file mode 100644 index 0000000..a6f309a --- /dev/null +++ b/payment-engine/src/main/java/de/tillhub/paymentengine/spos/data/ReceiptDto.kt @@ -0,0 +1,78 @@ +package de.tillhub.paymentengine.spos.data + +import android.os.Parcelable +import androidx.annotation.Keep +import kotlinx.parcelize.Parcelize +import org.simpleframework.xml.Attribute +import org.simpleframework.xml.Element +import org.simpleframework.xml.ElementList +import org.simpleframework.xml.Root +import org.simpleframework.xml.Text + +@Keep +@Root(name = "Receipt", strict = false) +internal data class ReceiptDto( + @field:Attribute(name = "numCols") + @param:Attribute(name = "numCols") + var numCols: Int = 0, + + @field:ElementList(inline = true, required = false) + @param:ElementList(inline = true, required = false) + var receiptLines: List? = null, +) { + fun toReceiptString(): String { + val sb = StringBuilder() + + receiptLines?.forEach { line -> + line.text?.value?.let { sb.appendLine(it) } + } + + return sb.toString() + } +} + +@Keep +@Root(name = "ReceiptLine", strict = false) +internal data class ReceiptLineDto( + @field:Attribute(name = "type") + @param:Attribute(name = "type") + var type: String = "", + + @field:Element(name = "Formats", required = false) + @param:Element(name = "Formats", required = false) + var formats: FormatsDto? = null, + @field:Element(name = "Text", required = false) + @param:Element(name = "Text", required = false) + var text: LineText? = null, +) + +@Keep +@Root(name = "Formats", strict = false) +internal data class FormatsDto( + @field:ElementList(inline = true, required = false) + @param:ElementList(inline = true, required = false) + var formats: List? = null, +) + +@Keep +@Root(name = "Format", strict = false) +internal data class FormatDto( + @field:Attribute(name = "from") + @param:Attribute(name = "from") + var from: Int = 0, + @field:Attribute(name = "to") + @param:Attribute(name = "to") + var to: Int = 0, + + @field:Text(required = false) + @param:Text(required = false) + var value: String? = null, +) + +@Keep +@Parcelize +internal data class LineText( + @field:Text(required = false) + @param:Text(required = false) + var value: String? = null, +) : Parcelable \ No newline at end of file diff --git a/payment-engine/src/main/java/de/tillhub/paymentengine/spos/data/SPOSKey.kt b/payment-engine/src/main/java/de/tillhub/paymentengine/spos/data/SPOSKey.kt new file mode 100644 index 0000000..e53217f --- /dev/null +++ b/payment-engine/src/main/java/de/tillhub/paymentengine/spos/data/SPOSKey.kt @@ -0,0 +1,41 @@ +package de.tillhub.paymentengine.spos.data + +internal object SPOSKey { + object Action { + const val CONNECT_ACTION = "de.spayment.akzeptanz.S_SWITCH_CONNECT" + const val DISCONNECT_ACTION = "de.spayment.akzeptanz.S_SWITCH_DISCONNECT" + const val TRANSACTION_ACTION = "de.spayment.akzeptanz.TRANSACTION" + const val RECONCILIATION_ACTION = "de.spayment.akzeptanz.RECONCILIATION" + } + + object Extra { + const val APP_ID = "APP_ID" + const val TRANSACTION_TYPE = "de.spayment.akzeptanz.TransactionType" + const val CURRENCY_ISO = "de.spayment.akzeptanz.CurrencyISO" + const val AMOUNT = "de.spayment.akzeptanz.Amount" + const val TIP_AMOUNT = "de.spayment.akzeptanz.TipAmount" + const val TAX_AMOUNT = "de.spayment.akzeptanz.TaxAmount" + const val TRANSACTION_ID = "de.spayment.akzeptanz.TransactionId" + const val TRANSACTION_DATA = "de.spayment.akzeptanz.TransactionData" + const val LANGUAGE_CODE = "de.spayment.akzeptanz.LanguageCode" + } + + object ResultExtra { + const val ERROR = "ERROR" + const val TRANSACTION_RESULT = "de.spayment.akzeptanz.TransactionResult" + const val TRANSACTION_TYPE = "de.spayment.akzeptanz.TransactionType" + const val RESULT_STATE = "de.spayment.akzeptanz.ResultState" + const val AMOUNT = "de.spayment.akzeptanz.Amount" + const val TIP_AMOUNT = "de.spayment.akzeptanz.TipAmount" + const val TAX_AMOUNT = "de.spayment.akzeptanz.TaxAmount" + const val TRANSACTION_DATA = "de.spayment.akzeptanz.TransactionData" + const val CARD_CIRCUIT = "de.spayment.akzeptanz.CardCircuit" + const val CARD_PAN = "de.spayment.akzeptanz.CardPAN" + const val MERCHANT = "de.spayment.akzeptanz.Merchant" + const val TERMINAL_ID = "de.spayment.akzeptanz.TerminalID" + const val RECEIPT_MERCHANT = "de.spayment.akzeptanz.MerchantReceipt" + const val RECEIPT_CUSTOMER = "de.spayment.akzeptanz.CustomerReceipt" + const val ERROR_MESSAGE = "de.spayment.akzeptanz.ErrorMessage" + const val RECONCILIATION_DATA = "de.spayment.akzeptanz.ReconciliationData" + } +} \ No newline at end of file diff --git a/payment-engine/src/main/java/de/tillhub/paymentengine/spos/data/SPOSResultState.kt b/payment-engine/src/main/java/de/tillhub/paymentengine/spos/data/SPOSResultState.kt new file mode 100644 index 0000000..118d05e --- /dev/null +++ b/payment-engine/src/main/java/de/tillhub/paymentengine/spos/data/SPOSResultState.kt @@ -0,0 +1,31 @@ +package de.tillhub.paymentengine.spos.data + +internal enum class SPOSResultState(val value: String) { + SUCCESS("Success"), + FAILURE("Failure"), + ABORTED("Aborted"), + BUSY("Busy"), + COMMUNICATION_ERROR("CommunicationError"), + DEVICE_CONFIGURATION_FAILURE("DeviceConfigurationFailure"), + DEVICE_UNAVAILABLE("DeviceUnavailable"), + FORMAT_ERROR("FormatError"), + MISSING_MANDATORY_DATA("MissingMandatoryData"), + NO_ACTIVE_PAYMENT("NoActivePayment"), + PARSING_ERROR("ParsingError"), + PARTIAL_FAILURE("PartialFailure"), + PAYMENT_ONGOING("PaymentOnGoing"), + PC_COMMUNICATION_FAILED("PcCommunicationFailed"), + DEVICE_CONFIGURATION_FAILED("DeviceConfigurationFailed"), + PRINT_LAST_TICKET("PrintLastTicket"), + TIMED_OUT("TimedOut"), + RECEIPT_CALL_FAILED("ReceiptCallFailed"), + TERMINAL_ALREADY_ACTIVATED("TerminalAlreadyActivated"), + VALIDATION_ERROR("ValidationError"), + TERMINAL_UNKNOWN("Unknown"), + UNKNOWN("unknown"); + + companion object { + fun find(type: String): SPOSResultState = + SPOSResultState.entries.find { it.value == type } ?: UNKNOWN + } +} \ No newline at end of file diff --git a/payment-engine/src/main/java/de/tillhub/paymentengine/spos/data/SPOSTransactionResult.kt b/payment-engine/src/main/java/de/tillhub/paymentengine/spos/data/SPOSTransactionResult.kt new file mode 100644 index 0000000..995fb1b --- /dev/null +++ b/payment-engine/src/main/java/de/tillhub/paymentengine/spos/data/SPOSTransactionResult.kt @@ -0,0 +1,12 @@ +package de.tillhub.paymentengine.spos.data + +internal enum class SPOSTransactionResult(val value: String) { + ACCEPTED("ACCEPTED"), + FAILED("FAILED"), + UNKNOWN("unknown"); + + companion object { + fun find(type: String): SPOSTransactionResult = + SPOSTransactionResult.entries.find { it.value == type } ?: UNKNOWN + } +} \ No newline at end of file diff --git a/payment-engine/src/main/java/de/tillhub/paymentengine/spos/data/SPOSTransactionType.kt b/payment-engine/src/main/java/de/tillhub/paymentengine/spos/data/SPOSTransactionType.kt new file mode 100644 index 0000000..7de3bb7 --- /dev/null +++ b/payment-engine/src/main/java/de/tillhub/paymentengine/spos/data/SPOSTransactionType.kt @@ -0,0 +1,13 @@ +package de.tillhub.paymentengine.spos.data + +internal enum class SPOSTransactionType(val value: String) { + PAYMENT("CardPayment"), + PAYMENT_REFUND("PaymentRefund"), + PAYMENT_REVERSAL("PaymentReversal"), + UNKNOWN("unknown"); + + companion object { + fun find(type: String): SPOSTransactionType = + SPOSTransactionType.entries.find { it.value == type } ?: UNKNOWN + } +} \ No newline at end of file diff --git a/payment-engine/src/main/java/de/tillhub/paymentengine/spos/data/StringToReceiptDtoConverter.kt b/payment-engine/src/main/java/de/tillhub/paymentengine/spos/data/StringToReceiptDtoConverter.kt new file mode 100644 index 0000000..3478432 --- /dev/null +++ b/payment-engine/src/main/java/de/tillhub/paymentengine/spos/data/StringToReceiptDtoConverter.kt @@ -0,0 +1,24 @@ +package de.tillhub.paymentengine.spos.data + +import org.simpleframework.xml.Serializer +import org.simpleframework.xml.core.Persister +import retrofit2.Converter +import java.io.IOException + +internal class StringToReceiptDtoConverter( + private val serializer: Serializer = Persister() +) : Converter { + + @Suppress("TooGenericExceptionThrown", "TooGenericExceptionCaught") + override fun convert(value: String): ReceiptDto = try { + val read: ReceiptDto = serializer.read(ReceiptDto::class.java, value, true) + ?: throw IllegalStateException("Could not deserialize body as ReceiptDto") + read + } catch (e: RuntimeException) { + throw e + } catch (e: IOException) { + throw e + } catch (e: Exception) { + throw RuntimeException(e) + } +} \ No newline at end of file diff --git a/payment-engine/src/main/java/de/tillhub/paymentengine/zvt/ui/CardPaymentActivity.kt b/payment-engine/src/main/java/de/tillhub/paymentengine/zvt/ui/CardPaymentActivity.kt index c481ec1..143c452 100644 --- a/payment-engine/src/main/java/de/tillhub/paymentengine/zvt/ui/CardPaymentActivity.kt +++ b/payment-engine/src/main/java/de/tillhub/paymentengine/zvt/ui/CardPaymentActivity.kt @@ -56,12 +56,6 @@ internal class CardPaymentActivity : CardTerminalActivity() { } override fun startOperation() { - analytics?.logOperation( - "Operation: CARD_PAYMENT(" + - "amount: $amount, " + - "currency: $currency)" + - "\n$config" - ) doPayment(Payment(amount, currency.value)) } diff --git a/payment-engine/src/main/java/de/tillhub/paymentengine/zvt/ui/CardPaymentPartialRefundActivity.kt b/payment-engine/src/main/java/de/tillhub/paymentengine/zvt/ui/CardPaymentPartialRefundActivity.kt index bac2e14..4e3b7e5 100644 --- a/payment-engine/src/main/java/de/tillhub/paymentengine/zvt/ui/CardPaymentPartialRefundActivity.kt +++ b/payment-engine/src/main/java/de/tillhub/paymentengine/zvt/ui/CardPaymentPartialRefundActivity.kt @@ -66,13 +66,6 @@ internal class CardPaymentPartialRefundActivity : CardTerminalActivity() { } override fun startOperation() { - analytics?.logOperation( - "Operation: PARTIAL_REFUND(" + - "amount: $amount, " + - "currency: $currency)" + - "\n$config" - ) - doPartialRefund( amount = amount, currency = currency diff --git a/payment-engine/src/main/java/de/tillhub/paymentengine/zvt/ui/CardPaymentReversalActivity.kt b/payment-engine/src/main/java/de/tillhub/paymentengine/zvt/ui/CardPaymentReversalActivity.kt index a278b40..57ef69a 100644 --- a/payment-engine/src/main/java/de/tillhub/paymentengine/zvt/ui/CardPaymentReversalActivity.kt +++ b/payment-engine/src/main/java/de/tillhub/paymentengine/zvt/ui/CardPaymentReversalActivity.kt @@ -51,11 +51,6 @@ internal class CardPaymentReversalActivity : CardTerminalActivity() { override fun startOperation() { viewModel.parseTransactionNumber(receiptNo).onSuccess { - analytics?.logOperation( - "Operation: CARD_PAYMENT_REVERSAL(" + - "receiptNo: $receiptNo)" + - "\n$config" - ) doCancellation(it) } } diff --git a/payment-engine/src/main/java/de/tillhub/paymentengine/zvt/ui/TerminalReconciliationActivity.kt b/payment-engine/src/main/java/de/tillhub/paymentengine/zvt/ui/TerminalReconciliationActivity.kt index 35ae46f..71a56c4 100644 --- a/payment-engine/src/main/java/de/tillhub/paymentengine/zvt/ui/TerminalReconciliationActivity.kt +++ b/payment-engine/src/main/java/de/tillhub/paymentengine/zvt/ui/TerminalReconciliationActivity.kt @@ -34,8 +34,6 @@ internal class TerminalReconciliationActivity : CardTerminalActivity() { } override fun startOperation() { - analytics?.logOperation("Operation: RECONCILIATION\n$config") - doReconciliation() } diff --git a/payment-engine/src/main/res/values/strings.xml b/payment-engine/src/main/res/values/strings.xml index a5fa3f7..7aa0e80 100644 --- a/payment-engine/src/main/res/values/strings.xml +++ b/payment-engine/src/main/res/values/strings.xml @@ -320,4 +320,27 @@ message number out of sequence request in progress violation of business arrangement + + + POS is not connected to terminal + Terminal has not been onboarded + Transaction failure + Transaction aborted + Terminal is busy + Terminal communication error + Terminal configuration failure + Terminal unavailable + Format error + Missing mandatory data + No active payment + Parsing error + Partial failure + Payment ongoing + PC communication failed + Print last ticket + Transaction timed out + Receipt call failed + Terminal already activated + Validation error + Unknown terminal error diff --git a/payment-engine/src/test/java/de/tillhub/paymentengine/CardManagerTest.kt b/payment-engine/src/test/java/de/tillhub/paymentengine/CardManagerTest.kt new file mode 100644 index 0000000..8f57c3d --- /dev/null +++ b/payment-engine/src/test/java/de/tillhub/paymentengine/CardManagerTest.kt @@ -0,0 +1,42 @@ +package de.tillhub.paymentengine + +import de.tillhub.paymentengine.data.Terminal +import de.tillhub.paymentengine.data.TerminalOperationStatus +import io.kotest.core.spec.style.FunSpec +import io.kotest.matchers.shouldBe +import io.mockk.mockk +import io.mockk.spyk +import io.mockk.verify +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableStateFlow + +@ExperimentalCoroutinesApi +class CardManagerTest : FunSpec({ + + lateinit var configs: MutableMap + lateinit var terminalState: MutableStateFlow + lateinit var target: CardManagerImpl + + beforeAny { + configs = mockk(relaxed = true) + terminalState = spyk(MutableStateFlow(TerminalOperationStatus.Waiting)) + + target = object : CardManagerImpl(configs, terminalState) {} + } + + test("putTerminalConfig should add terminal to the configs map") { + val terminal = Terminal.OPI() + target.putTerminalConfig(terminal) + verify { configs[terminal.name] = terminal } + } + + test("observePaymentState should return the terminal state flow") { + val result = target.observePaymentState() + result shouldBe terminalState + } + + test("defaultConfig should return default Terminal configuration") { + val defaultTerminal = target.defaultConfig + defaultTerminal shouldBe Terminal.ZVT() + } +}) diff --git a/payment-engine/src/test/java/de/tillhub/paymentengine/ConnectionManagerTest.kt b/payment-engine/src/test/java/de/tillhub/paymentengine/ConnectionManagerTest.kt new file mode 100644 index 0000000..98d2dc8 --- /dev/null +++ b/payment-engine/src/test/java/de/tillhub/paymentengine/ConnectionManagerTest.kt @@ -0,0 +1,123 @@ +package de.tillhub.paymentengine + +import androidx.activity.result.ActivityResultCaller +import androidx.activity.result.ActivityResultLauncher +import de.tillhub.paymentengine.contract.TerminalConnectContract +import de.tillhub.paymentengine.contract.TerminalDisconnectContract +import de.tillhub.paymentengine.data.Terminal +import de.tillhub.paymentengine.data.TerminalOperationStatus +import io.kotest.core.spec.style.FunSpec +import io.kotest.matchers.shouldBe +import io.mockk.Ordering +import io.mockk.every +import io.mockk.mockk +import io.mockk.spyk +import io.mockk.verify +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableStateFlow + +@ExperimentalCoroutinesApi +class ConnectionManagerTest : FunSpec({ + + lateinit var configs: MutableMap + lateinit var terminalState: MutableStateFlow + lateinit var resultCaller: ActivityResultCaller + lateinit var connectContract: ActivityResultLauncher + lateinit var disconnectContract: ActivityResultLauncher + + lateinit var target: ConnectionManager + + beforeAny { + configs = mutableMapOf() + terminalState = spyk(MutableStateFlow(TerminalOperationStatus.Waiting)) + resultCaller = mockk(relaxed = true) + connectContract = mockk(relaxed = true) + disconnectContract = mockk(relaxed = true) + + every { + resultCaller.registerForActivityResult(ofType(TerminalConnectContract::class), any()) + } returns connectContract + every { + resultCaller.registerForActivityResult(any(), any()) + } returns disconnectContract + + target = ConnectionManagerImpl( + configs = configs, + terminalState = terminalState, + resultCaller = resultCaller, + connectContract = connectContract, + disconnectContract = disconnectContract + ) + } + + test("startSPOSConnect by default terminal ") { + target.startSPOSConnect() + + verify(ordering = Ordering.ORDERED) { + terminalState.tryEmit(TerminalOperationStatus.Pending.Connecting) + connectContract.launch(Terminal.ZVT()) + } + + terminalState.value shouldBe TerminalOperationStatus.Pending.Connecting + } + + test("startSPOSConnect by config name") { + val terminal = Terminal.OPI() + configs["opi"] = terminal + target.startSPOSConnect("opi") + + verify(ordering = Ordering.ORDERED) { + terminalState.tryEmit(TerminalOperationStatus.Pending.Connecting) + connectContract.launch(terminal) + } + + terminalState.value shouldBe TerminalOperationStatus.Pending.Connecting + } + + test("startSPOSConnect by terminal") { + val terminal = Terminal.SPOS() + target.startSPOSConnect(terminal) + + verify(ordering = Ordering.ORDERED) { + terminalState.tryEmit(TerminalOperationStatus.Pending.Connecting) + connectContract.launch(terminal) + } + + terminalState.value shouldBe TerminalOperationStatus.Pending.Connecting + } + + test("startSPOSDisconnect by default terminal ") { + target.startSPOSDisconnect() + + verify(ordering = Ordering.ORDERED) { + terminalState.tryEmit(TerminalOperationStatus.Pending.Disconnecting) + disconnectContract.launch(Terminal.ZVT()) + } + terminalState.value shouldBe TerminalOperationStatus.Pending.Disconnecting + } + + test("startSPOSDisconnect by config name") { + val terminal = Terminal.OPI() + configs["opi"] = terminal + target.startSPOSDisconnect("opi") + + verify(ordering = Ordering.ORDERED) { + terminalState.tryEmit(TerminalOperationStatus.Pending.Disconnecting) + disconnectContract.launch(terminal) + } + + terminalState.value shouldBe TerminalOperationStatus.Pending.Disconnecting + } + + test("startSPOSDisconnect by terminal") { + val terminal = Terminal.SPOS() + target.startSPOSDisconnect(terminal) + + verify(ordering = Ordering.ORDERED) { + terminalState.tryEmit(TerminalOperationStatus.Pending.Disconnecting) + disconnectContract.launch(terminal) + } + + terminalState.value shouldBe TerminalOperationStatus.Pending.Disconnecting + } +}) diff --git a/payment-engine/src/test/java/de/tillhub/paymentengine/PaymentManagerTest.kt b/payment-engine/src/test/java/de/tillhub/paymentengine/PaymentManagerTest.kt new file mode 100644 index 0000000..ce9d17d --- /dev/null +++ b/payment-engine/src/test/java/de/tillhub/paymentengine/PaymentManagerTest.kt @@ -0,0 +1,119 @@ +package de.tillhub.paymentengine + +import androidx.activity.result.ActivityResultCaller +import androidx.activity.result.ActivityResultLauncher +import de.tillhub.paymentengine.contract.PaymentRequest +import de.tillhub.paymentengine.contract.PaymentResultContract +import de.tillhub.paymentengine.data.ISOAlphaCurrency +import de.tillhub.paymentengine.data.Terminal +import de.tillhub.paymentengine.data.TerminalOperationStatus +import io.kotest.core.spec.style.FunSpec +import io.kotest.matchers.shouldBe +import io.mockk.every +import io.mockk.mockk +import io.mockk.spyk +import io.mockk.verify +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableStateFlow +import java.math.BigDecimal + +@ExperimentalCoroutinesApi +class PaymentManagerTest : FunSpec({ + + lateinit var configs: MutableMap + lateinit var terminalState: MutableStateFlow + lateinit var resultCaller: ActivityResultCaller + lateinit var paymentResultContract: ActivityResultLauncher + + lateinit var target: PaymentManager + + beforeEach { + configs = mutableMapOf() + terminalState = spyk(MutableStateFlow(TerminalOperationStatus.Waiting)) + resultCaller = mockk(relaxed = true) + paymentResultContract = mockk(relaxed = true) + + every { + resultCaller.registerForActivityResult(ofType(PaymentResultContract::class), any()) + } returns paymentResultContract + + target = PaymentManagerImpl( + configs = configs, + terminalState = terminalState, + resultCaller = resultCaller, + paymentResultContract = paymentResultContract + ) + } + + test("startPaymentTransaction should use default config when no configName provided") { + val transactionId = "tx789" + val amount = BigDecimal(300) + val tip = BigDecimal(30) + val currency = ISOAlphaCurrency("EUR") + + target.startPaymentTransaction(transactionId, amount, tip, currency) + + verify { + paymentResultContract.launch( + match { + it.transactionId == transactionId && + it.amount == amount && + it.tip == tip && + it.currency == currency && + it.config == Terminal.ZVT() + } + ) + } + + terminalState.value shouldBe TerminalOperationStatus.Pending.Payment(amount, currency) + } + + test("startPaymentTransaction with configName should launch payment result contract") { + val transactionId = "tx456" + val amount = BigDecimal(200) + val tip = BigDecimal(20) + val currency = ISOAlphaCurrency("EUR") + val terminal = Terminal.OPI() + configs["opi"] = terminal + + target.startPaymentTransaction(transactionId, amount, tip, currency, "opi") + + verify { + paymentResultContract.launch( + match { + it.transactionId == transactionId && + it.amount == amount && + it.tip == tip && + it.currency == currency && + it.config == terminal + } + ) + } + + terminalState.value shouldBe TerminalOperationStatus.Pending.Payment(amount, currency) + } + + test("startPaymentTransaction with Terminal should launch payment result contract") { + val transactionId = "tx123" + val amount = BigDecimal(100) + val tip = BigDecimal(10) + val currency = ISOAlphaCurrency("EUR") + val terminal = Terminal.SPOS() + + target.startPaymentTransaction(transactionId, amount, tip, currency, terminal) + + verify { + paymentResultContract.launch( + match { + it.transactionId == transactionId && + it.amount == amount && + it.tip == tip && + it.currency == currency && + it.config == terminal + } + ) + } + + terminalState.value shouldBe TerminalOperationStatus.Pending.Payment(amount, currency) + } +}) diff --git a/payment-engine/src/test/java/de/tillhub/paymentengine/ReconciliationManagerTest.kt b/payment-engine/src/test/java/de/tillhub/paymentengine/ReconciliationManagerTest.kt new file mode 100644 index 0000000..894bfb0 --- /dev/null +++ b/payment-engine/src/test/java/de/tillhub/paymentengine/ReconciliationManagerTest.kt @@ -0,0 +1,68 @@ +package de.tillhub.paymentengine + +import androidx.activity.result.ActivityResultCaller +import androidx.activity.result.ActivityResultLauncher +import de.tillhub.paymentengine.contract.TerminalReconciliationContract +import de.tillhub.paymentengine.data.Terminal +import de.tillhub.paymentengine.data.TerminalOperationStatus +import io.kotest.core.spec.style.FunSpec +import io.kotest.matchers.shouldBe +import io.mockk.* +import kotlinx.coroutines.flow.MutableStateFlow + +class ReconciliationManagerTest : FunSpec({ + + lateinit var configs: MutableMap + lateinit var terminalState: MutableStateFlow + lateinit var resultCaller: ActivityResultCaller + lateinit var reconciliationContract: ActivityResultLauncher + + lateinit var target: ReconciliationManager + + beforeEach { + configs = mutableMapOf() + terminalState = spyk(MutableStateFlow(TerminalOperationStatus.Waiting)) + resultCaller = mockk(relaxed = true) + reconciliationContract = mockk(relaxed = true) + + every { + resultCaller.registerForActivityResult(ofType(TerminalReconciliationContract::class), any()) + } returns reconciliationContract + + target = ReconciliationManagerImpl( + configs = configs, + terminalState = terminalState, + resultCaller = resultCaller, + reconciliationContract = reconciliationContract + ) + } + + test("startReconciliation should use default config when no configName provided") { + target.startReconciliation() + + verify { reconciliationContract.launch(Terminal.ZVT()) } + + terminalState.value shouldBe TerminalOperationStatus.Pending.Reconciliation + } + + test("startReconciliation with configName should and launch reconciliation contract") { + val terminal = Terminal.OPI() + configs["opi"] = terminal + + target.startReconciliation(configName = "opi") + + verify { reconciliationContract.launch(terminal) } + + terminalState.value shouldBe TerminalOperationStatus.Pending.Reconciliation + } + + test("startReconciliation with custom Terminal should and launch reconciliation contract") { + val customTerminal = Terminal.SPOS() + + target.startReconciliation(customTerminal) + + verify { reconciliationContract.launch(customTerminal) } + + terminalState.value shouldBe TerminalOperationStatus.Pending.Reconciliation + } +}) diff --git a/payment-engine/src/test/java/de/tillhub/paymentengine/RefundManagerTest.kt b/payment-engine/src/test/java/de/tillhub/paymentengine/RefundManagerTest.kt new file mode 100644 index 0000000..d22aa35 --- /dev/null +++ b/payment-engine/src/test/java/de/tillhub/paymentengine/RefundManagerTest.kt @@ -0,0 +1,123 @@ +package de.tillhub.paymentengine + +import androidx.activity.result.ActivityResultCaller +import androidx.activity.result.ActivityResultLauncher +import de.tillhub.paymentengine.contract.PaymentRefundContract +import de.tillhub.paymentengine.contract.RefundRequest +import de.tillhub.paymentengine.data.ISOAlphaCurrency +import de.tillhub.paymentengine.data.Terminal +import de.tillhub.paymentengine.data.TerminalOperationStatus +import io.kotest.core.spec.style.FunSpec +import io.kotest.matchers.shouldBe +import io.mockk.* +import kotlinx.coroutines.flow.MutableStateFlow +import java.math.BigDecimal + +class RefundManagerTest : FunSpec({ + + lateinit var configs: MutableMap + lateinit var terminalState: MutableStateFlow + lateinit var resultCaller: ActivityResultCaller + lateinit var refundContract: ActivityResultLauncher + + lateinit var target: RefundManager + + beforeEach { + configs = mutableMapOf() + terminalState = spyk(MutableStateFlow(TerminalOperationStatus.Waiting)) + resultCaller = mockk(relaxed = true) + refundContract = mockk(relaxed = true) + + every { + resultCaller.registerForActivityResult(ofType(PaymentRefundContract::class), any()) + } returns refundContract + + target = RefundManagerImpl( + configs = configs, + terminalState = terminalState, + resultCaller = resultCaller + ) + } + + test("startRefundTransaction should use default config when no configName provided") { + val defaultTerminal = Terminal.ZVT() + configs.clear() + val transactionId = "12345" + val amount = BigDecimal(100) + val currency = ISOAlphaCurrency("EUR") + + target.startRefundTransaction( + transactionId = transactionId, + amount = amount, + currency = currency + ) + + verify { + refundContract.launch( + RefundRequest( + config = defaultTerminal, + transactionId = transactionId, + amount = amount, + currency = currency + ) + ) + } + + terminalState.value shouldBe TerminalOperationStatus.Pending.Refund(amount, currency) + } + + test("startRefundTransaction with configName should launch refund contract") { + val terminal = Terminal.OPI() + configs["opi"] = terminal + val transactionId = "12345" + val amount = BigDecimal(100) + val currency = ISOAlphaCurrency("EUR") + + target.startRefundTransaction( + transactionId = transactionId, + amount = amount, + currency = currency, + configName = "opi" + ) + + verify { + refundContract.launch( + RefundRequest( + config = terminal, + transactionId = transactionId, + amount = amount, + currency = currency + ) + ) + } + + terminalState.value shouldBe TerminalOperationStatus.Pending.Refund(amount, currency) + } + + test("startRefundTransaction with custom Terminal should launch refund contract") { + val customTerminal = Terminal.SPOS() + val transactionId = "12345" + val amount = BigDecimal(100) + val currency = ISOAlphaCurrency("EUR") + + target.startRefundTransaction( + transactionId = transactionId, + amount = amount, + currency = currency, + config = customTerminal + ) + + verify { + refundContract.launch( + RefundRequest( + config = customTerminal, + transactionId = transactionId, + amount = amount, + currency = currency + ) + ) + } + + terminalState.value shouldBe TerminalOperationStatus.Pending.Refund(amount, currency) + } +}) diff --git a/payment-engine/src/test/java/de/tillhub/paymentengine/ReversalManagerTest.kt b/payment-engine/src/test/java/de/tillhub/paymentengine/ReversalManagerTest.kt new file mode 100644 index 0000000..3f5716b --- /dev/null +++ b/payment-engine/src/test/java/de/tillhub/paymentengine/ReversalManagerTest.kt @@ -0,0 +1,139 @@ +package de.tillhub.paymentengine + +import androidx.activity.result.ActivityResultCaller +import androidx.activity.result.ActivityResultLauncher +import de.tillhub.paymentengine.contract.PaymentReversalContract +import de.tillhub.paymentengine.contract.ReversalRequest +import de.tillhub.paymentengine.data.ISOAlphaCurrency +import de.tillhub.paymentengine.data.Terminal +import de.tillhub.paymentengine.data.TerminalOperationStatus +import io.kotest.core.spec.style.FunSpec +import io.kotest.matchers.shouldBe +import io.mockk.* +import kotlinx.coroutines.flow.MutableStateFlow +import java.math.BigDecimal + +class ReversalManagerTest : FunSpec({ + + lateinit var configs: MutableMap + lateinit var transactionState: MutableStateFlow + lateinit var resultCaller: ActivityResultCaller + lateinit var reversalContract: ActivityResultLauncher + + lateinit var target: ReversalManager + + beforeEach { + configs = mutableMapOf() + transactionState = spyk(MutableStateFlow(TerminalOperationStatus.Waiting)) + resultCaller = mockk(relaxed = true) + reversalContract = mockk(relaxed = true) + + every { + resultCaller.registerForActivityResult(ofType(PaymentReversalContract::class), any()) + } returns reversalContract + + target = ReversalManagerImpl( + configs = configs, + terminalState = transactionState, + resultCaller = resultCaller, + reversalContract = reversalContract + ) + } + + test("startReversalTransaction should use default config when no configName provided") { + val transactionId = "12345" + val amount = BigDecimal(100) + val currency = ISOAlphaCurrency("EUR") + val receiptNo = "R12345" + val tip = BigDecimal.ZERO + + target.startReversalTransaction( + transactionId = transactionId, + amount = amount, + tip = tip, + currency = currency, + receiptNo = receiptNo + ) + + verify { + reversalContract.launch( + ReversalRequest( + transactionId = transactionId, + amount = amount, + currency = currency, + tip = tip, + config = Terminal.ZVT(), + receiptNo = receiptNo + ) + ) + } + + transactionState.value shouldBe TerminalOperationStatus.Pending.Reversal(receiptNo) + } + + test("startReversalTransaction with configName should launch reversal contract") { + val terminal = Terminal.OPI() + configs["opi"] = terminal + val transactionId = "12345" + val amount = BigDecimal(100) + val currency = ISOAlphaCurrency("EUR") + val receiptNo = "R12345" + val tip = BigDecimal.ZERO + + target.startReversalTransaction( + transactionId = transactionId, + amount = amount, + tip = tip, + currency = currency, + configName = "opi", + receiptNo = receiptNo + ) + + verify { + reversalContract.launch( + ReversalRequest( + transactionId = transactionId, + amount = amount, + currency = currency, + tip = tip, + config = terminal, + receiptNo = receiptNo + ) + ) + } + + transactionState.value shouldBe TerminalOperationStatus.Pending.Reversal(receiptNo) + } + + test("startReversalTransaction custom Terminal should launch reversal contract") { + val transactionId = "12345" + val amount = BigDecimal(100) + val currency = ISOAlphaCurrency("EUR") + val receiptNo = "R12345" + val tip = BigDecimal.ZERO + + target.startReversalTransaction( + transactionId = transactionId, + amount = amount, + tip = tip, + currency = currency, + config = Terminal.SPOS(), + receiptNo = receiptNo + ) + + verify { + reversalContract.launch( + ReversalRequest( + transactionId = transactionId, + amount = amount, + currency = currency, + tip = tip, + config = Terminal.SPOS(), + receiptNo = receiptNo + ) + ) + } + + transactionState.value shouldBe TerminalOperationStatus.Pending.Reversal(receiptNo) + } +}) diff --git a/payment-engine/src/test/java/de/tillhub/paymentengine/contract/PaymentRefundContractTest.kt b/payment-engine/src/test/java/de/tillhub/paymentengine/contract/PaymentRefundContractTest.kt new file mode 100644 index 0000000..dc23b10 --- /dev/null +++ b/payment-engine/src/test/java/de/tillhub/paymentengine/contract/PaymentRefundContractTest.kt @@ -0,0 +1,283 @@ +package de.tillhub.paymentengine.contract + +import android.app.Activity +import android.content.Context +import android.content.Intent +import android.os.Build +import androidx.core.os.BundleCompat +import br.com.colman.kotest.android.extensions.robolectric.RobolectricTest +import de.tillhub.paymentengine.analytics.PaymentAnalytics +import de.tillhub.paymentengine.data.ExtraKeys +import de.tillhub.paymentengine.data.ISOAlphaCurrency +import de.tillhub.paymentengine.data.Terminal +import de.tillhub.paymentengine.data.TerminalOperationStatus +import de.tillhub.paymentengine.spos.data.SPOSKey +import io.kotest.core.spec.style.FunSpec +import io.kotest.matchers.shouldBe +import io.kotest.matchers.types.shouldBeInstanceOf +import io.mockk.Runs +import io.mockk.every +import io.mockk.just +import io.mockk.mockk +import io.mockk.spyk +import io.mockk.verify +import org.robolectric.RuntimeEnvironment +import org.robolectric.annotation.Config +import java.math.BigDecimal + +@RobolectricTest +@Config(sdk = [Build.VERSION_CODES.UPSIDE_DOWN_CAKE]) +class PaymentRefundContractTest : FunSpec({ + lateinit var context: Context + lateinit var analytics: PaymentAnalytics + + lateinit var target: PaymentRefundContract + + beforeTest { + context = spyk(RuntimeEnvironment.getApplication()) + analytics = mockk { + every { logOperation(any()) } just Runs + every { logCommunication(any(), any()) } just Runs + } + + target = PaymentRefundContract(analytics) + } + + test("createIntent SPOS") { + val result = target.createIntent( + context, + RefundRequest( + SPOS, + "UUID", + 500.toBigDecimal(), + ISOAlphaCurrency("EUR") + ) + ) + + result.shouldBeInstanceOf() + result.action shouldBe "de.spayment.akzeptanz.TRANSACTION" + result.extras?.getString(SPOSKey.Extra.TRANSACTION_TYPE) shouldBe "PaymentRefund" + result.extras?.getString(SPOSKey.Extra.CURRENCY_ISO) shouldBe "EUR" + result.extras?.getString(SPOSKey.Extra.AMOUNT) shouldBe "500" + result.extras?.getString(SPOSKey.Extra.TRANSACTION_ID) shouldBe "UUID" + result.extras?.getString(SPOSKey.Extra.TAX_AMOUNT) shouldBe "000" + + verify { + analytics.logOperation( + "Operation: PARTIAL_REFUND(" + + "amount: 500, " + + "currency: ISOAlphaCurrency(value=EUR))" + + "\nTerminal.SPOS(" + + "name=s-pos, " + + "appId=TESTCLIENT, " + + "saleConfig=CardSaleConfig(" + + "applicationName=Tillhub GO, " + + "operatorId=ah, " + + "saleId=registerProvider, " + + "pin=333333, " + + "poiId=66000001, " + + "poiSerialNumber=" + + "), " + + "currencyCode=EUR" + + ")" + ) + } + } + + test("createIntent OPI") { + val result = target.createIntent( + context, + RefundRequest( + OPI, + "UUID", + 500.toBigDecimal(), + ISOAlphaCurrency("EUR") + ) + ) + + result.shouldBeInstanceOf() + result.component?.className shouldBe "de.tillhub.paymentengine.opi.ui.OPIPartialRefundActivity" + BundleCompat.getParcelable( + result.extras!!, + ExtraKeys.EXTRA_CONFIG, + Terminal.OPI::class.java + ) shouldBe OPI + BundleCompat.getSerializable( + result.extras!!, + ExtraKeys.EXTRA_AMOUNT, + BigDecimal::class.java + ) shouldBe 500.toBigDecimal() + BundleCompat.getParcelable( + result.extras!!, + ExtraKeys.EXTRA_CURRENCY, + ISOAlphaCurrency::class.java + ) shouldBe ISOAlphaCurrency("EUR") + + verify { + analytics.logOperation( + "Operation: PARTIAL_REFUND(" + + "amount: 500, " + + "currency: ISOAlphaCurrency(value=EUR))" + + "\nTerminal.OPI(" + + "name=opi, " + + "ipAddress=127.0.0.1, " + + "port=20002, " + + "saleConfig=CardSaleConfig(" + + "applicationName=Tillhub GO, " + + "operatorId=ah, " + + "saleId=registerProvider, " + + "pin=333333, " + + "poiId=66000001, " + + "poiSerialNumber=" + + "), " + + "port2=20007, " + + "currencyCode=EUR" + + ")" + ) + } + } + + test("createIntent ZVT") { + val result = target.createIntent( + context, + RefundRequest( + ZVT, + "UUID", + 500.toBigDecimal(), + ISOAlphaCurrency("EUR") + ) + ) + + result.shouldBeInstanceOf() + result.component?.className shouldBe "de.tillhub.paymentengine.zvt.ui.CardPaymentPartialRefundActivity" + BundleCompat.getParcelable( + result.extras!!, + ExtraKeys.EXTRA_CONFIG, + Terminal.ZVT::class.java + ) shouldBe ZVT + BundleCompat.getSerializable( + result.extras!!, + ExtraKeys.EXTRA_AMOUNT, + BigDecimal::class.java + ) shouldBe 500.toBigDecimal() + BundleCompat.getParcelable( + result.extras!!, + ExtraKeys.EXTRA_CURRENCY, + ISOAlphaCurrency::class.java + ) shouldBe ISOAlphaCurrency("EUR") + + verify { + analytics.logOperation( + "Operation: PARTIAL_REFUND(" + + "amount: 500, " + + "currency: ISOAlphaCurrency(value=EUR))" + + "\nTerminal.ZVT(" + + "name=zvt, " + + "ipAddress=127.0.0.1, " + + "port=40007, " + + "saleConfig=CardSaleConfig(" + + "applicationName=Tillhub GO, " + + "operatorId=ah, " + + "saleId=registerProvider, " + + "pin=333333, " + + "poiId=66000001, " + + "poiSerialNumber=" + + "), " + + "terminalPrinterAvailable=true, " + + "isoCurrencyNumber=0978" + + ")" + ) + } + } + + test("parseResult SPOS: result OK") { + val intent = Intent().apply { + putExtra(SPOSKey.ResultExtra.RESULT_STATE, "Success") + putExtra(SPOSKey.ResultExtra.TRANSACTION_RESULT, "ACCEPTED") + putExtra(SPOSKey.ResultExtra.TERMINAL_ID, "terminal_id") + putExtra(SPOSKey.ResultExtra.TRANSACTION_DATA, "transaction_data") + putExtra(SPOSKey.ResultExtra.CARD_CIRCUIT, "card_circuit") + putExtra(SPOSKey.ResultExtra.CARD_PAN, "card_pan") + } + + val result = target.parseResult(Activity.RESULT_OK, intent) + + result.shouldBeInstanceOf() + verify { + analytics.logCommunication( + protocol = "SPOS", + message = "RESPONSE: RESULT OK\nExtras {\n" + + "${SPOSKey.ResultExtra.CARD_PAN} = card_pan\n" + + "${SPOSKey.ResultExtra.CARD_CIRCUIT} = card_circuit\n" + + "${SPOSKey.ResultExtra.TRANSACTION_DATA} = transaction_data\n" + + "${SPOSKey.ResultExtra.TERMINAL_ID} = terminal_id\n" + + "${SPOSKey.ResultExtra.TRANSACTION_RESULT} = ACCEPTED\n" + + "${SPOSKey.ResultExtra.RESULT_STATE} = Success\n" + + "}" + ) + } + } + + test("parseResult SPOS: result CANCELED") { + val intent = Intent().apply { + putExtra(SPOSKey.ResultExtra.ERROR, "CARD_PAYMENT_NOT_ONBOARDED") + } + + val result = target.parseResult(Activity.RESULT_CANCELED, intent) + + result.shouldBeInstanceOf() + + verify { + analytics.logCommunication( + protocol = "SPOS", + message = "RESPONSE: RESULT CANCELED\nExtras {\n" + + "ERROR = CARD_PAYMENT_NOT_ONBOARDED\n" + + "}" + ) + } + } + + test("parseResult OPI + ZVT: result OK") { + val intent = Intent().apply { + putExtra( + ExtraKeys.EXTRAS_RESULT, + TerminalOperationStatus.Success.OPI( + date = mockk(), + customerReceipt = "customerReceipt", + merchantReceipt = "merchantReceipt", + rawData = "rawData", + data = null + ) + ) + } + + val result = target.parseResult(Activity.RESULT_OK, intent) + + result.shouldBeInstanceOf() + } + + test("parseResult OPI + ZVT: result CANCELED") { + val intent = Intent() + + val result = target.parseResult(Activity.RESULT_CANCELED, intent) + + result.shouldBeInstanceOf() + } +}) { + companion object { + val ZVT = Terminal.ZVT( + name = "zvt", + ipAddress = "127.0.0.1", + port = 40007, + ) + val OPI = Terminal.OPI( + name = "opi", + ipAddress = "127.0.0.1", + port = 20002, + port2 = 20007 + ) + val SPOS = Terminal.SPOS( + name = "s-pos", + ) + } +} \ No newline at end of file diff --git a/payment-engine/src/test/java/de/tillhub/paymentengine/contract/PaymentResultContractTest.kt b/payment-engine/src/test/java/de/tillhub/paymentengine/contract/PaymentResultContractTest.kt new file mode 100644 index 0000000..1870266 --- /dev/null +++ b/payment-engine/src/test/java/de/tillhub/paymentengine/contract/PaymentResultContractTest.kt @@ -0,0 +1,269 @@ +package de.tillhub.paymentengine.contract + +import android.app.Activity +import android.content.Context +import android.content.Intent +import android.os.Build +import androidx.core.os.BundleCompat +import br.com.colman.kotest.android.extensions.robolectric.RobolectricTest +import de.tillhub.paymentengine.analytics.PaymentAnalytics +import de.tillhub.paymentengine.data.ExtraKeys +import de.tillhub.paymentengine.data.ISOAlphaCurrency +import de.tillhub.paymentengine.data.Terminal +import de.tillhub.paymentengine.data.TerminalOperationStatus +import de.tillhub.paymentengine.spos.data.SPOSKey +import io.kotest.core.spec.style.FunSpec +import io.kotest.matchers.shouldBe +import io.kotest.matchers.types.shouldBeInstanceOf +import io.mockk.Runs +import io.mockk.every +import io.mockk.just +import io.mockk.mockk +import io.mockk.spyk +import io.mockk.verify +import org.robolectric.RuntimeEnvironment +import org.robolectric.annotation.Config +import java.math.BigDecimal + +@RobolectricTest +@Config(sdk = [Build.VERSION_CODES.UPSIDE_DOWN_CAKE]) +class PaymentResultContractTest : FunSpec({ + + lateinit var context: Context + lateinit var analytics: PaymentAnalytics + + lateinit var target: PaymentResultContract + + beforeTest { + context = spyk(RuntimeEnvironment.getApplication()) + analytics = mockk { + every { logOperation(any()) } just Runs + every { logCommunication(any(), any()) } just Runs + } + + target = PaymentResultContract(analytics) + } + + test("createIntent SPOS") { + val result = target.createIntent( + context, + PaymentRequest( + SPOS, + "UUID", + 500.toBigDecimal(), + 100.toBigDecimal(), + ISOAlphaCurrency("EUR") + ) + ) + + result.shouldBeInstanceOf() + result.action shouldBe "de.spayment.akzeptanz.TRANSACTION" + result.extras?.getString(SPOSKey.Extra.TRANSACTION_TYPE) shouldBe "CardPayment" + result.extras?.getString(SPOSKey.Extra.CURRENCY_ISO) shouldBe "EUR" + result.extras?.getString(SPOSKey.Extra.AMOUNT) shouldBe "500" + result.extras?.getString(SPOSKey.Extra.TIP_AMOUNT) shouldBe "100" + result.extras?.getString(SPOSKey.Extra.TRANSACTION_ID) shouldBe "UUID" + result.extras?.getString(SPOSKey.Extra.TAX_AMOUNT) shouldBe "000" + + verify { + analytics.logOperation( + "Operation: CARD_PAYMENT(" + + "amount: 500, " + + "tip: 100, " + + "currency: ISOAlphaCurrency(value=EUR))" + + "\nTerminal.SPOS(" + + "name=s-pos, " + + "appId=TESTCLIENT, " + + "saleConfig=CardSaleConfig(" + + "applicationName=Tillhub GO, " + + "operatorId=ah, " + + "saleId=registerProvider, " + + "pin=333333, " + + "poiId=66000001, " + + "poiSerialNumber=" + + "), " + + "currencyCode=EUR" + + ")" + ) + } + } + + test("createIntent OPI") { + val result = target.createIntent( + context, + PaymentRequest( + OPI, + "UUID", + 500.toBigDecimal(), + 100.toBigDecimal(), + ISOAlphaCurrency("EUR") + ) + ) + + result.shouldBeInstanceOf() + result.component?.className shouldBe "de.tillhub.paymentengine.opi.ui.OPIPaymentActivity" + BundleCompat.getParcelable( + result.extras!!, + ExtraKeys.EXTRA_CONFIG, + Terminal.OPI::class.java + ) shouldBe OPI + BundleCompat.getSerializable( + result.extras!!, + ExtraKeys.EXTRA_AMOUNT, + BigDecimal::class.java + ) shouldBe 600.toBigDecimal() + BundleCompat.getParcelable( + result.extras!!, + ExtraKeys.EXTRA_CURRENCY, + ISOAlphaCurrency::class.java + ) shouldBe ISOAlphaCurrency("EUR") + + verify { + analytics.logOperation( + "Operation: CARD_PAYMENT(" + + "amount: 500, " + + "tip: 100, " + + "currency: ISOAlphaCurrency(value=EUR))" + + "\nTerminal.OPI(" + + "name=opi, " + + "ipAddress=127.0.0.1, " + + "port=20002, " + + "saleConfig=CardSaleConfig(" + + "applicationName=Tillhub GO, " + + "operatorId=ah, " + + "saleId=registerProvider, " + + "pin=333333, " + + "poiId=66000001, " + + "poiSerialNumber=" + + "), " + + "port2=20007, " + + "currencyCode=EUR" + + ")" + ) + } + } + + test("createIntent ZVT") { + val result = target.createIntent( + context, + PaymentRequest( + ZVT, + "UUID", + 500.toBigDecimal(), + 100.toBigDecimal(), + ISOAlphaCurrency("EUR") + ) + ) + + result.shouldBeInstanceOf() + result.component?.className shouldBe "de.tillhub.paymentengine.zvt.ui.CardPaymentActivity" + BundleCompat.getParcelable( + result.extras!!, + ExtraKeys.EXTRA_CONFIG, + Terminal.ZVT::class.java + ) shouldBe ZVT + BundleCompat.getSerializable( + result.extras!!, + ExtraKeys.EXTRA_AMOUNT, + BigDecimal::class.java + ) shouldBe 600.toBigDecimal() + BundleCompat.getParcelable( + result.extras!!, + ExtraKeys.EXTRA_CURRENCY, + ISOAlphaCurrency::class.java + ) shouldBe ISOAlphaCurrency("EUR") + + verify { + analytics.logOperation( + "Operation: CARD_PAYMENT(" + + "amount: 500, " + + "tip: 100, " + + "currency: ISOAlphaCurrency(value=EUR))" + + "\nTerminal.ZVT(" + + "name=zvt, " + + "ipAddress=127.0.0.1, " + + "port=40007, " + + "saleConfig=CardSaleConfig(" + + "applicationName=Tillhub GO, " + + "operatorId=ah, " + + "saleId=registerProvider, " + + "pin=333333, " + + "poiId=66000001, " + + "poiSerialNumber=" + + "), " + + "terminalPrinterAvailable=true, " + + "isoCurrencyNumber=0978" + + ")" + ) + } + } + + test("parseResult SPOS: result OK") { + val intent = Intent().apply { + putExtra(SPOSKey.ResultExtra.RESULT_STATE, "Success") + putExtra(SPOSKey.ResultExtra.TRANSACTION_RESULT, "ACCEPTED") + putExtra(SPOSKey.ResultExtra.TERMINAL_ID, "terminal_id") + putExtra(SPOSKey.ResultExtra.TRANSACTION_DATA, "transaction_data") + putExtra(SPOSKey.ResultExtra.CARD_CIRCUIT, "card_circuit") + putExtra(SPOSKey.ResultExtra.CARD_PAN, "card_pan") + } + + val result = target.parseResult(Activity.RESULT_OK, intent) + + result.shouldBeInstanceOf() + } + + test("parseResult SPOS: result CANCELED") { + val intent = Intent().apply { + putExtra(SPOSKey.ResultExtra.ERROR, "CARD_PAYMENT_NOT_ONBOARDED") + } + + val result = target.parseResult(Activity.RESULT_CANCELED, intent) + + result.shouldBeInstanceOf() + } + + test("parseResult OPI + ZVT: result OK") { + val intent = Intent().apply { + putExtra( + ExtraKeys.EXTRAS_RESULT, + TerminalOperationStatus.Success.OPI( + date = mockk(), + customerReceipt = "customerReceipt", + merchantReceipt = "merchantReceipt", + rawData = "rawData", + data = null + ) + ) + } + + val result = target.parseResult(Activity.RESULT_OK, intent) + + result.shouldBeInstanceOf() + } + + test("parseResult OPI + ZVT: result CANCELED") { + val intent = Intent() + + val result = target.parseResult(Activity.RESULT_CANCELED, intent) + + result.shouldBeInstanceOf() + } +}) { + companion object { + val ZVT = Terminal.ZVT( + name = "zvt", + ipAddress = "127.0.0.1", + port = 40007, + ) + val OPI = Terminal.OPI( + name = "opi", + ipAddress = "127.0.0.1", + port = 20002, + port2 = 20007 + ) + val SPOS = Terminal.SPOS( + name = "s-pos", + ) + } +} diff --git a/payment-engine/src/test/java/de/tillhub/paymentengine/contract/PaymentReversalContractTest.kt b/payment-engine/src/test/java/de/tillhub/paymentengine/contract/PaymentReversalContractTest.kt new file mode 100644 index 0000000..2f72a61 --- /dev/null +++ b/payment-engine/src/test/java/de/tillhub/paymentengine/contract/PaymentReversalContractTest.kt @@ -0,0 +1,247 @@ +package de.tillhub.paymentengine.contract + +import android.app.Activity +import android.content.Context +import android.content.Intent +import android.os.Build +import androidx.core.os.BundleCompat +import br.com.colman.kotest.android.extensions.robolectric.RobolectricTest +import de.tillhub.paymentengine.analytics.PaymentAnalytics +import de.tillhub.paymentengine.data.ExtraKeys +import de.tillhub.paymentengine.data.ISOAlphaCurrency +import de.tillhub.paymentengine.data.Terminal +import de.tillhub.paymentengine.data.TerminalOperationStatus +import de.tillhub.paymentengine.spos.data.SPOSKey +import io.kotest.core.spec.style.FunSpec +import io.kotest.matchers.shouldBe +import io.kotest.matchers.types.shouldBeInstanceOf +import io.mockk.Runs +import io.mockk.every +import io.mockk.just +import io.mockk.mockk +import io.mockk.spyk +import io.mockk.verify +import org.robolectric.RuntimeEnvironment +import org.robolectric.annotation.Config + +@RobolectricTest +@Config(sdk = [Build.VERSION_CODES.UPSIDE_DOWN_CAKE]) +class PaymentReversalContractTest : FunSpec({ + lateinit var context: Context + lateinit var analytics: PaymentAnalytics + + lateinit var target: PaymentReversalContract + + beforeTest { + context = spyk(RuntimeEnvironment.getApplication()) + analytics = mockk { + every { logOperation(any()) } just Runs + every { logCommunication(any(), any()) } just Runs + } + + target = PaymentReversalContract(analytics) + } + + test("createIntent SPOS") { + val result = target.createIntent( + context, + ReversalRequest( + SPOS, + "UUID", + 500.toBigDecimal(), + 100.toBigDecimal(), + ISOAlphaCurrency("EUR"), + "receiptNo" + ) + ) + + result.shouldBeInstanceOf() + result.action shouldBe "de.spayment.akzeptanz.TRANSACTION" + result.extras?.getString(SPOSKey.Extra.TRANSACTION_TYPE) shouldBe "PaymentReversal" + result.extras?.getString(SPOSKey.Extra.CURRENCY_ISO) shouldBe "EUR" + result.extras?.getString(SPOSKey.Extra.AMOUNT) shouldBe "500" + result.extras?.getString(SPOSKey.Extra.TIP_AMOUNT) shouldBe "100" + result.extras?.getString(SPOSKey.Extra.TRANSACTION_ID) shouldBe "UUID" + result.extras?.getString(SPOSKey.Extra.TAX_AMOUNT) shouldBe "000" + result.extras?.getString(SPOSKey.Extra.TRANSACTION_DATA) shouldBe "receiptNo" + + verify { + analytics.logOperation( + "Operation: CARD_PAYMENT_REVERSAL(" + + "stan: receiptNo)" + + "\nTerminal.SPOS(" + + "name=s-pos, " + + "appId=TESTCLIENT, " + + "saleConfig=CardSaleConfig(" + + "applicationName=Tillhub GO, " + + "operatorId=ah, " + + "saleId=registerProvider, " + + "pin=333333, " + + "poiId=66000001, " + + "poiSerialNumber=" + + "), " + + "currencyCode=EUR" + + ")" + ) + } + } + + test("createIntent OPI") { + val result = target.createIntent( + context, + ReversalRequest( + OPI, + "UUID", + 500.toBigDecimal(), + 100.toBigDecimal(), + ISOAlphaCurrency("EUR"), + "receiptNo" + ) + ) + + result.shouldBeInstanceOf() + result.component?.className shouldBe "de.tillhub.paymentengine.opi.ui.OPIPaymentReversalActivity" + BundleCompat.getParcelable( + result.extras!!, + ExtraKeys.EXTRA_CONFIG, + Terminal.OPI::class.java + ) shouldBe OPI + result.extras?.getString(ExtraKeys.EXTRA_RECEIPT_NO) shouldBe "receiptNo" + + verify { + analytics.logOperation( + "Operation: CARD_PAYMENT_REVERSAL(" + + "stan: receiptNo)" + + "\nTerminal.OPI(" + + "name=opi, " + + "ipAddress=127.0.0.1, " + + "port=20002, " + + "saleConfig=CardSaleConfig(" + + "applicationName=Tillhub GO, " + + "operatorId=ah, " + + "saleId=registerProvider, " + + "pin=333333, " + + "poiId=66000001, " + + "poiSerialNumber=" + + "), " + + "port2=20007, " + + "currencyCode=EUR" + + ")" + ) + } + } + + test("createIntent ZVT") { + val result = target.createIntent( + context, + ReversalRequest( + ZVT, + "UUID", + 500.toBigDecimal(), + 100.toBigDecimal(), + ISOAlphaCurrency("EUR"), + "receiptNo" + ) + ) + + result.shouldBeInstanceOf() + result.component?.className shouldBe "de.tillhub.paymentengine.zvt.ui.CardPaymentReversalActivity" + BundleCompat.getParcelable( + result.extras!!, + ExtraKeys.EXTRA_CONFIG, + Terminal.ZVT::class.java + ) shouldBe ZVT + result.extras?.getString(ExtraKeys.EXTRA_RECEIPT_NO) shouldBe "receiptNo" + + verify { + analytics.logOperation( + "Operation: CARD_PAYMENT_REVERSAL(" + + "stan: receiptNo)" + + "\nTerminal.ZVT(" + + "name=zvt, " + + "ipAddress=127.0.0.1, " + + "port=40007, " + + "saleConfig=CardSaleConfig(" + + "applicationName=Tillhub GO, " + + "operatorId=ah, " + + "saleId=registerProvider, " + + "pin=333333, " + + "poiId=66000001, " + + "poiSerialNumber=" + + "), " + + "terminalPrinterAvailable=true, " + + "isoCurrencyNumber=0978" + + ")" + ) + } + } + + test("parseResult SPOS: result OK") { + val intent = Intent().apply { + putExtra(SPOSKey.ResultExtra.RESULT_STATE, "Success") + putExtra(SPOSKey.ResultExtra.TRANSACTION_RESULT, "ACCEPTED") + putExtra(SPOSKey.ResultExtra.TERMINAL_ID, "terminal_id") + putExtra(SPOSKey.ResultExtra.TRANSACTION_DATA, "transaction_data") + putExtra(SPOSKey.ResultExtra.CARD_CIRCUIT, "card_circuit") + putExtra(SPOSKey.ResultExtra.CARD_PAN, "card_pan") + } + + val result = target.parseResult(Activity.RESULT_OK, intent) + + result.shouldBeInstanceOf() + } + + test("parseResult SPOS: result CANCELED") { + val intent = Intent().apply { + putExtra(SPOSKey.ResultExtra.ERROR, "CARD_PAYMENT_NOT_ONBOARDED") + } + + val result = target.parseResult(Activity.RESULT_CANCELED, intent) + + result.shouldBeInstanceOf() + } + + test("parseResult OPI + ZVT: result OK") { + val intent = Intent().apply { + putExtra( + ExtraKeys.EXTRAS_RESULT, + TerminalOperationStatus.Success.OPI( + date = mockk(), + customerReceipt = "customerReceipt", + merchantReceipt = "merchantReceipt", + rawData = "rawData", + data = null + ) + ) + } + + val result = target.parseResult(Activity.RESULT_OK, intent) + + result.shouldBeInstanceOf() + } + + test("parseResult OPI + ZVT: result CANCELED") { + val intent = Intent() + + val result = target.parseResult(Activity.RESULT_CANCELED, intent) + + result.shouldBeInstanceOf() + } +}) { + companion object { + val ZVT = Terminal.ZVT( + name = "zvt", + ipAddress = "127.0.0.1", + port = 40007, + ) + val OPI = Terminal.OPI( + name = "opi", + ipAddress = "127.0.0.1", + port = 20002, + port2 = 20007 + ) + val SPOS = Terminal.SPOS( + name = "s-pos", + ) + } +} diff --git a/payment-engine/src/test/java/de/tillhub/paymentengine/contract/TerminalConnectContractTest.kt b/payment-engine/src/test/java/de/tillhub/paymentengine/contract/TerminalConnectContractTest.kt new file mode 100644 index 0000000..d40a753 --- /dev/null +++ b/payment-engine/src/test/java/de/tillhub/paymentengine/contract/TerminalConnectContractTest.kt @@ -0,0 +1,133 @@ +package de.tillhub.paymentengine.contract + +import android.app.Activity +import android.content.Context +import android.content.Intent +import android.os.Build +import br.com.colman.kotest.android.extensions.robolectric.RobolectricTest +import de.tillhub.paymentengine.analytics.PaymentAnalytics +import de.tillhub.paymentengine.data.Terminal +import de.tillhub.paymentengine.data.TerminalOperationStatus +import de.tillhub.paymentengine.spos.data.SPOSKey +import io.kotest.core.spec.style.FunSpec +import io.kotest.matchers.shouldBe +import io.kotest.matchers.types.shouldBeInstanceOf +import io.mockk.Runs +import io.mockk.every +import io.mockk.just +import io.mockk.mockk +import io.mockk.spyk +import io.mockk.verify +import org.robolectric.RuntimeEnvironment +import org.robolectric.annotation.Config + +@RobolectricTest +@Config(sdk = [Build.VERSION_CODES.UPSIDE_DOWN_CAKE]) +class TerminalConnectContractTest : FunSpec({ + + lateinit var context: Context + lateinit var analytics: PaymentAnalytics + + lateinit var target: TerminalConnectContract + + beforeTest { + context = spyk(RuntimeEnvironment.getApplication()) + analytics = mockk { + every { logOperation(any()) } just Runs + every { logCommunication(any(), any()) } just Runs + } + + target = TerminalConnectContract(analytics) + } + + test("createIntent SPOS") { + val result = target.createIntent( + context, + SPOS + ) + + result.shouldBeInstanceOf() + result.action shouldBe "de.spayment.akzeptanz.S_SWITCH_CONNECT" + result.extras?.getString(SPOSKey.Extra.APP_ID) shouldBe "TESTCLIENT" + + verify { + analytics.logOperation( + "Operation: TERMINAL_CONNECT" + + "\nTerminal.SPOS(" + + "name=s-pos, " + + "appId=TESTCLIENT, " + + "saleConfig=CardSaleConfig(" + + "applicationName=Tillhub GO, " + + "operatorId=ah, " + + "saleId=registerProvider, " + + "pin=333333, " + + "poiId=66000001, " + + "poiSerialNumber=" + + "), " + + "currencyCode=EUR" + + ")" + ) + } + } + + test("createIntent OPI + ZVT") { + try { + target.createIntent( + context, + OPI + ) + } catch (e: Exception) { + e.shouldBeInstanceOf() + e.message shouldBe "Connect only supported for S-POS terminals" + } + + verify(inverse = true) { + analytics.logOperation(any()) + } + } + + test("parseResult SPOS: result OK") { + val intent = Intent() + + val result = target.parseResult(Activity.RESULT_OK, intent) + + result.shouldBeInstanceOf() + + verify { + analytics.logCommunication( + protocol = "SPOS", + message = "RESPONSE: RESULT OK" + ) + } + } + + test("parseResult SPOS: result CANCELED") { + val intent = Intent().apply { + putExtra(SPOSKey.ResultExtra.ERROR, "CARD_PAYMENT_NOT_ONBOARDED") + } + + val result = target.parseResult(Activity.RESULT_CANCELED, intent) + + result.shouldBeInstanceOf() + + verify { + analytics.logCommunication( + protocol = "SPOS", + message = "RESPONSE: RESULT CANCELED\nExtras {\n" + + "ERROR = CARD_PAYMENT_NOT_ONBOARDED\n}" + ) + } + } +}) { + companion object { + val OPI = Terminal.OPI( + name = "opi", + ipAddress = "127.0.0.1", + port = 20002, + port2 = 20007 + ) + val SPOS = Terminal.SPOS( + name = "s-pos", + ) + } +} diff --git a/payment-engine/src/test/java/de/tillhub/paymentengine/contract/TerminalDisconnectContractTest.kt b/payment-engine/src/test/java/de/tillhub/paymentengine/contract/TerminalDisconnectContractTest.kt new file mode 100644 index 0000000..87bef24 --- /dev/null +++ b/payment-engine/src/test/java/de/tillhub/paymentengine/contract/TerminalDisconnectContractTest.kt @@ -0,0 +1,127 @@ +package de.tillhub.paymentengine.contract + +import android.app.Activity +import android.content.Context +import android.content.Intent +import android.os.Build +import br.com.colman.kotest.android.extensions.robolectric.RobolectricTest +import de.tillhub.paymentengine.analytics.PaymentAnalytics +import de.tillhub.paymentengine.data.Terminal +import de.tillhub.paymentengine.data.TerminalOperationStatus +import de.tillhub.paymentengine.spos.data.SPOSKey +import io.kotest.core.spec.style.FunSpec +import io.kotest.matchers.shouldBe +import io.kotest.matchers.types.shouldBeInstanceOf +import io.mockk.Runs +import io.mockk.every +import io.mockk.just +import io.mockk.mockk +import io.mockk.spyk +import io.mockk.verify +import org.robolectric.RuntimeEnvironment +import org.robolectric.annotation.Config + +@RobolectricTest +@Config(sdk = [Build.VERSION_CODES.UPSIDE_DOWN_CAKE]) +class TerminalDisconnectContractTest : FunSpec({ + + lateinit var context: Context + lateinit var analytics: PaymentAnalytics + + lateinit var target: TerminalDisconnectContract + + beforeTest { + context = spyk(RuntimeEnvironment.getApplication()) + analytics = mockk { + every { logOperation(any()) } just Runs + every { logCommunication(any(), any()) } just Runs + } + + target = TerminalDisconnectContract(analytics) + } + + test("createIntent SPOS") { + val result = target.createIntent( + context, + SPOS + ) + + result.shouldBeInstanceOf() + result.action shouldBe "de.spayment.akzeptanz.S_SWITCH_DISCONNECT" + result.extras?.getString(SPOSKey.Extra.APP_ID) shouldBe "TESTCLIENT" + + verify { + analytics.logOperation( + "Operation: TERMINAL_DISCONNECT" + + "\nTerminal.SPOS(" + + "name=s-pos, " + + "appId=TESTCLIENT, " + + "saleConfig=CardSaleConfig(" + + "applicationName=Tillhub GO, " + + "operatorId=ah, " + + "saleId=registerProvider, " + + "pin=333333, " + + "poiId=66000001, " + + "poiSerialNumber=" + + "), " + + "currencyCode=EUR" + + ")" + ) + } + } + + test("createIntent OPI + ZVT") { + try { + target.createIntent( + context, + OPI + ) + } catch (e: Exception) { + e.shouldBeInstanceOf() + e.message shouldBe "Disconnect only supported for S-POS terminals" + } + verify(inverse = true) { + analytics.logOperation(any()) + } + } + + test("parseResult SPOS: result OK") { + val intent = Intent() + + val result = target.parseResult(Activity.RESULT_OK, intent) + + result.shouldBeInstanceOf() + verify { + analytics.logCommunication( + protocol = "SPOS", + message = "RESPONSE: RESULT OK" + ) + } + } + + test("parseResult SPOS: result CANCELED") { + val intent = Intent() + + val result = target.parseResult(Activity.RESULT_CANCELED, intent) + + result.shouldBeInstanceOf() + verify { + analytics.logCommunication( + protocol = "SPOS", + message = "RESPONSE: RESULT CANCELED" + ) + } + } +}) { + companion object { + val OPI = Terminal.OPI( + name = "opi", + ipAddress = "127.0.0.1", + port = 20002, + port2 = 20007 + ) + val SPOS = Terminal.SPOS( + name = "s-pos", + ) + } +} \ No newline at end of file diff --git a/payment-engine/src/test/java/de/tillhub/paymentengine/contract/TerminalReconciliationContractTest.kt b/payment-engine/src/test/java/de/tillhub/paymentengine/contract/TerminalReconciliationContractTest.kt new file mode 100644 index 0000000..5a3f119 --- /dev/null +++ b/payment-engine/src/test/java/de/tillhub/paymentengine/contract/TerminalReconciliationContractTest.kt @@ -0,0 +1,215 @@ +package de.tillhub.paymentengine.contract + +import android.app.Activity +import android.content.Context +import android.content.Intent +import android.os.Build +import androidx.core.os.BundleCompat +import br.com.colman.kotest.android.extensions.robolectric.RobolectricTest +import de.tillhub.paymentengine.analytics.PaymentAnalytics +import de.tillhub.paymentengine.data.ExtraKeys +import de.tillhub.paymentengine.data.Terminal +import de.tillhub.paymentengine.data.TerminalOperationStatus +import de.tillhub.paymentengine.spos.data.SPOSKey +import io.kotest.core.spec.style.FunSpec +import io.kotest.matchers.nulls.shouldBeNull +import io.kotest.matchers.shouldBe +import io.kotest.matchers.types.shouldBeInstanceOf +import io.mockk.Runs +import io.mockk.every +import io.mockk.just +import io.mockk.mockk +import io.mockk.spyk +import io.mockk.verify +import org.robolectric.RuntimeEnvironment +import org.robolectric.annotation.Config + +@RobolectricTest +@Config(sdk = [Build.VERSION_CODES.UPSIDE_DOWN_CAKE]) +class TerminalReconciliationContractTest : FunSpec({ + lateinit var context: Context + lateinit var analytics: PaymentAnalytics + + lateinit var target: TerminalReconciliationContract + + beforeTest { + context = spyk(RuntimeEnvironment.getApplication()) + analytics = mockk { + every { logOperation(any()) } just Runs + every { logCommunication(any(), any()) } just Runs + } + + target = TerminalReconciliationContract(analytics) + } + + test("createIntent SPOS") { + val result = target.createIntent( + context, + SPOS + ) + + result.shouldBeInstanceOf() + result.action shouldBe "de.spayment.akzeptanz.RECONCILIATION" + result.extras.shouldBeNull() + + verify { + analytics.logOperation( + "Operation: RECONCILIATION" + + "\nTerminal.SPOS(" + + "name=s-pos, " + + "appId=TESTCLIENT, " + + "saleConfig=CardSaleConfig(" + + "applicationName=Tillhub GO, " + + "operatorId=ah, " + + "saleId=registerProvider, " + + "pin=333333, " + + "poiId=66000001, " + + "poiSerialNumber=" + + "), " + + "currencyCode=EUR" + + ")" + ) + } + } + + test("createIntent OPI") { + val result = target.createIntent( + context, + OPI + ) + + result.shouldBeInstanceOf() + result.component?.className shouldBe "de.tillhub.paymentengine.opi.ui.OPIReconciliationActivity" + BundleCompat.getParcelable( + result.extras!!, + ExtraKeys.EXTRA_CONFIG, + Terminal.OPI::class.java + ) shouldBe OPI + + verify { + analytics.logOperation( + "Operation: RECONCILIATION" + + "\nTerminal.OPI(" + + "name=opi, " + + "ipAddress=127.0.0.1, " + + "port=20002, " + + "saleConfig=CardSaleConfig(" + + "applicationName=Tillhub GO, " + + "operatorId=ah, " + + "saleId=registerProvider, " + + "pin=333333, " + + "poiId=66000001, " + + "poiSerialNumber=" + + "), " + + "port2=20007, " + + "currencyCode=EUR" + + ")" + ) + } + } + + test("createIntent ZVT") { + val result = target.createIntent( + context, + ZVT + ) + + result.shouldBeInstanceOf() + result.component?.className shouldBe "de.tillhub.paymentengine.zvt.ui.TerminalReconciliationActivity" + BundleCompat.getParcelable( + result.extras!!, + ExtraKeys.EXTRA_CONFIG, + Terminal.ZVT::class.java + ) shouldBe ZVT + + verify { + analytics.logOperation( + "Operation: RECONCILIATION" + + "\nTerminal.ZVT(" + + "name=zvt, " + + "ipAddress=127.0.0.1, " + + "port=40007, " + + "saleConfig=CardSaleConfig(" + + "applicationName=Tillhub GO, " + + "operatorId=ah, " + + "saleId=registerProvider, " + + "pin=333333, " + + "poiId=66000001, " + + "poiSerialNumber=" + + "), " + + "terminalPrinterAvailable=true, " + + "isoCurrencyNumber=0978" + + ")" + ) + } + } + + test("parseResult SPOS: result OK") { + val intent = Intent().apply { + putExtra(SPOSKey.ResultExtra.RESULT_STATE, "Success") + putExtra(SPOSKey.ResultExtra.TRANSACTION_RESULT, "ACCEPTED") + putExtra(SPOSKey.ResultExtra.TERMINAL_ID, "terminal_id") + putExtra(SPOSKey.ResultExtra.TRANSACTION_DATA, "transaction_data") + putExtra(SPOSKey.ResultExtra.CARD_CIRCUIT, "card_circuit") + putExtra(SPOSKey.ResultExtra.CARD_PAN, "card_pan") + } + + val result = target.parseResult(Activity.RESULT_OK, intent) + + result.shouldBeInstanceOf() + } + + test("parseResult SPOS: result CANCELED") { + val intent = Intent().apply { + putExtra(SPOSKey.ResultExtra.ERROR, "CARD_PAYMENT_NOT_ONBOARDED") + } + + val result = target.parseResult(Activity.RESULT_CANCELED, intent) + + result.shouldBeInstanceOf() + } + + test("parseResult OPI + ZVT: result OK") { + val intent = Intent().apply { + putExtra( + ExtraKeys.EXTRAS_RESULT, + TerminalOperationStatus.Success.OPI( + date = mockk(), + customerReceipt = "customerReceipt", + merchantReceipt = "merchantReceipt", + rawData = "rawData", + data = null + ) + ) + } + + val result = target.parseResult(Activity.RESULT_OK, intent) + + result.shouldBeInstanceOf() + } + + test("parseResult OPI + ZVT: result CANCELED") { + val intent = Intent() + + val result = target.parseResult(Activity.RESULT_CANCELED, intent) + + result.shouldBeInstanceOf() + } +}) { + companion object { + val ZVT = Terminal.ZVT( + name = "zvt", + ipAddress = "127.0.0.1", + port = 40007, + ) + val OPI = Terminal.OPI( + name = "opi", + ipAddress = "127.0.0.1", + port = 20002, + port2 = 20007 + ) + val SPOS = Terminal.SPOS( + name = "s-pos", + ) + } +} \ No newline at end of file diff --git a/payment-engine/src/test/java/de/tillhub/paymentengine/opi/OPIChannelControllerImplTest.kt b/payment-engine/src/test/java/de/tillhub/paymentengine/opi/OPIChannelControllerImplTest.kt index f1f7849..facdf80 100644 --- a/payment-engine/src/test/java/de/tillhub/paymentengine/opi/OPIChannelControllerImplTest.kt +++ b/payment-engine/src/test/java/de/tillhub/paymentengine/opi/OPIChannelControllerImplTest.kt @@ -294,7 +294,6 @@ internal class OPIChannelControllerImplTest : DescribeSpec({ target.operationState.value shouldBe OPIOperationStatus.Pending.Operation(NOW) verify(Ordering.ORDERED) { - analytics.logOperation("Operation: CARD_PAYMENT(amount: 6.0, currency: ISOAlphaCurrency(value=EUR))\n$TERMINAL_STRING") converterFactory.newStringToDtoConverter(DeviceRequest::class.java) converterFactory.newDtoToStringConverter() opiChannel1.setOnError(any()) @@ -475,7 +474,6 @@ internal class OPIChannelControllerImplTest : DescribeSpec({ target.operationState.value shouldBe OPIOperationStatus.Pending.Operation(NOW) verify(Ordering.ORDERED) { - analytics.logOperation("Operation: CARD_PAYMENT_REVERSAL(stan: 223)\n$TERMINAL_STRING") converterFactory.newStringToDtoConverter(DeviceRequest::class.java) converterFactory.newDtoToStringConverter() opiChannel1.setOnError(any()) @@ -628,7 +626,6 @@ internal class OPIChannelControllerImplTest : DescribeSpec({ target.operationState.value shouldBe OPIOperationStatus.Pending.Operation(NOW) verify(Ordering.ORDERED) { - analytics.logOperation("Operation: PARTIAL_REFUND(amount: 6.0, currency: ISOAlphaCurrency(value=EUR))\n$TERMINAL_STRING") converterFactory.newStringToDtoConverter(DeviceRequest::class.java) converterFactory.newDtoToStringConverter() opiChannel1.setOnError(any()) @@ -786,7 +783,6 @@ internal class OPIChannelControllerImplTest : DescribeSpec({ target.operationState.value shouldBe OPIOperationStatus.Pending.Operation(NOW) verify(Ordering.ORDERED) { - analytics.logOperation("Operation: RECONCILIATION\n$TERMINAL_STRING") converterFactory.newStringToDtoConverter(DeviceRequest::class.java) converterFactory.newDtoToStringConverter() opiChannel1.setOnError(any()) diff --git a/payment-engine/src/test/java/de/tillhub/paymentengine/spos/SPOSResponseHandlerTest.kt b/payment-engine/src/test/java/de/tillhub/paymentengine/spos/SPOSResponseHandlerTest.kt new file mode 100644 index 0000000..e194de8 --- /dev/null +++ b/payment-engine/src/test/java/de/tillhub/paymentengine/spos/SPOSResponseHandlerTest.kt @@ -0,0 +1,283 @@ +package de.tillhub.paymentengine.spos + +import android.app.Activity +import android.content.Intent +import de.tillhub.paymentengine.R +import de.tillhub.paymentengine.data.TerminalOperationStatus +import de.tillhub.paymentengine.data.TransactionResultCode +import de.tillhub.paymentengine.spos.data.ReceiptDto +import de.tillhub.paymentengine.spos.data.SPOSKey +import de.tillhub.paymentengine.spos.data.StringToReceiptDtoConverter +import io.kotest.core.spec.style.DescribeSpec +import io.kotest.matchers.shouldBe +import io.kotest.matchers.types.shouldBeInstanceOf +import io.mockk.every +import io.mockk.mockk + +class SPOSResponseHandlerTest : DescribeSpec({ + lateinit var intent: Intent + lateinit var receiptDto: ReceiptDto + lateinit var receiptConverter: StringToReceiptDtoConverter + + beforeTest { + receiptDto = mockk { + every { toReceiptString() } returns "RECEIPT" + } + + intent = mockk { + every { extras } returns null + } + + receiptConverter = mockk { + every { convert(any()) } returns receiptDto + } + } + + describe("handleTerminalConnectResponse") { + it("success") { + val result = SPOSResponseHandler.handleTerminalConnectResponse( + Activity.RESULT_OK, + intent + ) + + result.shouldBeInstanceOf() + } + + it("error") { + every { + intent.extras?.getString(SPOSKey.ResultExtra.ERROR) + } returns "CARD_PAYMENT_NOT_ONBOARDED" + every { + intent.extras?.keySet() + } returns setOf(SPOSKey.ResultExtra.ERROR) + + val result = SPOSResponseHandler.handleTerminalConnectResponse( + Activity.RESULT_CANCELED, + intent + ) + + result.shouldBeInstanceOf() + result.resultCode.shouldBeInstanceOf() + result.resultCode.errorMessage shouldBe R.string.spos_error_terminal_not_onboarded + } + + it("canceled") { + val result = SPOSResponseHandler.handleTerminalConnectResponse( + Activity.RESULT_CANCELED, + intent + ) + + result.shouldBeInstanceOf() + } + } + + describe("handleTerminalDisconnectResponse") { + it("success") { + val result = SPOSResponseHandler.handleTerminalDisconnectResponse( + Activity.RESULT_OK + ) + + result.shouldBeInstanceOf() + } + + it("canceled") { + val result = SPOSResponseHandler.handleTerminalDisconnectResponse( + Activity.RESULT_CANCELED + ) + + result.shouldBeInstanceOf() + } + } + + describe("handleTransactionResponse") { + it("canceled") { + val result = SPOSResponseHandler.handleTransactionResponse( + Activity.RESULT_CANCELED, + intent + ) + + result.shouldBeInstanceOf() + } + + describe("error") { + it("not onboarded") { + every { + intent.extras?.getString(any()) + } returns null + every { + intent.extras?.getString(SPOSKey.ResultExtra.ERROR) + } returns "CARD_PAYMENT_NOT_ONBOARDED" + + every { + intent.extras?.keySet() + } returns setOf(SPOSKey.ResultExtra.ERROR) + + val result = SPOSResponseHandler.handleTransactionResponse( + Activity.RESULT_CANCELED, + intent + ) + + result.shouldBeInstanceOf() + result.resultCode.shouldBeInstanceOf() + result.resultCode.errorMessage shouldBe R.string.spos_error_terminal_not_onboarded + } + + it("no extras") { + every { + intent.extras?.keySet() + } returns setOf() + + every { intent.extras?.getString(any()) } returns null + + val result = SPOSResponseHandler.handleTransactionResponse( + Activity.RESULT_OK, + intent + ) + + result.shouldBeInstanceOf() + result.resultCode.shouldBeInstanceOf() + result.resultCode.errorMessage shouldBe R.string.zvt_error_code_unknown + } + + it("tx error") { + every { + intent.extras?.keySet() + } returns setOf( + SPOSKey.ResultExtra.RECEIPT_MERCHANT, + SPOSKey.ResultExtra.RECEIPT_CUSTOMER, + SPOSKey.ResultExtra.RESULT_STATE, + SPOSKey.ResultExtra.TRANSACTION_RESULT, + SPOSKey.ResultExtra.TERMINAL_ID, + SPOSKey.ResultExtra.TRANSACTION_DATA, + SPOSKey.ResultExtra.CARD_CIRCUIT, + SPOSKey.ResultExtra.CARD_PAN, + SPOSKey.ResultExtra.MERCHANT + ) + every { + intent.extras?.getString(SPOSKey.ResultExtra.RECEIPT_MERCHANT) + } returns "merchant_receipt" + every { + intent.extras?.getString(SPOSKey.ResultExtra.RECEIPT_CUSTOMER) + } returns "customer_receipt" + every { + intent.extras?.getString(SPOSKey.ResultExtra.RESULT_STATE) + } returns "Failure" + every { + intent.extras?.getString(SPOSKey.ResultExtra.TRANSACTION_RESULT) + } returns "FAILED" + every { + intent.extras?.getString(SPOSKey.ResultExtra.TERMINAL_ID) + } returns "terminal_id" + every { + intent.extras?.getString(SPOSKey.ResultExtra.TRANSACTION_DATA) + } returns "transaction_data" + every { + intent.extras?.getString(SPOSKey.ResultExtra.CARD_CIRCUIT) + } returns "card_circuit" + every { + intent.extras?.getString(SPOSKey.ResultExtra.CARD_PAN) + } returns "card_pan" + every { + intent.extras?.getString(SPOSKey.ResultExtra.MERCHANT) + } returns "merchant" + + val result = SPOSResponseHandler.handleTransactionResponse( + Activity.RESULT_OK, + intent, + receiptConverter + ) + + result.shouldBeInstanceOf() + result.resultCode.shouldBeInstanceOf() + result.resultCode.errorMessage shouldBe R.string.spos_error_failure + result.customerReceipt shouldBe "RECEIPT" + result.merchantReceipt shouldBe "RECEIPT" + result.rawData shouldBe "Extras {\n" + + "${SPOSKey.ResultExtra.RECEIPT_MERCHANT} = merchant_receipt\n" + + "${SPOSKey.ResultExtra.RECEIPT_CUSTOMER} = customer_receipt\n" + + "${SPOSKey.ResultExtra.RESULT_STATE} = Failure\n" + + "${SPOSKey.ResultExtra.TRANSACTION_RESULT} = FAILED\n" + + "${SPOSKey.ResultExtra.TERMINAL_ID} = terminal_id\n" + + "${SPOSKey.ResultExtra.TRANSACTION_DATA} = transaction_data\n" + + "${SPOSKey.ResultExtra.CARD_CIRCUIT} = card_circuit\n" + + "${SPOSKey.ResultExtra.CARD_PAN} = card_pan\n" + + "${SPOSKey.ResultExtra.MERCHANT} = merchant\n" + + "}" + result.data?.terminalId shouldBe "terminal_id" + result.data?.transactionId shouldBe "transaction_data" + result.data?.cardCircuit shouldBe "card_circuit" + result.data?.cardPan shouldBe "card_pan" + result.data?.paymentProvider shouldBe "merchant" + } + } + + it("success") { + every { + intent.extras?.keySet() + } returns setOf( + SPOSKey.ResultExtra.RECEIPT_MERCHANT, + SPOSKey.ResultExtra.RECEIPT_CUSTOMER, + SPOSKey.ResultExtra.RESULT_STATE, + SPOSKey.ResultExtra.TRANSACTION_RESULT, + SPOSKey.ResultExtra.TERMINAL_ID, + SPOSKey.ResultExtra.TRANSACTION_DATA, + SPOSKey.ResultExtra.CARD_CIRCUIT, + SPOSKey.ResultExtra.CARD_PAN, + SPOSKey.ResultExtra.MERCHANT + ) + every { + intent.extras?.getString(SPOSKey.ResultExtra.RECEIPT_MERCHANT) + } returns "merchant_receipt" + every { + intent.extras?.getString(SPOSKey.ResultExtra.RECEIPT_CUSTOMER) + } returns "customer_receipt" + every { + intent.extras?.getString(SPOSKey.ResultExtra.RESULT_STATE) + } returns "Success" + every { + intent.extras?.getString(SPOSKey.ResultExtra.TRANSACTION_RESULT) + } returns "ACCEPTED" + every { + intent.extras?.getString(SPOSKey.ResultExtra.TERMINAL_ID) + } returns "terminal_id" + every { + intent.extras?.getString(SPOSKey.ResultExtra.TRANSACTION_DATA) + } returns "transaction_data" + every { + intent.extras?.getString(SPOSKey.ResultExtra.CARD_CIRCUIT) + } returns "card_circuit" + every { + intent.extras?.getString(SPOSKey.ResultExtra.CARD_PAN) + } returns "card_pan" + every { + intent.extras?.getString(SPOSKey.ResultExtra.MERCHANT) + } returns "merchant" + + val result = SPOSResponseHandler.handleTransactionResponse( + Activity.RESULT_OK, + intent, + receiptConverter + ) + + result.shouldBeInstanceOf() + result.customerReceipt shouldBe "RECEIPT" + result.merchantReceipt shouldBe "RECEIPT" + result.rawData shouldBe "Extras {\n" + + "${SPOSKey.ResultExtra.RECEIPT_MERCHANT} = merchant_receipt\n" + + "${SPOSKey.ResultExtra.RECEIPT_CUSTOMER} = customer_receipt\n" + + "${SPOSKey.ResultExtra.RESULT_STATE} = Success\n" + + "${SPOSKey.ResultExtra.TRANSACTION_RESULT} = ACCEPTED\n" + + "${SPOSKey.ResultExtra.TERMINAL_ID} = terminal_id\n" + + "${SPOSKey.ResultExtra.TRANSACTION_DATA} = transaction_data\n" + + "${SPOSKey.ResultExtra.CARD_CIRCUIT} = card_circuit\n" + + "${SPOSKey.ResultExtra.CARD_PAN} = card_pan\n" + + "${SPOSKey.ResultExtra.MERCHANT} = merchant\n" + + "}" + result.data?.terminalId shouldBe "terminal_id" + result.data?.transactionId shouldBe "transaction_data" + result.data?.cardCircuit shouldBe "card_circuit" + result.data?.cardPan shouldBe "card_pan" + result.data?.paymentProvider shouldBe "merchant" + } + } +}) diff --git a/payment-engine/src/test/java/de/tillhub/paymentengine/spos/data/StringToReceiptDtoConverterTest.kt b/payment-engine/src/test/java/de/tillhub/paymentengine/spos/data/StringToReceiptDtoConverterTest.kt new file mode 100644 index 0000000..6890073 --- /dev/null +++ b/payment-engine/src/test/java/de/tillhub/paymentengine/spos/data/StringToReceiptDtoConverterTest.kt @@ -0,0 +1,164 @@ +package de.tillhub.paymentengine.spos.data + +import io.kotest.core.spec.style.FunSpec +import io.kotest.matchers.shouldBe + +class StringToReceiptDtoConverterTest : FunSpec({ + + test("convert") { + val converter = StringToReceiptDtoConverter() + + val result = converter.convert(RECEIPT_XML) + + result.toReceiptString() shouldBe RECEIPT + } +}) { + companion object { + private const val RECEIPT_XML = """ + + + + NORMAL + + - Kundenbeleg - + + + + NORMAL + + Testterminal SPOS + + + + NORMAL + + + + + + NORMAL + + Kartenzahlung + + + + NORMAL + + VISA CREDIT + + + + NORMAL + + + + + + NORMAL + + Betrag: EUR 6,00 + + + + NORMAL + + + + + + NORMAL + + Datum: 14.11.24 Zeit: 14:03 + + + + NORMAL + + Terminal-Nr.: 55754825 + + + + NORMAL + + Trace-Nr.: 000014 + + + + NORMAL + + Beleg: 6 + + + + NORMAL + + Karten-Nr.: ############0010 + + + + NORMAL + + Folge-Nr.: 0001 + + + + NORMAL + + Kartendaten: Kontaktlos + + + + NORMAL + + AID: 096299 + + + + NORMAL + + VU-Nr.: 455600152753 + + + + NORMAL + + EMV-AID: A0000000031010 + + + + NORMAL + + + + + + NORMAL + + Zahlung erfolgt + + + """ + + const val RECEIPT = """ - Kundenbeleg - +Testterminal SPOS + + Kartenzahlung + VISA CREDIT + +Betrag: EUR 6,00 + +Datum: 14.11.24 Zeit: 14:03 +Terminal-Nr.: 55754825 +Trace-Nr.: 000014 +Beleg: 6 +Karten-Nr.: ############0010 +Folge-Nr.: 0001 +Kartendaten: Kontaktlos +AID: 096299 +VU-Nr.: 455600152753 +EMV-AID: A0000000031010 + + Zahlung erfolgt +""" + } +} diff --git a/sample/src/main/java/de/tillhub/paymentengine/demo/MainActivity.kt b/sample/src/main/java/de/tillhub/paymentengine/demo/MainActivity.kt index ac57bbb..2b30319 100644 --- a/sample/src/main/java/de/tillhub/paymentengine/demo/MainActivity.kt +++ b/sample/src/main/java/de/tillhub/paymentengine/demo/MainActivity.kt @@ -40,6 +40,7 @@ class MainActivity : ComponentActivity() { viewModel.initRefundManager(paymentEngine.newRefundManager(this)) viewModel.initReversalManager(paymentEngine.newReversalManager(this)) viewModel.initReconciliationManager(paymentEngine.newReconciliationManager(this)) + viewModel.initConnectionManager(paymentEngine.newConnectionManager(this)) paymentEngine.setAnalytics(object : PaymentAnalytics { override fun logOperation(request: String) { @@ -77,6 +78,13 @@ class MainActivity : ComponentActivity() { ActionButton("Reconciliation") { viewModel.startReconciliation() } + + ActionButton("S-POS connect") { + viewModel.startSPOSConnect() + } + ActionButton("S-POS disconnect") { + viewModel.startSPOSDisconnect() + } } } } diff --git a/sample/src/main/java/de/tillhub/paymentengine/demo/MainViewModel.kt b/sample/src/main/java/de/tillhub/paymentengine/demo/MainViewModel.kt index 497550b..ad307e5 100644 --- a/sample/src/main/java/de/tillhub/paymentengine/demo/MainViewModel.kt +++ b/sample/src/main/java/de/tillhub/paymentengine/demo/MainViewModel.kt @@ -3,6 +3,7 @@ package de.tillhub.paymentengine.demo import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import de.tillhub.paymentengine.CardManager +import de.tillhub.paymentengine.ConnectionManager import de.tillhub.paymentengine.PaymentManager import de.tillhub.paymentengine.ReconciliationManager import de.tillhub.paymentengine.RefundManager @@ -14,6 +15,7 @@ import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.merge import kotlinx.coroutines.flow.stateIn +import java.util.UUID class MainViewModel : ViewModel() { @@ -21,17 +23,19 @@ class MainViewModel : ViewModel() { private lateinit var refundManager: RefundManager private lateinit var reversalManager: ReversalManager private lateinit var reconciliationManager: ReconciliationManager + private lateinit var connectionManager: ConnectionManager val cardManagerState: StateFlow by lazy { merge( paymentManager.observePaymentState(), refundManager.observePaymentState(), reversalManager.observePaymentState(), - reconciliationManager.observePaymentState() + reconciliationManager.observePaymentState(), + connectionManager.observePaymentState() ).stateIn( - viewModelScope, - SharingStarted.Eagerly, - TerminalOperationStatus.Waiting + viewModelScope, + SharingStarted.Eagerly, + TerminalOperationStatus.Waiting ) } @@ -59,35 +63,86 @@ class MainViewModel : ViewModel() { } } + fun initConnectionManager(connectionManager: ConnectionManager) { + this.connectionManager = connectionManager.apply { + setupTerminalConfigs(this) + } + } + private fun setupTerminalConfigs(cardManager: CardManager) { -// cardManager.putTerminalConfig(Terminal.OPI( -// name = "opi", -// ipAddress = "127.0.0.1", -// port = 20002, -// port2 = 20006 -// )) - cardManager.putTerminalConfig(Terminal.ZVT( - name = "zvt-local", - ipAddress = "192.168.1.121", - port = 20007 -// ipAddress = "127.0.0.1", -// port = 40007 - )) + cardManager.putTerminalConfig( + Terminal.ZVT( + name = "zvt-remote", + ipAddress = REMOTE_IP, + port = 20007 + ) + ) + cardManager.putTerminalConfig( + Terminal.ZVT( + name = "zvt-local", + ipAddress = "127.0.0.1", + port = 40007 + ) + ) + cardManager.putTerminalConfig( + Terminal.OPI( + name = "opi", + ipAddress = REMOTE_IP, + port = 20002, + port2 = 20007 + ) + ) + cardManager.putTerminalConfig( + Terminal.SPOS( + name = "s-pos", + ) + ) } fun startPayment() { - paymentManager.startPaymentTransaction(500.toBigDecimal(), ISOAlphaCurrency("EUR"), "zvt-local") + paymentManager.startPaymentTransaction( + UUID.randomUUID().toString(), + 500.toBigDecimal(), + 100.toBigDecimal(), + ISOAlphaCurrency("EUR"), + CONFIG_IN_USE + ) } fun startRefund() { - refundManager.startRefundTransaction(600.toBigDecimal(), ISOAlphaCurrency("EUR"), "opi") + refundManager.startRefundTransaction( + transactionId = UUID.randomUUID().toString(), + amount = 600.toBigDecimal(), + currency = ISOAlphaCurrency("EUR"), + configName = CONFIG_IN_USE + ) } fun startReversal() { - reversalManager.startReversalTransaction("374", "opi") + reversalManager.startReversalTransaction( + transactionId = UUID.randomUUID().toString(), + amount = 500.toBigDecimal(), + tip = 100.toBigDecimal(), + currency = ISOAlphaCurrency("EUR"), + receiptNo = "374", + configName = CONFIG_IN_USE + ) } fun startReconciliation() { - reconciliationManager.startReconciliation("opi") + reconciliationManager.startReconciliation(CONFIG_IN_USE) + } + + fun startSPOSConnect() { + connectionManager.startSPOSConnect(CONFIG_IN_USE) + } + + fun startSPOSDisconnect() { + connectionManager.startSPOSDisconnect(CONFIG_IN_USE) + } + + companion object { + private const val CONFIG_IN_USE = "s-pos" + private const val REMOTE_IP = "192.168.1.121" } } \ No newline at end of file