Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
cc339f2
Add Email receipt action with action section to point of sale order d…
staskus Sep 9, 2025
1bf4399
Conditional actions presentation
staskus Sep 9, 2025
e4e58e6
Extract receipt related logic to POSReceiptController
staskus Sep 9, 2025
fcc4550
Move receipt-related tests to POSReceiptControllerTests
staskus Sep 9, 2025
c2dfab9
Update POSSendReceiptView to allow injecting receipt-sending logic
staskus Sep 9, 2025
acb223b
Update POSTabCoordinator dependency injection
staskus Sep 9, 2025
68b8aca
Create updatePOSOrderEmail in OrdersRemote for targeted email update …
staskus Sep 9, 2025
c60ac56
Remove dependency of Order from POSOrderService updatePOSOrder email
staskus Sep 9, 2025
3be0b8c
Remove Order dependency from POSReceiptService sendReceipt method
staskus Sep 9, 2025
348d86e
Update POSReceiptController to rely on orderID and not Order
staskus Sep 9, 2025
134e27b
Send receipt from PointOfSaleOrderDetailsView
staskus Sep 9, 2025
c6c2999
Allow modifying posHeaderBackButton through a view modifier
staskus Sep 9, 2025
5f87a42
Speed up POS Receipt Sending - only update order when sending with le…
staskus Sep 9, 2025
76e692f
Remove unused code
staskus Sep 9, 2025
9b2d86b
Only modify back button icon from POSPageHeaderView
staskus Sep 10, 2025
b39f801
Merge branch 'trunk' into woomob-1140-woo-poshistorical-orders-order-…
staskus Sep 11, 2025
f259d64
Make POSReceiptControllerTests properties private
staskus Sep 11, 2025
ac9435a
Fix OrdersRemote formatting
staskus Sep 11, 2025
b16a96b
Fix POSOrderServiceTests formatting
staskus Sep 11, 2025
199d2f4
Remove import Observation from POSReceiptController
staskus Sep 11, 2025
95f5b26
Name posHeaderBackButtonIcon with systemName
staskus Sep 11, 2025
4a397e7
Make error throw from mocks more reusable
staskus Sep 11, 2025
274c455
Rename POSReceiptController to POSReceiptSender
staskus Sep 11, 2025
9504721
Use site timezone for order date formatting
staskus Sep 11, 2025
9c6c91e
Hold orderListModel in PointOfSaleEntryPointView as aggregateModel
staskus Sep 11, 2025
4ae1b3c
Update PointOfSaleEntryPointView.swift
staskus Sep 11, 2025
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
10 changes: 7 additions & 3 deletions Modules/Sources/Networking/Remote/ReceiptRemote.swift
Original file line number Diff line number Diff line change
Expand Up @@ -53,27 +53,31 @@ public final class ReceiptRemote: Remote {
/// - Parameters:
/// - siteID: Site which hosts the Order.
/// - orderID: ID of the order that the receipt is associated to.
public func sendPOSReceipt(siteID: Int64, orderID: Int64) async throws {
public func sendPOSReceipt(siteID: Int64, orderID: Int64, emailAddress: String) async throws {
let sendEmailPath = "\(Constants.ordersPath)/\(orderID)/\(Constants.actionsPath)/send_email"
let sendEmailRequest = JetpackRequest(wooApiVersion: .mark3,
method: .post,
siteID: siteID,
path: sendEmailPath,
parameters: [
ParameterKeys.templateID: POSConstants.receiptTemplateID
ParameterKeys.templateID: POSConstants.receiptTemplateID,
ParameterKeys.email: emailAddress,
ParameterKeys.forceEmailUpdate: true
],
availableAsRESTRequest: true)
try await enqueue(sendEmailRequest)
}
}

extension ReceiptRemote: POSReceiptsRemoteProtocol { }
extension ReceiptRemote: POSReceiptsRemoteProtocol {}

private extension ReceiptRemote {
enum ParameterKeys {
static let expirationDays: String = "expiration_days"
static let forceRegenerate: String = "force_new"
static let templateID: String = "template_id"
static let forceEmailUpdate: String = "force_email_update"
static let email: String = "email"
}

enum Constants {
Expand Down
18 changes: 18 additions & 0 deletions Modules/Sources/NetworkingCore/Remote/OrdersRemote.swift
Original file line number Diff line number Diff line change
Expand Up @@ -451,6 +451,24 @@ extension OrdersRemote: POSOrdersRemoteProtocol {
}
}

public func updatePOSOrderEmail(siteID: Int64, orderID: Int64, emailAddress: String) async throws {
let parameters: [String: Any] = [
"billing": [
"email": emailAddress
]
Comment on lines +456 to +458
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just wondering, does this affect other billing fields than the email field like the address? I'm guessing/hoping it doesn't. If it does, we might want to stay with the previous order remote update.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, it doesn't affect other billing fields. I tested. It targets only the email address.

]

let path = "\(Constants.ordersPath)/\(orderID)"
let request = JetpackRequest(wooApiVersion: .mark3,
method: .post,
siteID: siteID,
path: path,
parameters: parameters,
availableAsRESTRequest: true)

try await enqueue(request)
}

public func loadPOSOrders(siteID: Int64, pageNumber: Int, pageSize: Int) async throws -> PagedItems<Order> {
let parameters: [String: Any] = [
ParameterKeys.page: String(pageNumber),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import Foundation

public protocol POSReceiptsRemoteProtocol {
func sendReceipt(siteID: Int64, orderID: Int64) async throws
func sendPOSReceipt(siteID: Int64, orderID: Int64) async throws
func sendPOSReceipt(siteID: Int64, orderID: Int64, emailAddress: String) async throws
}

public protocol POSOrdersRemoteProtocol {
Expand All @@ -11,6 +11,10 @@ public protocol POSOrdersRemoteProtocol {
cashPaymentChangeDueAmount: String?,
fields: [OrdersRemote.UpdateOrderField]) async throws -> Order

func updatePOSOrderEmail(siteID: Int64,
orderID: Int64,
emailAddress: String) async throws

func createPOSOrder(siteID: Int64,
order: Order,
fields: [OrdersRemote.CreateOrderField]) async throws -> Order
Expand Down
18 changes: 3 additions & 15 deletions Modules/Sources/Yosemite/Tools/POS/POSOrderService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ public protocol POSOrderServiceProtocol {
/// - cart: Cart with different types of items and quantities.
/// - Returns: Order from the remote sync.
func syncOrder(cart: POSCart, currency: CurrencyCode) async throws -> Order
func updatePOSOrder(order: Order, recipientEmail: String) async throws
func updatePOSOrder(orderID: Int64, recipientEmail: String) async throws
func markOrderAsCompletedWithCashPayment(order: Order, changeDueAmount: String?) async throws
}

Expand Down Expand Up @@ -46,20 +46,9 @@ public final class POSOrderService: POSOrderServiceProtocol {
return try await ordersRemote.createPOSOrder(siteID: siteID, order: order, fields: [.items, .status, .currency, .couponLines])
}

public func updatePOSOrder(order: Order, recipientEmail: String) async throws {
guard order.billingAddress?.email == nil || order.billingAddress?.email == "" else {
throw POSOrderServiceError.emailAlreadySet
}
let updatedBillingAddress = order.billingAddress?.copy(email: recipientEmail)
let updatedOrder = order.copy(billingAddress: updatedBillingAddress)

public func updatePOSOrder(orderID: Int64, recipientEmail: String) async throws {
do {
let _ = try await ordersRemote.updatePOSOrder(
siteID: siteID,
order: updatedOrder,
cashPaymentChangeDueAmount: nil,
fields: [.billingAddress]
)
try await ordersRemote.updatePOSOrderEmail(siteID: siteID, orderID: orderID, emailAddress: recipientEmail)
} catch {
throw POSOrderServiceError.updateOrderFailed
}
Expand Down Expand Up @@ -105,7 +94,6 @@ private extension Order {

private extension POSOrderService {
enum POSOrderServiceError: Error {
case emailAlreadySet
case updateOrderFailed
}
}
Expand Down
8 changes: 4 additions & 4 deletions Modules/Sources/Yosemite/Tools/POS/POSReceiptService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import SwiftUI
import Networking

public protocol POSReceiptServiceProtocol {
func sendReceipt(order: Order, recipientEmail: String, isEligibleForPOSReceipt: Bool) async throws
func sendReceipt(orderID: Int64, recipientEmail: String, isEligibleForPOSReceipt: Bool) async throws
}

public final class POSReceiptService: POSReceiptServiceProtocol {
Expand All @@ -25,12 +25,12 @@ public final class POSReceiptService: POSReceiptServiceProtocol {
self.receiptsRemote = receiptsRemote
}

public func sendReceipt(order: Yosemite.Order, recipientEmail: String, isEligibleForPOSReceipt: Bool) async throws {
public func sendReceipt(orderID: Int64, recipientEmail: String, isEligibleForPOSReceipt: Bool) async throws {
do {
if isEligibleForPOSReceipt {
try await receiptsRemote.sendPOSReceipt(siteID: siteID, orderID: order.orderID)
try await receiptsRemote.sendPOSReceipt(siteID: siteID, orderID: orderID, emailAddress: recipientEmail)
} else {
try await receiptsRemote.sendReceipt(siteID: siteID, orderID: order.orderID)
try await receiptsRemote.sendReceipt(siteID: siteID, orderID: orderID)
}
} catch {
throw POSReceiptServiceError.sendReceiptFailed(underlyingError: error as NSError)
Expand Down
8 changes: 6 additions & 2 deletions Modules/Tests/NetworkingTests/Remote/ReceiptRemoteTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -129,29 +129,33 @@ final class ReceiptRemoteTests: XCTestCase {
// Given
let remote = ReceiptRemote(network: network)
let posReceiptTemplateID = "customer_pos_completed_order"
let testEmail = "[email protected]"

network.simulateResponse(
requestUrlSuffix: "orders/\(sampleOrderID)/actions/send_email",
filename: "orders-actions-send-email-success"
)

// When
try await remote.sendPOSReceipt(siteID: sampleSiteID, orderID: sampleOrderID)
try await remote.sendPOSReceipt(siteID: sampleSiteID, orderID: sampleOrderID, emailAddress: testEmail)

// Then the send email request was made with correct parameters.
let sendEmailRequest = try XCTUnwrap(network.requestsForResponseData.last as? JetpackRequest)
XCTAssertEqual(sendEmailRequest.method, .post)
XCTAssertEqual(sendEmailRequest.path, "orders/\(sampleOrderID)/actions/send_email")
XCTAssertEqual(sendEmailRequest.parameters["template_id"] as? String, posReceiptTemplateID)
XCTAssertEqual(sendEmailRequest.parameters["email"] as? String, testEmail)
XCTAssertEqual(sendEmailRequest.parameters["force_email_update"] as? Bool, true)
}

func test_sendPOSReceipt_when_no_reponse_exist_throws_error() async {
// Given
let remote = ReceiptRemote(network: network)
let testEmail = "[email protected]"

await assertThrowsError({
// When
try await remote.sendPOSReceipt(siteID: sampleSiteID, orderID: sampleOrderID)
try await remote.sendPOSReceipt(siteID: sampleSiteID, orderID: sampleOrderID, emailAddress: testEmail)
}, errorAssert: { error in
// Then
return error is NetworkError
Expand Down
19 changes: 19 additions & 0 deletions Modules/Tests/YosemiteTests/Mocks/MockPOSOrdersRemote.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,25 @@ final class MockPOSOrdersRemote: POSOrdersRemoteProtocol {
}
}

var updatePOSOrderEmailCalled: Bool = false
var spyUpdatePOSOrderEmailSiteID: Int64?
var spyUpdatePOSOrderEmailOrderID: Int64?
var spyUpdatePOSOrderEmailAddress: String?
var updatePOSOrderEmailResult: Result<Void, Error> = .success(())

func updatePOSOrderEmail(siteID: Int64, orderID: Int64, emailAddress: String) async throws {
updatePOSOrderEmailCalled = true
spyUpdatePOSOrderEmailSiteID = siteID
spyUpdatePOSOrderEmailOrderID = orderID
spyUpdatePOSOrderEmailAddress = emailAddress
switch updatePOSOrderEmailResult {
case .success:
return
case .failure(let error):
throw error
}
}

var createPOSOrderCalled: Bool = false
var spyCreatePOSOrder: Order?
var spyCreatePOSOrderFields: [OrdersRemote.CreateOrderField]?
Expand Down
10 changes: 9 additions & 1 deletion Modules/Tests/YosemiteTests/Mocks/MockPOSReceiptsRemote.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ final class MockPOSReceiptsRemote: POSReceiptsRemoteProtocol {
var sendPOSReceiptCalled = false
var spySiteID: Int64?
var spyOrderID: Int64?
var spyEmail: String?
var shouldThrowError: Error?

func sendReceipt(siteID: Int64, orderID: Int64) async throws {
Expand All @@ -17,7 +18,14 @@ final class MockPOSReceiptsRemote: POSReceiptsRemoteProtocol {
}
}

func sendPOSReceipt(siteID: Int64, orderID: Int64) async throws {
func sendPOSReceipt(siteID: Int64, orderID: Int64, emailAddress: String) async throws {
sendPOSReceiptCalled = true
spySiteID = siteID
spyOrderID = orderID
spyEmail = emailAddress

if let shouldThrowError {
throw shouldThrowError
}
}
}
29 changes: 29 additions & 0 deletions Modules/Tests/YosemiteTests/Tools/POS/POSOrderServiceTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,35 @@ struct POSOrderServiceTests {
return true
})
}

@Test func updatePOSOrder_calls_remote_updatePOSOrderEmail_with_correct_parameters() async throws {
// Given
let siteID: Int64 = 123
let orderID: Int64 = 456
let recipientEmail = "[email protected]"

// When
try await sut.updatePOSOrder(orderID: orderID, recipientEmail: recipientEmail)

// Then
#expect(mockOrdersRemote.updatePOSOrderEmailCalled == true)
#expect(mockOrdersRemote.spyUpdatePOSOrderEmailSiteID == siteID)
#expect(mockOrdersRemote.spyUpdatePOSOrderEmailOrderID == orderID)
#expect(mockOrdersRemote.spyUpdatePOSOrderEmailAddress == recipientEmail)
}

@Test func updatePOSOrder_throws_error_when_remote_call_fails() async throws {
// Given
mockOrdersRemote.updatePOSOrderEmailResult = .failure(NSError(domain: "", code: 0))

// When/Then
await #expect(performing: {
try await sut.updatePOSOrder(orderID: 456, recipientEmail: "[email protected]")
}, throws: { _ in
// The actual error `POSOrderServiceError.updateOrderFailed` is private, thus we cannot check against the exact error.
return true
})
}
}

private func makePOSCartItem(
Expand Down
13 changes: 10 additions & 3 deletions Modules/Tests/YosemiteTests/Tools/POS/POSReceiptServiceTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ struct POSReceiptServiceTests {
let email = "[email protected]"

// When
try await sut.sendReceipt(order: order, recipientEmail: email, isEligibleForPOSReceipt: false)
try await sut.sendReceipt(orderID: order.orderID, recipientEmail: email, isEligibleForPOSReceipt: false)

// Then
#expect(receiptsRemote.sendReceiptCalled)
Expand All @@ -36,7 +36,7 @@ struct POSReceiptServiceTests {

// When/Then
do {
try await sut.sendReceipt(order: order, recipientEmail: "[email protected]", isEligibleForPOSReceipt: false)
try await sut.sendReceipt(orderID: order.orderID, recipientEmail: "[email protected]", isEligibleForPOSReceipt: false)
XCTFail("Expected error to be thrown")
} catch {
guard case POSReceiptService.POSReceiptServiceError.sendReceiptFailed = error else {
Expand All @@ -48,10 +48,17 @@ struct POSReceiptServiceTests {

@Test
func sendReceipt_calls_remote_when_isEligibleForPOSReceipt_is_true() async throws {
// Given
let email = "[email protected]"
let orderID: Int64 = 789

// When
try await sut.sendReceipt(order: Order.fake(), recipientEmail: "[email protected]", isEligibleForPOSReceipt: true)
try await sut.sendReceipt(orderID: orderID, recipientEmail: email, isEligibleForPOSReceipt: true)

// Then
#expect(receiptsRemote.sendPOSReceiptCalled)
#expect(receiptsRemote.spySiteID == 123)
#expect(receiptsRemote.spyOrderID == orderID)
#expect(receiptsRemote.spyEmail == email)
}
}
Loading