diff --git a/Networking/Networking/Extensions/KeyedDecodingContainer+Woo.swift b/Networking/Networking/Extensions/KeyedDecodingContainer+Woo.swift index 481301e3377..28dd44c4fb2 100644 --- a/Networking/Networking/Extensions/KeyedDecodingContainer+Woo.swift +++ b/Networking/Networking/Extensions/KeyedDecodingContainer+Woo.swift @@ -116,6 +116,11 @@ extension KeyedDecodingContainer { return nil } + /// Decodes a Decimal for the specified key. Supported Encodings = [Decimal / Int / String] + /// + /// This method *does NOT throw*. We want this behavior so that if a malformed entity is received, we just skip it, rather + /// than breaking the entire parsing chain. + /// func failsafeDecodeIfPresent(decimalForKey key: KeyedDecodingContainer.Key) -> Decimal? { if let decimal = failsafeDecodeIfPresent(Decimal.self, forKey: key) { return decimal @@ -125,6 +130,10 @@ extension KeyedDecodingContainer { return Decimal(integerLiteral: integerAsDecimal) } + if let stringAsDecimal = failsafeDecodeIfPresent(String.self, forKey: key) { + return Decimal(string: stringAsDecimal) + } + return nil } diff --git a/Networking/Networking/Model/Product/Product.swift b/Networking/Networking/Model/Product/Product.swift index d33ae1f5125..93e6671548e 100644 --- a/Networking/Networking/Model/Product/Product.swift +++ b/Networking/Networking/Model/Product/Product.swift @@ -425,7 +425,11 @@ public struct Product: Codable, GeneratedCopiable, Equatable, GeneratedFakeable }) ]) ?? false - let stockQuantity = try container.decodeIfPresent(Decimal.self, forKey: .stockQuantity) + // Even though WooCommerce Core returns Int or null values, + // some plugins alter the field value from Int to Decimal or String. + // We handle this as an optional Decimal value. + let stockQuantity = container.failsafeDecodeIfPresent(decimalForKey: .stockQuantity) + let stockStatusKey = try container.decode(String.self, forKey: .stockStatusKey) let backordersKey = try container.decode(String.self, forKey: .backordersKey) diff --git a/Networking/Networking/Model/Product/ProductVariation.swift b/Networking/Networking/Model/Product/ProductVariation.swift index 5089ca1ea49..ed3016d6229 100644 --- a/Networking/Networking/Model/Product/ProductVariation.swift +++ b/Networking/Networking/Model/Product/ProductVariation.swift @@ -274,7 +274,11 @@ public struct ProductVariation: Codable, GeneratedCopiable, Equatable, Generated }) ]) ?? false - let stockQuantity = try container.decodeIfPresent(Decimal.self, forKey: .stockQuantity) + // Even though WooCommerce Core returns Int or null values, + // some plugins alter the field value from Int to Decimal or String. + // We handle this as an optional Decimal value. + let stockQuantity = container.failsafeDecodeIfPresent(decimalForKey: .stockQuantity) + let stockStatusKey = try container.decode(String.self, forKey: .stockStatusKey) let stockStatus = ProductStockStatus(rawValue: stockStatusKey) let backordersKey = try container.decode(String.self, forKey: .backordersKey) diff --git a/Networking/NetworkingTests/Mapper/ProductMapperTests.swift b/Networking/NetworkingTests/Mapper/ProductMapperTests.swift index 681dd6ee356..9cb1b23c071 100644 --- a/Networking/NetworkingTests/Mapper/ProductMapperTests.swift +++ b/Networking/NetworkingTests/Mapper/ProductMapperTests.swift @@ -125,6 +125,7 @@ final class ProductMapperTests: XCTestCase { XCTAssertEqual(product.downloads.first?.downloadID, "12345") XCTAssertEqual(product.backordersAllowed, true) XCTAssertEqual(product.onSale, false) + XCTAssertNil(product.stockQuantity) } /// Verifies that the `salePrice` field of the Product are parsed correctly when the product is on sale, and the sale price is an empty string diff --git a/Networking/NetworkingTests/Mapper/ProductVariationMapperTests.swift b/Networking/NetworkingTests/Mapper/ProductVariationMapperTests.swift index e2e18baac5c..be2b50a2793 100644 --- a/Networking/NetworkingTests/Mapper/ProductVariationMapperTests.swift +++ b/Networking/NetworkingTests/Mapper/ProductVariationMapperTests.swift @@ -44,6 +44,7 @@ final class ProductVariationMapperTests: XCTestCase { XCTAssertEqual(productVariation.weight, "2.5") XCTAssertEqual(productVariation.backordersAllowed, true) XCTAssertEqual(productVariation.onSale, false) + XCTAssertEqual(productVariation.stockQuantity, 16) } /// Test that the fields for variations of a subscription product are properly parsed. diff --git a/Networking/NetworkingTests/Responses/product-alternative-types.json b/Networking/NetworkingTests/Responses/product-alternative-types.json index bd1326c7eab..11aa43a4d81 100644 --- a/Networking/NetworkingTests/Responses/product-alternative-types.json +++ b/Networking/NetworkingTests/Responses/product-alternative-types.json @@ -40,7 +40,7 @@ "tax_status": "taxable", "tax_class": "", "manage_stock": "parent", - "stock_quantity": null, + "stock_quantity": "", "stock_status": "instock", "backorders": "no", "backorders_allowed": "1", diff --git a/Networking/NetworkingTests/Responses/product-variation-alternative-types.json b/Networking/NetworkingTests/Responses/product-variation-alternative-types.json index a49b21a5708..33eefc4a21a 100644 --- a/Networking/NetworkingTests/Responses/product-variation-alternative-types.json +++ b/Networking/NetworkingTests/Responses/product-variation-alternative-types.json @@ -27,7 +27,7 @@ "tax_status": "taxable", "tax_class": "", "manage_stock": "parent", - "stock_quantity": 16, + "stock_quantity": "16", "stock_status": "instock", "backorders": "notify", "backorders_allowed": "1", diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt index b6d74c444ea..615099d1220 100644 --- a/RELEASE-NOTES.txt +++ b/RELEASE-NOTES.txt @@ -4,6 +4,7 @@ ----- - [*] Orders: Allow alternative types for the `taxID` in `ShippingLineTax` or `sku` in `OrderItem`, as some third-party plugins alter the type in the API. This helps with the order list not loading due to order decoding errors. [https://github.com/woocommerce/woocommerce-ios/pull/9844] - [*] Payments: Location permissions request is not shown to TTP users who grant "Allow once" permission on first foregrounding the app any more [https://github.com/woocommerce/woocommerce-ios/pull/9821] +- [*] Products: Allow alternative types for `stockQuantity` in `Product` and `ProductVariation`, as some third-party plugins alter the type in the API. This helps with the product list not loading due to product decoding errors. [https://github.com/woocommerce/woocommerce-ios/pull/9850] - [*] Products: Allow alternative types for the `backordersAllowed` and `onSale` in `Product` and `ProductVariation`, as some third-party plugins alter the types in the API. This helps with the product list not loading due to product decoding errors. [https://github.com/woocommerce/woocommerce-ios/pull/9849] - [*] Products: Allow alternative types for the `sku` and `weight` in `ProductVariation`, as some third-party plugins alter the types in the API. This helps with the product variation list not loading due to product variation decoding errors. [https://github.com/woocommerce/woocommerce-ios/pull/9847] - [*] Products: Allow alternative types for the `sku` and `weight` in `Product`, the dimensions in `ProductDimensions`, and the `downloadID` in `ProductDownload`, as some third-party plugins alter the types in the API. This helps with the product list not loading due to product decoding errors. [https://github.com/woocommerce/woocommerce-ios/pull/9846]