Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
142 commits
Select commit Hold shift + click to select a range
3c37928
Temporariliy point to new Authenticator changes
mindgraffiti Jul 27, 2018
50ad6f9
Add new color definitions from Woo handbook
mindgraffiti Jul 27, 2018
53e1aaf
Add the Woo logo to assets
mindgraffiti Jul 27, 2018
3218213
`pod update`
mindgraffiti Jul 27, 2018
d4574bf
WIP: OrderStats models and remote
bummytime Jul 30, 2018
5fa9cbf
Added OrderStatsMapper
bummytime Jul 30, 2018
16c7c80
Param name fix
bummytime Jul 30, 2018
e3768c0
WIP: OrderStatsMapper tests
bummytime Jul 30, 2018
76449b0
Merge branch 'develop' of github.com:woocommerce/woocommerce-ios into…
bummytime Jul 31, 2018
bfd6d1c
Increasing version to Mark 0.4.3
jleandroperez Jul 31, 2018
7c4ec47
Nukes CoreDataManager+Woo
jleandroperez Jul 31, 2018
43af64c
AppDelegate: StorageManager property
jleandroperez Jul 31, 2018
e5c2688
CoreDataManager: Nuking viewContext property
jleandroperez Jul 31, 2018
72df052
ResultsController: New convenience initializer
jleandroperez Jul 31, 2018
62d6593
StorePickerViewController: Nuking Storage Import
jleandroperez Jul 31, 2018
ea66e1a
OrderDetailsViewController: Nuking Storage Import
jleandroperez Jul 31, 2018
acf4bc1
AuthenticatedState: OrderDetailsViewController
jleandroperez Jul 31, 2018
4c14406
Model: Exporting Storage Model(s)
jleandroperez Jul 31, 2018
cca4c59
Updates order stats models, decoder, and tests
bummytime Jul 31, 2018
faea35a
Eliminate the .debug app ID and enable push notifications
astralbodies Aug 1, 2018
d2d1b81
ResultsController: MutableType Alias
jleandroperez Aug 1, 2018
e1ec411
Storage: Removing xcdatamodeld from build phase
jleandroperez Aug 1, 2018
ed098bf
Implements ResultsController+UIKit
jleandroperez Aug 1, 2018
6f3af9d
Updates Project
jleandroperez Aug 1, 2018
22a0349
MockupStorage: Nukes viewContext
jleandroperez Aug 1, 2018
54a4b93
Implements MockupStorageManager:Sample
jleandroperez Aug 1, 2018
df1bfb9
Yosemite: Exporting Storage.Account
jleandroperez Aug 1, 2018
17d93fc
Yosemite: Simplifying ResultsController Tests
jleandroperez Aug 1, 2018
0eab005
Implements MockupTableView
jleandroperez Aug 1, 2018
e695ab1
MockupTableView: Fixing Typo
jleandroperez Aug 1, 2018
a36a1cc
Implements ResultsControllerUIKitTests
jleandroperez Aug 1, 2018
791b528
ResultsController+UIKit: Adds missing documentation
jleandroperez Aug 1, 2018
e1af80e
Updates WooCommerce Project
jleandroperez Aug 1, 2018
47683c2
Merge pull request #201 from woocommerce/issue/preventing-storage-imp…
jleandroperez Aug 1, 2018
1c85845
Merge remote-tracking branch 'origin/develop' into issue/133-results-…
jleandroperez Aug 1, 2018
dab1e97
WIP: Added AnyCodable
bummytime Aug 1, 2018
1aeb20a
More tests and OrderStatsItem parsing
bummytime Aug 2, 2018
98de549
Merge branch 'develop' of github.com:woocommerce/woocommerce-ios into…
bummytime Aug 2, 2018
06ec905
Updated OrderStatGranularity
bummytime Aug 2, 2018
19a7dcc
Added OrderStatsRemoteTests
bummytime Aug 2, 2018
075209b
Fixed typo on order stats remote param name
bummytime Aug 2, 2018
ada2e12
OrderAction: Updates Callback
jleandroperez Aug 2, 2018
c1bde6c
ResultsController: New Public API
jleandroperez Aug 2, 2018
21b59b5
OrdersViewController: Wiring ResultsController
jleandroperez Aug 2, 2018
cb60bf8
Implements OrderStatus+Woo
jleandroperez Aug 2, 2018
7489962
OrderStatusViewModel: Nukes Static Helpers
jleandroperez Aug 2, 2018
75efeb7
OrdersViewController: Updating Filtering Support
jleandroperez Aug 2, 2018
5b1f41a
OrderStoreTests: Fixing Unit Tests
jleandroperez Aug 2, 2018
34acc95
OrdersViewController: Renames few methods
jleandroperez Aug 2, 2018
f415176
OrdersViewController: Adds TODO(s)
jleandroperez Aug 2, 2018
284b311
Merge pull request #202 from woocommerce/eliminate-debug
jleandroperez Aug 2, 2018
efbb976
Cleaned up OrderStatGranularity
bummytime Aug 2, 2018
aae35d5
Merge pull request #206 from woocommerce/feature/177-dashboard-mark1
bummytime Aug 2, 2018
45e73aa
Merge remote-tracking branch 'origin/develop' into issue/133-results-…
jleandroperez Aug 2, 2018
b648b1e
ResultsController+UIKit: Updating Comment
jleandroperez Aug 2, 2018
3a03a9f
Merge pull request #203 from woocommerce/issue/133-results-controller…
jleandroperez Aug 2, 2018
9d5cc3e
Merge remote-tracking branch 'origin/develop' into issue/204-wiring-r…
jleandroperez Aug 2, 2018
09bc60d
OrdersViewController: Renames nested type
jleandroperez Aug 2, 2018
46741cc
Added OrderStatsStore and OrderStatsAction
bummytime Aug 2, 2018
1e5f96c
Merge branch 'develop' of github.com:woocommerce/woocommerce-ios into…
bummytime Aug 2, 2018
ae0c629
Nukes Duplicated JSON(s)
jleandroperez Aug 2, 2018
758800d
Updating Sample JSON(s)
jleandroperez Aug 2, 2018
b264fe9
Networking: Fixing Unit Tests
jleandroperez Aug 2, 2018
6fa5c29
Yosemite: Fixing Unit Tests
jleandroperez Aug 2, 2018
b94a879
Loader: Recursive Lookup
jleandroperez Aug 2, 2018
3695da3
Project Cleanup
jleandroperez Aug 2, 2018
95ed35e
Merge pull request #207 from woocommerce/issue/204-wiring-resultscont…
jleandroperez Aug 2, 2018
11ad91b
Podfile: Dropping ARMv7 Workaround
jleandroperez Aug 2, 2018
de13388
Nuking explicit architecture settings
jleandroperez Aug 2, 2018
ba90b0e
DotcomRequestTests: Fixing Tests with random failures
jleandroperez Aug 3, 2018
6e6b034
Fixing Xcode 10's AutoLinking Framework(s) Missing
jleandroperez Aug 3, 2018
4a100d2
Updating Project
jleandroperez Aug 3, 2018
357b48c
WooAnalyticsTests: Fixing Xcode 10 Random Failure
jleandroperez Aug 3, 2018
472db2b
NSManagedObject+Object: Fixing iOS 12 Console Errors (in Unit Tests)
jleandroperez Aug 3, 2018
79d480d
Merge remote-tracking branch 'origin/develop' into issue/208-fixing-x…
jleandroperez Aug 3, 2018
9a0afca
Merge branch 'develop' of github.com:woocommerce/woocommerce-ios into…
bummytime Aug 3, 2018
785a35e
Added tests for the OrderStatsStore+action
bummytime Aug 3, 2018
fa7f7ec
Implements FabricManager
jleandroperez Aug 3, 2018
691d46c
SessionManager: Posting Account Update Notifications
jleandroperez Aug 3, 2018
3ad58a2
AppDelegate: Wiring FabricManager
jleandroperez Aug 3, 2018
7f21520
Added FIXME comment
bummytime Aug 3, 2018
40b4dfe
Removing Extra Spaces
jleandroperez Aug 3, 2018
fcb0c61
Merge pull request #214 from woocommerce/feature/177-dashboard-mark2
bummytime Aug 3, 2018
5674908
Merge remote-tracking branch 'origin/develop' into issue/209-deduplic…
jleandroperez Aug 3, 2018
4cc9ab0
Relocating Responses JSON files
jleandroperez Aug 3, 2018
cc553f0
Merge pull request #216 from woocommerce/issue/fixing-random-double-s…
jleandroperez Aug 3, 2018
6130423
Merge pull request #215 from woocommerce/issue/205-fabric-metadata
jleandroperez Aug 3, 2018
c04c323
Merge pull request #211 from woocommerce/issue/209-deduplicating-test…
jleandroperez Aug 3, 2018
8ac1099
Merge branch 'develop' into feature/more-configurations
mindgraffiti Aug 3, 2018
6853319
Added MIContainer
bummytime Aug 3, 2018
190193b
Fixed typo
bummytime Aug 3, 2018
4972321
Merge pull request #220 from woocommerce/feature/177-dashboard-mark3
bummytime Aug 6, 2018
7ea2a43
Added SiteVisitStats remote + models
bummytime Aug 6, 2018
403f2cf
Added SiteVisitStats to Yosemite model
bummytime Aug 6, 2018
627b70d
Added SiteVisitStatsMapperTests
bummytime Aug 6, 2018
0e815ca
Added SiteVisitStatsRemoteTests
bummytime Aug 6, 2018
a885753
Minor refactoring which yields StatsAction & StatsStore; added site v…
bummytime Aug 6, 2018
b6520f2
Refactored StatsStoreTests; added StatsAction.retrieveSiteVisitStats …
bummytime Aug 6, 2018
6bccfd3
Implements EntityListener
jleandroperez Aug 6, 2018
0a49a79
Implements EntityListenerTests
jleandroperez Aug 6, 2018
5d11d61
Updates Project
jleandroperez Aug 6, 2018
2113bf2
ReadOnlyConvertible: Type Erasure Workaround
jleandroperez Aug 6, 2018
6bb61a0
Account+ReadOnlyConvertible: Type Erasure Conformance
jleandroperez Aug 6, 2018
cf620d6
Site+ReadOnlyConvertible: Type Erasure Conformance
jleandroperez Aug 6, 2018
3bf9504
Order+ReadOnlyConvertible: Type Erasure Conformance
jleandroperez Aug 6, 2018
d7d307c
OrderCoupon+ReadOnlyConvertible: Type Erasure Conformance
jleandroperez Aug 6, 2018
6a8c12f
OrderItem+ReadOnlyConvertible: Type Erasure Conformance
jleandroperez Aug 6, 2018
b124492
OrderNote+ReadOnlyConvertible: Type Erasure Conformance
jleandroperez Aug 6, 2018
7a7189c
Added some StatsStoreTests
bummytime Aug 7, 2018
7a73324
Fixed API path issue
bummytime Aug 7, 2018
f50b7bc
Relocates FetchedResultsControllerDelegateWrapper
jleandroperez Aug 7, 2018
c5e505f
Relocates ManagedObjectsDidChangeNotification
jleandroperez Aug 7, 2018
36edfd0
ReadOnlyTypeErasedConvertible: Removing default implementation
jleandroperez Aug 7, 2018
8087009
Merge pull request #223 from woocommerce/feature/177-dashboard-mark4
bummytime Aug 7, 2018
8abf078
Implements TypeErasedConvertible
jleandroperez Aug 7, 2018
752ed2f
Point to new WordPressAuthenticator version 1.0.5, `pod install`.
mindgraffiti Aug 7, 2018
29e0f5c
Merge branch 'develop' into feature/more-configurations
mindgraffiti Aug 7, 2018
53025ea
Merge pull request #195 from woocommerce/feature/more-configurations
mindgraffiti Aug 7, 2018
3b97541
ReadOnlyConvertible: Cleanup
jleandroperez Aug 7, 2018
dbed12d
EntityListener: Wiring TypeErased protocol
jleandroperez Aug 7, 2018
8380ef8
Implements ReadOnlyRepresentation
jleandroperez Aug 7, 2018
f39b175
ReadOnlyRepresentation > ReadOnlyType
jleandroperez Aug 7, 2018
e120351
Implements ReadOnlyType Conformance
jleandroperez Aug 7, 2018
76a34e3
Relocates Model Files
jleandroperez Aug 7, 2018
97d505f
Merge remote-tracking branch 'origin/develop' into issue/212-entity-l…
jleandroperez Aug 7, 2018
08a0aef
ReadOnlyType: Adds missing documentation
jleandroperez Aug 7, 2018
ff658d9
Merge pull request #222 from woocommerce/issue/212-entity-listener
jleandroperez Aug 8, 2018
f4e0eb7
Merge remote-tracking branch 'origin/develop' into issue/208-fixing-x…
jleandroperez Aug 8, 2018
962559f
WooAnalyticsTests: Fixing typo
jleandroperez Aug 8, 2018
6eecb62
Merge pull request #213 from woocommerce/issue/208-fixing-xcode10-war…
jleandroperez Aug 8, 2018
e615bdd
OrderListMapper: New Unit Test
jleandroperez Aug 8, 2018
2cb0e1b
Updates broken-orders-mark-2.json
jleandroperez Aug 8, 2018
a1600eb
OrderItem: sku is now optional
jleandroperez Aug 8, 2018
3d806e5
OrderListMapperTests: new Unit Test
jleandroperez Aug 8, 2018
461c183
OrderItemViewModel: Handling null SKU
jleandroperez Aug 8, 2018
300b1a5
ProductDetailsTableViewCell: Handling null SKU
jleandroperez Aug 8, 2018
bea9d5b
OrderStoreTests: New Unit Test
jleandroperez Aug 8, 2018
0065cd7
Merge pull request #227 from woocommerce/issue/221-orders-list-empty
jleandroperez Aug 10, 2018
e46689d
Updating the bg color of the order screens
bummytime Aug 13, 2018
59c7ae6
Merge pull request #231 from woocommerce/fix/order-bg-color
bummytime Aug 13, 2018
8e30b2b
Increasing Version to Mark 0.5
jleandroperez Aug 13, 2018
f0021e6
Merge remote-tracking branch 'origin/master' into develop
jleandroperez Aug 13, 2018
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
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