diff --git a/Storage/Storage/Tools/StorageType+Extensions.swift b/Storage/Storage/Tools/StorageType+Extensions.swift index e8925f18845..c542061911f 100644 --- a/Storage/Storage/Tools/StorageType+Extensions.swift +++ b/Storage/Storage/Tools/StorageType+Extensions.swift @@ -149,6 +149,13 @@ public extension StorageType { return firstObject(ofType: OrderStatsV4Interval.self, matching: predicate) } + /// Retrieves the Stored SiteSummaryStats. + /// + func loadSiteSummaryStats(date: String, period: String) -> SiteSummaryStats? { + let predicate = \SiteSummaryStats.date =~ date && \SiteSummaryStats.period =~ period + return firstObject(ofType: SiteSummaryStats.self, matching: predicate) + } + // MARK: - Order Statuses /// Retrieves all of the Stores OrderStatuses for the provided siteID. diff --git a/WooCommerce/Classes/ViewRelated/Dashboard/Analytics Hub/AnalyticsHubViewModel.swift b/WooCommerce/Classes/ViewRelated/Dashboard/Analytics Hub/AnalyticsHubViewModel.swift index 915019cbeea..7645a6f908d 100644 --- a/WooCommerce/Classes/ViewRelated/Dashboard/Analytics Hub/AnalyticsHubViewModel.swift +++ b/WooCommerce/Classes/ViewRelated/Dashboard/Analytics Hub/AnalyticsHubViewModel.swift @@ -215,7 +215,8 @@ private extension AnalyticsHubViewModel { let action = StatsActionV4.retrieveSiteSummaryStats(siteID: siteID, period: period, quantity: timeRangeSelectionType.quantity, - latestDateToInclude: latestDateToInclude) { result in + latestDateToInclude: latestDateToInclude, + saveInStorage: false) { result in continuation.resume(with: result) } stores.dispatch(action) diff --git a/WooCommerce/WooCommerceTests/ViewRelated/Dashboard/Analytics Hub/AnalyticsHubViewModelTests.swift b/WooCommerce/WooCommerceTests/ViewRelated/Dashboard/Analytics Hub/AnalyticsHubViewModelTests.swift index 6e51f4ebb96..b1ef6f912c9 100644 --- a/WooCommerce/WooCommerceTests/ViewRelated/Dashboard/Analytics Hub/AnalyticsHubViewModelTests.swift +++ b/WooCommerce/WooCommerceTests/ViewRelated/Dashboard/Analytics Hub/AnalyticsHubViewModelTests.swift @@ -26,7 +26,7 @@ final class AnalyticsHubViewModelTests: XCTestCase { case let .retrieveTopEarnerStats(_, _, _, _, _, _, _, completion): let topEarners = TopEarnerStats.fake().copy(items: [.fake()]) completion(.success(topEarners)) - case let .retrieveSiteSummaryStats(_, _, _, _, completion): + case let .retrieveSiteSummaryStats(_, _, _, _, _, completion): let siteStats = SiteSummaryStats.fake().copy(visitors: 30, views: 53) completion(.success(siteStats)) default: @@ -61,7 +61,7 @@ final class AnalyticsHubViewModelTests: XCTestCase { completion(.failure(NSError(domain: "Test", code: 1))) case let .retrieveTopEarnerStats(_, _, _, _, _, _, _, completion): completion(.failure(NSError(domain: "Test", code: 1))) - case let .retrieveSiteSummaryStats(_, _, _, _, completion): + case let .retrieveSiteSummaryStats(_, _, _, _, _, completion): completion(.failure(NSError(domain: "Test", code: 1))) default: break @@ -99,7 +99,7 @@ final class AnalyticsHubViewModelTests: XCTestCase { case let .retrieveTopEarnerStats(_, _, _, _, _, _, _, completion): let topEarners = TopEarnerStats.fake().copy(items: [.fake()]) completion(.success(topEarners)) - case let .retrieveSiteSummaryStats(_, _, _, _, completion): + case let .retrieveSiteSummaryStats(_, _, _, _, _, completion): let siteStats = SiteSummaryStats.fake() loadingSessionsCard = vm.sessionsCard completion(.success(siteStats)) diff --git a/Yosemite/Yosemite.xcodeproj/project.pbxproj b/Yosemite/Yosemite.xcodeproj/project.pbxproj index 084ddab2690..6a0192f703d 100644 --- a/Yosemite/Yosemite.xcodeproj/project.pbxproj +++ b/Yosemite/Yosemite.xcodeproj/project.pbxproj @@ -334,6 +334,7 @@ CC2C036C262F316600928C9C /* ShippingLabelAccountSettings+ReadOnlyConvertible.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC2C036B262F316600928C9C /* ShippingLabelAccountSettings+ReadOnlyConvertible.swift */; }; CC2C0372262F32D800928C9C /* ShippingLabelPaymentMethod+ReadonlyConvertible.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC2C0371262F32D800928C9C /* ShippingLabelPaymentMethod+ReadonlyConvertible.swift */; }; CC6A054628773F75002C144E /* OrderMetaData+ReadOnlyConvertible.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC6A054528773F75002C144E /* OrderMetaData+ReadOnlyConvertible.swift */; }; + CC80E40C294B454A00D5FF45 /* SiteSummaryStats+ReadOnlyConvertible.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC80E40B294B454A00D5FF45 /* SiteSummaryStats+ReadOnlyConvertible.swift */; }; CE01014F2368C41600783459 /* Refund+ReadOnlyType.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE01014E2368C41600783459 /* Refund+ReadOnlyType.swift */; }; CE0DB6C0233EB3F300A27E7A /* OrderRefundCondensed+ReadOnlyConvertible.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE0DB6BF233EB3F300A27E7A /* OrderRefundCondensed+ReadOnlyConvertible.swift */; }; CE12FBDB221F406100C59248 /* OrderStatus+ReadOnlyConvertible.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE12FBDA221F406100C59248 /* OrderStatus+ReadOnlyConvertible.swift */; }; @@ -768,6 +769,7 @@ CC2C036B262F316600928C9C /* ShippingLabelAccountSettings+ReadOnlyConvertible.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ShippingLabelAccountSettings+ReadOnlyConvertible.swift"; sourceTree = ""; }; CC2C0371262F32D800928C9C /* ShippingLabelPaymentMethod+ReadonlyConvertible.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ShippingLabelPaymentMethod+ReadonlyConvertible.swift"; sourceTree = ""; }; CC6A054528773F75002C144E /* OrderMetaData+ReadOnlyConvertible.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OrderMetaData+ReadOnlyConvertible.swift"; sourceTree = ""; }; + CC80E40B294B454A00D5FF45 /* SiteSummaryStats+ReadOnlyConvertible.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SiteSummaryStats+ReadOnlyConvertible.swift"; sourceTree = ""; }; CE01014E2368C41600783459 /* Refund+ReadOnlyType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Refund+ReadOnlyType.swift"; sourceTree = ""; }; CE0DB6BF233EB3F300A27E7A /* OrderRefundCondensed+ReadOnlyConvertible.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OrderRefundCondensed+ReadOnlyConvertible.swift"; sourceTree = ""; }; CE12FBDA221F406100C59248 /* OrderStatus+ReadOnlyConvertible.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OrderStatus+ReadOnlyConvertible.swift"; sourceTree = ""; }; @@ -1293,6 +1295,7 @@ 2618707B2540B6A4006522A1 /* ShippingLineTax+ReadOnlyConvertible.swift */, B505254B20EE6491008090F5 /* Site+ReadOnlyConvertible.swift */, 7492FAD8217FAD1000ED2C69 /* SiteSetting+ReadOnlyConvertible.swift */, + CC80E40B294B454A00D5FF45 /* SiteSummaryStats+ReadOnlyConvertible.swift */, 744A3216216D55F80051439B /* SiteVisitStats+ReadOnlyConvertible.swift */, 744A3217216D55F80051439B /* SiteVisitStatsItem+ReadOnlyConvertible.swift */, 45E4620F2684C63700011BF2 /* StateOfACountry+ReadOnlyConvertible.swift */, @@ -1978,6 +1981,7 @@ 03FBDA222631521100ACE257 /* CouponAction.swift in Sources */, CE4FD4562350FD4800A16B31 /* Refund+ReadOnlyConvertible.swift in Sources */, CE3B7AD92229C3570050FE4B /* OrderStatus+ReadOnlyType.swift in Sources */, + CC80E40C294B454A00D5FF45 /* SiteSummaryStats+ReadOnlyConvertible.swift in Sources */, 026CF62C237D92DC009563D4 /* ProductVariationAttribute+ReadOnlyConvertible.swift in Sources */, 247CE7AB2582DB9300F9D9D1 /* String+Extensions.swift in Sources */, FE28F6F026844231004465C7 /* UserStore.swift in Sources */, diff --git a/Yosemite/Yosemite/Actions/StatsActionV4.swift b/Yosemite/Yosemite/Actions/StatsActionV4.swift index b310688c492..b096fb06a8f 100644 --- a/Yosemite/Yosemite/Actions/StatsActionV4.swift +++ b/Yosemite/Yosemite/Actions/StatsActionV4.swift @@ -49,11 +49,13 @@ public enum StatsActionV4: Action { saveInStorage: Bool, onCompletion: (Result) -> Void) - /// Retrieves the site summary stats for the provided site ID, period(s), and date, without saving them to the Storage layer. + /// Retrieves the site summary stats for the provided site ID, period(s), and date. + /// Conditionally saves them to storage, if a single period is retrieved. /// case retrieveSiteSummaryStats(siteID: Int64, period: StatGranularity, quantity: Int, latestDateToInclude: Date, + saveInStorage: Bool, onCompletion: (Result) -> Void) } diff --git a/Yosemite/Yosemite/Model/Storage/SiteSummaryStats+ReadOnlyConvertible.swift b/Yosemite/Yosemite/Model/Storage/SiteSummaryStats+ReadOnlyConvertible.swift new file mode 100644 index 00000000000..ca8ff841201 --- /dev/null +++ b/Yosemite/Yosemite/Model/Storage/SiteSummaryStats+ReadOnlyConvertible.swift @@ -0,0 +1,28 @@ +import Foundation +import Storage + + +// MARK: - Storage.SiteSummaryStats: ReadOnlyConvertible +// +extension Storage.SiteSummaryStats: ReadOnlyConvertible { + + /// Updates the Storage.SiteSummaryStats with the ReadOnly. + /// + public func update(with stats: Yosemite.SiteSummaryStats) { + siteID = stats.siteID + date = stats.date + period = stats.period.rawValue + visitors = Int64(stats.visitors) + views = Int64(stats.views) + } + + /// Returns a ReadOnly version of the receiver. + /// + public func toReadOnly() -> Yosemite.SiteSummaryStats { + SiteSummaryStats(siteID: siteID, + date: date, + period: StatGranularity(rawValue: period) ?? .day, + visitors: Int(visitors), + views: Int(views)) + } +} diff --git a/Yosemite/Yosemite/Stores/StatsStoreV4.swift b/Yosemite/Yosemite/Stores/StatsStoreV4.swift index c7500a2eece..b31e2d617b1 100644 --- a/Yosemite/Yosemite/Stores/StatsStoreV4.swift +++ b/Yosemite/Yosemite/Stores/StatsStoreV4.swift @@ -94,11 +94,13 @@ public final class StatsStoreV4: Store { let period, let quantity, let latestDateToInclude, + let saveInStorage, let onCompletion): retrieveSiteSummaryStats(siteID: siteID, period: period, quantity: quantity, latestDateToInclude: latestDateToInclude, + saveInStorage: saveInStorage, onCompletion: onCompletion) } } @@ -189,19 +191,24 @@ private extension StatsStoreV4 { } } - /// Retrieves the site summary stats for the provided site ID, period(s), and date, without saving them to the Storage layer. + /// Retrieves the site summary stats for the provided site ID, period(s), and date. + /// Conditionally saves them to storage, if a single period is retrieved. /// func retrieveSiteSummaryStats(siteID: Int64, period: StatGranularity, quantity: Int, latestDateToInclude: Date, + saveInStorage: Bool, onCompletion: @escaping (Result) -> Void) { if quantity == 1 { siteStatsRemote.loadSiteSummaryStats(for: siteID, period: period, - includingDate: latestDateToInclude) { result in + includingDate: latestDateToInclude) { [weak self] result in switch result { case .success(let siteSummaryStats): + if saveInStorage { + self?.upsertStoredSiteSummaryStats(readOnlyStats: siteSummaryStats) + } onCompletion(.success(siteSummaryStats)) case .failure(let error): onCompletion(.failure(SiteStatsStoreError(error: error))) @@ -486,6 +493,20 @@ extension StatsStoreV4 { } } +// MARK: Site summary stats +extension StatsStoreV4 { + /// Updates (OR Inserts) the specified ReadOnly SiteSummaryStats Entity into the Storage Layer. + /// + func upsertStoredSiteSummaryStats(readOnlyStats: Networking.SiteSummaryStats) { + assert(Thread.isMainThread) + + let storage = storageManager.viewStorage + let storageSiteSummaryStats = storage.loadSiteSummaryStats(date: readOnlyStats.date, period: readOnlyStats.period.rawValue) + ?? storage.insertNewObject(ofType: Storage.SiteSummaryStats.self) + storageSiteSummaryStats.update(with: readOnlyStats) + storage.saveIfNeeded() + } +} // MARK: Convert Leaderboard into TopEarnerStats // diff --git a/Yosemite/YosemiteTests/Stores/StatsStoreV4Tests.swift b/Yosemite/YosemiteTests/Stores/StatsStoreV4Tests.swift index 1a72a686a53..ae9209034bf 100644 --- a/Yosemite/YosemiteTests/Stores/StatsStoreV4Tests.swift +++ b/Yosemite/YosemiteTests/Stores/StatsStoreV4Tests.swift @@ -518,7 +518,8 @@ final class StatsStoreV4Tests: XCTestCase { let action = StatsActionV4.retrieveSiteSummaryStats(siteID: self.sampleSiteID, period: .day, quantity: 1, - latestDateToInclude: DateFormatter.dateFromString(with: "2022-12-09T17:06:55")) { result in + latestDateToInclude: DateFormatter.dateFromString(with: "2022-12-09T17:06:55"), + saveInStorage: false) { result in promise(result) } store.onAction(action) @@ -541,7 +542,8 @@ final class StatsStoreV4Tests: XCTestCase { let action = StatsActionV4.retrieveSiteSummaryStats(siteID: self.sampleSiteID, period: .month, quantity: 3, - latestDateToInclude: DateFormatter.dateFromString(with: "2022-12-31T17:06:55")) { _ in + latestDateToInclude: DateFormatter.dateFromString(with: "2022-12-31T17:06:55"), + saveInStorage: false) { _ in promise(()) } store.onAction(action) @@ -567,7 +569,8 @@ final class StatsStoreV4Tests: XCTestCase { let action = StatsActionV4.retrieveSiteSummaryStats(siteID: self.sampleSiteID, period: .month, quantity: 3, - latestDateToInclude: DateFormatter.dateFromString(with: "2022-12-31T17:06:55")) { result in + latestDateToInclude: DateFormatter.dateFromString(with: "2022-12-31T17:06:55"), + saveInStorage: false) { result in promise(result) } store.onAction(action) @@ -591,7 +594,8 @@ final class StatsStoreV4Tests: XCTestCase { let action = StatsActionV4.retrieveSiteSummaryStats(siteID: self.sampleSiteID, period: .day, quantity: 1, - latestDateToInclude: DateFormatter.dateFromString(with: "2022-12-09T17:06:55")) { result in + latestDateToInclude: DateFormatter.dateFromString(with: "2022-12-09T17:06:55"), + saveInStorage: false) { result in promise(result) } store.onAction(action) @@ -612,7 +616,8 @@ final class StatsStoreV4Tests: XCTestCase { let action = StatsActionV4.retrieveSiteSummaryStats(siteID: self.sampleSiteID, period: .day, quantity: 1, - latestDateToInclude: DateFormatter.dateFromString(with: "2022-12-09T17:06:55")) { result in + latestDateToInclude: DateFormatter.dateFromString(with: "2022-12-09T17:06:55"), + saveInStorage: false) { result in promise(result) } store.onAction(action) @@ -621,6 +626,50 @@ final class StatsStoreV4Tests: XCTestCase { // Then XCTAssertTrue(result.isFailure) } + + /// Verifies that `StatsActionV4.retrieveSiteSummaryStats` effectively persists any retrieved SiteSummaryStats. + /// + func test_retrieveSiteSummaryStats_effectively_persists_retrieved_stats() { + // Given + let store = StatsStoreV4(dispatcher: dispatcher, storageManager: storageManager, network: network) + network.simulateResponse(requestUrlSuffix: "sites/\(sampleSiteID)/stats/summary/", filename: "site-summary-stats") + XCTAssertEqual(viewStorage.countObjects(ofType: Storage.SiteSummaryStats.self), 0) + + // When + let result: Result = waitFor { promise in + let action = StatsActionV4.retrieveSiteSummaryStats(siteID: self.sampleSiteID, + period: .day, + quantity: 1, + latestDateToInclude: DateFormatter.dateFromString(with: "2022-12-09T17:06:55"), + saveInStorage: true) { result in + promise(result) + } + store.onAction(action) + } + + // Then + XCTAssertTrue(result.isSuccess) + XCTAssertEqual(viewStorage.countObjects(ofType: Storage.SiteSummaryStats.self), 1) + + let readOnlySiteSummaryStats = viewStorage.firstObject(ofType: Storage.SiteSummaryStats.self)?.toReadOnly() + XCTAssertEqual(readOnlySiteSummaryStats, sampleSiteSummaryStats()) + } + + /// Verifies that `upsertStoredSiteSummaryStats` does not produce duplicate entries. + /// + func test_upsertStoredSiteSummaryStats_effectively_updates_preexistant_SiteSummaryStats() { + let statsStore = StatsStoreV4(dispatcher: dispatcher, storageManager: storageManager, network: network) + + XCTAssertNil(viewStorage.loadSiteSummaryStats(date: "2022-12-09", period: StatGranularity.day.rawValue)) + statsStore.upsertStoredSiteSummaryStats(readOnlyStats: sampleSiteSummaryStats()) + XCTAssertEqual(viewStorage.countObjects(ofType: Storage.SiteSummaryStats.self), 1) + statsStore.upsertStoredSiteSummaryStats(readOnlyStats: sampleSiteSummaryStatsMutated()) + XCTAssertEqual(viewStorage.countObjects(ofType: Storage.SiteSummaryStats.self), 1) + + let expectedSiteSummaryStats = sampleSiteSummaryStatsMutated() + let storageSiteSummaryStats = viewStorage.loadSiteSummaryStats(date: "2022-12-09", period: StatGranularity.day.rawValue) + XCTAssertEqual(storageSiteSummaryStats?.toReadOnly(), expectedSiteSummaryStats) + } } @@ -812,6 +861,14 @@ private extension StatsStoreV4Tests { views: 123) } + func sampleSiteSummaryStatsMutated() -> Networking.SiteSummaryStats { + return SiteSummaryStats(siteID: sampleSiteID, + date: "2022-12-09", + period: .day, + visitors: 15, + views: 127) + } + func sampleSiteSummaryStatsQuarter() -> Networking.SiteSummaryStats { return SiteSummaryStats(siteID: sampleSiteID, date: "2022-12-09",