Skip to content

Commit

Permalink
Updated amount and conversion cells to new backend scheme (#129)
Browse files Browse the repository at this point in the history
* #128: update amount and conversion cells to new backend scheme

* Fix amount type in deserializer from BigDecimal to String when used for formatted values & fix tests

* Return null when required amountFormatted or currencyFormatted would be null for amount & conversion cell and write tests for deserialization
  • Loading branch information
Hopsaheysa authored Jan 12, 2024
1 parent 76fe2c3 commit bdafca8
Show file tree
Hide file tree
Showing 3 changed files with 88 additions and 29 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import com.google.gson.stream.JsonToken
import com.google.gson.stream.JsonWriter
import com.wultra.android.mtokensdk.api.operation.model.*
import java.math.BigDecimal
import java.math.RoundingMode

/**
* Type adapter for deserializing sealed class Attribute with its hierarchy types.
Expand Down Expand Up @@ -94,18 +95,24 @@ internal class AttributeTypeAdapter : TypeAdapter<Attribute>() {
fun build(): Attribute? {
return when (type) {
Attribute.Type.AMOUNT -> {
val amount: BigDecimal = attr("amount") ?: return null
val currency: String = attr("currency") ?: return null
AmountAttribute(amount, currency, attr("amountFormatted"), attr("currencyFormatted"), attr("valueFormatted"), label)
// For backward compatibility with legacy implementation, where the `amountFormatted` and `currencyFormatted` values might not be present,
// we decode from `amount` and `currency` which were not nullable
// as this is a legacy fallback, to fix a precision loss problem when converting BigDecimal to String we are rounding the amount to two digits
val amountFormatted: String = attr("amountFormatted") ?: attr<BigDecimal>("amount")?.setScale(2, RoundingMode.HALF_EVEN)?.toString() ?: return null
val currencyFormatted: String = attr("currencyFormatted") ?: attr("currency") ?: return null
AmountAttribute(amountFormatted, currencyFormatted, attr("amount"), attr("currency"), attr("valueFormatted"), label)
}
Attribute.Type.KEY_VALUE -> KeyValueAttribute(attr("value") ?: return null, label)
Attribute.Type.NOTE -> NoteAttribute(attr("note") ?: return null, label)
Attribute.Type.HEADING -> HeadingAttribute(label)
Attribute.Type.PARTY_INFO -> PartyInfoAttribute(PartyInfoAttribute.PartyInfo(partyInfoMap), label)
Attribute.Type.AMOUNT_CONVERSION -> ConversionAttribute(
attr("dynamic") ?: return null,
ConversionAttribute.Money(attr("sourceAmount") ?: return null, attr("sourceCurrency") ?: return null, attr("sourceAmountFormatted"), attr("sourceCurrencyFormatted"), attr("sourceValueFormatted")),
ConversionAttribute.Money(attr("targetAmount") ?: return null, attr("targetCurrency") ?: return null, attr("targetAmountFormatted"), attr("targetCurrencyFormatted"), attr("targetValueFormatted")),
// For backward compatibility with legacy implementation, where the `sourceAmountFormatted`/`targetAmountFormatted` and `sourceCurrencyFormatted`/`targetCurrencyFormatted` values might not be present,
// we decode from `sourceAmount`/`targetAmount` and `sourceCurrency`/`targetCurrency` which were not nullable
// as this is a legacy fallback, to fix a precision loss problem when converting BigDecimal to String we are rounding the amount to two digits
ConversionAttribute.Money(attr("sourceAmountFormatted") ?: attr<BigDecimal>("sourceAmount")?.setScale(2, RoundingMode.HALF_EVEN)?.toString() ?: return null, attr("sourceCurrencyFormatted") ?: attr("sourceCurrency") ?: return null, attr("sourceAmount"), attr("sourceCurrency"), attr("sourceValueFormatted")),
ConversionAttribute.Money(attr("targetAmountFormatted") ?: attr<BigDecimal>("targetAmount")?.setScale(2, RoundingMode.HALF_EVEN)?.toString() ?: return null, attr("targetCurrencyFormatted") ?: attr("targetCurrency") ?: return null, attr("targetAmount"), attr("targetCurrency"), attr("targetValueFormatted")),
label
)
Attribute.Type.IMAGE -> ImageAttribute(attr("thumbnailUrl") ?: return null, attr("originalUrl"), label)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,23 +55,23 @@ open class Attribute(

/** Amount attribute is 1 row in operation that represents "Payment Amount" */
class AmountAttribute(
/** Payment amount */
val amount: BigDecimal,

/** Currency */
val currency: String,

/** Formatted amount for presentation.
*
* This property will be properly formatted based on the response language.
* For example when amount is 100 and the acceptLanguage is "cs" for czech,
* he amountFormatted will be "100,00". */
val amountFormatted: String?,
val amountFormatted: String,

/** Formatted currency to the locale based on acceptLanguage.
*
* For example when the currency is CZK, this property will be "Kč" */
val currencyFormatted: String?,
val currencyFormatted: String,

/** Payment amount */
val amount: BigDecimal?,

/** Currency */
val currency: String?,

/** Formatted amount and currency to the locale based on acceptLanguage
*
Expand Down Expand Up @@ -149,33 +149,32 @@ class ConversionAttribute(
) : Attribute(Type.AMOUNT_CONVERSION, label) {

data class Money(

/**
* Payment amount
*
* Amount might not be precise (due to floating point conversion during deserialization from json)
* use amountFormatted property instead when available
*/
val amount: BigDecimal,

/** Currency */
val currency: String,

/**
* Formatted amount for presentation.
*
* This property will be properly formatted based on the response language.
* For example when amount is 100 and the acceptLanguage is "cs" for czech,
* the amountFormatted will be "100,00".
*/
val amountFormatted: String?,
val amountFormatted: String,

/**
* Formatted currency to the locale based on acceptLanguage
*
* For example when the currency is CZK, this property will be "Kč"
*/
val currencyFormatted: String?,
val currencyFormatted: String,

/**
* Payment amount
*
* Amount might not be precise (due to floating point conversion during deserialization from json)
* use amountFormatted property instead when available
*/
val amount: BigDecimal?,

/** Currency */
val currency: String?,

/**
* Formatted amount and currency to the locale based on acceptLanguage
Expand Down
57 changes: 55 additions & 2 deletions library/src/test/java/OperationJsonDeserializationTests.kt
Original file line number Diff line number Diff line change
Expand Up @@ -186,8 +186,9 @@ class OperationJsonDeserializationTests {
Assert.assertEquals(BigDecimal(965165234082.23), amountAttr.amount)
Assert.assertEquals("CZK", amountAttr.currency)
Assert.assertEquals("965165234082.23 CZK", amountAttr.valueFormatted)
Assert.assertNull(amountAttr.amountFormatted)
Assert.assertNull(amountAttr.currencyFormatted)
// old implementation was null, now formatted values are not nullable and are made from amount and currency
Assert.assertNotNull(amountAttr.amountFormatted)
Assert.assertNotNull(amountAttr.currencyFormatted)

val kva = operation.formData.attributes[1] as KeyValueAttribute
Assert.assertEquals(Attribute.Type.KEY_VALUE, kva.type)
Expand Down Expand Up @@ -246,6 +247,58 @@ class OperationJsonDeserializationTests {
Assert.assertEquals(null, ia3.originalUrl)
}

@Test
fun `test Amount & Conversion Attributes response with only amount and currency - legacy backend`() {
val json = """{"status":"OK", "currentTimestamp":"2023-02-10T12:30:42+0000", "responseObject":[{"id":"930febe7-f350-419a-8bc0-c8883e7f71e3", "name":"authorize_payment", "data":"A1*A100CZK*Q238400856/0300**D20170629*NUtility Bill Payment - 05/2017", "operationCreated":"2018-08-08T12:30:42+0000", "operationExpires":"2018-08-08T12:35:43+0000", "allowedSignatureType": {"type":"2FA", "variants": ["possession_knowledge", "possession_biometry"]}, "formData": {"title":"Potvrzení platby", "message":"Dobrý den,prosíme o potvrzení následující platby:", "attributes": [{"type":"AMOUNT", "id":"operation.amount", "label":"Částka", "amount":965165234082.23, "currency":"CZK"}, { "type": "AMOUNT_CONVERSION", "id": "operation.conversion", "label": "Conversion", "dynamic": true, "sourceAmount": 1.26, "sourceCurrency": "ETC", "targetAmount": 1710.98, "targetCurrency": "USD"}]}}]}
""".trimIndent()

val response = typeAdapter.fromJson(json)
Assert.assertNotNull(response)

val amountAttr = response.responseObject[0].formData.attributes[0] as AmountAttribute
Assert.assertEquals(BigDecimal(965165234082.23), amountAttr.amount)
Assert.assertEquals("CZK", amountAttr.currency)
Assert.assertEquals("965165234082.23", amountAttr.amountFormatted)
Assert.assertEquals("CZK", amountAttr.currencyFormatted)

val conversionAttr = response.responseObject[0].formData.attributes[1] as ConversionAttribute
Assert.assertEquals(BigDecimal(1.26), conversionAttr.source.amount)
Assert.assertEquals("ETC", conversionAttr.source.currency)
Assert.assertEquals(BigDecimal(1710.98), conversionAttr.target.amount)
Assert.assertEquals("USD", conversionAttr.target.currency)

Assert.assertEquals("1.26", conversionAttr.source.amountFormatted)
Assert.assertEquals("ETC", conversionAttr.source.currencyFormatted)
Assert.assertEquals("1710.98", conversionAttr.target.amountFormatted)
Assert.assertEquals("USD", conversionAttr.target.currencyFormatted)
}

@Test
fun `test Amount & Conversion Attributes response with only amountFormatted and currencyFormatted`() {
val json = """{"status":"OK", "currentTimestamp":"2023-02-10T12:30:42+0000", "responseObject":[{"id":"930febe7-f350-419a-8bc0-c8883e7f71e3", "name":"authorize_payment", "data":"A1*A100CZK*Q238400856/0300**D20170629*NUtility Bill Payment - 05/2017", "operationCreated":"2018-08-08T12:30:42+0000", "operationExpires":"2018-08-08T12:35:43+0000", "allowedSignatureType": {"type":"2FA", "variants": ["possession_knowledge", "possession_biometry"]}, "formData": {"title":"Potvrzení platby", "message":"Dobrý den,prosíme o potvrzení následující platby:", "attributes": [{"type":"AMOUNT", "id":"operation.amount", "label":"Částka", "amountFormatted":"965165234082.23", "currencyFormatted":"CZK"}, { "type": "AMOUNT_CONVERSION", "id": "operation.conversion", "label": "Conversion", "dynamic": true, "sourceAmountFormatted": "1.26", "sourceCurrencyFormatted": "ETC", "targetAmountFormatted": "1710.98", "targetCurrencyFormatted": "USD"}]}}]}
""".trimIndent()

val response = typeAdapter.fromJson(json)
Assert.assertNotNull(response)

val amountAttr = response.responseObject[0].formData.attributes[0] as AmountAttribute
Assert.assertNull(amountAttr.amount)
Assert.assertNull(amountAttr.currency)
Assert.assertEquals("965165234082.23", amountAttr.amountFormatted)
Assert.assertEquals("CZK", amountAttr.currencyFormatted)

val conversionAttr = response.responseObject[0].formData.attributes[1] as ConversionAttribute
Assert.assertNull(conversionAttr.source.amount)
Assert.assertNull(conversionAttr.source.currency)
Assert.assertNull(conversionAttr.target.amount)
Assert.assertNull(conversionAttr.target.currency)

Assert.assertEquals("1.26", conversionAttr.source.amountFormatted)
Assert.assertEquals("ETC", conversionAttr.source.currencyFormatted)
Assert.assertEquals("1710.98", conversionAttr.target.amountFormatted)
Assert.assertEquals("USD", conversionAttr.target.currencyFormatted)
}

@Test
fun `test unknown attribute`() {
val json = """{"status":"OK","responseObject":[{"id":"930febe7-f350-419a-8bc0-c8883e7f71e3","name":"authorize_payment","data":"A1*A100CZK*Q238400856/0300**D20170629*NUtility Bill Payment - 05/2017","operationCreated":"2018-08-08T12:30:42+0000","operationExpires":"2018-08-08T12:35:43+0000","allowedSignatureType":{"type":"2FA","variants":["possession_knowledge", "possession_biometry"]},"formData":{"title":"Potvrzení platby","message":"Dobrý den,prosíme o potvrzení následující platby:","attributes":[{"type":"THIS_IS_FAKE_ATTR","id":"operation.amount","label":"Částka","amount":965165234082.23,"currency":"CZK","valueFormatted":"965165234082.23 CZK"},{"type":"KEY_VALUE","id":"operation.account","label":"Na účet","value":"238400856/0300"}]}}]}"""
Expand Down

0 comments on commit bdafca8

Please sign in to comment.