Skip to content

Commit 056e419

Browse files
Merge pull request #233 from woocommerce/develop
Merging Mark 0.5 into Master
2 parents b941529 + f0021e6 commit 056e419

File tree

111 files changed

+18673
-1916
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

111 files changed

+18673
-1916
lines changed

Networking/Networking.xcodeproj/project.pbxproj

Lines changed: 137 additions & 11 deletions
Large diffs are not rendered by default.

Networking/Networking/Extensions/DateFormatter+Woo.swift

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,70 @@ import Foundation
33

44
/// DateFormatter Extensions
55
///
6-
extension DateFormatter {
6+
public extension DateFormatter {
77

88
/// Default Formatters
99
///
1010
struct Defaults {
1111

1212
/// Date And Time Formatter
1313
///
14-
static let dateTimeFormatter: DateFormatter = {
14+
public static let dateTimeFormatter: DateFormatter = {
1515
let formatter = DateFormatter()
1616
formatter.locale = Locale(identifier: "en_US_POSIX")
1717
formatter.timeZone = TimeZone(identifier: "GMT")
1818
formatter.dateFormat = "yyyy'-'MM'-'dd'T'HH:mm:ss"
1919
return formatter
2020
}()
2121
}
22+
23+
24+
/// Stats Formatters
25+
///
26+
struct Stats {
27+
28+
/// Date formatter used for creating the properly-formatted date string for **day** granularity. Typically
29+
/// used when setting the `latestDateToInclude` on `OrderStatsRemote`.
30+
///
31+
public static let statsDayFormatter: DateFormatter = {
32+
let formatter = DateFormatter()
33+
formatter.locale = Locale(identifier: "en_US_POSIX")
34+
formatter.timeZone = TimeZone(identifier: "GMT")
35+
formatter.dateFormat = "yyyy'-'MM'-'dd"
36+
return formatter
37+
}()
38+
39+
/// Date formatter used for creating the properly-formatted date string for **week** granularity. Typically
40+
/// used when setting the `latestDateToInclude` on `OrderStatsRemote`.
41+
///
42+
public static let statsWeekFormatter: DateFormatter = {
43+
let formatter = DateFormatter()
44+
formatter.locale = Locale(identifier: "en_US_POSIX")
45+
formatter.timeZone = TimeZone(identifier: "GMT")
46+
formatter.dateFormat = "yyyy'-W'ww"
47+
return formatter
48+
}()
49+
50+
/// Date formatter used for creating the properly-formatted date string for **month** granularity. Typically
51+
/// used when setting the `latestDateToInclude` on `OrderStatsRemote`.
52+
///
53+
public static let statsMonthFormatter: DateFormatter = {
54+
let formatter = DateFormatter()
55+
formatter.locale = Locale(identifier: "en_US_POSIX")
56+
formatter.timeZone = TimeZone(identifier: "GMT")
57+
formatter.dateFormat = "yyyy'-'MM"
58+
return formatter
59+
}()
60+
61+
/// Date formatter used for creating the properly-formatted date string for **year** granularity. Typically
62+
/// used when setting the `latestDateToInclude` on `OrderStatsRemote`.
63+
///
64+
public static let statsYearFormatter: DateFormatter = {
65+
let formatter = DateFormatter()
66+
formatter.locale = Locale(identifier: "en_US_POSIX")
67+
formatter.timeZone = TimeZone(identifier: "GMT")
68+
formatter.dateFormat = "yyyy"
69+
return formatter
70+
}()
71+
}
2272
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import Foundation
2+
3+
4+
/// Mapper: OrderStats
5+
///
6+
class OrderStatsMapper: Mapper {
7+
8+
/// (Attempts) to convert a dictionary into an OrderStats entity.
9+
///
10+
func map(response: Data) throws -> OrderStats {
11+
let decoder = JSONDecoder()
12+
return try decoder.decode(OrderStats.self, from: response)
13+
}
14+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import Foundation
2+
3+
4+
/// Mapper: SiteVisitStats
5+
///
6+
class SiteVisitStatsMapper: Mapper {
7+
8+
/// (Attempts) to convert a dictionary into an SiteVisitStats entity.
9+
///
10+
func map(response: Data) throws -> SiteVisitStats {
11+
let decoder = JSONDecoder()
12+
return try decoder.decode(SiteVisitStats.self, from: response)
13+
}
14+
}

Networking/Networking/Model/OrderItem.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ public struct OrderItem: Decodable {
88
public let name: String
99
public let productID: Int
1010
public let quantity: Int
11-
public let sku: String
11+
public let sku: String?
1212
public let subtotal: String
1313
public let subtotalTax: String
1414
public let taxClass: String
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import Foundation
2+
3+
/**
4+
This is a generic container data container used to hold an (unkeyed) data array
5+
of which its elements can be multiple types. Additionally, the field names
6+
are stored in a separate array where the specific index of a field name element
7+
corresponds to its matching element in the `data` array.
8+
9+
Why do we have this insanity? To deal with JSON payloads that can look like this:
10+
````
11+
{
12+
"fields": [
13+
"period",
14+
"orders",
15+
"total_sales",
16+
"total_tax",
17+
"total_shipping",
18+
"currency",
19+
"gross_sales"
20+
],
21+
"data": [
22+
[ "2018-06-01", 2, 14.24, 9.98, 0.28, "USD", 14.120000000000001 ],
23+
[ 2018, 2, 123123, 9.98, 0.0, "USD", 0]
24+
]
25+
...
26+
}
27+
````
28+
29+
A few accessor methods are also provided that will ensure the correct type is returned for a given field. This container
30+
will be especially useful when dealing with data returned from the stats endpoints. 😃
31+
*/
32+
public struct MIContainer {
33+
let data: [Any]
34+
let fieldNames: [String]
35+
36+
func fetchStringValue<T : RawRepresentable>(for field: T) -> String where T.RawValue == String {
37+
guard let index = fieldNames.index(of: field.rawValue) else {
38+
return ""
39+
}
40+
41+
// 😢 As crazy as it sounds, sometimes the server occasionally returns
42+
// String values as Ints — we need to account for this.
43+
if self.data[index] is Int {
44+
if let intValue = self.data[index] as? Int {
45+
return String(intValue)
46+
}
47+
return ""
48+
} else {
49+
return self.data[index] as? String ?? ""
50+
}
51+
}
52+
53+
func fetchIntValue<T : RawRepresentable>(for field: T) -> Int where T.RawValue == String {
54+
guard let index = fieldNames.index(of: field.rawValue),
55+
let returnValue = self.data[index] as? Int else {
56+
return 0
57+
}
58+
return returnValue
59+
}
60+
61+
func fetchDoubleValue<T : RawRepresentable>(for field: T) -> Double where T.RawValue == String {
62+
guard let index = fieldNames.index(of: field.rawValue) else {
63+
return 0
64+
}
65+
66+
if self.data[index] is Int {
67+
let intValue = self.data[index] as? Int ?? 0
68+
return Double(intValue)
69+
} else {
70+
return self.data[index] as? Double ?? 0
71+
}
72+
}
73+
}
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import Foundation
2+
3+
4+
/// Represents order stats over a specific period.
5+
///
6+
public struct OrderStats: Decodable {
7+
public let date: String
8+
public let granularity: StatGranularity
9+
public let quantity: String
10+
public let fields: [String]
11+
public let totalGrossSales: Float
12+
public let totalNetSales: Float
13+
public let totalOrders: Int
14+
public let totalProducts: Int
15+
public let averageGrossSales: Float
16+
public let averageNetSales: Float
17+
public let averageOrders: Float
18+
public let averageProducts: Float
19+
public let items: [OrderStatsItem]?
20+
21+
22+
/// The public initializer for order stats.
23+
///
24+
public init(from decoder: Decoder) throws {
25+
let container = try decoder.container(keyedBy: CodingKeys.self)
26+
27+
let date = try container.decode(String.self, forKey: .date)
28+
let granularity = try container.decode(StatGranularity.self, forKey: .unit)
29+
let quantity = try container.decode(String.self, forKey: .quantity)
30+
31+
let fields = try container.decode([String].self, forKey: .fields)
32+
let rawData: [[AnyCodable]] = try container.decode([[AnyCodable]].self, forKey: .data)
33+
34+
let totalGrossSales = try container.decode(Float.self, forKey: .totalGrossSales)
35+
let totalNetSales = try container.decode(Float.self, forKey: .totalNetSales)
36+
let totalOrders = try container.decode(Int.self, forKey: .totalOrders)
37+
let totalProducts = try container.decode(Int.self, forKey: .totalProducts)
38+
39+
let averageGrossSales = try container.decode(Float.self, forKey: .averageGrossSales)
40+
let averageNetSales = try container.decode(Float.self, forKey: .averageNetSales)
41+
let averageOrders = try container.decode(Float.self, forKey: .averageOrders)
42+
let averageProducts = try container.decode(Float.self, forKey: .averageProducts)
43+
44+
let items = rawData.map({ OrderStatsItem(fieldNames: fields, rawData: $0) })
45+
46+
self.init(date: date, granularity: granularity, quantity: quantity, fields: fields, items: items, totalGrossSales: totalGrossSales, totalNetSales: totalNetSales, totalOrders: totalOrders, totalProducts: totalProducts, averageGrossSales: averageGrossSales, averageNetSales: averageNetSales, averageOrders: averageOrders, averageProducts: averageProducts)
47+
}
48+
49+
50+
/// OrderStats struct initializer.
51+
///
52+
public init(date: String, granularity: StatGranularity, quantity: String, fields: [String], items: [OrderStatsItem]?, totalGrossSales: Float, totalNetSales: Float, totalOrders: Int, totalProducts: Int, averageGrossSales: Float, averageNetSales: Float, averageOrders: Float, averageProducts: Float) {
53+
self.date = date
54+
self.granularity = granularity
55+
self.quantity = quantity
56+
self.fields = fields
57+
self.totalGrossSales = totalGrossSales
58+
self.totalNetSales = totalNetSales
59+
self.totalOrders = totalOrders
60+
self.totalProducts = totalProducts
61+
self.averageGrossSales = averageGrossSales
62+
self.averageNetSales = averageNetSales
63+
self.averageOrders = averageOrders
64+
self.averageProducts = averageProducts
65+
self.items = items
66+
}
67+
}
68+
69+
70+
/// Defines all of the OrderStats CodingKeys.
71+
///
72+
private extension OrderStats {
73+
74+
enum CodingKeys: String, CodingKey {
75+
case date = "date"
76+
case unit = "unit"
77+
case quantity = "quantity"
78+
case fields = "fields"
79+
case data = "data"
80+
case totalGrossSales = "total_gross_sales"
81+
case totalNetSales = "total_net_sales"
82+
case totalOrders = "total_orders"
83+
case totalProducts = "total_products"
84+
case averageGrossSales = "avg_gross_sales"
85+
case averageNetSales = "avg_net_sales"
86+
case averageOrders = "avg_orders"
87+
case averageProducts = "avg_products"
88+
}
89+
}
90+
91+
92+
// MARK: - Comparable Conformance
93+
//
94+
extension OrderStats: Comparable {
95+
public static func == (lhs: OrderStats, rhs: OrderStats) -> Bool {
96+
return lhs.date == rhs.date &&
97+
lhs.granularity == rhs.granularity &&
98+
lhs.quantity == rhs.quantity &&
99+
lhs.fields == rhs.fields &&
100+
lhs.totalGrossSales == rhs.totalGrossSales &&
101+
lhs.totalNetSales == rhs.totalNetSales &&
102+
lhs.totalOrders == rhs.totalOrders &&
103+
lhs.totalProducts == rhs.totalProducts &&
104+
lhs.averageGrossSales == rhs.averageGrossSales &&
105+
lhs.averageNetSales == rhs.averageNetSales &&
106+
lhs.averageOrders == rhs.averageOrders &&
107+
lhs.averageProducts == rhs.averageProducts &&
108+
lhs.items == rhs.items
109+
}
110+
111+
public static func < (lhs: OrderStats, rhs: OrderStats) -> Bool {
112+
return lhs.date < rhs.date ||
113+
(lhs.date == rhs.date && lhs.quantity < rhs.quantity)
114+
}
115+
}

0 commit comments

Comments
 (0)