Skip to content

Commit 0013e84

Browse files
committed
Merge branch 'trunk' into WOOMOB-1489-fix-request-authenticator-swap-crash
2 parents 989494a + 20876f1 commit 0013e84

File tree

75 files changed

+3330
-143
lines changed

Some content is hidden

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

75 files changed

+3330
-143
lines changed

Modules/Sources/Fakes/Networking.generated.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -814,6 +814,7 @@ extension Networking.POSProduct {
814814
manageStock: .fake(),
815815
stockQuantity: .fake(),
816816
stockStatusKey: .fake(),
817+
statusKey: .fake(),
817818
variationIDs: .fake()
818819
)
819820
}

Modules/Sources/Networking/Model/Copiable/Models+Copiable.generated.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1371,6 +1371,7 @@ extension Networking.POSProduct {
13711371
manageStock: CopiableProp<Bool> = .copy,
13721372
stockQuantity: NullableCopiableProp<Decimal> = .copy,
13731373
stockStatusKey: CopiableProp<String> = .copy,
1374+
statusKey: CopiableProp<String> = .copy,
13741375
variationIDs: CopiableProp<[Int64]> = .copy
13751376
) -> Networking.POSProduct {
13761377
let siteID = siteID ?? self.siteID
@@ -1389,6 +1390,7 @@ extension Networking.POSProduct {
13891390
let manageStock = manageStock ?? self.manageStock
13901391
let stockQuantity = stockQuantity ?? self.stockQuantity
13911392
let stockStatusKey = stockStatusKey ?? self.stockStatusKey
1393+
let statusKey = statusKey ?? self.statusKey
13921394
let variationIDs = variationIDs ?? self.variationIDs
13931395

13941396
return Networking.POSProduct(
@@ -1408,6 +1410,7 @@ extension Networking.POSProduct {
14081410
manageStock: manageStock,
14091411
stockQuantity: stockQuantity,
14101412
stockStatusKey: stockStatusKey,
1413+
statusKey: statusKey,
14111414
variationIDs: variationIDs
14121415
)
14131416
}

Modules/Sources/Networking/Model/POSProduct.swift

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,12 @@ public struct POSProduct: Codable, Equatable, GeneratedCopiable, GeneratedFakeab
4343
public let stockQuantity: Decimal?
4444
public let stockStatusKey: String
4545

46+
public let statusKey: String
47+
48+
public var productStatus: ProductStatus {
49+
return ProductStatus(rawValue: statusKey)
50+
}
51+
4652
public let variationIDs: [Int64]
4753

4854
public init(siteID: Int64,
@@ -61,6 +67,7 @@ public struct POSProduct: Codable, Equatable, GeneratedCopiable, GeneratedFakeab
6167
manageStock: Bool,
6268
stockQuantity: Decimal?,
6369
stockStatusKey: String,
70+
statusKey: String,
6471
variationIDs: [Int64]) {
6572
self.siteID = siteID
6673
self.productID = productID
@@ -85,6 +92,8 @@ public struct POSProduct: Codable, Equatable, GeneratedCopiable, GeneratedFakeab
8592
self.stockQuantity = stockQuantity
8693
self.stockStatusKey = stockStatusKey
8794

95+
self.statusKey = statusKey
96+
8897
self.variationIDs = variationIDs
8998
}
9099

@@ -129,6 +138,8 @@ public struct POSProduct: Codable, Equatable, GeneratedCopiable, GeneratedFakeab
129138
let stockQuantity = container.failsafeDecodeIfPresent(decimalForKey: .stockQuantity)
130139
let stockStatusKey = try container.decode(String.self, forKey: .stockStatusKey)
131140

141+
let statusKey = try container.decode(String.self, forKey: .statusKey)
142+
132143
let variationIDs = try container.decodeIfPresent([Int64].self, forKey: .variationIDs) ?? []
133144

134145
self.init(siteID: siteID,
@@ -147,6 +158,7 @@ public struct POSProduct: Codable, Equatable, GeneratedCopiable, GeneratedFakeab
147158
manageStock: manageStock,
148159
stockQuantity: stockQuantity,
149160
stockStatusKey: stockStatusKey,
161+
statusKey: statusKey,
150162
variationIDs: variationIDs)
151163
}
152164

@@ -180,6 +192,7 @@ private extension POSProduct {
180192
case manageStock = "manage_stock"
181193
case stockQuantity = "stock_quantity"
182194
case stockStatusKey = "stock_status"
195+
case statusKey = "status"
183196
case variationIDs = "variations"
184197
}
185198
}

