diff --git a/Networking/Networking.xcodeproj/project.pbxproj b/Networking/Networking.xcodeproj/project.pbxproj index 84787f7f303..8854c6693b8 100644 --- a/Networking/Networking.xcodeproj/project.pbxproj +++ b/Networking/Networking.xcodeproj/project.pbxproj @@ -24,6 +24,7 @@ 748D424A210F92EA00CF7D1B /* OrderStatsItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 748D4249210F92EA00CF7D1B /* OrderStatsItem.swift */; }; 748D424C210FA34400CF7D1B /* OrderStatsMapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 748D424B210FA34400CF7D1B /* OrderStatsMapper.swift */; }; 748D424E210FB1F500CF7D1B /* order-stats-day.json in Resources */ = {isa = PBXBuildFile; fileRef = 748D424D210FB1F500CF7D1B /* order-stats-day.json */; }; + 7497376A2141F2BE0008C490 /* top-performers-week-alt.json in Resources */ = {isa = PBXBuildFile; fileRef = 749737692141F2BE0008C490 /* top-performers-week-alt.json */; }; 74A1196C2110F4BB00E1E5F0 /* OrderStats.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74A1196B2110F4BB00E1E5F0 /* OrderStats.swift */; }; 74A1D263211898F000931DFA /* site-visits-day.json in Resources */ = {isa = PBXBuildFile; fileRef = 74A1D25F211898F000931DFA /* site-visits-day.json */; }; 74A1D264211898F000931DFA /* site-visits-week.json in Resources */ = {isa = PBXBuildFile; fileRef = 74A1D260211898F000931DFA /* site-visits-week.json */; }; @@ -33,6 +34,15 @@ 74A1D26B21189B8100931DFA /* SiteVisitStatsItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74A1D26A21189B8100931DFA /* SiteVisitStatsItem.swift */; }; 74A1D26D21189DFF00931DFA /* SiteVisitStatsMapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74A1D26C21189DFE00931DFA /* SiteVisitStatsMapper.swift */; }; 74A1D26F21189EA100931DFA /* SiteVisitStatsRemote.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74A1D26E21189EA000931DFA /* SiteVisitStatsRemote.swift */; }; + 74ABA1C5213F17AA00FFAD30 /* top-performers-day.json in Resources */ = {isa = PBXBuildFile; fileRef = 74ABA1C4213F17AA00FFAD30 /* top-performers-day.json */; }; + 74ABA1C9213F19FE00FFAD30 /* top-performers-month.json in Resources */ = {isa = PBXBuildFile; fileRef = 74ABA1C6213F19FD00FFAD30 /* top-performers-month.json */; }; + 74ABA1CA213F19FE00FFAD30 /* top-performers-year.json in Resources */ = {isa = PBXBuildFile; fileRef = 74ABA1C7213F19FE00FFAD30 /* top-performers-year.json */; }; + 74ABA1CB213F19FE00FFAD30 /* top-performers-week.json in Resources */ = {isa = PBXBuildFile; fileRef = 74ABA1C8213F19FE00FFAD30 /* top-performers-week.json */; }; + 74ABA1CD213F1B6B00FFAD30 /* TopEarnerStats.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74ABA1CC213F1B6B00FFAD30 /* TopEarnerStats.swift */; }; + 74ABA1CF213F1D1600FFAD30 /* TopEarnerStatsItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74ABA1CE213F1D1600FFAD30 /* TopEarnerStatsItem.swift */; }; + 74ABA1D1213F22CA00FFAD30 /* TopEarnersStatsRemote.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74ABA1D0213F22CA00FFAD30 /* TopEarnersStatsRemote.swift */; }; + 74ABA1D3213F25AE00FFAD30 /* TopEarnerStatsMapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74ABA1D2213F25AE00FFAD30 /* TopEarnerStatsMapper.swift */; }; + 74ABA1D5213F26B300FFAD30 /* TopEarnerStatsMapperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74ABA1D4213F26B300FFAD30 /* TopEarnerStatsMapperTests.swift */; }; 74AE244D2113704C00CA8C54 /* OrderStatsRemoteTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74AE244C2113704C00CA8C54 /* OrderStatsRemoteTests.swift */; }; 74C8F06420EEB44800B6EDC9 /* OrderNote.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74C8F06320EEB44800B6EDC9 /* OrderNote.swift */; }; 74C8F06620EEB76400B6EDC9 /* order-notes.json in Resources */ = {isa = PBXBuildFile; fileRef = 74C8F06520EEB76400B6EDC9 /* order-notes.json */; }; @@ -41,6 +51,7 @@ 74C8F06C20EEBD5D00B6EDC9 /* broken-order.json in Resources */ = {isa = PBXBuildFile; fileRef = 74C8F06B20EEBD5D00B6EDC9 /* broken-order.json */; }; 74C8F06E20EEC1E800B6EDC9 /* OrderNotesMapperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74C8F06D20EEC1E700B6EDC9 /* OrderNotesMapperTests.swift */; }; 74C8F07020EEC3A800B6EDC9 /* broken-notes.json in Resources */ = {isa = PBXBuildFile; fileRef = 74C8F06F20EEC3A800B6EDC9 /* broken-notes.json */; }; + 74CF5E8421402C04000CED0A /* TopEarnerStatsRemoteTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74CF5E8321402C04000CED0A /* TopEarnerStatsRemoteTests.swift */; }; 74D3BD142114FE6900A6E85E /* MIContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74D3BD132114FE6900A6E85E /* MIContainer.swift */; }; 74D522B62113607F00042831 /* StatGranularity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74D522B52113607F00042831 /* StatGranularity.swift */; }; B505F6CD20BEE37E00BB1B69 /* AccountMapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = B505F6CC20BEE37E00BB1B69 /* AccountMapper.swift */; }; @@ -122,6 +133,7 @@ 748D4249210F92EA00CF7D1B /* OrderStatsItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrderStatsItem.swift; sourceTree = ""; }; 748D424B210FA34400CF7D1B /* OrderStatsMapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrderStatsMapper.swift; sourceTree = ""; }; 748D424D210FB1F500CF7D1B /* order-stats-day.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "order-stats-day.json"; sourceTree = ""; }; + 749737692141F2BE0008C490 /* top-performers-week-alt.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "top-performers-week-alt.json"; sourceTree = ""; }; 74A1196B2110F4BB00E1E5F0 /* OrderStats.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrderStats.swift; sourceTree = ""; }; 74A1D25F211898F000931DFA /* site-visits-day.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "site-visits-day.json"; sourceTree = ""; }; 74A1D260211898F000931DFA /* site-visits-week.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "site-visits-week.json"; sourceTree = ""; }; @@ -131,6 +143,15 @@ 74A1D26A21189B8100931DFA /* SiteVisitStatsItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SiteVisitStatsItem.swift; sourceTree = ""; }; 74A1D26C21189DFE00931DFA /* SiteVisitStatsMapper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SiteVisitStatsMapper.swift; sourceTree = ""; }; 74A1D26E21189EA000931DFA /* SiteVisitStatsRemote.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SiteVisitStatsRemote.swift; sourceTree = ""; }; + 74ABA1C4213F17AA00FFAD30 /* top-performers-day.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "top-performers-day.json"; sourceTree = ""; }; + 74ABA1C6213F19FD00FFAD30 /* top-performers-month.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "top-performers-month.json"; sourceTree = ""; }; + 74ABA1C7213F19FE00FFAD30 /* top-performers-year.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "top-performers-year.json"; sourceTree = ""; }; + 74ABA1C8213F19FE00FFAD30 /* top-performers-week.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "top-performers-week.json"; sourceTree = ""; }; + 74ABA1CC213F1B6B00FFAD30 /* TopEarnerStats.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TopEarnerStats.swift; sourceTree = ""; }; + 74ABA1CE213F1D1600FFAD30 /* TopEarnerStatsItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TopEarnerStatsItem.swift; sourceTree = ""; }; + 74ABA1D0213F22CA00FFAD30 /* TopEarnersStatsRemote.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TopEarnersStatsRemote.swift; sourceTree = ""; }; + 74ABA1D2213F25AE00FFAD30 /* TopEarnerStatsMapper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TopEarnerStatsMapper.swift; sourceTree = ""; }; + 74ABA1D4213F26B300FFAD30 /* TopEarnerStatsMapperTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TopEarnerStatsMapperTests.swift; sourceTree = ""; }; 74AE244C2113704C00CA8C54 /* OrderStatsRemoteTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrderStatsRemoteTests.swift; sourceTree = ""; }; 74C8F06320EEB44800B6EDC9 /* OrderNote.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OrderNote.swift; sourceTree = ""; }; 74C8F06520EEB76400B6EDC9 /* order-notes.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "order-notes.json"; sourceTree = ""; }; @@ -139,6 +160,7 @@ 74C8F06B20EEBD5D00B6EDC9 /* broken-order.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "broken-order.json"; sourceTree = ""; }; 74C8F06D20EEC1E700B6EDC9 /* OrderNotesMapperTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OrderNotesMapperTests.swift; sourceTree = ""; }; 74C8F06F20EEC3A800B6EDC9 /* broken-notes.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "broken-notes.json"; sourceTree = ""; }; + 74CF5E8321402C04000CED0A /* TopEarnerStatsRemoteTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TopEarnerStatsRemoteTests.swift; sourceTree = ""; }; 74D3BD132114FE6900A6E85E /* MIContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MIContainer.swift; sourceTree = ""; }; 74D522B52113607F00042831 /* StatGranularity.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StatGranularity.swift; sourceTree = ""; }; 753D6504FF01F09F6A33B73E /* Pods-Networking.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Networking.debug.xcconfig"; path = "../Pods/Target Support Files/Pods-Networking/Pods-Networking.debug.xcconfig"; sourceTree = ""; }; @@ -240,6 +262,8 @@ 748D4249210F92EA00CF7D1B /* OrderStatsItem.swift */, 74A1D26721189A7000931DFA /* SiteVisitStats.swift */, 74A1D26A21189B8100931DFA /* SiteVisitStatsItem.swift */, + 74ABA1CC213F1B6B00FFAD30 /* TopEarnerStats.swift */, + 74ABA1CE213F1D1600FFAD30 /* TopEarnerStatsItem.swift */, ); path = Stats; sourceTree = ""; @@ -275,6 +299,7 @@ B518662920A09C6F00037A38 /* OrdersRemoteTests.swift */, 74AE244C2113704C00CA8C54 /* OrderStatsRemoteTests.swift */, 74002D6B2118B88200A63C19 /* SiteVisitStatsRemoteTests.swift */, + 74CF5E8321402C04000CED0A /* TopEarnerStatsRemoteTests.swift */, ); path = Remote; sourceTree = ""; @@ -356,6 +381,7 @@ B557DA0120975500005962F4 /* OrdersRemote.swift */, 748D4247210F89ED00CF7D1B /* OrderStatsRemote.swift */, 74A1D26E21189EA000931DFA /* SiteVisitStatsRemote.swift */, + 74ABA1D0213F22CA00FFAD30 /* TopEarnersStatsRemote.swift */, ); path = Remote; sourceTree = ""; @@ -407,20 +433,25 @@ B58D10C72114D21C00107ED4 /* generic_error.json */, B505F6D420BEE4E600BB1B69 /* me.json */, B58D10C92114D22E00107ED4 /* new-order-note.json */, + B5C6FCD520A3768900A4F8E4 /* order.json */, + B559EBA920A0B5CD00836CD4 /* orders-load-all.json */, 74C8F06520EEB76400B6EDC9 /* order-notes.json */, 748D424D210FB1F500CF7D1B /* order-stats-day.json */, 743FDB9B210FB36900AC737F /* order-stats-week.json */, 743FDB99210FB36900AC737F /* order-stats-month.json */, 743FDB9A210FB36900AC737F /* order-stats-year.json */, B58D10C52114D1F100107ED4 /* order-stats.json */, + B56C1EB920EA7D2C00D749F9 /* sites.json */, 74A1D25F211898F000931DFA /* site-visits-day.json */, 74A1D260211898F000931DFA /* site-visits-week.json */, 74A1D261211898F000931DFA /* site-visits-month.json */, 74A1D262211898F000931DFA /* site-visits-year.json */, 743BF8BD21191B63008A9D87 /* site-visits.json */, - B5C6FCD520A3768900A4F8E4 /* order.json */, - B559EBA920A0B5CD00836CD4 /* orders-load-all.json */, - B56C1EB920EA7D2C00D749F9 /* sites.json */, + 74ABA1C4213F17AA00FFAD30 /* top-performers-day.json */, + 74ABA1C8213F19FE00FFAD30 /* top-performers-week.json */, + 749737692141F2BE0008C490 /* top-performers-week-alt.json */, + 74ABA1C6213F19FD00FFAD30 /* top-performers-month.json */, + 74ABA1C7213F19FE00FFAD30 /* top-performers-year.json */, ); path = Responses; sourceTree = ""; @@ -437,6 +468,7 @@ CE583A0D2109154500D73C1C /* OrderNoteMapper.swift */, 748D424B210FA34400CF7D1B /* OrderStatsMapper.swift */, 74A1D26C21189DFE00931DFA /* SiteVisitStatsMapper.swift */, + 74ABA1D2213F25AE00FFAD30 /* TopEarnerStatsMapper.swift */, ); path = Mapper; sourceTree = ""; @@ -467,6 +499,7 @@ 74C8F06D20EEC1E700B6EDC9 /* OrderNotesMapperTests.swift */, 743FDB9F210FB3E500AC737F /* OrderStatsMapperTests.swift */, 74002D692118B26000A63C19 /* SiteVisitStatsMapperTests.swift */, + 74ABA1D4213F26B300FFAD30 /* TopEarnerStatsMapperTests.swift */, ); path = Mapper; sourceTree = ""; @@ -587,8 +620,10 @@ 74C8F06620EEB76400B6EDC9 /* order-notes.json in Resources */, B58D10CA2114D22E00107ED4 /* new-order-note.json in Resources */, 74C8F06C20EEBD5D00B6EDC9 /* broken-order.json in Resources */, + 74ABA1C9213F19FE00FFAD30 /* top-performers-month.json in Resources */, B58D10C82114D21D00107ED4 /* generic_error.json in Resources */, B5147876211B9227007562E5 /* broken-orders-mark-2.json in Resources */, + 7497376A2141F2BE0008C490 /* top-performers-week-alt.json in Resources */, 743BF8BE21191B63008A9D87 /* site-visits.json in Resources */, B505F6D520BEE4E700BB1B69 /* me.json in Resources */, B5C6FCD620A3768900A4F8E4 /* order.json in Resources */, @@ -598,12 +633,15 @@ 743FDB9E210FB36900AC737F /* order-stats-week.json in Resources */, 74A1D264211898F000931DFA /* site-visits-week.json in Resources */, 743FDB9D210FB36900AC737F /* order-stats-year.json in Resources */, + 74ABA1CB213F19FE00FFAD30 /* top-performers-week.json in Resources */, B559EBAA20A0B5CD00836CD4 /* orders-load-all.json in Resources */, 74A1D263211898F000931DFA /* site-visits-day.json in Resources */, B56C1EBA20EA7D2C00D749F9 /* sites.json in Resources */, CE20179320E3EFA7005B4C18 /* broken-orders.json in Resources */, + 74ABA1CA213F19FE00FFAD30 /* top-performers-year.json in Resources */, 74A1D265211898F000931DFA /* site-visits-month.json in Resources */, B58D10C62114D1F200107ED4 /* order-stats.json in Resources */, + 74ABA1C5213F17AA00FFAD30 /* top-performers-day.json in Resources */, 74C8F07020EEC3A800B6EDC9 /* broken-notes.json in Resources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -674,18 +712,22 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 74ABA1CF213F1D1600FFAD30 /* TopEarnerStatsItem.swift in Sources */, 7452387421124B7700A973CD /* AnyDecodable.swift in Sources */, B56C1EB620EA757B00D749F9 /* SiteListMapper.swift in Sources */, B557DA1A20979D66005962F4 /* Settings.swift in Sources */, + 74ABA1D1213F22CA00FFAD30 /* TopEarnersStatsRemote.swift in Sources */, 74A1196C2110F4BB00E1E5F0 /* OrderStats.swift in Sources */, B58E5BEA20FFB3D0003C986E /* CodingUserInfoKey+Woo.swift in Sources */, 741B950120EBC8A700DD6E2D /* OrderCouponLine.swift in Sources */, 74C8F06420EEB44800B6EDC9 /* OrderNote.swift in Sources */, + 74ABA1D3213F25AE00FFAD30 /* TopEarnerStatsMapper.swift in Sources */, B5BB1D0C20A2050300112D92 /* DateFormatter+Woo.swift in Sources */, B567AF2520A0CCA300AB6C62 /* AuthenticatedRequest.swift in Sources */, B505F6EA20BEFC3700BB1B69 /* MockupNetwork.swift in Sources */, 748D4248210F89ED00CF7D1B /* OrderStatsRemote.swift in Sources */, 7452387221124B7700A973CD /* AnyEncodable.swift in Sources */, + 74ABA1CD213F1B6B00FFAD30 /* TopEarnerStats.swift in Sources */, B557DA0220975500005962F4 /* JetpackRequest.swift in Sources */, B56C1EB820EA76F500D749F9 /* Site.swift in Sources */, B505F6CD20BEE37E00BB1B69 /* AccountMapper.swift in Sources */, @@ -726,6 +768,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 74CF5E8421402C04000CED0A /* TopEarnerStatsRemoteTests.swift in Sources */, 74AE244D2113704C00CA8C54 /* OrderStatsRemoteTests.swift in Sources */, B505F6D320BEE3A500BB1B69 /* AccountMapperTests.swift in Sources */, B5C6FCC820A32E4800A4F8E4 /* DateFormatterWooTests.swift in Sources */, @@ -735,6 +778,7 @@ B518662A20A09C6F00037A38 /* OrdersRemoteTests.swift in Sources */, B5969E1520A47F99005E9DF1 /* RemoteTests.swift in Sources */, 74C8F06E20EEC1E800B6EDC9 /* OrderNotesMapperTests.swift in Sources */, + 74ABA1D5213F26B300FFAD30 /* TopEarnerStatsMapperTests.swift in Sources */, B567AF2F20A0FB8F00AB6C62 /* AuthenticatedRequestTests.swift in Sources */, B5C6FCCD20A34B8300A4F8E4 /* OrderListMapperTests.swift in Sources */, B518663520A0A2E800037A38 /* Constants.swift in Sources */, diff --git a/Networking/Networking/Mapper/TopEarnerStatsMapper.swift b/Networking/Networking/Mapper/TopEarnerStatsMapper.swift new file mode 100644 index 00000000000..ef6463b672a --- /dev/null +++ b/Networking/Networking/Mapper/TopEarnerStatsMapper.swift @@ -0,0 +1,14 @@ +import Foundation + + +/// Mapper: TopEarnerStats +/// +class TopEarnerStatsMapper: Mapper { + + /// (Attempts) to convert a dictionary into an TopEarnerStats entity. + /// + func map(response: Data) throws -> TopEarnerStats { + let decoder = JSONDecoder() + return try decoder.decode(TopEarnerStats.self, from: response) + } +} diff --git a/Networking/Networking/Model/Account.swift b/Networking/Networking/Model/Account.swift index 6434f89bea4..4c14e0f4104 100644 --- a/Networking/Networking/Model/Account.swift +++ b/Networking/Networking/Model/Account.swift @@ -50,3 +50,22 @@ private extension Account { case gravatarUrl = "avatar_URL" } } + + +// MARK: - Comparable Conformance +// +extension Account: Comparable { + public static func == (lhs: Account, rhs: Account) -> Bool { + return lhs.userID == rhs.userID && + lhs.displayName == rhs.displayName && + lhs.email == rhs.email && + lhs.username == rhs.username && + lhs.gravatarUrl == rhs.gravatarUrl + } + + public static func < (lhs: Account, rhs: Account) -> Bool { + return lhs.userID < rhs.userID || + (lhs.userID == rhs.userID && lhs.username < rhs.username) || + (lhs.userID == rhs.userID && lhs.username == rhs.username && lhs.displayName < rhs.displayName) + } +} diff --git a/Networking/Networking/Model/Order.swift b/Networking/Networking/Model/Order.swift index ad96d893eb2..20fdb753735 100644 --- a/Networking/Networking/Model/Order.swift +++ b/Networking/Networking/Model/Order.swift @@ -183,7 +183,8 @@ extension Order: Comparable { lhs.paymentMethodTitle == rhs.paymentMethodTitle && lhs.billingAddress == rhs.billingAddress && lhs.shippingAddress == rhs.shippingAddress && - lhs.coupons == rhs.coupons && + lhs.coupons.count == rhs.coupons.count && + lhs.coupons.sorted() == rhs.coupons.sorted() && lhs.items.count == rhs.items.count && lhs.items.sorted() == rhs.items.sorted() } diff --git a/Networking/Networking/Model/Site.swift b/Networking/Networking/Model/Site.swift index b95a7a1c7ee..e703fda3d8e 100644 --- a/Networking/Networking/Model/Site.swift +++ b/Networking/Networking/Model/Site.swift @@ -58,6 +58,26 @@ public struct Site: Decodable { } +// MARK: - Comparable Conformance +// +extension Site: Comparable { + public static func == (lhs: Site, rhs: Site) -> Bool { + return lhs.siteID == rhs.siteID && + lhs.name == rhs.name && + lhs.description == rhs.description && + lhs.url == rhs.url && + lhs.isWooCommerceActive == rhs.isWooCommerceActive && + lhs.isWordPressStore == rhs.isWordPressStore + } + + public static func < (lhs: Site, rhs: Site) -> Bool { + return lhs.siteID < rhs.siteID || + (lhs.siteID == rhs.siteID && lhs.name < rhs.name) || + (lhs.siteID == rhs.siteID && lhs.name == rhs.name && lhs.description < rhs.description) + } +} + + /// Defines all of the Site CodingKeys. /// private extension Site { diff --git a/Networking/Networking/Model/Stats/TopEarnerStats.swift b/Networking/Networking/Model/Stats/TopEarnerStats.swift new file mode 100644 index 00000000000..d9158805df8 --- /dev/null +++ b/Networking/Networking/Model/Stats/TopEarnerStats.swift @@ -0,0 +1,65 @@ +import Foundation + + +/// Represents Top Earner (aka top performer) stats over a specific period. +/// +public struct TopEarnerStats: Decodable { + public let date: String + public let granularity: StatGranularity + public let limit: String + public let items: [TopEarnerStatsItem]? + + + /// The public initializer for top earner stats. + /// + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + let date = try container.decode(String.self, forKey: .date) + let granularity = try container.decode(StatGranularity.self, forKey: .unit) + let limit = try container.decode(String.self, forKey: .limit) + let items = try container.decode([TopEarnerStatsItem].self, forKey: .items) + + self.init(date: date, granularity: granularity, limit: limit, items: items) + } + + + /// TopEarnerStats struct initializer. + /// + public init(date: String, granularity: StatGranularity, limit: String, items: [TopEarnerStatsItem]?) { + self.date = date + self.granularity = granularity + self.limit = limit + self.items = items + } +} + + +/// Defines all of the TopEarnerStats CodingKeys. +/// +private extension TopEarnerStats { + enum CodingKeys: String, CodingKey { + case date = "date" + case unit = "unit" + case limit = "limit" + case items = "data" + } +} + + +// MARK: - Comparable Conformance +// +extension TopEarnerStats: Comparable { + public static func == (lhs: TopEarnerStats, rhs: TopEarnerStats) -> Bool { + return lhs.date == rhs.date && + lhs.granularity == rhs.granularity && + lhs.limit == rhs.limit && + lhs.items?.count == rhs.items?.count && + lhs.items?.sorted() == rhs.items?.sorted() + } + + public static func < (lhs: TopEarnerStats, rhs: TopEarnerStats) -> Bool { + return lhs.date < rhs.date || + (lhs.date == rhs.date && lhs.limit < rhs.limit) + } +} diff --git a/Networking/Networking/Model/Stats/TopEarnerStatsItem.swift b/Networking/Networking/Model/Stats/TopEarnerStatsItem.swift new file mode 100644 index 00000000000..17a34380c45 --- /dev/null +++ b/Networking/Networking/Model/Stats/TopEarnerStatsItem.swift @@ -0,0 +1,83 @@ +import Foundation + + +/// Represents a single top earner stat for a specific period. +/// +public struct TopEarnerStatsItem: Decodable { + + /// Product ID + /// + public let productID: Int + + /// Product name + /// + public let productName: String + + /// Quantity sold + /// + public let quantity: Int + + /// Average price of item + /// + public let price: Double + + /// Total revenue from product + /// + public let total: Double + + /// Currency + /// + public let currency: String + + /// Image URL for product + /// + public let imageUrl: String? + + + /// Designated Initializer. + /// + public init(productID: Int, productName: String, quantity: Int, price: Double, total: Double, currency: String, imageUrl: String?) { + self.productID = productID + self.productName = productName + self.quantity = quantity + self.price = price + self.total = total + self.currency = currency + self.imageUrl = imageUrl + } +} + + +/// Defines all of the TopEarnerStatsItem CodingKeys. +/// +private extension TopEarnerStatsItem { + enum CodingKeys: String, CodingKey { + case productID = "ID" + case productName = "name" + case total = "total" + case quantity = "quantity" + case price = "price" + case imageUrl = "image" + case currency = "currency" + } +} + + +// MARK: - Comparable Conformance +// +extension TopEarnerStatsItem: Comparable { + public static func == (lhs: TopEarnerStatsItem, rhs: TopEarnerStatsItem) -> Bool { + return lhs.productID == rhs.productID && + lhs.productName == rhs.productName && + lhs.quantity == rhs.quantity && + lhs.price == rhs.price && + lhs.total == rhs.total && + lhs.currency == rhs.currency && + lhs.imageUrl == rhs.imageUrl + } + + public static func < (lhs: TopEarnerStatsItem, rhs: TopEarnerStatsItem) -> Bool { + return lhs.quantity < rhs.quantity || + (lhs.quantity == rhs.quantity && lhs.productName < rhs.productName) + } +} diff --git a/Networking/Networking/Remote/TopEarnersStatsRemote.swift b/Networking/Networking/Remote/TopEarnersStatsRemote.swift new file mode 100644 index 00000000000..25f980bc4e4 --- /dev/null +++ b/Networking/Networking/Remote/TopEarnersStatsRemote.swift @@ -0,0 +1,45 @@ +import Foundation +import Alamofire + + +/// TopEarnersStats: Remote Endpoints +/// +public class TopEarnersStatsRemote: Remote { + + /// Fetch the top earner (aka Top Performer) stats ( stats for a given site for the current day, week, month, or year (depends on the given granularity of the `unit` parameter). + /// + /// - Parameters: + /// - siteID: The site ID + /// - unit: Defines the granularity of the stats we are fetching (one of 'day', 'week', 'month', or 'year') + /// - latestDateToInclude: The latest date to include in the results (see Note below) + /// - limit: Maximum number of `unit`s to fetch + /// - completion: Closure to be executed upon completion. + /// + /// Note: `latestDateToInclude` date string must be formatted appropriately given the `unit` param. See: `DateFormatter.Stats` extension for some helper funcs. + /// + public func loadTopEarnersStats(for siteID: Int, unit: StatGranularity, latestDateToInclude: String, limit: Int, completion: @escaping (TopEarnerStats?, Error?) -> Void) { + let path = "\(Constants.sitesPath)/\(siteID)/\(Constants.topEarnersStatsPath)/" + let parameters = [ParameterKeys.unit: unit.rawValue, + ParameterKeys.date: latestDateToInclude, + ParameterKeys.limit: String(limit)] + let request = DotcomRequest(wordpressApiVersion: .wpcomMark2, method: .get, path: path, parameters: parameters) + let mapper = TopEarnerStatsMapper() + enqueue(request, mapper: mapper, completion: completion) + } +} + + +// MARK: - Constants! +// +private extension TopEarnersStatsRemote { + enum Constants { + static let sitesPath: String = "sites" + static let topEarnersStatsPath: String = "stats/top-earners" + } + + enum ParameterKeys { + static let unit: String = "unit" + static let date: String = "date" + static let limit: String = "limit" + } +} diff --git a/Networking/NetworkingTests/Mapper/TopEarnerStatsMapperTests.swift b/Networking/NetworkingTests/Mapper/TopEarnerStatsMapperTests.swift new file mode 100644 index 00000000000..e3deb7a0ee3 --- /dev/null +++ b/Networking/NetworkingTests/Mapper/TopEarnerStatsMapperTests.swift @@ -0,0 +1,167 @@ +import XCTest +@testable import Networking + + +/// TopEarnerStatsMapper Unit Tests +/// +class TopEarnerStatsMapperTests: XCTestCase { + + /// Verifies that all of the day unit TopEarnerStats fields are parsed correctly. + /// + func testDayUnitStatFieldsAreProperlyParsed() { + guard let dayStats = mapTopEarnerStatsWithDayUnitResponse() else { + XCTFail() + return + } + + XCTAssertEqual(dayStats.granularity, .day) + XCTAssertEqual(dayStats.date, "2018-06-08") + XCTAssertEqual(dayStats.limit, "5") + XCTAssertEqual(dayStats.items!.count, 1) + + let sampleItem1 = dayStats.items![0] + XCTAssertEqual(sampleItem1.imageUrl, "https://jamosova3.mystagingwebsite.com/wp-content/uploads/2017/05/hoodie-with-logo.jpg?w=801") + XCTAssertEqual(sampleItem1.currency, "USD") + XCTAssertEqual(sampleItem1.price, 40.0) + XCTAssertEqual(sampleItem1.productID, 296) + XCTAssertEqual(sampleItem1.productName, "Funky Hoodie") + XCTAssertEqual(sampleItem1.quantity, 1) + XCTAssertEqual(sampleItem1.total, 40.0) + } + + /// Verifies that all of the week unit TopEarnerStats fields are parsed correctly. + /// + func testWeekUnitStatFieldsAreProperlyParsed() { + guard let weekStats = mapTopEarnerStatsWithWeekUnitResponse() else { + XCTFail() + return + } + + XCTAssertEqual(weekStats.granularity, .week) + XCTAssertEqual(weekStats.date, "2018-W12") + XCTAssertEqual(weekStats.limit, "5") + XCTAssertEqual(weekStats.items!.count, 3) + + let sampleItem1 = weekStats.items![0] + XCTAssertEqual(sampleItem1.imageUrl, "https://jamosova3.mystagingwebsite.com/wp-content/uploads/2017/05/hoodie-with-logo.jpg?w=801") + XCTAssertEqual(sampleItem1.currency, "USD") + XCTAssertEqual(sampleItem1.price, 40.0) + XCTAssertEqual(sampleItem1.productID, 296) + XCTAssertEqual(sampleItem1.productName, "Funky Hoodie") + XCTAssertEqual(sampleItem1.quantity, 1) + XCTAssertEqual(sampleItem1.total, 0.0) + + let sampleItem2 = weekStats.items![2] + XCTAssertEqual(sampleItem2.imageUrl, "https://jamosova3.mystagingwebsite.com/wp-content/uploads/2018/04/smile.gif?w=480") + XCTAssertEqual(sampleItem2.currency, "USD") + XCTAssertEqual(sampleItem2.price, 80.0) + XCTAssertEqual(sampleItem2.productID, 1033) + XCTAssertEqual(sampleItem2.productName, "Smile T-Shirt") + XCTAssertEqual(sampleItem2.quantity, 2) + XCTAssertEqual(sampleItem2.total, 160.0) + } + + /// Verifies that all of the month unit TopEarnerStats fields are parsed correctly. + /// + func testMonthUnitStatFieldsAreProperlyParsed() { + guard let monthStats = mapTopEarnerStatsWithMonthUnitResponse() else { + XCTFail() + return + } + + XCTAssertEqual(monthStats.granularity, .month) + XCTAssertEqual(monthStats.date, "2018-08") + XCTAssertEqual(monthStats.limit, "5") + XCTAssertEqual(monthStats.items!.count, 5) + + let sampleItem1 = monthStats.items![0] + XCTAssertEqual(sampleItem1.imageUrl, "https://jamosova3.mystagingwebsite.com/wp-content/uploads/2017/08/freediving.jpg?w=768") + XCTAssertEqual(sampleItem1.currency, "USD") + XCTAssertEqual(sampleItem1.price, 249.34) + XCTAssertEqual(sampleItem1.productID, 601) + XCTAssertEqual(sampleItem1.productName, "Ultimate Freediving Experience") + XCTAssertEqual(sampleItem1.quantity, 5) + XCTAssertEqual(sampleItem1.total, 1245.0) + + let sampleItem2 = monthStats.items![3] + XCTAssertEqual(sampleItem2.imageUrl, "https://jamosova3.mystagingwebsite.com/wp-content/uploads/2018/08/00030000053201_CL___JPEG_3.jpg?w=500") + XCTAssertEqual(sampleItem2.currency, "USD") + XCTAssertEqual(sampleItem2.price, 4.49) + XCTAssertEqual(sampleItem2.productID, 1293) + XCTAssertEqual(sampleItem2.productName, "Pancake Mix - 2lb") + XCTAssertEqual(sampleItem2.quantity, 26) + XCTAssertEqual(sampleItem2.total, 112.253) + } + + /// Verifies that all of the year unit TopEarnerStats fields are parsed correctly. + /// + func testYearUnitStatFieldsAreProperlyParsed() { + guard let yearStats = mapTopEarnerStatsWithYearUnitResponse() else { + XCTFail() + return + } + + XCTAssertEqual(yearStats.granularity, .year) + XCTAssertEqual(yearStats.date, "2018") + XCTAssertEqual(yearStats.limit, "5") + XCTAssertEqual(yearStats.items!.count, 4) + + let sampleItem1 = yearStats.items![0] + XCTAssertEqual(sampleItem1.imageUrl, "https://jamosova3.mystagingwebsite.com/wp-content/uploads/2017/08/freediving.jpg?w=768") + XCTAssertEqual(sampleItem1.currency, "USD") + XCTAssertEqual(sampleItem1.price, 249) + XCTAssertEqual(sampleItem1.productID, 601) + XCTAssertEqual(sampleItem1.productName, "Ultimate Freediving Experience") + XCTAssertEqual(sampleItem1.quantity, 5) + XCTAssertEqual(sampleItem1.total, 1245.0) + + let sampleItem2 = yearStats.items![1] + XCTAssertEqual(sampleItem2.imageUrl, "https://jamosova3.mystagingwebsite.com/wp-content/uploads/2017/07/hm-black.jpg?w=640") + XCTAssertEqual(sampleItem2.currency, "USD") + XCTAssertEqual(sampleItem2.price, -1234.23424) + XCTAssertEqual(sampleItem2.productID, 373) + XCTAssertEqual(sampleItem2.productName, "Black Dress (H&M)") + XCTAssertEqual(sampleItem2.quantity, 1231323) + XCTAssertEqual(sampleItem2.total, 585234234.00) + } +} + + +/// Private Methods +/// +private extension TopEarnerStatsMapperTests { + + /// Returns the TopEarnerStatsMapper output upon receiving `filename` (Data Encoded) + /// + func mapStatItems(from filename: String) -> TopEarnerStats? { + guard let response = Loader.contentsOf(filename) else { + return nil + } + + return try! TopEarnerStatsMapper().map(response: response) + } + + /// Returns the TopEarnerStatsMapper output upon receiving `top-performers-day` + /// + func mapTopEarnerStatsWithDayUnitResponse() -> TopEarnerStats? { + return mapStatItems(from: "top-performers-day") + } + + /// Returns the TopEarnerStatsMapper output upon receiving `top-performers-week` + /// + func mapTopEarnerStatsWithWeekUnitResponse() -> TopEarnerStats? { + return mapStatItems(from: "top-performers-week") + } + + /// Returns the TopEarnerStatsMapper output upon receiving `top-performers-month` + /// + func mapTopEarnerStatsWithMonthUnitResponse() -> TopEarnerStats? { + return mapStatItems(from: "top-performers-month") + } + + /// Returns the TopEarnerStatsMapper output upon receiving `top-performers-year` + /// + func mapTopEarnerStatsWithYearUnitResponse() -> TopEarnerStats? { + return mapStatItems(from: "top-performers-year") + } +} diff --git a/Networking/NetworkingTests/Remote/TopEarnerStatsRemoteTests.swift b/Networking/NetworkingTests/Remote/TopEarnerStatsRemoteTests.swift new file mode 100644 index 00000000000..84fb54940ad --- /dev/null +++ b/Networking/NetworkingTests/Remote/TopEarnerStatsRemoteTests.swift @@ -0,0 +1,55 @@ +import XCTest +@testable import Networking + + +/// TopEarnerStatsRemote Unit Tests +/// +class TopEarnerStatsRemoteTests: XCTestCase { + + /// Dummy Network Wrapper + /// + let network = MockupNetwork() + + /// Dummy Site ID + /// + let sampleSiteID = 1234 + + /// Repeat always! + /// + override func setUp() { + network.removeAllSimulatedResponses() + } + + + /// Verifies that loadTopEarnersStats properly returns the `topEarnerStats` response. + /// + func testLoadTopEarnerStatsProperlyReturnsParsedStats() { + let remote = TopEarnersStatsRemote(network: network) + let expectation = self.expectation(description: "Load top earner stats") + + network.simulateResponse(requestUrlSuffix: "sites/\(sampleSiteID)/stats/top-earners/", filename: "top-performers-year") + remote.loadTopEarnersStats(for: sampleSiteID, unit: .year, latestDateToInclude: "2018", limit: 5) { (topEarnerStats, error) in + XCTAssertNil(error) + XCTAssertNotNil(topEarnerStats) + XCTAssertEqual(topEarnerStats?.items?.count, 4) + expectation.fulfill() + } + + wait(for: [expectation], timeout: Constants.expectationTimeout) + } + + /// Verifies that loadTopEarnersStats properly relays Networking Layer errors. + /// + func testLoadTopEarnerStatsProperlyRelaysNetwokingErrors() { + let remote = TopEarnersStatsRemote(network: network) + let expectation = self.expectation(description: "Load top earner stats contains errors") + + remote.loadTopEarnersStats(for: sampleSiteID, unit: .year, latestDateToInclude: "2018", limit: 5) { (topEarnerStats, error) in + XCTAssertNil(topEarnerStats) + XCTAssertNotNil(error) + expectation.fulfill() + } + + wait(for: [expectation], timeout: Constants.expectationTimeout) + } +} diff --git a/Networking/NetworkingTests/Responses/top-performers-day.json b/Networking/NetworkingTests/Responses/top-performers-day.json new file mode 100644 index 00000000000..e0caefa4786 --- /dev/null +++ b/Networking/NetworkingTests/Responses/top-performers-day.json @@ -0,0 +1,16 @@ +{ + "date": "2018-06-08", + "unit": "day", + "limit": "5", + "data": [ + { + "ID": 296, + "name": "Funky Hoodie", + "total": 40, + "quantity": 1, + "price": 40, + "image": "https:\/\/jamosova3.mystagingwebsite.com\/wp-content\/uploads\/2017\/05\/hoodie-with-logo.jpg?w=801", + "currency": "USD" + } + ] +} diff --git a/Networking/NetworkingTests/Responses/top-performers-month.json b/Networking/NetworkingTests/Responses/top-performers-month.json new file mode 100644 index 00000000000..ee57848934d --- /dev/null +++ b/Networking/NetworkingTests/Responses/top-performers-month.json @@ -0,0 +1,52 @@ +{ + "date": "2018-08", + "unit": "month", + "limit": "5", + "data": [ + { + "ID": 601, + "name": "Ultimate Freediving Experience", + "total": 1245, + "quantity": 5, + "price": 249.34, + "image": "https:\/\/jamosova3.mystagingwebsite.com\/wp-content\/uploads\/2017\/08\/freediving.jpg?w=768", + "currency": "USD" + }, + { + "ID": 1033, + "name": "Smile T-Shirt", + "total": 260, + "quantity": 5, + "price": 80, + "image": "https:\/\/jamosova3.mystagingwebsite.com\/wp-content\/uploads\/2018\/04\/smile.gif?w=480", + "currency": "USD" + }, + { + "ID": 371, + "name": "Blue Dress (Zara)", + "total": 120, + "quantity": 2, + "price": 60, + "image": "https:\/\/jamosova3.mystagingwebsite.com\/wp-content\/uploads\/2017\/07\/blue-zara.jpg?w=395", + "currency": "USD" + }, + { + "ID": 1293, + "name": "Pancake Mix - 2lb", + "total": 112.253, + "quantity": 26, + "price": 4.49, + "image": "https:\/\/jamosova3.mystagingwebsite.com\/wp-content\/uploads\/2018\/08\/00030000053201_CL___JPEG_3.jpg?w=500", + "currency": "USD" + }, + { + "ID": 373, + "name": "Black Dress (H&M)", + "total": 90, + "quantity": 3, + "price": 30, + "image": "https:\/\/jamosova3.mystagingwebsite.com\/wp-content\/uploads\/2017\/07\/hm-black.jpg?w=640", + "currency": "USD" + } + ] +} diff --git a/Networking/NetworkingTests/Responses/top-performers-week-alt.json b/Networking/NetworkingTests/Responses/top-performers-week-alt.json new file mode 100644 index 00000000000..4324d720457 --- /dev/null +++ b/Networking/NetworkingTests/Responses/top-performers-week-alt.json @@ -0,0 +1,25 @@ +{ + "date": "2018-W12", + "unit": "week", + "limit": "4", + "data": [ + { + "ID": 996, + "name": "Funky Hoodie 2", + "total": 2, + "quantity": 444, + "price": 40, + "image": "https:\/\/jamosova3.mystagingwebsite.com\/wp-content\/uploads\/2017\/05\/hoodie-with-logo.jpg", + "currency": "USD" + }, + { + "ID": 933, + "name": "Smile T-Shirt 2", + "total": 161.00, + "quantity": 555, + "price": 55.44, + "image": "https:\/\/jamosova3.mystagingwebsite.com\/wp-content\/uploads\/2018\/04\/smile.gif", + "currency": "USD" + } + ] +} diff --git a/Networking/NetworkingTests/Responses/top-performers-week.json b/Networking/NetworkingTests/Responses/top-performers-week.json new file mode 100644 index 00000000000..4b7a538b963 --- /dev/null +++ b/Networking/NetworkingTests/Responses/top-performers-week.json @@ -0,0 +1,34 @@ +{ + "date": "2018-W12", + "unit": "week", + "limit": "5", + "data": [ + { + "ID": 296, + "name": "Funky Hoodie", + "total": 0, + "quantity": 1, + "price": 40, + "image": "https:\/\/jamosova3.mystagingwebsite.com\/wp-content\/uploads\/2017\/05\/hoodie-with-logo.jpg?w=801", + "currency": "USD" + }, + { + "ID": 373, + "name": "Black Dress (H&M)", + "total": 120, + "quantity": 4, + "price": 30, + "image": "https:\/\/jamosova3.mystagingwebsite.com\/wp-content\/uploads\/2017\/07\/hm-black.jpg?w=640", + "currency": "USD" + }, + { + "ID": 1033, + "name": "Smile T-Shirt", + "total": 160, + "quantity": 2, + "price": 80, + "image": "https:\/\/jamosova3.mystagingwebsite.com\/wp-content\/uploads\/2018\/04\/smile.gif?w=480", + "currency": "USD" + } + ] +} diff --git a/Networking/NetworkingTests/Responses/top-performers-year.json b/Networking/NetworkingTests/Responses/top-performers-year.json new file mode 100644 index 00000000000..46eed6460b7 --- /dev/null +++ b/Networking/NetworkingTests/Responses/top-performers-year.json @@ -0,0 +1,43 @@ +{ + "date": "2018", + "unit": "year", + "limit": "5", + "data": [ + { + "ID": 601, + "name": "Ultimate Freediving Experience", + "total": 1245, + "quantity": 5, + "price": 249, + "image": "https:\/\/jamosova3.mystagingwebsite.com\/wp-content\/uploads\/2017\/08\/freediving.jpg?w=768", + "currency": "USD" + }, + { + "ID": 373, + "name": "Black Dress (H&M)", + "total": 585234234.00, + "quantity": 1231323, + "price": -1234.23424, + "image": "https:\/\/jamosova3.mystagingwebsite.com\/wp-content\/uploads\/2017\/07\/hm-black.jpg?w=640", + "currency": "USD" + }, + { + "ID": 1033, + "name": "Smile T-Shirt", + "total": 580, + "quantity": 9, + "price": 80, + "image": "https:\/\/jamosova3.mystagingwebsite.com\/wp-content\/uploads\/2018\/04\/smile.gif?w=480", + "currency": "USD" + }, + { + "ID": 296, + "name": "Funky Hoodie", + "total": 340, + "quantity": 8, + "price": 40, + "image": "https:\/\/jamosova3.mystagingwebsite.com\/wp-content\/uploads\/2017\/05\/hoodie-with-logo.jpg?w=801", + "currency": "USD" + } + ] +} diff --git a/Podfile b/Podfile index a85372a11d3..24257309787 100644 --- a/Podfile +++ b/Podfile @@ -20,6 +20,7 @@ target 'WooCommerce' do pod 'Gridicons', '0.15' pod 'WordPressAuthenticator', '1.0.6' pod 'WordPressShared', '1.0.8' + pod 'WordPressUI', '~> 1.0' # External Libraries diff --git a/Podfile.lock b/Podfile.lock index dde98ce35a9..fc6c792c832 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -55,7 +55,7 @@ PODS: - WordPressShared (~> 1.0) - WordPressUI (~> 1.0) - wpxmlrpc (~> 0.8) - - WordPressKit (1.2.1): + - WordPressKit (1.3.0): - Alamofire (~> 4.7) - CocoaLumberjack (= 3.4.2) - NSObject-SafeExpectations (= 0.0.3) @@ -65,7 +65,7 @@ PODS: - WordPressShared (1.0.8): - CocoaLumberjack (~> 3.4) - FormatterKit/TimeIntervalFormatter (= 1.8.2) - - WordPressUI (1.0.6) + - WordPressUI (1.0.7) - wpxmlrpc (0.8.3) - XLPagerTabStrip (8.0.1) @@ -79,6 +79,7 @@ DEPENDENCIES: - KeychainAccess (~> 3.1) - WordPressAuthenticator (= 1.0.6) - WordPressShared (= 1.0.8) + - WordPressUI (~> 1.0) - XLPagerTabStrip (~> 8.0) SPEC REPOS: @@ -137,12 +138,12 @@ SPEC CHECKSUMS: SVProgressHUD: 1428aafac632c1f86f62aa4243ec12008d7a51d6 UIDeviceIdentifier: a959a6d4f51036b4180dd31fb26483a820f1cc46 WordPressAuthenticator: 56538a229185640b41912c10c3f1891c2cc9bbb9 - WordPressKit: a4a3849684f631a3abf579f6d3f15a32677cbb30 + WordPressKit: b464bb489ee65609371ca4d414c7776801b86592 WordPressShared: 063e1e8b1a7aaf635abf17f091a2d235a068abdc - WordPressUI: af141587ec444f9af753a00605bd0d3f14d8d8a3 + WordPressUI: cfcac4a2a033e3ed5def6504bb8e28447c54423b wpxmlrpc: bfc572f62ce7ee897f6f38b098d2ba08732ecef4 XLPagerTabStrip: c908b17cbf42fcd2598ee1adfc49bae25444d88a -PODFILE CHECKSUM: 579bb6345aecc3f27e78d220e137a749e9ee5f08 +PODFILE CHECKSUM: 4a4cce281d403e847d55dd30fad270674ffdceb5 COCOAPODS: 1.5.3 diff --git a/Storage/Storage.xcodeproj/project.pbxproj b/Storage/Storage.xcodeproj/project.pbxproj index 7945972568c..dc07f67ff6b 100644 --- a/Storage/Storage.xcodeproj/project.pbxproj +++ b/Storage/Storage.xcodeproj/project.pbxproj @@ -15,6 +15,10 @@ 7426A05120F69D00002A4E07 /* OrderCoupon+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7426A04F20F69D00002A4E07 /* OrderCoupon+CoreDataProperties.swift */; }; 7426A05420F69DA4002A4E07 /* OrderItem+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7426A05220F69DA4002A4E07 /* OrderItem+CoreDataClass.swift */; }; 7426A05520F69DA4002A4E07 /* OrderItem+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7426A05320F69DA4002A4E07 /* OrderItem+CoreDataProperties.swift */; }; + 746A9D21214078080013F6FF /* TopEarnerStats+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 746A9D1D214078080013F6FF /* TopEarnerStats+CoreDataClass.swift */; }; + 746A9D22214078080013F6FF /* TopEarnerStats+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 746A9D1E214078080013F6FF /* TopEarnerStats+CoreDataProperties.swift */; }; + 746A9D23214078080013F6FF /* TopEarnerStatsItem+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 746A9D1F214078080013F6FF /* TopEarnerStatsItem+CoreDataClass.swift */; }; + 746A9D24214078080013F6FF /* TopEarnerStatsItem+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 746A9D20214078080013F6FF /* TopEarnerStatsItem+CoreDataProperties.swift */; }; 74B7D6AD20F90CBB002667AC /* OrderNote+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74B7D6AB20F90CBB002667AC /* OrderNote+CoreDataClass.swift */; }; 74B7D6AE20F90CBB002667AC /* OrderNote+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74B7D6AC20F90CBB002667AC /* OrderNote+CoreDataProperties.swift */; }; B505255420EE6914008090F5 /* StorageType+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = B505255320EE6914008090F5 /* StorageType+Extensions.swift */; }; @@ -59,6 +63,12 @@ 7426A04F20F69D00002A4E07 /* OrderCoupon+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OrderCoupon+CoreDataProperties.swift"; sourceTree = ""; }; 7426A05220F69DA4002A4E07 /* OrderItem+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OrderItem+CoreDataClass.swift"; sourceTree = ""; }; 7426A05320F69DA4002A4E07 /* OrderItem+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OrderItem+CoreDataProperties.swift"; sourceTree = ""; }; + 746A9D12214071EB0013F6FF /* MIGRATIONS.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = MIGRATIONS.md; sourceTree = ""; }; + 746A9D14214071F90013F6FF /* Model 2.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Model 2.xcdatamodel"; sourceTree = ""; }; + 746A9D1D214078080013F6FF /* TopEarnerStats+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TopEarnerStats+CoreDataClass.swift"; sourceTree = ""; }; + 746A9D1E214078080013F6FF /* TopEarnerStats+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TopEarnerStats+CoreDataProperties.swift"; sourceTree = ""; }; + 746A9D1F214078080013F6FF /* TopEarnerStatsItem+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TopEarnerStatsItem+CoreDataClass.swift"; sourceTree = ""; }; + 746A9D20214078080013F6FF /* TopEarnerStatsItem+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TopEarnerStatsItem+CoreDataProperties.swift"; sourceTree = ""; }; 74B7D6AB20F90CBB002667AC /* OrderNote+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OrderNote+CoreDataClass.swift"; sourceTree = ""; }; 74B7D6AC20F90CBB002667AC /* OrderNote+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OrderNote+CoreDataProperties.swift"; sourceTree = ""; }; 7C81935EDD982072BBDCC837 /* Pods-Storage.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Storage.release.xcconfig"; path = "../Pods/Target Support Files/Pods-Storage/Pods-Storage.release.xcconfig"; sourceTree = ""; }; @@ -236,6 +246,7 @@ B59E11D720A9CFF3004121A4 /* Model */ = { isa = PBXGroup; children = ( + 746A9D12214071EB0013F6FF /* MIGRATIONS.md */, B59E11D820A9D00C004121A4 /* WooCommerce.xcdatamodeld */, B505F6D920BEEA3200BB1B69 /* Account+CoreDataClass.swift */, B505F6D820BEEA3100BB1B69 /* Account+CoreDataProperties.swift */, @@ -249,6 +260,10 @@ 7426A05320F69DA4002A4E07 /* OrderItem+CoreDataProperties.swift */, 74B7D6AB20F90CBB002667AC /* OrderNote+CoreDataClass.swift */, 74B7D6AC20F90CBB002667AC /* OrderNote+CoreDataProperties.swift */, + 746A9D1D214078080013F6FF /* TopEarnerStats+CoreDataClass.swift */, + 746A9D1E214078080013F6FF /* TopEarnerStats+CoreDataProperties.swift */, + 746A9D1F214078080013F6FF /* TopEarnerStatsItem+CoreDataClass.swift */, + 746A9D20214078080013F6FF /* TopEarnerStatsItem+CoreDataProperties.swift */, ); path = Model; sourceTree = ""; @@ -441,17 +456,21 @@ 7426A05520F69DA4002A4E07 /* OrderItem+CoreDataProperties.swift in Sources */, 7426A04720F68F27002A4E07 /* Order+CoreDataClass.swift in Sources */, B54CA5BD20A4BD3B00F38CD1 /* NSManagedObjectContext+Storage.swift in Sources */, + 746A9D21214078080013F6FF /* TopEarnerStats+CoreDataClass.swift in Sources */, 74B7D6AD20F90CBB002667AC /* OrderNote+CoreDataClass.swift in Sources */, B52B0F7920AA287C00477698 /* StorageManagerType.swift in Sources */, 7426A05420F69DA4002A4E07 /* OrderItem+CoreDataClass.swift in Sources */, B505F6E020BEEA8100BB1B69 /* StorageType.swift in Sources */, + 746A9D22214078080013F6FF /* TopEarnerStats+CoreDataProperties.swift in Sources */, B54CA5C920A4C17800F38CD1 /* NSObject+Storage.swift in Sources */, B505F6DA20BEEA3200BB1B69 /* Account+CoreDataProperties.swift in Sources */, 74B7D6AE20F90CBB002667AC /* OrderNote+CoreDataProperties.swift in Sources */, B505255420EE6914008090F5 /* StorageType+Extensions.swift in Sources */, B52B0F7B20AA28A800477698 /* Object.swift in Sources */, 7426A05020F69D00002A4E07 /* OrderCoupon+CoreDataClass.swift in Sources */, + 746A9D23214078080013F6FF /* TopEarnerStatsItem+CoreDataClass.swift in Sources */, B505F6DE20BEEA4F00BB1B69 /* CoreDataManager.swift in Sources */, + 746A9D24214078080013F6FF /* TopEarnerStatsItem+CoreDataProperties.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -742,9 +761,10 @@ B59E11D820A9D00C004121A4 /* WooCommerce.xcdatamodeld */ = { isa = XCVersionGroup; children = ( + 746A9D14214071F90013F6FF /* Model 2.xcdatamodel */, B59E11D920A9D00C004121A4 /* Model.xcdatamodel */, ); - currentVersion = B59E11D920A9D00C004121A4 /* Model.xcdatamodel */; + currentVersion = 746A9D14214071F90013F6FF /* Model 2.xcdatamodel */; path = WooCommerce.xcdatamodeld; sourceTree = ""; versionGroupType = wrapper.xcdatamodel; diff --git a/Storage/Storage/CoreData/CoreDataManager.swift b/Storage/Storage/CoreData/CoreDataManager.swift index dd3861bfc91..80ac45e4dc6 100644 --- a/Storage/Storage/CoreData/CoreDataManager.swift +++ b/Storage/Storage/CoreData/CoreDataManager.swift @@ -74,6 +74,34 @@ public class CoreDataManager: StorageManagerType { closure(context as StorageType) } } + + /// This method effectively destroys all of the stored data, and generates a blank Persistent Store from scratch. + /// + public func reset() { + let storeCoordinator = persistentContainer.persistentStoreCoordinator + let storeDescriptor = self.storeDescription + let viewContext = persistentContainer.viewContext + + viewContext.performAndWait { + do { + viewContext.reset() + try storeCoordinator.destroyPersistentStore(at: self.storeURL, ofType: storeDescriptor.type, options: storeDescriptor.options) + } catch { + fatalError("☠️ [CoreDataManager] Cannot Destroy persistentStore! \(error)") + } + + storeCoordinator.addPersistentStore(with: storeDescriptor) { (_, error) in + guard let error = error else { + return + } + + fatalError("☠️ [CoreDataManager] Unable to regenerate Persistent Store! \(error)") + } + + NSLog("💣 [CoreDataManager] Stack Destroyed!") + NotificationCenter.default.post(name: .StorageManagerDidResetStorage, object: self) + } + } } diff --git a/Storage/Storage/Model/MIGRATIONS.md b/Storage/Storage/Model/MIGRATIONS.md new file mode 100644 index 00000000000..9230e732c72 --- /dev/null +++ b/Storage/Storage/Model/MIGRATIONS.md @@ -0,0 +1,8 @@ +# Core Data Migrations + +This file documents changes in the extensions data model. Please explain any changes to the data model as well as any custom migrations. + +## Model 2 +- @bummytime 2018-09-05 +- Added new entity: `TopEarnerStats`, to encapsulate all of the top earner stats for a given site & granularity +- Added new entity: `TopEarnerStatsItem`, to encapsulate all the top earner stats for a specific product diff --git a/Storage/Storage/Model/TopEarnerStats+CoreDataClass.swift b/Storage/Storage/Model/TopEarnerStats+CoreDataClass.swift new file mode 100644 index 00000000000..c5d2ba3e48f --- /dev/null +++ b/Storage/Storage/Model/TopEarnerStats+CoreDataClass.swift @@ -0,0 +1,7 @@ +import Foundation +import CoreData + +@objc(TopEarnerStats) +public class TopEarnerStats: NSManagedObject { + +} diff --git a/Storage/Storage/Model/TopEarnerStats+CoreDataProperties.swift b/Storage/Storage/Model/TopEarnerStats+CoreDataProperties.swift new file mode 100644 index 00000000000..2af7e2875ab --- /dev/null +++ b/Storage/Storage/Model/TopEarnerStats+CoreDataProperties.swift @@ -0,0 +1,31 @@ +import Foundation +import CoreData + + +extension TopEarnerStats { + + @nonobjc public class func fetchRequest() -> NSFetchRequest { + return NSFetchRequest(entityName: "TopEarnerStats") + } + + @NSManaged public var granularity: String + @NSManaged public var limit: String + @NSManaged public var date: String + @NSManaged public var items: Set? +} + +// MARK: Generated accessors for items +extension TopEarnerStats { + + @objc(addItemsObject:) + @NSManaged public func addToItems(_ value: TopEarnerStatsItem) + + @objc(removeItemsObject:) + @NSManaged public func removeFromItems(_ value: TopEarnerStatsItem) + + @objc(addItems:) + @NSManaged public func addToItems(_ values: NSSet) + + @objc(removeItems:) + @NSManaged public func removeFromItems(_ values: NSSet) +} diff --git a/Storage/Storage/Model/TopEarnerStatsItem+CoreDataClass.swift b/Storage/Storage/Model/TopEarnerStatsItem+CoreDataClass.swift new file mode 100644 index 00000000000..74624a87b39 --- /dev/null +++ b/Storage/Storage/Model/TopEarnerStatsItem+CoreDataClass.swift @@ -0,0 +1,7 @@ +import Foundation +import CoreData + +@objc(TopEarnerStatsItem) +public class TopEarnerStatsItem: NSManagedObject { + +} diff --git a/Storage/Storage/Model/TopEarnerStatsItem+CoreDataProperties.swift b/Storage/Storage/Model/TopEarnerStatsItem+CoreDataProperties.swift new file mode 100644 index 00000000000..06a790c631b --- /dev/null +++ b/Storage/Storage/Model/TopEarnerStatsItem+CoreDataProperties.swift @@ -0,0 +1,19 @@ +import Foundation +import CoreData + + +extension TopEarnerStatsItem { + + @nonobjc public class func fetchRequest() -> NSFetchRequest { + return NSFetchRequest(entityName: "TopEarnerStatsItem") + } + + @NSManaged public var productID: Int64 + @NSManaged public var productName: String? + @NSManaged public var quantity: Int16 + @NSManaged public var price: Double + @NSManaged public var total: Double + @NSManaged public var currency: String? + @NSManaged public var imageUrl: String? + @NSManaged public var stats: TopEarnerStats? +} diff --git a/Storage/Storage/Model/WooCommerce.xcdatamodeld/.xccurrentversion b/Storage/Storage/Model/WooCommerce.xcdatamodeld/.xccurrentversion new file mode 100644 index 00000000000..ffe2690fe5a --- /dev/null +++ b/Storage/Storage/Model/WooCommerce.xcdatamodeld/.xccurrentversion @@ -0,0 +1,8 @@ + + + + + _XCCurrentVersionName + Model 2.xcdatamodel + + diff --git a/Storage/Storage/Model/WooCommerce.xcdatamodeld/Model 2.xcdatamodel/contents b/Storage/Storage/Model/WooCommerce.xcdatamodeld/Model 2.xcdatamodel/contents new file mode 100644 index 00000000000..22943df1b52 --- /dev/null +++ b/Storage/Storage/Model/WooCommerce.xcdatamodeld/Model 2.xcdatamodel/contents @@ -0,0 +1,117 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Storage/Storage/Protocols/StorageManagerType.swift b/Storage/Storage/Protocols/StorageManagerType.swift index 9b0e43c49e9..258715e6f35 100644 --- a/Storage/Storage/Protocols/StorageManagerType.swift +++ b/Storage/Storage/Protocols/StorageManagerType.swift @@ -1,6 +1,13 @@ import Foundation +// MARK: - StorageManagerType Notifications +// +public extension NSNotification.Name { + public static let StorageManagerDidResetStorage = NSNotification.Name(rawValue: "StorageManagerDidResetStorage") +} + + /// Defines the methods and properties implemented by any concrete StorageManager implementation. /// public protocol StorageManagerType { @@ -13,4 +20,9 @@ public protocol StorageManagerType { /// Note that you must NEVER use the viewStorage within the backgroundClosure. /// func performBackgroundTask(_ closure: @escaping (StorageType) -> Void) + + /// This method is expected to destroy all persisted data. A notification of type `StorageManagerDidResetStorage` should get + /// posted. + /// + func reset() } diff --git a/Storage/Storage/Tools/StorageType+Extensions.swift b/Storage/Storage/Tools/StorageType+Extensions.swift index e5ba2a1253f..42ae822fee7 100644 --- a/Storage/Storage/Tools/StorageType+Extensions.swift +++ b/Storage/Storage/Tools/StorageType+Extensions.swift @@ -46,4 +46,18 @@ public extension StorageType { let predicate = NSPredicate(format: "noteID = %ld", noteID) return firstObject(ofType: OrderNote.self, matching: predicate) } + + /// Retrieves the Stored TopEarnerStats. + /// + public func loadTopEarnerStats(date: String, granularity: String) -> TopEarnerStats? { + let predicate = NSPredicate(format: "date ==[c] %@ AND granularity ==[c] %@", date, granularity) + return firstObject(ofType: TopEarnerStats.self, matching: predicate) + } + + /// Retrieves the Stored TopEarnerStats Item. + /// + public func loadTopEarnerStatsItem(productID: Int) -> TopEarnerStatsItem? { + let predicate = NSPredicate(format: "productID = %ld", productID) + return firstObject(ofType: TopEarnerStatsItem.self, matching: predicate) + } } diff --git a/WooCommerce/Classes/AppDelegate.swift b/WooCommerce/Classes/AppDelegate.swift index 0f04f5d508d..2cdea92b080 100644 --- a/WooCommerce/Classes/AppDelegate.swift +++ b/WooCommerce/Classes/AppDelegate.swift @@ -40,6 +40,11 @@ class AppDelegate: UIResponder, UIApplicationDelegate { /// let storageManager = CoreDataManager(name: WooConstants.databaseStackName) + /// Tab Bar Controller + /// + var tabBarController: MainTabBarController? { + return window?.rootViewController as? MainTabBarController + } // MARK: - AppDelegate Methods diff --git a/WooCommerce/Classes/Authentication/Epilogue/AccountHeaderView.xib b/WooCommerce/Classes/Authentication/Epilogue/AccountHeaderView.xib index 144b918e472..27a0fe313d1 100644 --- a/WooCommerce/Classes/Authentication/Epilogue/AccountHeaderView.xib +++ b/WooCommerce/Classes/Authentication/Epilogue/AccountHeaderView.xib @@ -22,13 +22,13 @@ -