Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Networking/Networking.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -552,6 +552,7 @@
CC0786C7267BB10700BA9AC1 /* shipping-label-status-success.json in Resources */ = {isa = PBXBuildFile; fileRef = CC0786C6267BB10700BA9AC1 /* shipping-label-status-success.json */; };
CC0786C9267BB32800BA9AC1 /* ShippingLabelStatusMapperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0786C8267BB32800BA9AC1 /* ShippingLabelStatusMapperTests.swift */; };
CC6A1FF5270E042200F6AF4A /* OrderMetaData.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC6A1FF4270E042200F6AF4A /* OrderMetaData.swift */; };
CC80E3F92948C8BC00D5FF45 /* site-visits-quarter.json in Resources */ = {isa = PBXBuildFile; fileRef = CC80E3F82948C8BC00D5FF45 /* site-visits-quarter.json */; };
CC851D0625E51ADF00249E9C /* Decimal+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC851D0525E51ADF00249E9C /* Decimal+Extensions.swift */; };
CC851D1425E52AB500249E9C /* Decimal+ExtensionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC851D1325E52AB500249E9C /* Decimal+ExtensionsTests.swift */; };
CC9A24A32641BCF2005DE56E /* ShippingLabelCreationEligibilityResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC9A24A22641BCF2005DE56E /* ShippingLabelCreationEligibilityResponse.swift */; };
Expand Down Expand Up @@ -1314,6 +1315,7 @@
CC0786C6267BB10700BA9AC1 /* shipping-label-status-success.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "shipping-label-status-success.json"; sourceTree = "<group>"; };
CC0786C8267BB32800BA9AC1 /* ShippingLabelStatusMapperTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShippingLabelStatusMapperTests.swift; sourceTree = "<group>"; };
CC6A1FF4270E042200F6AF4A /* OrderMetaData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrderMetaData.swift; sourceTree = "<group>"; };
CC80E3F82948C8BC00D5FF45 /* site-visits-quarter.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "site-visits-quarter.json"; sourceTree = "<group>"; };
CC851D0525E51ADF00249E9C /* Decimal+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Decimal+Extensions.swift"; sourceTree = "<group>"; };
CC851D1325E52AB500249E9C /* Decimal+ExtensionsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Decimal+ExtensionsTests.swift"; sourceTree = "<group>"; };
CC9A24A22641BCF2005DE56E /* ShippingLabelCreationEligibilityResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShippingLabelCreationEligibilityResponse.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -2213,6 +2215,7 @@
74A1D25F211898F000931DFA /* site-visits-day.json */,
74A1D260211898F000931DFA /* site-visits-week.json */,
74A1D261211898F000931DFA /* site-visits-month.json */,
CC80E3F82948C8BC00D5FF45 /* site-visits-quarter.json */,
74A1D262211898F000931DFA /* site-visits-year.json */,
743BF8BD21191B63008A9D87 /* site-visits.json */,
74E30950216E8DCE00ABCE4C /* site-visits-alt.json */,
Expand Down Expand Up @@ -2942,6 +2945,7 @@
45AB8B2024AB3E1F00B5B36E /* product-tags-empty.json in Resources */,
0359EA2127AAE58C0048DE2D /* wcpay-charge-card-present.json in Resources */,
451274A625276C82009911FF /* product-variation.json in Resources */,
CC80E3F92948C8BC00D5FF45 /* site-visits-quarter.json in Resources */,
020D07C223D858BB00FD9580 /* media-upload.json in Resources */,
31A451D127863A2E00FE81AA /* stripe-account-rejected-other.json in Resources */,
74ABA1CA213F19FE00FFAD30 /* top-performers-year.json in Resources */,
Expand Down
2 changes: 1 addition & 1 deletion Networking/Networking/Model/Stats/SiteSummaryStats.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import Codegen