Modules/Sources/Networking/Model/Product/ProductStatus.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ public enum ProductStatus: Codable, Hashable, GeneratedFakeable {
1010
case privateStatus // `private` is a reserved keyword
1111
case autoDraft
1212
case importing // used for placeholder products from a product import or template
13+
case trash
1314
case custom(String) // in case there are extensions modifying product statuses
1415
}
1516

@@ -34,6 +35,8 @@ extension ProductStatus: RawRepresentable {
3435
self = .autoDraft
3536
case Keys.importing:
3637
self = .importing
38+
case Keys.trash:
39+
self = .trash
3740
default:
3841
self = .custom(rawValue)
3942
}
@@ -49,6 +52,7 @@ extension ProductStatus: RawRepresentable {
4952
case .privateStatus: return Keys.privateStatus
5053
case .autoDraft: return Keys.autoDraft
5154
case .importing: return Keys.importing
55+
case .trash: return Keys.trash
5256
case .custom(let payload): return payload
5357
}
5458
}
@@ -69,6 +73,8 @@ extension ProductStatus: RawRepresentable {
6973
return "Auto Draft" // We don't need to localize this now.
7074
case .importing:
7175
return "Importing" // We don't need to localize this now.
76+
case .trash:
77+
return "Trash" // We don't need to localize this now.
7278
case .custom(let payload):
7379
return payload // unable to localize at runtime.
7480
}
@@ -85,4 +91,5 @@ private enum Keys {
8591
static let privateStatus = "private"
8692
static let autoDraft = "auto-draft"
8793
static let importing = "importing"
94+
static let trash = "trash"
8895
}

Modules/Sources/Networking/Model/Product/ProductType.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,5 +105,5 @@ private enum Keys {
105105
static let variableSubscription = "variable-subscription"
106106
static let bundle = "bundle"
107107
static let composite = "composite"
108-
static let booking = "booking"
108+
static let booking = "bookable-service"
109109
}

Modules/Sources/Networking/Remote/BookingsRemote.swift

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ public protocol BookingsRemoteProtocol {
2020
from siteID: Int64,
2121
bookingID: Int64,
2222
attendanceStatus: BookingAttendanceStatus?,
23-
bookingStatus: BookingStatus?
23+
bookingStatus: BookingStatus?,
24+
note: String?
2425
) async throws -> Booking?
2526

