-
Notifications
You must be signed in to change notification settings - Fork 121
Bookings: Update Networking layer to support fetching Bookings #16162
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
a52d5d3
17b6355
b239a5b
a26c237
6705358
0e16d10
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,187 @@ | ||
| // periphery:ignore:all | ||
| import Codegen | ||
| import Foundation | ||
|
|
||
| /// Represents a Booking Entity. | ||
| /// | ||
| public struct Booking: Codable, GeneratedCopiable, Equatable, GeneratedFakeable { | ||
| public let siteID: Int64 | ||
| public let bookingID: Int64 | ||
| public let allDay: Bool | ||
| public let cost: String | ||
| public let customerID: Int64 | ||
| public let dateCreated: Date | ||
| public let dateModified: Date | ||
| public let endDate: Date | ||
| public let googleCalendarEventID: String? | ||
| public let orderID: Int64 | ||
| public let orderItemID: Int64 | ||
| public let parentID: Int64 | ||
| public let productID: Int64 | ||
| public let resourceID: Int64 | ||
| public let startDate: Date | ||
| public let statusKey: String | ||
| public let localTimezone: String | ||
|
|
||
| /// Computed Properties | ||
| /// | ||
| public var bookingStatus: BookingStatus { | ||
| return BookingStatus(rawValue: statusKey) ?? .unknown | ||
| } | ||
|
|
||
| /// Booking struct initializer. | ||
| /// | ||
| public init(siteID: Int64, | ||
| bookingID: Int64, | ||
| allDay: Bool, | ||
| cost: String, | ||
| customerID: Int64, | ||
| dateCreated: Date, | ||
| dateModified: Date, | ||
| endDate: Date, | ||
| googleCalendarEventID: String?, | ||
| orderID: Int64, | ||
| orderItemID: Int64, | ||
| parentID: Int64, | ||
| productID: Int64, | ||
| resourceID: Int64, | ||
| startDate: Date, | ||
| statusKey: String, | ||
| localTimezone: String) { | ||
| self.siteID = siteID | ||
| self.bookingID = bookingID | ||
| self.allDay = allDay | ||
| self.cost = cost | ||
| self.customerID = customerID | ||
| self.dateCreated = dateCreated | ||
| self.dateModified = dateModified | ||
| self.endDate = endDate | ||
| self.googleCalendarEventID = googleCalendarEventID | ||
| self.orderID = orderID | ||
| self.orderItemID = orderItemID | ||
| self.parentID = parentID | ||
| self.productID = productID | ||
| self.resourceID = resourceID | ||
| self.startDate = startDate | ||
| self.statusKey = statusKey | ||
| self.localTimezone = localTimezone | ||
| } | ||
|
|
||
| /// The public initializer for Booking. | ||
| /// | ||
| public init(from decoder: Decoder) throws { | ||
| guard let siteID = decoder.userInfo[.siteID] as? Int64 else { | ||
| throw BookingDecodingError.missingSiteID | ||
| } | ||
|
|
||
| let container = try decoder.container(keyedBy: CodingKeys.self) | ||
|
|
||
| let bookingID = try container.decode(Int64.self, forKey: .bookingID) | ||
| let allDay = try container.decode(Bool.self, forKey: .allDay) | ||
|
|
||
| // Cost may come as string or number | ||
| let cost = container.failsafeDecodeIfPresent(targetType: String.self, | ||
| forKey: .cost, | ||
| alternativeTypes: [.decimal(transform: { NSDecimalNumber(decimal: $0).stringValue })]) ?? "" | ||
|
|
||
| let customerID = try container.decode(Int64.self, forKey: .customerID) | ||
| let dateCreated = Date(timeIntervalSince1970: try container.decode(Double.self, forKey: .dateCreated)) | ||
| let dateModified = Date(timeIntervalSince1970: try container.decode(Double.self, forKey: .dateModified)) | ||
| let endDate = Date(timeIntervalSince1970: try container.decode(Double.self, forKey: .endDate)) | ||
| let googleCalendarEventID = try container.decodeIfPresent(String.self, forKey: .googleCalendarEventID) | ||
| let orderID = try container.decode(Int64.self, forKey: .orderID) | ||
| let orderItemID = try container.decode(Int64.self, forKey: .orderItemID) | ||
| let parentID = try container.decode(Int64.self, forKey: .parentID) | ||
| let productID = try container.decode(Int64.self, forKey: .productID) | ||
| let resourceID = try container.decode(Int64.self, forKey: .resourceID) | ||
| let startDate = Date(timeIntervalSince1970: try container.decode(Double.self, forKey: .startDate)) | ||
| let statusKey = try container.decode(String.self, forKey: .statusKey) | ||
| let localTimezone = try container.decode(String.self, forKey: .localTimezone) | ||
|
|
||
| self.init(siteID: siteID, | ||
| bookingID: bookingID, | ||
| allDay: allDay, | ||
| cost: cost, | ||
| customerID: customerID, | ||
| dateCreated: dateCreated, | ||
| dateModified: dateModified, | ||
| endDate: endDate, | ||
| googleCalendarEventID: googleCalendarEventID, | ||
| orderID: orderID, | ||
| orderItemID: orderItemID, | ||
| parentID: parentID, | ||
| productID: productID, | ||
| resourceID: resourceID, | ||
| startDate: startDate, | ||
| statusKey: statusKey, | ||
| localTimezone: localTimezone) | ||
| } | ||
|
|
||
| public func encode(to encoder: Encoder) throws { | ||
| var container = encoder.container(keyedBy: CodingKeys.self) | ||
|
|
||
| try container.encode(bookingID, forKey: .bookingID) | ||
| try container.encode(allDay, forKey: .allDay) | ||
| try container.encode(cost, forKey: .cost) | ||
| try container.encode(customerID, forKey: .customerID) | ||
| try container.encode(dateCreated, forKey: .dateCreated) | ||
| try container.encode(dateModified, forKey: .dateModified) | ||
| try container.encode(endDate, forKey: .endDate) | ||
| try container.encode(googleCalendarEventID, forKey: .googleCalendarEventID) | ||
| try container.encode(orderID, forKey: .orderID) | ||
| try container.encode(orderItemID, forKey: .orderItemID) | ||
| try container.encode(parentID, forKey: .parentID) | ||
| try container.encode(productID, forKey: .productID) | ||
| try container.encode(resourceID, forKey: .resourceID) | ||
| try container.encode(startDate, forKey: .startDate) | ||
| try container.encode(statusKey, forKey: .statusKey) | ||
| try container.encode(localTimezone, forKey: .localTimezone) | ||
| } | ||
| } | ||
|
|
||
| /// Defines all of the Booking CodingKeys | ||
| /// | ||
| private extension Booking { | ||
|
|
||
| enum CodingKeys: String, CodingKey { | ||
| case bookingID = "id" | ||
| case allDay = "all_day" | ||
| case cost | ||
| case customerID = "customer_id" | ||
| case dateCreated = "date_created" | ||
| case dateModified = "date_modified" | ||
| case endDate = "end" | ||
| case googleCalendarEventID = "google_calendar_event_id" | ||
| case orderID = "order_id" | ||
| case orderItemID = "order_item_id" | ||
| case parentID = "parent_id" | ||
| case personCounts = "person_counts" | ||
| case productID = "product_id" | ||
| case resourceID = "resource_id" | ||
| case startDate = "start" | ||
| case statusKey = "status" | ||
| case localTimezone = "local_timezone" | ||
| } | ||
| } | ||
|
|
||
| // MARK: - Decoding Errors | ||
| // | ||
| enum BookingDecodingError: Error { | ||
| case missingSiteID | ||
| } | ||
|
|
||
| // MARK: - Supporting Types | ||
| // | ||
|
|
||
| /// Represents a Booking Status. | ||
| /// | ||
| public enum BookingStatus: String, CaseIterable { | ||
| case complete | ||
| case paid | ||
| case unpaid | ||
| case cancelled | ||
| case pendingConfirmation = "pending-confirmation" | ||
| case confirmed | ||
| case inCart = "in-cart" | ||
| case unknown | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,59 @@ | ||
| // periphery:ignore:all | ||
| import Foundation | ||
|
|
||
| /// Protocol for `BookingsRemote` mainly used for mocking. | ||
| /// | ||
| /// The required methods are intentionally incomplete. Feel free to add the other ones. | ||
| /// | ||
| public protocol BookingsRemoteProtocol { | ||
| func loadAllBookings(for siteID: Int64, | ||
| pageNumber: Int, | ||
| pageSize: Int) async throws -> [Booking] | ||
| } | ||
|
|
||
| /// Booking: Remote Endpoints | ||
| /// | ||
| public final class BookingsRemote: Remote, BookingsRemoteProtocol { | ||
|
|
||
| // MARK: - Bookings | ||
|
|
||
| /// Retrieves all of the `Bookings` available. | ||
| /// | ||
| /// - Parameters: | ||
| /// - siteID: Site for which we'll fetch remote bookings. | ||
| /// - pageNumber: Number of page that should be retrieved. | ||
| /// - pageSize: Number of bookings to be retrieved per page. | ||
| /// | ||
| public func loadAllBookings(for siteID: Int64, | ||
| pageNumber: Int = Default.pageNumber, | ||
| pageSize: Int = Default.pageSize) async throws -> [Booking] { | ||
| let parameters = [ | ||
| ParameterKey.page: String(pageNumber), | ||
| ParameterKey.perPage: String(pageSize) | ||
| ] | ||
|
|
||
| let path = Path.bookings | ||
| let request = JetpackRequest(wooApiVersion: .wcBookings, method: .get, siteID: siteID, path: path, parameters: parameters, availableAsRESTRequest: true) | ||
| let mapper = ListMapper<Booking>(siteID: siteID) | ||
|
|
||
| return try await enqueue(request, mapper: mapper) | ||
| } | ||
| } | ||
|
|
||
| // MARK: - Constants | ||
| // | ||
| public extension BookingsRemote { | ||
| enum Default { | ||
| public static let pageSize: Int = 25 | ||
| public static let pageNumber: Int = Remote.Default.firstPageNumber | ||
| } | ||
|
|
||
| private enum Path { | ||
| static let bookings = "bookings" | ||
| } | ||
|
|
||
| private enum ParameterKey { | ||
| static let page: String = "page" | ||
| static let perPage: String = "per_page" | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -50,6 +50,10 @@ public enum WooAPIVersion: String { | |
| /// | ||
| case wooShipping = "wcshipping/v1" | ||
|
|
||
| /// WooCommerce Bookings Plugin V1. | ||
| /// | ||
| case wcBookings = "wc-bookings/v2" | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is it
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We will have to use v2 for the MVP. There's a long thread about testing Bookings for the MVP in p1758101141913349-slack-C03L1NF1EA3, but tl;dr you can follow the steps:
This would allow you to test the v2 API. However, I was not able to overwrite the Bookings plugin in the garden sites I created so Jorge invited me to his site instead. Let me know if you need an invitation too. |
||
|
|
||
| /// Returns the path for the current API Version | ||
| /// | ||
| var path: String { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Was the
person_countsskipped on purpose? Not sure though what it could be used for.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I skipped it since I'm unsure about the structure (sometimes it's an array sometimes dictionary). Since we don't have a clear idea how to use it/what for, I figure we can add it later when needed. WDYT?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm good