Skip to content

Commit

Permalink
Merge pull request #233 from woocommerce/develop
Browse files Browse the repository at this point in the history
Merging Mark 0.5 into Master
  • Loading branch information
jleandroperez authored Aug 13, 2018
2 parents b941529 + f0021e6 commit 056e419
Show file tree
Hide file tree
Showing 111 changed files with 18,673 additions and 1,916 deletions.
148 changes: 137 additions & 11 deletions Networking/Networking.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

54 changes: 52 additions & 2 deletions Networking/Networking/Extensions/DateFormatter+Woo.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,70 @@ import Foundation

/// DateFormatter Extensions
///
extension DateFormatter {
public extension DateFormatter {

/// Default Formatters
///
struct Defaults {

/// Date And Time Formatter
///
static let dateTimeFormatter: DateFormatter = {
public static let dateTimeFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.timeZone = TimeZone(identifier: "GMT")
formatter.dateFormat = "yyyy'-'MM'-'dd'T'HH:mm:ss"
return formatter
}()
}


/// Stats Formatters
///
struct Stats {

/// Date formatter used for creating the properly-formatted date string for **day** granularity. Typically
/// used when setting the `latestDateToInclude` on `OrderStatsRemote`.
///
public static let statsDayFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.timeZone = TimeZone(identifier: "GMT")
formatter.dateFormat = "yyyy'-'MM'-'dd"
return formatter
}()

/// Date formatter used for creating the properly-formatted date string for **week** granularity. Typically
/// used when setting the `latestDateToInclude` on `OrderStatsRemote`.
///
public static let statsWeekFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.timeZone = TimeZone(identifier: "GMT")
formatter.dateFormat = "yyyy'-W'ww"
return formatter
}()

/// Date formatter used for creating the properly-formatted date string for **month** granularity. Typically
/// used when setting the `latestDateToInclude` on `OrderStatsRemote`.
///
public static let statsMonthFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.timeZone = TimeZone(identifier: "GMT")
formatter.dateFormat = "yyyy'-'MM"
return formatter
}()

/// Date formatter used for creating the properly-formatted date string for **year** granularity. Typically
/// used when setting the `latestDateToInclude` on `OrderStatsRemote`.
///
public static let statsYearFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.timeZone = TimeZone(identifier: "GMT")
formatter.dateFormat = "yyyy"
return formatter
}()
}
}
14 changes: 14 additions & 0 deletions Networking/Networking/Mapper/OrderStatsMapper.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import Foundation


/// Mapper: OrderStats
///
class OrderStatsMapper: Mapper {

/// (Attempts) to convert a dictionary into an OrderStats entity.
///
func map(response: Data) throws -> OrderStats {
let decoder = JSONDecoder()
return try decoder.decode(OrderStats.self, from: response)
}
}
14 changes: 14 additions & 0 deletions Networking/Networking/Mapper/SiteVisitStatsMapper.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import Foundation


/// Mapper: SiteVisitStats
///
class SiteVisitStatsMapper: Mapper {

/// (Attempts) to convert a dictionary into an SiteVisitStats entity.
///
func map(response: Data) throws -> SiteVisitStats {
let decoder = JSONDecoder()
return try decoder.decode(SiteVisitStats.self, from: response)
}
}
2 changes: 1 addition & 1 deletion Networking/Networking/Model/OrderItem.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ public struct OrderItem: Decodable {
public let name: String
public let productID: Int
public let quantity: Int
public let sku: String
public let sku: String?
public let subtotal: String
public let subtotalTax: String
public let taxClass: String
Expand Down
73 changes: 73 additions & 0 deletions Networking/Networking/Model/Stats/MIContainer.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import Foundation

/**
This is a generic container data container used to hold an (unkeyed) data array
of which its elements can be multiple types. Additionally, the field names
are stored in a separate array where the specific index of a field name element
corresponds to its matching element in the `data` array.
Why do we have this insanity? To deal with JSON payloads that can look like this:
````
{
"fields": [
"period",
"orders",
"total_sales",
"total_tax",
"total_shipping",
"currency",
"gross_sales"
],
"data": [
[ "2018-06-01", 2, 14.24, 9.98, 0.28, "USD", 14.120000000000001 ],
[ 2018, 2, 123123, 9.98, 0.0, "USD", 0]
]
...
}
````
A few accessor methods are also provided that will ensure the correct type is returned for a given field. This container
will be especially useful when dealing with data returned from the stats endpoints. 😃
*/
public struct MIContainer {
let data: [Any]
let fieldNames: [String]

func fetchStringValue<T : RawRepresentable>(for field: T) -> String where T.RawValue == String {
guard let index = fieldNames.index(of: field.rawValue) else {
return ""
}

// 😢 As crazy as it sounds, sometimes the server occasionally returns
// String values as Ints — we need to account for this.
if self.data[index] is Int {
if let intValue = self.data[index] as? Int {
return String(intValue)
}
return ""
} else {
return self.data[index] as? String ?? ""
}
}

func fetchIntValue<T : RawRepresentable>(for field: T) -> Int where T.RawValue == String {
guard let index = fieldNames.index(of: field.rawValue),
let returnValue = self.data[index] as? Int else {
return 0
}
return returnValue
}

func fetchDoubleValue<T : RawRepresentable>(for field: T) -> Double where T.RawValue == String {
guard let index = fieldNames.index(of: field.rawValue) else {
return 0
}

if self.data[index] is Int {
let intValue = self.data[index] as? Int ?? 0
return Double(intValue)
} else {
return self.data[index] as? Double ?? 0
}
}
}
115 changes: 115 additions & 0 deletions Networking/Networking/Model/Stats/OrderStats.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import Foundation


/// Represents order stats over a specific period.
///
public struct OrderStats: Decodable {
public let date: String
public let granularity: StatGranularity
public let quantity: String
public let fields: [String]
public let totalGrossSales: Float
public let totalNetSales: Float
public let totalOrders: Int
public let totalProducts: Int
public let averageGrossSales: Float
public let averageNetSales: Float
public let averageOrders: Float
public let averageProducts: Float
public let items: [OrderStatsItem]?


/// The public initializer for order 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 quantity = try container.decode(String.self, forKey: .quantity)

let fields = try container.decode([String].self, forKey: .fields)
let rawData: [[AnyCodable]] = try container.decode([[AnyCodable]].self, forKey: .data)

let totalGrossSales = try container.decode(Float.self, forKey: .totalGrossSales)
let totalNetSales = try container.decode(Float.self, forKey: .totalNetSales)
let totalOrders = try container.decode(Int.self, forKey: .totalOrders)
let totalProducts = try container.decode(Int.self, forKey: .totalProducts)

let averageGrossSales = try container.decode(Float.self, forKey: .averageGrossSales)
let averageNetSales = try container.decode(Float.self, forKey: .averageNetSales)
let averageOrders = try container.decode(Float.self, forKey: .averageOrders)
let averageProducts = try container.decode(Float.self, forKey: .averageProducts)

let items = rawData.map({ OrderStatsItem(fieldNames: fields, rawData: $0) })

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)
}


/// OrderStats struct initializer.
///
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) {
self.date = date
self.granularity = granularity
self.quantity = quantity
self.fields = fields
self.totalGrossSales = totalGrossSales
self.totalNetSales = totalNetSales
self.totalOrders = totalOrders
self.totalProducts = totalProducts
self.averageGrossSales = averageGrossSales
self.averageNetSales = averageNetSales
self.averageOrders = averageOrders
self.averageProducts = averageProducts
self.items = items
}
}


/// Defines all of the OrderStats CodingKeys.
///
private extension OrderStats {

enum CodingKeys: String, CodingKey {
case date = "date"
case unit = "unit"
case quantity = "quantity"
case fields = "fields"
case data = "data"
case totalGrossSales = "total_gross_sales"
case totalNetSales = "total_net_sales"
case totalOrders = "total_orders"
case totalProducts = "total_products"
case averageGrossSales = "avg_gross_sales"
case averageNetSales = "avg_net_sales"
case averageOrders = "avg_orders"
case averageProducts = "avg_products"
}
}


// MARK: - Comparable Conformance
//
extension OrderStats: Comparable {
public static func == (lhs: OrderStats, rhs: OrderStats) -> Bool {
return lhs.date == rhs.date &&
lhs.granularity == rhs.granularity &&
lhs.quantity == rhs.quantity &&
lhs.fields == rhs.fields &&
lhs.totalGrossSales == rhs.totalGrossSales &&
lhs.totalNetSales == rhs.totalNetSales &&
lhs.totalOrders == rhs.totalOrders &&
lhs.totalProducts == rhs.totalProducts &&
lhs.averageGrossSales == rhs.averageGrossSales &&
lhs.averageNetSales == rhs.averageNetSales &&
lhs.averageOrders == rhs.averageOrders &&
lhs.averageProducts == rhs.averageProducts &&
lhs.items == rhs.items
}

public static func < (lhs: OrderStats, rhs: OrderStats) -> Bool {
return lhs.date < rhs.date ||
(lhs.date == rhs.date && lhs.quantity < rhs.quantity)
}
}
Loading

0 comments on commit 056e419

Please sign in to comment.