/// Represents site summary stats for a specific period.
///
public struct SiteSummaryStats: Decodable, GeneratedCopiable, GeneratedFakeable {
public struct SiteSummaryStats: Decodable, Equatable, GeneratedCopiable, GeneratedFakeable {
public let siteID: Int64
public let date: String
public let period: StatGranularity
Expand Down
26 changes: 26 additions & 0 deletions Networking/NetworkingTests/Responses/site-visits-quarter.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"date": "2022-12-09",
"unit": "month",
"fields": [
"period",
"views",
"visitors"
],
"data": [
[
"2022-10-01",
448,
224
],
[
"2022-11-01",
14,
7
],
[
"2022-12-01",
24,
12
]
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -483,7 +483,7 @@ private extension StoreStatsAndTopPerformersViewController {
}
}

func handleSiteVisitStatsStoreError(error: SiteVisitStatsStoreError) {
func handleSiteStatsStoreError(error: SiteStatsStoreError) {
switch error {
case .noPermission:
updateSiteVisitors(mode: .hidden)
Expand All @@ -501,8 +501,8 @@ private extension StoreStatsAndTopPerformersViewController {

private func handleSyncError(error: Error) {
switch error {
case let siteVisitStatsStoreError as SiteVisitStatsStoreError:
handleSiteVisitStatsStoreError(error: siteVisitStatsStoreError)
case let siteStatsStoreError as SiteStatsStoreError:
handleSiteStatsStoreError(error: siteStatsStoreError)
default:
displaySyncingError()
}
Expand Down
8 changes: 8 additions & 0 deletions Yosemite/Yosemite/Actions/StatsActionV4.swift
Original file line number Diff line number Diff line change
Expand Up @@ -48,4 +48,12 @@ public enum StatsActionV4: Action {
forceRefresh: Bool,
saveInStorage: Bool,
onCompletion: (Result<TopEarnerStats, Error>) -> Void)

/// Retrieves the site summary stats for the provided site ID, period(s), and date, without saving them to the Storage layer.
///
case retrieveSiteSummaryStats(siteID: Int64,
period: StatGranularity,
quantity: Int,
latestDateToInclude: Date,
onCompletion: (Result<SiteSummaryStats, Error>) -> Void)
}
63 changes: 58 additions & 5 deletions Yosemite/Yosemite/Stores/StatsStoreV4.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@ import WooFoundation
// MARK: - StatsStoreV4
//
public final class StatsStoreV4: Store {
private let siteVisitStatsRemote: SiteStatsRemote
private let siteStatsRemote: SiteStatsRemote
private let leaderboardsRemote: LeaderboardsRemote
private let orderStatsRemote: OrderStatsRemoteV4
private let productsRemote: ProductsRemote

public override init(dispatcher: Dispatcher, storageManager: StorageManagerType, network: Network) {
self.siteVisitStatsRemote = SiteStatsRemote(network: network)
self.siteStatsRemote = SiteStatsRemote(network: network)
self.leaderboardsRemote = LeaderboardsRemote(network: network)
self.orderStatsRemote = OrderStatsRemoteV4(network: network)
self.productsRemote = ProductsRemote(network: network)
Expand Down Expand Up @@ -90,6 +90,16 @@ public final class StatsStoreV4: Store {
forceRefresh: forceRefresh,
saveInStorage: saveInStorage,
onCompletion: onCompletion)
case .retrieveSiteSummaryStats(let siteID,
let period,
let quantity,
let latestDateToInclude,
let onCompletion):
retrieveSiteSummaryStats(siteID: siteID,
period: period,
quantity: quantity,
latestDateToInclude: latestDateToInclude,
onCompletion: onCompletion)
}
}
}
Expand Down Expand Up @@ -164,7 +174,7 @@ private extension StatsStoreV4 {

let quantity = timeRange.siteVisitStatsQuantity(date: latestDateToInclude, siteTimezone: siteTimezone)

siteVisitStatsRemote.loadSiteVisitorStats(for: siteID,
siteStatsRemote.loadSiteVisitorStats(for: siteID,
siteTimezone: siteTimezone,
unit: timeRange.siteVisitStatsGranularity,
latestDateToInclude: latestDateToInclude,
Expand All @@ -174,7 +184,50 @@ private extension StatsStoreV4 {
self?.upsertStoredSiteVisitStats(readOnlyStats: siteVisitStats, timeRange: timeRange)
onCompletion(.success(()))
case .failure(let error):
onCompletion(.failure(SiteVisitStatsStoreError(error: error)))
onCompletion(.failure(SiteStatsStoreError(error: error)))
}
}
}

/// Retrieves the site summary stats for the provided site ID, period(s), and date, without saving them to the Storage layer.
///
func retrieveSiteSummaryStats(siteID: Int64,
period: StatGranularity,
quantity: Int,
latestDateToInclude: Date,
onCompletion: @escaping (Result<SiteSummaryStats, Error>) -> Void) {
if quantity == 1 {
siteStatsRemote.loadSiteSummaryStats(for: siteID,
period: period,
includingDate: latestDateToInclude) { result in
switch result {
case .success(let siteSummaryStats):
onCompletion(.success(siteSummaryStats))
case .failure(let error):
onCompletion(.failure(SiteStatsStoreError(error: error)))
}
}
} else {
// If we are not fetching stats for a single period, we need to summarize the stats manually.
// The remote summary stats endpoint only retrieves visitor stats for a single period.
// We should only do this for periods of a month or greater; otherwise the visitor total is inaccurate.
// See: pe5uwI-5c-p2
siteStatsRemote.loadSiteVisitorStats(for: siteID,
unit: period,
latestDateToInclude: latestDateToInclude,
quantity: quantity) { result in
switch result {
case .success(let siteVisitStats):
let totalViews = siteVisitStats.items?.map({ $0.views }).reduce(0, +) ?? 0
let summaryStats = SiteSummaryStats(siteID: siteID,
date: siteVisitStats.date,
period: siteVisitStats.granularity,
visitors: siteVisitStats.totalVisitors,
views: totalViews)
onCompletion(.success(summaryStats))
case .failure(let error):
onCompletion(.failure(SiteStatsStoreError(error: error)))
}
}
}
}
Expand Down Expand Up @@ -571,7 +624,7 @@ public enum StatsStoreV4Error: Error {
/// - statsModuleDisabled: Jetpack site stats module is disabled for the site.
/// - unknown: other error cases.
///
public enum SiteVisitStatsStoreError: Error {
public enum SiteStatsStoreError: Error {
case statsModuleDisabled
case noPermission
case unknown
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,28 @@ import XCTest
@testable import Yosemite
@testable import Networking

class SiteVisitStatsStoreErrorTests: XCTestCase {
class SiteStatsStoreErrorTests: XCTestCase {
func testNoPermissionError() {
let remoteError = DotcomError.noStatsPermission
let error = SiteVisitStatsStoreError(error: remoteError)
let error = SiteStatsStoreError(error: remoteError)
XCTAssertEqual(error, .noPermission)
}

func testStatsModuleDisabledError() {
let remoteError = DotcomError.statsModuleDisabled
let error = SiteVisitStatsStoreError(error: remoteError)
let error = SiteStatsStoreError(error: remoteError)
XCTAssertEqual(error, .statsModuleDisabled)
}

func testOtherDotcomError() {
let remoteError = DotcomError.unknown(code: "invalid_blog", message: "This blog does not have Jetpack connected")
let error = SiteVisitStatsStoreError(error: remoteError)
let error = SiteStatsStoreError(error: remoteError)
XCTAssertEqual(error, .unknown)
}

func testNonDotcomRemoteError() {
let remoteError = NSError(domain: "Woo", code: 404, userInfo: nil)
let error = SiteVisitStatsStoreError(error: remoteError)
let error = SiteStatsStoreError(error: remoteError)
XCTAssertEqual(error, .unknown)
}
}
136 changes: 136 additions & 0 deletions Yosemite/YosemiteTests/Stores/StatsStoreV4Tests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -503,6 +503,124 @@ final class StatsStoreV4Tests: XCTestCase {
let storageTopEarnerStats = viewStorage.loadTopEarnerStats(date: "2020", granularity: StatGranularity.year.rawValue)
XCTAssertEqual(storageTopEarnerStats?.toReadOnly(), expectedTopEarnerStats)
}

// MARK: - StatsStoreV4.retrieveSiteSummaryStats

/// Verifies that `StatsActionV4.retrieveSiteSummaryStats` returns any retrieved SiteSummaryStats.
///
func test_retrieveSiteSummaryStats_returns_retrieved_stats() throws {
// Given
let store = StatsStoreV4(dispatcher: dispatcher, storageManager: storageManager, network: network)
network.simulateResponse(requestUrlSuffix: "sites/\(sampleSiteID)/stats/summary/", filename: "site-summary-stats")

// When
let result: Result<SiteSummaryStats, Error> = waitFor { promise in
let action = StatsActionV4.retrieveSiteSummaryStats(siteID: self.sampleSiteID,
period: .day,
quantity: 1,
latestDateToInclude: DateFormatter.dateFromString(with: "2022-12-09T17:06:55")) { result in
promise(result)
}
store.onAction(action)
}

// Then
XCTAssertTrue(result.isSuccess)
let siteSummaryStats = try XCTUnwrap(result).get()
XCTAssertEqual(siteSummaryStats, sampleSiteSummaryStats())
}

/// Verifies that `StatsActionV4.retrieveSiteSummaryStats` makes the expected alternate network request for multiple stats periods.
///
func test_retrieveSiteSummaryStats_makes_expected_network_request_for_multiple_periods() throws {
// Given
let store = StatsStoreV4(dispatcher: dispatcher, storageManager: storageManager, network: network)

// When
let _: Void = waitFor { promise in
let action = StatsActionV4.retrieveSiteSummaryStats(siteID: self.sampleSiteID,
period: .month,
quantity: 3,
latestDateToInclude: DateFormatter.dateFromString(with: "2022-12-31T17:06:55")) { _ in
promise(())
}
store.onAction(action)
}

// Then
let request = try XCTUnwrap(network.requestsForResponseData.first as? DotcomRequest)
XCTAssertEqual(request.path, "sites/\(sampleSiteID)/stats/visits/")
XCTAssertEqual(request.parameters?["date"] as? String, "2022-12-31")
XCTAssertEqual(request.parameters?["unit"] as? String, "month")
XCTAssertEqual(request.parameters?["quantity"] as? String, "3")
}

/// Verifies that `StatsActionV4.retrieveSiteSummaryStats` converts and returns SiteSummaryStats for multiple periods.
///
func test_retrieveSiteSummaryStats_returns_retrieved_quarter_stats() throws {
// Given
let store = StatsStoreV4(dispatcher: dispatcher, storageManager: storageManager, network: network)
network.simulateResponse(requestUrlSuffix: "sites/\(sampleSiteID)/stats/visits/", filename: "site-visits-quarter")

// When
let result: Result<SiteSummaryStats, Error> = waitFor { promise in
let action = StatsActionV4.retrieveSiteSummaryStats(siteID: self.sampleSiteID,
period: .month,
quantity: 3,
latestDateToInclude: DateFormatter.dateFromString(with: "2022-12-31T17:06:55")) { result in
promise(result)
}
store.onAction(action)
}

// Then
XCTAssertTrue(result.isSuccess)
let siteSummaryStats = try XCTUnwrap(result).get()
XCTAssertEqual(siteSummaryStats, sampleSiteSummaryStatsQuarter())
}

/// Verifies that `StatsActionV4.retrieveSiteSummaryStats` returns an error whenever there is an error response from the backend.
///
func test_retrieveSiteSummaryStats_returns_error_upon_response_error() {
// Given
let store = StatsStoreV4(dispatcher: dispatcher, storageManager: storageManager, network: network)
network.simulateResponse(requestUrlSuffix: "sites/\(sampleSiteID)/stats/summary/", filename: "generic_error")

// When
let result: Result<SiteSummaryStats, Error> = waitFor { promise in
let action = StatsActionV4.retrieveSiteSummaryStats(siteID: self.sampleSiteID,
period: .day,
quantity: 1,
latestDateToInclude: DateFormatter.dateFromString(with: "2022-12-09T17:06:55")) { result in
promise(result)
}
store.onAction(action)
}

// Then
XCTAssertTrue(result.isFailure)
}

/// Verifies that `StatsActionV4.retrieveSiteSummaryStats` returns an error whenever there is no backend response.
///
func test_retrieveSiteSummaryStats_returns_error_upon_empty_response() {
// Given
let store = StatsStoreV4(dispatcher: dispatcher, storageManager: storageManager, network: network)

// When
let result: Result<SiteSummaryStats, Error> = waitFor { promise in
let action = StatsActionV4.retrieveSiteSummaryStats(siteID: self.sampleSiteID,
period: .day,
quantity: 1,
latestDateToInclude: DateFormatter.dateFromString(with: "2022-12-09T17:06:55")) { result in
promise(result)
}
store.onAction(action)
}

// Then
XCTAssertTrue(result.isFailure)
}
}


Expand Down Expand Up @@ -683,4 +801,22 @@ private extension StatsStoreV4Tests {
currency: "",
imageUrl: "https://dulces.mystagingwebsite.com/wp-content/uploads/2020/07/img_7472-scaled.jpeg")
}

// MARK: - Site Summary Stats Sample

func sampleSiteSummaryStats() -> Networking.SiteSummaryStats {
return SiteSummaryStats(siteID: sampleSiteID,
date: "2022-12-09",
period: .day,
visitors: 12,
views: 123)
}

func sampleSiteSummaryStatsQuarter() -> Networking.SiteSummaryStats {
return SiteSummaryStats(siteID: sampleSiteID,
date: "2022-12-09",
period: .month,
visitors: 243,
views: 486)
}
}