From 43484c5071429cde66472c9e939a11b10a2c245b Mon Sep 17 00:00:00 2001 From: Marek Stransky Date: Thu, 11 Jan 2024 14:46:34 +0100 Subject: [PATCH 1/4] #139: update attribute amount to correspond with new backend scheme --- .../WMTOperationAttributeAmount.swift | 40 ++++++++++--------- 1 file changed, 22 insertions(+), 18 deletions(-) diff --git a/WultraMobileTokenSDK/Operations/Model/UserOperation/Attributes/WMTOperationAttributeAmount.swift b/WultraMobileTokenSDK/Operations/Model/UserOperation/Attributes/WMTOperationAttributeAmount.swift index dcde0e5..4b37667 100644 --- a/WultraMobileTokenSDK/Operations/Model/UserOperation/Attributes/WMTOperationAttributeAmount.swift +++ b/WultraMobileTokenSDK/Operations/Model/UserOperation/Attributes/WMTOperationAttributeAmount.swift @@ -19,26 +19,26 @@ import Foundation /// Amount attribute is 1 row in operation, that represents "Payment Amount" public class WMTOperationAttributeAmount: WMTOperationAttribute { - /// Payment amount - /// - /// Amount might not be precise (due to floating point conversion during deserialization from json) - /// use amountFormatted property instead when available - public let amount: Decimal - - /// Currency - public let 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". - public let amountFormatted: String? + public let amountFormatted: String /// Formatted currency to the locale based on acceptLanguage /// /// For example when the currency is CZK, this property will be "Kč" - public let currencyFormatted: String? + public let currencyFormatted: String + + /// Payment amount + /// + /// Amount might not be precise (due to floating point conversion during deserialization from json) + /// use amountFormatted property instead when available + public let amount: Decimal? + + /// Currency + public let currency: String? /// Formatted value and currency to the locale based on acceptLanguage /// @@ -52,12 +52,12 @@ public class WMTOperationAttributeAmount: WMTOperationAttribute { case amount, amountFormatted, currency, currencyFormatted, valueFormatted } - public init(label: AttributeLabel, amount: Decimal, currency: String, amountFormatted: String?, currencyFormatted: String?, valueFormatted: String?) { - self.amount = amount - self.currency = currency + public init(label: AttributeLabel, amountFormatted: String, currencyFormatted: String, valueFormatted: String?, amount: Decimal?, currency: String? ) { self.amountFormatted = amountFormatted self.currencyFormatted = currencyFormatted self.valueFormatted = valueFormatted + self.amount = amount + self.currency = currency super.init(type: .amount, label: label) } @@ -65,10 +65,14 @@ public class WMTOperationAttributeAmount: WMTOperationAttribute { let c = try decoder.container(keyedBy: Keys.self) - amount = (try c.decode(Double.self, forKey: .amount) as NSNumber).decimalValue - currency = try c.decode(String.self, forKey: .currency) - amountFormatted = try? c.decode(String.self, forKey: .amountFormatted) - currencyFormatted = try? c.decode(String.self, forKey: .currencyFormatted) + // For backward compatibility with legacy implementation, where the `amountFormatted` and `currencyFormatted` values might not be present, + // we directly decode from `amount` and `currency`. + amountFormatted = try c.decodeIfPresent(String.self, forKey: .amountFormatted) ?? String(c.decode(Double.self, forKey: .amount)) + currencyFormatted = try c.decodeIfPresent(String.self, forKey: .currencyFormatted) ?? c.decode(String.self, forKey: .currency) + + amount = (try? c.decode(Double.self, forKey: .amount) as NSNumber)?.decimalValue + currency = try? c.decode(String.self, forKey: .currency) + valueFormatted = try? c.decode(String.self, forKey: .valueFormatted) try super.init(from: decoder) } From ccb04b1e5aa924682f7b163223251f00ce8152dc Mon Sep 17 00:00:00 2001 From: Marek Stransky Date: Thu, 11 Jan 2024 14:46:44 +0100 Subject: [PATCH 2/4] #139: update conversion amount to correspond with new backend scheme --- ...MTOperationAttributeAmountConversion.swift | 43 +++++++++++-------- 1 file changed, 24 insertions(+), 19 deletions(-) diff --git a/WultraMobileTokenSDK/Operations/Model/UserOperation/Attributes/WMTOperationAttributeAmountConversion.swift b/WultraMobileTokenSDK/Operations/Model/UserOperation/Attributes/WMTOperationAttributeAmountConversion.swift index d9ff222..a8f46c7 100644 --- a/WultraMobileTokenSDK/Operations/Model/UserOperation/Attributes/WMTOperationAttributeAmountConversion.swift +++ b/WultraMobileTokenSDK/Operations/Model/UserOperation/Attributes/WMTOperationAttributeAmountConversion.swift @@ -21,26 +21,27 @@ public class WMTOperationAttributeAmountConversion: WMTOperationAttribute { public struct Money { - /// Payment amount - /// - /// Amount might not be precise (due to floating point conversion during deserialization from json) - /// use amountFormatted property instead when available - public let amount: Decimal - - /// Currency - public let 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". - public let amountFormatted: String? + public let amountFormatted: String /// Formatted currency to the locale based on acceptLanguage /// /// For example when the currency is CZK, this property will be "Kč" - public let currencyFormatted: String? + public let currencyFormatted: String + + /// Payment amount + /// + /// Amount might not be precise (due to floating point conversion during deserialization from json) + /// use amountFormatted property instead when available + public let amount: Decimal? + + /// Currency + public let currency: String? + /// Formatted currency and amount to the locale based on acceptLanguage /// @@ -75,18 +76,22 @@ public class WMTOperationAttributeAmountConversion: WMTOperationAttribute { let c = try decoder.container(keyedBy: Keys.self) self.dynamic = try c.decode(Bool.self, forKey: .dynamic) + // For backward compatibility with legacy implementation, where the `sourceAmountFormatted` and `sourceCurrencyFormatted` values might not be present, + // we directly decode from `sourceAmount` and `sourceCurrency`. self.source = .init( - amount: try c.decode(Decimal.self, forKey: .sourceAmount), - currency: try c.decode(String.self, forKey: .sourceCurrency), - amountFormatted: try? c.decode(String.self, forKey: .sourceAmountFormatted), - currencyFormatted: try? c.decode(String.self, forKey: .sourceCurrencyFormatted), + amountFormatted: try c.decodeIfPresent(String.self, forKey: .sourceAmountFormatted) ?? String(c.decode(Double.self, forKey: .sourceAmount)), + currencyFormatted: try c.decodeIfPresent(String.self, forKey: .sourceCurrencyFormatted) ?? c.decode(String.self, forKey: .sourceCurrency), + amount: try? c.decode(Decimal.self, forKey: .sourceAmount), + currency: try? c.decode(String.self, forKey: .sourceCurrency), valueFormatted: try? c.decode(String.self, forKey: .sourceValueFormatted) ) + // For backward compatibility with legacy implementation, where the `targetAmountFormatted` and `targetCurrencyFormatted` values might not be present, + // we directly decode from `targetAmount` and `targetCurrency`. self.target = .init( - amount: try c.decode(Decimal.self, forKey: .targetAmount), - currency: try c.decode(String.self, forKey: .targetCurrency), - amountFormatted: try? c.decode(String.self, forKey: .targetAmountFormatted), - currencyFormatted: try? c.decode(String.self, forKey: .targetCurrencyFormatted), + amountFormatted: try c.decodeIfPresent(String.self, forKey: .targetAmountFormatted) ?? String(c.decode(Double.self, forKey: .targetAmount)), + currencyFormatted: try c.decodeIfPresent(String.self, forKey: .targetCurrencyFormatted) ?? c.decode(String.self, forKey: .targetCurrency), + amount: try? c.decode(Decimal.self, forKey: .targetAmount), + currency: try? c.decode(String.self, forKey: .targetCurrency), valueFormatted: try? c.decode(String.self, forKey: .targetValueFormatted) ) From c210217e490c65d97a5e7e9609ad25be239efb38 Mon Sep 17 00:00:00 2001 From: Marek Stransky Date: Thu, 11 Jan 2024 16:12:17 +0100 Subject: [PATCH 3/4] Fix lint --- .../Attributes/WMTOperationAttributeAmountConversion.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/WultraMobileTokenSDK/Operations/Model/UserOperation/Attributes/WMTOperationAttributeAmountConversion.swift b/WultraMobileTokenSDK/Operations/Model/UserOperation/Attributes/WMTOperationAttributeAmountConversion.swift index a8f46c7..353af62 100644 --- a/WultraMobileTokenSDK/Operations/Model/UserOperation/Attributes/WMTOperationAttributeAmountConversion.swift +++ b/WultraMobileTokenSDK/Operations/Model/UserOperation/Attributes/WMTOperationAttributeAmountConversion.swift @@ -42,7 +42,6 @@ public class WMTOperationAttributeAmountConversion: WMTOperationAttribute { /// Currency public let currency: String? - /// Formatted currency and amount to the locale based on acceptLanguage /// /// Both amount and currency are formatted, String will show e.g. "€" in front of the amount From 2d463f7e75d11c8a124966ed84e4a08e0e25b4ae Mon Sep 17 00:00:00 2001 From: Marek Stransky Date: Fri, 12 Jan 2024 14:04:45 +0100 Subject: [PATCH 4/4] Replace try? with decodeIfPresent and Double for Decimal decoding & add tests --- .../WMTOperationAttributeAmount.swift | 10 ++- ...MTOperationAttributeAmountConversion.swift | 16 ++--- .../NetworkingObjectsTests.swift | 72 +++++++++++++++++++ 3 files changed, 84 insertions(+), 14 deletions(-) diff --git a/WultraMobileTokenSDK/Operations/Model/UserOperation/Attributes/WMTOperationAttributeAmount.swift b/WultraMobileTokenSDK/Operations/Model/UserOperation/Attributes/WMTOperationAttributeAmount.swift index 4b37667..59cc631 100644 --- a/WultraMobileTokenSDK/Operations/Model/UserOperation/Attributes/WMTOperationAttributeAmount.swift +++ b/WultraMobileTokenSDK/Operations/Model/UserOperation/Attributes/WMTOperationAttributeAmount.swift @@ -67,13 +67,11 @@ public class WMTOperationAttributeAmount: WMTOperationAttribute { // For backward compatibility with legacy implementation, where the `amountFormatted` and `currencyFormatted` values might not be present, // we directly decode from `amount` and `currency`. - amountFormatted = try c.decodeIfPresent(String.self, forKey: .amountFormatted) ?? String(c.decode(Double.self, forKey: .amount)) + amountFormatted = try c.decodeIfPresent(String.self, forKey: .amountFormatted) ?? c.decode(Decimal.self, forKey: .amount).description currencyFormatted = try c.decodeIfPresent(String.self, forKey: .currencyFormatted) ?? c.decode(String.self, forKey: .currency) - - amount = (try? c.decode(Double.self, forKey: .amount) as NSNumber)?.decimalValue - currency = try? c.decode(String.self, forKey: .currency) - - valueFormatted = try? c.decode(String.self, forKey: .valueFormatted) + valueFormatted = try c.decodeIfPresent(String.self, forKey: .valueFormatted) + amount = try c.decodeIfPresent(Decimal.self, forKey: .amount) + currency = try c.decodeIfPresent(String.self, forKey: .currency) try super.init(from: decoder) } } diff --git a/WultraMobileTokenSDK/Operations/Model/UserOperation/Attributes/WMTOperationAttributeAmountConversion.swift b/WultraMobileTokenSDK/Operations/Model/UserOperation/Attributes/WMTOperationAttributeAmountConversion.swift index 353af62..2a91829 100644 --- a/WultraMobileTokenSDK/Operations/Model/UserOperation/Attributes/WMTOperationAttributeAmountConversion.swift +++ b/WultraMobileTokenSDK/Operations/Model/UserOperation/Attributes/WMTOperationAttributeAmountConversion.swift @@ -78,20 +78,20 @@ public class WMTOperationAttributeAmountConversion: WMTOperationAttribute { // For backward compatibility with legacy implementation, where the `sourceAmountFormatted` and `sourceCurrencyFormatted` values might not be present, // we directly decode from `sourceAmount` and `sourceCurrency`. self.source = .init( - amountFormatted: try c.decodeIfPresent(String.self, forKey: .sourceAmountFormatted) ?? String(c.decode(Double.self, forKey: .sourceAmount)), + amountFormatted: try c.decodeIfPresent(String.self, forKey: .sourceAmountFormatted) ?? c.decode(Decimal.self, forKey: .sourceAmount).description, currencyFormatted: try c.decodeIfPresent(String.self, forKey: .sourceCurrencyFormatted) ?? c.decode(String.self, forKey: .sourceCurrency), - amount: try? c.decode(Decimal.self, forKey: .sourceAmount), - currency: try? c.decode(String.self, forKey: .sourceCurrency), - valueFormatted: try? c.decode(String.self, forKey: .sourceValueFormatted) + amount: try c.decodeIfPresent(Decimal.self, forKey: .sourceAmount), + currency: try c.decodeIfPresent(String.self, forKey: .sourceCurrency), + valueFormatted: try c.decodeIfPresent(String.self, forKey: .sourceValueFormatted) ) // For backward compatibility with legacy implementation, where the `targetAmountFormatted` and `targetCurrencyFormatted` values might not be present, // we directly decode from `targetAmount` and `targetCurrency`. self.target = .init( - amountFormatted: try c.decodeIfPresent(String.self, forKey: .targetAmountFormatted) ?? String(c.decode(Double.self, forKey: .targetAmount)), + amountFormatted: try c.decodeIfPresent(String.self, forKey: .targetAmountFormatted) ?? c.decode(Decimal.self, forKey: .targetAmount).description, currencyFormatted: try c.decodeIfPresent(String.self, forKey: .targetCurrencyFormatted) ?? c.decode(String.self, forKey: .targetCurrency), - amount: try? c.decode(Decimal.self, forKey: .targetAmount), - currency: try? c.decode(String.self, forKey: .targetCurrency), - valueFormatted: try? c.decode(String.self, forKey: .targetValueFormatted) + amount: try c.decodeIfPresent(Decimal.self, forKey: .targetAmount), + currency: try c.decodeIfPresent(String.self, forKey: .targetCurrency), + valueFormatted: try c.decodeIfPresent(String.self, forKey: .targetValueFormatted) ) try super.init(from: decoder) diff --git a/WultraMobileTokenSDKTests/NetworkingObjectsTests.swift b/WultraMobileTokenSDKTests/NetworkingObjectsTests.swift index 9974f2d..8d24a02 100644 --- a/WultraMobileTokenSDKTests/NetworkingObjectsTests.swift +++ b/WultraMobileTokenSDKTests/NetworkingObjectsTests.swift @@ -170,6 +170,78 @@ class NetworkingObjectsTests: XCTestCase { XCTAssert(op2.allowedSignatureType.signatureFactors.count == 1 && op2.allowedSignatureType.signatureFactors.contains(.possessionKnowledge)) } + func testOnlyAmountAndConversionAttributesLegacyBackend() { + let 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"}]}}]} + """ + + guard let result = try? jsonDecoder.decode(WPNResponseArray.self, from: json.data(using: .utf8)!) else { + XCTFail("Failed to parse JSON data") + return + } + + guard let amountAttr = result.responseObject?[0].formData.attributes[0] as? WMTOperationAttributeAmount else { + XCTFail("amount attribute not recognized") + return + } + XCTAssertEqual(Decimal(string: "965165234082.23"), amountAttr.amount) + XCTAssertEqual("CZK", amountAttr.currency) + XCTAssertEqual("965165234082.23", amountAttr.amountFormatted) + XCTAssertEqual("CZK", amountAttr.currencyFormatted) + + + guard let conversionAttr = result.responseObject?[0].formData.attributes[1] as? WMTOperationAttributeAmountConversion else { + XCTFail("conversion attribute not recognized") + return + } + + XCTAssertEqual(Decimal(string: "1.26"), conversionAttr.source.amount) + XCTAssertEqual("ETC", conversionAttr.source.currency) + XCTAssertEqual(Decimal(string: "1710.98"), conversionAttr.target.amount) + XCTAssertEqual("USD", conversionAttr.target.currency) + + XCTAssertEqual("1.26", conversionAttr.source.amountFormatted) + XCTAssertEqual("ETC", conversionAttr.source.currencyFormatted) + XCTAssertEqual("1710.98", conversionAttr.target.amountFormatted) + XCTAssertEqual("USD", conversionAttr.target.currencyFormatted) + } + + func testAmountAndConversionAttributesOnlyFormattedValues() { + let 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"}]}}]} + """.trimmingCharacters(in: .whitespacesAndNewlines) + + guard let result = try? jsonDecoder.decode(WPNResponseArray.self, from: json.data(using: .utf8)!) else { + XCTFail("Failed to parse JSON data") + return + } + + guard let amountAttr = result.responseObject?[0].formData.attributes[0] as? WMTOperationAttributeAmount else { + XCTFail("amount attribute not recognized") + return + } + + XCTAssertNil(amountAttr.amount) + XCTAssertNil(amountAttr.currency) + XCTAssertEqual("965165234082.23", amountAttr.amountFormatted) + XCTAssertEqual("CZK", amountAttr.currencyFormatted) + + guard let conversionAttr = result.responseObject?[0].formData.attributes[1] as? WMTOperationAttributeAmountConversion else { + XCTFail("conversion attribute not recognized") + return + } + + XCTAssertNil(conversionAttr.source.amount) + XCTAssertNil(conversionAttr.source.currency) + XCTAssertNil(conversionAttr.target.amount) + XCTAssertNil(conversionAttr.target.currency) + + XCTAssertEqual("1.26", conversionAttr.source.amountFormatted) + XCTAssertEqual("ETC", conversionAttr.source.currencyFormatted) + XCTAssertEqual("1710.98", conversionAttr.target.amountFormatted) + XCTAssertEqual("USD", conversionAttr.target.currencyFormatted) + } + func testErrorResponse() { let response = """