2627
func fetchResource(resourceID: Int64,
@@ -158,7 +159,8 @@ public final class BookingsRemote: Remote, BookingsRemoteProtocol {
158159
from siteID: Int64,
159160
bookingID: Int64,
160161
attendanceStatus: BookingAttendanceStatus?,
161-
bookingStatus: BookingStatus?
162+
bookingStatus: BookingStatus?,
163+
note: String?
162164
) async throws -> Booking? {
163165
let path = "\(Path.bookings)/\(bookingID)"
164166
var parameters: [String: String] = [:]
@@ -171,6 +173,10 @@ public final class BookingsRemote: Remote, BookingsRemoteProtocol {
171173
parameters[ParameterKey.status] = bookingStatus.rawValue
172174
}
173175

176+
if let note {
177+
parameters[ParameterKey.note] = note
178+
}
179+
174180
let request = JetpackRequest(
175181
wooApiVersion: .wcBookings,
176182
method: .put,
@@ -265,5 +271,6 @@ public extension BookingsRemote {
265271
static let attendanceStatus = "attendance_status"
266272
static let paymentStatus = "booking_status" // to be updated later when payment filtering is supported
267273
static let status: String = "status"
274+
static let note: String = "note"
268275
}
269276
}

Modules/Sources/Networking/Remote/POSCatalogSyncRemote.swift

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,11 @@ public protocol POSCatalogSyncRemoteProtocol {
99
/// - modifiedAfter: Only products modified after this date will be returned.
1010
/// - siteID: Site ID to load products from.
1111
/// - pageNumber: Page number for pagination.
12+
/// - includeStatus: Optional status to include (e.g., "trash" to fetch trashed products).
1213
/// - Returns: Paginated list of POS products.
1314
// TODO - remove the periphery ignore comment when the incremental sync is integrated with POS.
1415
// periphery:ignore
15-
func loadProducts(modifiedAfter: Date, siteID: Int64, pageNumber: Int) async throws -> PagedItems<POSProduct>
16+
func loadProducts(modifiedAfter: Date, siteID: Int64, pageNumber: Int, includeStatus: String?) async throws -> PagedItems<POSProduct>
1617

1718
/// Loads POS product variations modified after the specified date for incremental sync.
1819
///
@@ -109,18 +110,23 @@ public class POSCatalogSyncRemote: Remote, POSCatalogSyncRemoteProtocol {
109110
/// - modifiedAfter: Only products modified after this date will be returned.
110111
/// - siteID: Site ID to load products from.
111112
/// - pageNumber: Page number for pagination.
113+
/// - includeStatus: Optional status to include (e.g., "trash" to fetch trashed products).
112114
/// - Returns: Paginated list of POS products.
113115
///
114-
public func loadProducts(modifiedAfter: Date, siteID: Int64, pageNumber: Int)
116+
public func loadProducts(modifiedAfter: Date, siteID: Int64, pageNumber: Int, includeStatus: String? = nil)
115117
async throws -> PagedItems<POSProduct> {
116118
let path = Path.products
117-
let parameters = [
119+
var parameters: [String: String] = [
118120
ParameterKey.modifiedAfter: dateFormatter.string(from: modifiedAfter),
119121
ParameterKey.page: String(pageNumber),
120122
ParameterKey.perPage: String(Constants.defaultPageSize),
121123
ParameterKey.fields: POSProduct.requestFields.joined(separator: ",")
122124
]
123125

126+
if let includeStatus = includeStatus {
127+
parameters[ParameterKey.includeStatus] = includeStatus
128+
}
129+
124130
let request = JetpackRequest(
125131
wooApiVersion: .mark3,
126132
method: .get,
@@ -399,6 +405,7 @@ private extension POSCatalogSyncRemote {
399405
static let fields = "_fields"
400406
static let fullSyncFields = "fields"
401407
static let forceGenerate = "force_generate"
408+
static let includeStatus = "include_status"
402409
}
403410

404411
enum Path {

Modules/Sources/NetworkingCore/Network/NetworkError.swift

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,14 +51,24 @@ public enum NetworkError: Error, Equatable {
5151
}
5252

5353
/// Content of the `code` field in the response if available
54-
var errorCode: String? {
54+
public var errorCode: String? {
5555
guard let response else { return nil }
5656
let decoder = JSONDecoder()
5757
guard let decodedResponse = try? decoder.decode(NetworkErrorResponse.self, from: response) else {
5858
return nil
5959
}
6060
return decodedResponse.code
6161
}
62+
63+
/// Content of the `data` field in the response if available
64+
public var errorData: [String: AnyDecodable]? {
65+
guard let response else { return nil }
66+
let decoder = JSONDecoder()
67+
guard let decodedResponse = try? decoder.decode(NetworkErrorResponse.self, from: response) else {
68+
return nil
69+
}
70+
return decodedResponse.data
71+
}
6272
}
6373

6474

@@ -134,6 +144,7 @@ extension NetworkError: CustomStringConvertible {
134144

135145
struct NetworkErrorResponse: Decodable {
136146
let code: String?
147+
let data: [String: AnyDecodable]?
137148

138149
init(from decoder: Decoder) throws {
139150
let container = try decoder.container(keyedBy: CodingKeys.self)
@@ -144,12 +155,14 @@ struct NetworkErrorResponse: Decodable {
144155
}
145156
return try container.decodeIfPresent(String.self, forKey: .code)
146157
}()
158+
self.data = try container.decodeIfPresent([String: AnyDecodable].self, forKey: .data)
147159
}
148160

149161
/// Coding Keys
150162
///
151163
private enum CodingKeys: String, CodingKey {
152164
case error
153165
case code
166+
case data
154167
}
155168
}

Modules/Sources/PointOfSale/Analytics/WooAnalyticsEvent+PointOfSale.swift

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ extension WooAnalyticsEvent {
4444
static let listPosition = "list_position"
4545
static let daysSinceCreated = "days_since_created"
4646
static let pageNumber = "page_number"
47+
static let reason = "reason"
48+
static let syncStrategy = "sync_strategy"
4749
}
4850

4951
/// Source of the event where the event is triggered
@@ -457,6 +459,47 @@ extension WooAnalyticsEvent {
457459
static func ordersListLoaded() -> WooAnalyticsEvent {
458460
WooAnalyticsEvent(statName: .ordersListLoaded, properties: [:])
459461
}
462+
463+
// MARK: - Checkout Outdated Item Detection Events
464+
465+
static func checkoutOutdatedItemDetectedScreenShown(
466+
reason: String,
467+
syncStrategy: String
468+
) -> WooAnalyticsEvent {
469+
WooAnalyticsEvent(
470+
statName: .pointOfSaleCheckoutOutdatedItemDetectedScreenShown,
471+
properties: [
472+
Key.reason: reason,
473+
Key.syncStrategy: syncStrategy
474+
]
475+
)
476+
}
477+
478+
static func checkoutOutdatedItemDetectedEditOrderTapped(
479+
reason: String,
480+
syncStrategy: String
481+
) -> WooAnalyticsEvent {
482+
WooAnalyticsEvent(
483+
statName: .pointOfSaleCheckoutOutdatedItemDetectedEditOrderTapped,
484+
properties: [
485+
Key.reason: reason,
486+
Key.syncStrategy: syncStrategy
487+
]
488+
)
489+
}
490+
491+
static func checkoutOutdatedItemDetectedRemoveTapped(
492+
reason: String,
493+
syncStrategy: String
494+
) -> WooAnalyticsEvent {
495+
WooAnalyticsEvent(
496+
statName: .pointOfSaleCheckoutOutdatedItemDetectedRemoveTapped,
497+
properties: [
498+
Key.reason: reason,
499+
Key.syncStrategy: syncStrategy
500+
]
501+
)
502+
}
460503
}
461504
}
462505

Modules/Sources/PointOfSale/Controllers/PointOfSaleOrderController.swift

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import Observation
33
import protocol Experiments.FeatureFlagService
44
import class WooFoundation.VersionHelpers
55
import protocol Yosemite.POSOrderServiceProtocol
6+
import class Yosemite.POSOrderService
67
import protocol Yosemite.POSReceiptServiceProtocol
78
import protocol Yosemite.PluginsServiceProtocol
89
import protocol Yosemite.PaymentCaptureCelebrationProtocol
@@ -11,6 +12,7 @@ import struct Yosemite.Order
1112
import struct Yosemite.POSCart
1213
import struct Yosemite.POSCartItem
1314
import struct Yosemite.POSCoupon
15+
import struct Yosemite.POSVariation
1416
import struct Yosemite.CouponsError
1517
import enum Yosemite.OrderAction
1618
import enum Yosemite.OrderUpdateField
@@ -20,7 +22,6 @@ import class WooFoundation.CurrencySettings
2022
import class Yosemite.PluginsService
2123
import enum WooFoundation.CurrencyCode
2224
import protocol WooFoundation.Analytics
23-
import enum Alamofire.AFError
2425
import class Yosemite.OrderTotalsCalculator
2526
import struct WooFoundation.WooAnalyticsEvent
2627
import protocol WooFoundationCore.WooAnalyticsEventPropertyType
@@ -188,17 +189,25 @@ private extension PointOfSaleOrderController {
188189

189190
private extension PointOfSaleOrderController {
190191
func orderStateError(from error: Error) -> PointOfSaleOrderState.OrderStateError {
191-
if let couponsError = CouponsError(underlyingError: error) {
192+
// Check for missing products error first
193+
if case .missingProductsInOrder(let missingItems) = error as? POSOrderService.POSOrderServiceError {
194+
let missingProductInfo = missingItems.map {
195+
PointOfSaleOrderState.OrderStateError.MissingProductInfo(
196+
productID: $0.productID,
197+
variationID: $0.variationID,
198+
name: $0.name
199+
)
200+
}
201+
return .missingProducts(missingProductInfo)
202+
}
203+
else if let couponsError = CouponsError(underlyingError: error) {
192204
return .invalidCoupon(couponsError.message)
193-
} else if let afErrorDescription = (error as? AFError)?.underlyingError?.localizedDescription {
194-
return .other(afErrorDescription)
195205
} else {
196206
return .other(error.localizedDescription)
197207
}
198208
}
199209
}
200210

201-
202211
// This is named to note that it is for use within the AggregateModel and OrderController.
203212
// Conversely, PointOfSaleOrderState is available to the Views, as it doesn't include the Order.
204213
enum PointOfSaleInternalOrderState {
@@ -261,6 +270,8 @@ private extension PointOfSaleOrderController {
261270

262271
if let _ = CouponsError(underlyingError: error) {
263272
errorType = .invalidCoupon
273+
} else if case .missingProductsInOrder = error as? POSOrderService.POSOrderServiceError {
274+
errorType = .missingProducts
264275
}
265276

266277
analytics.track(event: WooAnalyticsEvent.Orders.orderCreationFailed(
@@ -290,9 +301,9 @@ private extension WooAnalyticsEvent {
290301
// MARK: - Order Creation Events
291302

292303
/// Matches errors on Android for consistency
293-
/// Only coupon tracking is relevant for now
294304
enum OrderCreationErrorType: String {
295305
case invalidCoupon = "INVALID_COUPON"
306+
case missingProducts = "MISSING_PRODUCTS"
296307
}
297308

298309
static func orderCreationFailed(

0 commit comments

Comments
 (0)