diff --git a/Fakes/Fakes/Networking.generated.swift b/Fakes/Fakes/Networking.generated.swift index 4daa7f8b7a0..e1f8e0f77fa 100644 --- a/Fakes/Fakes/Networking.generated.swift +++ b/Fakes/Fakes/Networking.generated.swift @@ -1593,6 +1593,19 @@ extension SiteSettingGroup { .general } } +extension Networking.SiteSummaryStats { + /// Returns a "ready to use" type filled with fake values. + /// + public static func fake() -> Networking.SiteSummaryStats { + .init( + siteID: .fake(), + date: .fake(), + period: .fake(), + visitors: .fake(), + views: .fake() + ) + } +} extension Networking.SiteVisitStats { /// Returns a "ready to use" type filled with fake values. /// diff --git a/Networking/Networking/Model/Copiable/Models+Copiable.generated.swift b/Networking/Networking/Model/Copiable/Models+Copiable.generated.swift index 8a6f03fe222..8368414410d 100644 --- a/Networking/Networking/Model/Copiable/Models+Copiable.generated.swift +++ b/Networking/Networking/Model/Copiable/Models+Copiable.generated.swift @@ -1912,6 +1912,30 @@ extension Networking.SiteSetting { } } +extension Networking.SiteSummaryStats { + public func copy( + siteID: CopiableProp = .copy, + date: CopiableProp = .copy, + period: CopiableProp = .copy, + visitors: CopiableProp = .copy, + views: CopiableProp = .copy + ) -> Networking.SiteSummaryStats { + let siteID = siteID ?? self.siteID + let date = date ?? self.date + let period = period ?? self.period + let visitors = visitors ?? self.visitors + let views = views ?? self.views + + return Networking.SiteSummaryStats( + siteID: siteID, + date: date, + period: period, + visitors: visitors, + views: views + ) + } +} + extension Networking.SiteVisitStats { public func copy( siteID: CopiableProp = .copy, diff --git a/Networking/Networking/Model/Stats/SiteSummaryStats.swift b/Networking/Networking/Model/Stats/SiteSummaryStats.swift index 20ee4f7dcb9..64897f89a8e 100644 --- a/Networking/Networking/Model/Stats/SiteSummaryStats.swift +++ b/Networking/Networking/Model/Stats/SiteSummaryStats.swift @@ -3,7 +3,7 @@ import Codegen /// Represents site summary stats for a specific period. /// -public struct SiteSummaryStats: Decodable { +public struct SiteSummaryStats: Decodable, GeneratedCopiable, GeneratedFakeable { public let siteID: Int64 public let date: String public let period: StatGranularity diff --git a/WooCommerce/Classes/ViewRelated/Dashboard/Factories/StatsDataTextFormatter.swift b/WooCommerce/Classes/ViewRelated/Dashboard/Factories/StatsDataTextFormatter.swift index 422e03d1acf..63a05847558 100644 --- a/WooCommerce/Classes/ViewRelated/Dashboard/Factories/StatsDataTextFormatter.swift +++ b/WooCommerce/Classes/ViewRelated/Dashboard/Factories/StatsDataTextFormatter.swift @@ -118,27 +118,37 @@ struct StatsDataTextFormatter { return createDeltaPercentage(from: previousCount, to: currentCount) } + /// Creates the text to display for the views count. + /// + static func createViewsCountText(siteStats: SiteSummaryStats?) -> String { + guard let viewsCount = siteStats?.views else { + return Constants.placeholderText + } + + return Double(viewsCount).humanReadableString() + } + // MARK: Conversion Stats /// Creates the text to display for the conversion rate. /// static func createConversionRateText(orderStats: OrderStatsV4?, siteStats: SiteVisitStats?, selectedIntervalIndex: Int?) -> String { - let visitors = visitorCount(at: selectedIntervalIndex, siteStats: siteStats) - let orders = orderCount(at: selectedIntervalIndex, orderStats: orderStats) + guard let visitors = visitorCount(at: selectedIntervalIndex, siteStats: siteStats), + let orders = orderCount(at: selectedIntervalIndex, orderStats: orderStats) else { + return Constants.placeholderText + } - let numberFormatter = NumberFormatter() - numberFormatter.numberStyle = .percent - numberFormatter.minimumFractionDigits = 1 + return createConversionRateText(converted: orders, total: visitors) + } - if let visitors, let orders { - // Maximum conversion rate is 100%. - let conversionRate = visitors > 0 ? min(orders/visitors, 1): 0 - let minimumFractionDigits = floor(conversionRate * 100.0) == conversionRate * 100.0 ? 0: 1 - numberFormatter.minimumFractionDigits = minimumFractionDigits - return numberFormatter.string(from: conversionRate as NSNumber) ?? Constants.placeholderText - } else { + /// Creates the text to display for the conversion rate based on SiteSummaryStats data. + /// + static func createConversionRateText(orderStats: OrderStatsV4?, siteStats: SiteSummaryStats?) -> String { + guard let visitors = siteStats?.visitors, let orders = orderStats?.totals.totalOrders else { return Constants.placeholderText } + + return createConversionRateText(converted: Double(orders), total: Double(visitors)) } // MARK: Product Stats @@ -261,9 +271,21 @@ private extension StatsDataTextFormatter { } } + /// Creates the text to display for the conversion rate from 2 input values. + /// + static func createConversionRateText(converted: Double, total: Double) -> String { + let numberFormatter = NumberFormatter() + numberFormatter.numberStyle = .percent + numberFormatter.minimumFractionDigits = 1 + + // Maximum conversion rate is 100%. + let conversionRate = total > 0 ? min(converted/total, 1) : 0 + let minimumFractionDigits = floor(conversionRate * 100.0) == conversionRate * 100.0 ? 0 : 1 + numberFormatter.minimumFractionDigits = minimumFractionDigits + return numberFormatter.string(from: conversionRate as NSNumber) ?? Constants.placeholderText + } + enum Constants { static let placeholderText = "-" } - - } diff --git a/WooCommerce/WooCommerceTests/ViewRelated/Dashboard/Factories/StatsDataTextFormatterTests.swift b/WooCommerce/WooCommerceTests/ViewRelated/Dashboard/Factories/StatsDataTextFormatterTests.swift index 5fdb95f458f..f99beff415c 100644 --- a/WooCommerce/WooCommerceTests/ViewRelated/Dashboard/Factories/StatsDataTextFormatterTests.swift +++ b/WooCommerce/WooCommerceTests/ViewRelated/Dashboard/Factories/StatsDataTextFormatterTests.swift @@ -333,9 +333,20 @@ final class StatsDataTextFormatterTests: XCTestCase { XCTAssertEqual(visitorCountDelta.direction, .positive) } + func test_createViewsCountText_returns_expected_views_stats() { + // Given + let siteVisitStats = SiteSummaryStats.fake().copy(views: 250) + + // When + let viewsCount = StatsDataTextFormatter.createViewsCountText(siteStats: siteVisitStats) + + // Then + XCTAssertEqual(viewsCount, "250") + } + // MARK: Conversion Stats - func test_createConversionRateText_returns_placeholder_when_visitor_count_is_zero() { + func test_createConversionRateText_for_SiteVisitStats_returns_placeholder_when_visitor_count_is_zero() { // Given let siteVisitStats = Yosemite.SiteVisitStats.fake().copy(items: [.fake().copy(visitors: 0)]) let orderStats = OrderStatsV4.fake().copy(totals: .fake().copy(totalOrders: 3)) @@ -347,7 +358,7 @@ final class StatsDataTextFormatterTests: XCTestCase { XCTAssertEqual(conversionRate, "0%") } - func test_createConversionRateText_returns_one_decimal_point_when_percentage_value_has_two_decimal_points() { + func test_createConversionRateText_for_SiteVisitStats_returns_one_decimal_point_when_percentage_value_has_two_decimal_points() { // Given let siteVisitStats = Yosemite.SiteVisitStats.fake().copy(items: [.fake().copy(visitors: 10000)]) let orderStats = OrderStatsV4.fake().copy(totals: .fake().copy(totalOrders: 3557)) @@ -359,7 +370,7 @@ final class StatsDataTextFormatterTests: XCTestCase { XCTAssertEqual(conversionRate, "35.6%") // order count: 3557, visitor count: 10000 => 0.3557 (35.57%) } - func test_createConversionRateText_returns_no_decimal_point_when_percentage_value_is_integer() { + func test_createConversionRateText_for_SiteVisitStats_returns_no_decimal_point_when_percentage_value_is_integer() { // Given let siteVisitStats = Yosemite.SiteVisitStats.fake().copy(items: [.fake().copy(visitors: 10)]) let orderStats = OrderStatsV4.fake().copy(totals: .fake().copy(totalOrders: 3)) @@ -371,7 +382,7 @@ final class StatsDataTextFormatterTests: XCTestCase { XCTAssertEqual(conversionRate, "30%") // order count: 3, visitor count: 10 => 0.3 (30%) } - func test_createConversionRateText_returns_expected_text_for_selected_interval() { + func test_createConversionRateText_for_SiteVisitStats_returns_expected_text_for_selected_interval() { // Given let siteVisitStats = Yosemite.SiteVisitStats.fake().copy(items: [.fake().copy(visitors: 10)]) let orderStats = OrderStatsV4.fake().copy(totals: .fake().copy(totalOrders: 2), @@ -384,6 +395,42 @@ final class StatsDataTextFormatterTests: XCTestCase { XCTAssertEqual(conversionRate, "10%") } + func test_createConversionRateText_for_SiteSummaryStats_returns_placeholder_when_visitor_count_is_zero() { + // Given + let siteSummaryStats = SiteSummaryStats.fake().copy(visitors: 0) + let orderStats = OrderStatsV4.fake().copy(totals: .fake().copy(totalOrders: 3)) + + // When + let conversionRate = StatsDataTextFormatter.createConversionRateText(orderStats: orderStats, siteStats: siteSummaryStats) + + // Then + XCTAssertEqual(conversionRate, "0%") + } + + func test_createConversionRateText_for_SiteSummaryStats_returns_one_decimal_point_when_percentage_value_has_two_decimal_points() { + // Given + let siteSummaryStats = SiteSummaryStats.fake().copy(visitors: 10000) + let orderStats = OrderStatsV4.fake().copy(totals: .fake().copy(totalOrders: 3557)) + + // When + let conversionRate = StatsDataTextFormatter.createConversionRateText(orderStats: orderStats, siteStats: siteSummaryStats) + + // Then + XCTAssertEqual(conversionRate, "35.6%") // order count: 3557, visitor count: 10000 => 0.3557 (35.57%) + } + + func test_createConversionRateText_for_SiteSummaryStats_returns_no_decimal_point_when_percentage_value_is_integer() { + // Given + let siteSummaryStats = SiteSummaryStats.fake().copy(visitors: 10) + let orderStats = OrderStatsV4.fake().copy(totals: .fake().copy(totalOrders: 3)) + + // When + let conversionRate = StatsDataTextFormatter.createConversionRateText(orderStats: orderStats, siteStats: siteSummaryStats) + + // Then + XCTAssertEqual(conversionRate, "30%") // order count: 3, visitor count: 10 => 0.3 (30%) + } + // MARK: Delta Calculations func test_createDeltaPercentage_returns_expected_positive_delta() { diff --git a/Yosemite/Yosemite/Model/Model.swift b/Yosemite/Yosemite/Model/Model.swift index 8cd0cdce160..acd021c5547 100644 --- a/Yosemite/Yosemite/Model/Model.swift +++ b/Yosemite/Yosemite/Model/Model.swift @@ -125,6 +125,7 @@ public typealias SitePlugin = Networking.SitePlugin public typealias SitePluginStatusEnum = Networking.SitePluginStatusEnum public typealias SiteSetting = Networking.SiteSetting public typealias SiteSettingGroup = Networking.SiteSettingGroup +public typealias SiteSummaryStats = Networking.SiteSummaryStats public typealias SiteVisitStats = Networking.SiteVisitStats public typealias SiteVisitStatsItem = Networking.SiteVisitStatsItem public typealias StateOfACountry = Networking.StateOfACountry