diff --git a/Networking/Networking.xcodeproj/project.pbxproj b/Networking/Networking.xcodeproj/project.pbxproj index 82d602e2f0a..260f1360c8b 100644 --- a/Networking/Networking.xcodeproj/project.pbxproj +++ b/Networking/Networking.xcodeproj/project.pbxproj @@ -641,6 +641,7 @@ CE19CB11222486A600E8AF7A /* order-statuses.json in Resources */ = {isa = PBXBuildFile; fileRef = CE19CB10222486A500E8AF7A /* order-statuses.json */; }; CE20179320E3EFA7005B4C18 /* broken-orders.json in Resources */ = {isa = PBXBuildFile; fileRef = CE20179220E3EFA7005B4C18 /* broken-orders.json */; }; CE227093228DD44C00C0626C /* ProductStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE227092228DD44C00C0626C /* ProductStatus.swift */; }; + CE2678432A26102A00FD9AEB /* order-alternative-types.json in Resources */ = {isa = PBXBuildFile; fileRef = CE2678422A26102A00FD9AEB /* order-alternative-types.json */; }; CE43066A23465F340073CBFF /* Refund.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE43066923465F340073CBFF /* Refund.swift */; }; CE43066C2347C5F90073CBFF /* OrderItemRefund.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE43066B2347C5F90073CBFF /* OrderItemRefund.swift */; }; CE43066E2347CBA70073CBFF /* OrderItemTaxRefund.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE43066D2347CBA70073CBFF /* OrderItemTaxRefund.swift */; }; @@ -1579,6 +1580,7 @@ CE19CB10222486A500E8AF7A /* order-statuses.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "order-statuses.json"; sourceTree = ""; }; CE20179220E3EFA7005B4C18 /* broken-orders.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "broken-orders.json"; sourceTree = ""; }; CE227092228DD44C00C0626C /* ProductStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductStatus.swift; sourceTree = ""; }; + CE2678422A26102A00FD9AEB /* order-alternative-types.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "order-alternative-types.json"; sourceTree = ""; }; CE43066923465F340073CBFF /* Refund.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Refund.swift; sourceTree = ""; }; CE43066B2347C5F90073CBFF /* OrderItemRefund.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrderItemRefund.swift; sourceTree = ""; }; CE43066D2347CBA70073CBFF /* OrderItemTaxRefund.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrderItemTaxRefund.swift; sourceTree = ""; }; @@ -2541,6 +2543,7 @@ B554FA8C2180B59700C54DFF /* notifications-load-hashes.json */, AE2D6624272A941C004A2C3A /* null-data.json */, B5C6FCD520A3768900A4F8E4 /* order.json */, + CE2678422A26102A00FD9AEB /* order-alternative-types.json */, DEF13C5F29668C420024A02B /* order-without-data.json */, 034480C227A42F9100DFACD2 /* order-with-charge.json */, 02C254D62563999200A04423 /* order-shipping-labels.json */, @@ -3620,6 +3623,7 @@ 74ABA1C9213F19FE00FFAD30 /* top-performers-month.json in Resources */, 3158FE7026129D7500E566B9 /* wcpay-account-rejected-listed.json in Resources */, 743E84F622172D3E00FAC9D7 /* shipment_tracking_single.json in Resources */, + CE2678432A26102A00FD9AEB /* order-alternative-types.json in Resources */, D865CE5B278CA10B002C8520 /* stripe-payment-intent-requires-payment-method.json in Resources */, CC9A254626442CA7005DE56E /* shipping-label-eligibility-failure.json in Resources */, 4524CD9C242CEFAB00B2F20A /* product-on-sale-with-empty-sale-price.json in Resources */, diff --git a/Networking/Networking/Model/OrderItem.swift b/Networking/Networking/Model/OrderItem.swift index c3a3afaa4d7..fa4bc7b0789 100644 --- a/Networking/Networking/Model/OrderItem.swift +++ b/Networking/Networking/Model/OrderItem.swift @@ -88,7 +88,11 @@ public struct OrderItem: Codable, Equatable, Hashable, GeneratedFakeable, Genera let decimalPrice = try container.decodeIfPresent(Decimal.self, forKey: .price) ?? Decimal(0) let price = NSDecimalNumber(decimal: decimalPrice) - let sku = try container.decodeIfPresent(String.self, forKey: .sku) + // Even though a plain install of WooCommerce Core provides String values, + // some plugins alter the field value from String to Int or Decimal. + let sku = container.failsafeDecodeIfPresent(targetType: String.self, + forKey: .sku, + alternativeTypes: [.decimal(transform: { NSDecimalNumber(decimal: $0).stringValue })]) let subtotal = try container.decode(String.self, forKey: .subtotal) let subtotalTax = try container.decode(String.self, forKey: .subtotalTax) let taxClass = try container.decode(String.self, forKey: .taxClass) diff --git a/Networking/Networking/Model/ShippingLineTax.swift b/Networking/Networking/Model/ShippingLineTax.swift index 44e94caf874..fbddac62713 100644 --- a/Networking/Networking/Model/ShippingLineTax.swift +++ b/Networking/Networking/Model/ShippingLineTax.swift @@ -24,6 +24,22 @@ public struct ShippingLineTax: Decodable, Hashable, GeneratedFakeable { self.subtotal = subtotal self.total = total } + + /// The public initializer for ShippingLineTax. + /// + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + // Even though a plain install of WooCommerce Core provides Int values, + // some plugins alter the field value from Int to String. + let taxID = container.failsafeDecodeIfPresent(targetType: Int64.self, + forKey: .taxID, + alternativeTypes: [.string(transform: { Int64($0) ?? 0 })]) ?? 0 + let subtotal = try container.decode(String.self, forKey: .subtotal) + let total = try container.decode(String.self, forKey: .total) + + self.init(taxID: taxID, subtotal: subtotal, total: total) + } } /// Defines all of the ShippingLineTax CodingKeys. diff --git a/Networking/NetworkingTests/Mapper/OrderMapperTests.swift b/Networking/NetworkingTests/Mapper/OrderMapperTests.swift index 0431a47460f..2a957218567 100644 --- a/Networking/NetworkingTests/Mapper/OrderMapperTests.swift +++ b/Networking/NetworkingTests/Mapper/OrderMapperTests.swift @@ -429,6 +429,15 @@ final class OrderMapperTests: XCTestCase { XCTAssertNil(compositeProduct.parent) XCTAssertEqual(component.parent, 830) } + + func test_that_order_alternative_types_are_properly_parsed() throws { + // Given + let order = try XCTUnwrap(mapLoadOrderResponseWithAlternativeTypes()) + + // Then + XCTAssertEqual(order.shippingLines.first?.taxes.first?.taxID, 1) + XCTAssertEqual(order.items.first?.sku, "123") + } } @@ -531,4 +540,10 @@ private extension OrderMapperTests { return mapOrder(from: "order-with-composite-product") } + /// Returns the Order output upon receiving `order-alternative-types` + /// + func mapLoadOrderResponseWithAlternativeTypes() -> Order? { + return mapOrder(from: "order-alternative-types") + } + } diff --git a/Networking/NetworkingTests/Responses/order-alternative-types.json b/Networking/NetworkingTests/Responses/order-alternative-types.json new file mode 100644 index 00000000000..3c2c1d7d561 --- /dev/null +++ b/Networking/NetworkingTests/Responses/order-alternative-types.json @@ -0,0 +1,209 @@ +{ + "data": { + "id": 963, + "parent_id": 0, + "is_editable": true, + "needs_payment": true, + "needs_processing": true, + "number": "963", + "status": "processing", + "order_key": "abc123", + "currency": "USD", + "date_created_gmt": "2018-04-03T23:05:12", + "date_modified_gmt": "2018-04-03T23:05:14", + "discount_total": "30.00", + "discount_tax": "1.20", + "shipping_total": "0.00", + "shipping_tax": "0.00", + "cart_tax": "1.20", + "total": "31.20", + "total_tax": "1.20", + "customer_id": 11, + "customer_note": "", + "billing": { + "first_name": "Johnny", + "last_name": "Appleseed", + "company": "", + "address_1": "234 70th Street", + "address_2": "", + "city": "Niagara Falls", + "state": "NY", + "postcode": "14304", + "country": "US", + "email": "scrambled@scrambled.com", + "phone": "333-333-3333" + }, + "shipping": { + "first_name": "Johnny", + "last_name": "Appleseed", + "company": "", + "address_1": "234 70th Street", + "address_2": "", + "city": "Niagara Falls", + "state": "NY", + "postcode": "14304", + "country": "US", + "email": "scrambled@scrambled.com", + "phone": "333-333-3333" + }, + "shipping_lines": [ + { + "id": 123, + "method_title": "International Priority Mail Express Flat Rate", + "method_id": "usps", + "instance_id": "5", + "total": "133.00", + "total_tax": "0.00", + "taxes": [ + { + "id": "1", + "total": "0.62125", + "subtotal": "" + } + ], + "meta_data": [ + { + "id": 1307, + "key": "Package 1", + "value": "1 × 1 × 1 (in) 4lbs × 1" + }, + { + "id": 1308, + "key": "Package 2", + "value": "1 × 1 × 1 (in) 1lbs × 1" + } + ] + } + ], + "fee_lines": [ + { + "id": 60, + "name": "$125.50 fee", + "tax_class": "", + "tax_status": "taxable", + "amount": "125.5", + "total": "125.50", + "total_tax": "0.00", + "taxes": [], + "meta_data": [] + } + ], + "tax_lines": [ + { + "id": 1330, + "rate_code": "US-NY-STATE-2", + "rate_id": 6, + "label": "State", + "compound": true, + "tax_total": "7.71", + "shipping_tax_total": "0.00", + "rate_percent": 4.5, + "meta_data": [] + } + ], + "payment_method": "stripe", + "payment_method_title": "Credit Card (Stripe)", + "payment_url": "http://www.automattic.com", + "date_paid_gmt": "2018-04-03T23:05:14", + "date_completed_gmt": null, + "line_items": [ + { + "id": 890, + "name": "Fruits Basket (Mix & Match Product)", + "product_id": 52, + "variation_id": 0, + "quantity": 2, + "tax_class": "", + "subtotal": "50.00", + "subtotal_tax": "2.00", + "total": "30.00", + "total_tax": "1.20", + "taxes": [ + { + "id": 1, + "total": "1.2", + "subtotal": "2" + } + ], + "meta_data": [], + "sku": 123, + "price": 30 + }, + { + "id": 891, + "name": "Fruits Bundle", + "product_id": 234, + "variation_id": 0, + "quantity": 1.5, + "tax_class": "", + "subtotal": "10.00", + "subtotal_tax": "0.40", + "total": "0.00", + "total_tax": "0.00", + "taxes": [ + { + "id": 1, + "total": "0", + "subtotal": "0.4" + } + ], + "meta_data": [], + "sku": "5555-A", + "price": 0.00 + } + ], + "coupon_lines": [ + { + "id": 894, + "code": "30$off", + "discount": "30", + "discount_tax": "1.2", + "meta_data": [ + { + "id": 6515, + "key": "coupon_data", + "value": { + "id": 673, + "code": "30$off", + "amount": "30", + "date_created": { + "date": "2017-10-29 18:20:07.000000", + "timezone_type": 3, + "timezone": "America/New_York" + }, + "date_modified": { + "date": "2017-10-29 18:20:07.000000", + "timezone_type": 3, + "timezone": "America/New_York" + }, + "date_expires": null, + "discount_type": "fixed_cart", + "description": "", + "usage_count": 2, + "individual_use": false, + "product_ids": [], + "excluded_product_ids": [], + "usage_limit": 0, + "usage_limit_per_user": 0, + "limit_usage_to_x_items": null, + "free_shipping": false, + "product_categories": [], + "excluded_product_categories": [], + "exclude_sale_items": false, + "minimum_amount": "", + "maximum_amount": "", + "email_restrictions": [], + "used_by": [ + "1", + "1" + ], + "virtual": false, + "meta_data": [] + } + } + ] + } + ] + } +} + diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt index e4e3bb9b543..e33a01d867b 100644 --- a/RELEASE-NOTES.txt +++ b/RELEASE-NOTES.txt @@ -2,6 +2,7 @@ 13.9 ----- +- [*] 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] 13.8