fix: use backend-authoritative change amount for cash transactions
Instead of calculating change client-side (which doesn't account for balance applied, fees, etc.), the app now uses the Change value returned by the /tasks/complete.php endpoint after processing. Changes: - APIService.completeTask now returns CashCompletionResult with backend values - Added CashCompletionResult struct (cashReceived, orderTotal, change, fees, routing) - CashCollectionSheet shows confirmed backend change after completion - Added ratingRequired error case to APIError enum - Client-side estimate still shown as preview before confirmation
This commit is contained in:
parent
0639fe12c2
commit
46e008b20b
2 changed files with 82 additions and 7 deletions
|
|
@ -18,6 +18,7 @@ enum APIError: LocalizedError {
|
|||
case serverError(String)
|
||||
case unauthorized
|
||||
case networkError(String)
|
||||
case ratingRequired(customerUserId: Int)
|
||||
|
||||
var errorDescription: String? {
|
||||
switch self {
|
||||
|
|
@ -27,6 +28,7 @@ enum APIError: LocalizedError {
|
|||
case .serverError(let msg): return msg
|
||||
case .unauthorized: return "Unauthorized"
|
||||
case .networkError(let msg): return msg
|
||||
case .ratingRequired: return "Customer rating is required before completing this task"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -54,6 +56,22 @@ struct ChatMessagesResult {
|
|||
let chatClosed: Bool
|
||||
}
|
||||
|
||||
/// Backend-authoritative cash completion result.
|
||||
/// All monetary values are formatted strings (e.g. "12.50") from the server.
|
||||
struct CashCompletionResult {
|
||||
let cashReceived: String
|
||||
let orderTotal: String
|
||||
let change: String
|
||||
let customerFee: String?
|
||||
let businessFee: String?
|
||||
let cashRoutedTo: String // "worker" or "business"
|
||||
let balanceApplied: String?
|
||||
|
||||
var changeDollars: Double {
|
||||
Double(change) ?? 0
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - API Service
|
||||
|
||||
actor APIService {
|
||||
|
|
@ -472,7 +490,9 @@ actor APIService {
|
|||
return arr.map { WorkTask(json: $0) }
|
||||
}
|
||||
|
||||
func completeTask(taskId: Int, cashReceivedCents: Int? = nil, cancelOrder: Bool = false) async throws {
|
||||
/// Complete a task. Returns CashCompletionResult when cashReceivedCents is provided.
|
||||
@discardableResult
|
||||
func completeTask(taskId: Int, cashReceivedCents: Int? = nil, cancelOrder: Bool = false) async throws -> CashCompletionResult? {
|
||||
var payload: [String: Any] = [
|
||||
"TaskID": taskId,
|
||||
"UserID": userId ?? 0
|
||||
|
|
@ -485,8 +505,27 @@ actor APIService {
|
|||
}
|
||||
let json = try await postJSON("/tasks/complete.php", payload: payload)
|
||||
guard ok(json) else {
|
||||
throw APIError.serverError("Failed to complete task: \(err(json))")
|
||||
let errorMsg = err(json)
|
||||
if errorMsg == "rating_required" {
|
||||
let customerUserId = json["CustomerUserID"] as? Int ?? 0
|
||||
throw APIError.ratingRequired(customerUserId: customerUserId)
|
||||
}
|
||||
throw APIError.serverError("Failed to complete task: \(errorMsg)")
|
||||
}
|
||||
|
||||
// Parse cash completion response from backend (authoritative values)
|
||||
if let _ = cashReceivedCents, json["CashProcessed"] as? Bool == true {
|
||||
return CashCompletionResult(
|
||||
cashReceived: json["CashReceived"] as? String ?? "0.00",
|
||||
orderTotal: json["OrderTotal"] as? String ?? "0.00",
|
||||
change: json["Change"] as? String ?? "0.00",
|
||||
customerFee: json["CustomerFee"] as? String,
|
||||
businessFee: json["BusinessFee"] as? String,
|
||||
cashRoutedTo: json["CashRoutedTo"] as? String ?? "worker",
|
||||
balanceApplied: json["BalanceApplied"] as? String
|
||||
)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func closeChat(taskId: Int) async throws {
|
||||
|
|
|
|||
|
|
@ -785,6 +785,7 @@ struct CashCollectionSheet: View {
|
|||
@State private var cashReceivedText = ""
|
||||
@State private var isProcessing = false
|
||||
@State private var errorMessage: String?
|
||||
@State private var completionResult: CashCompletionResult?
|
||||
|
||||
private var isAdmin: Bool { roleId >= 2 }
|
||||
|
||||
|
|
@ -795,7 +796,8 @@ struct CashCollectionSheet: View {
|
|||
return Int(round(dollars * 100))
|
||||
}
|
||||
|
||||
private var changeDue: Double? {
|
||||
/// Client-side estimate shown before confirmation (preview only)
|
||||
private var estimatedChangeDue: Double? {
|
||||
guard let receivedCents = cashReceivedCents else { return nil }
|
||||
let change = Double(receivedCents - orderTotalCents) / 100
|
||||
return change >= 0 ? change : nil
|
||||
|
|
@ -839,8 +841,35 @@ struct CashCollectionSheet: View {
|
|||
.cornerRadius(12)
|
||||
}
|
||||
|
||||
// Change display
|
||||
if let change = changeDue {
|
||||
// Change display — backend-authoritative after completion, estimate before
|
||||
if let result = completionResult {
|
||||
// Show confirmed change from backend
|
||||
VStack(spacing: 8) {
|
||||
HStack {
|
||||
Image(systemName: "checkmark.circle.fill")
|
||||
.foregroundColor(.payfritGreen)
|
||||
Text("Change Due:")
|
||||
.font(.body.weight(.medium))
|
||||
Spacer()
|
||||
Text("$\(result.change)")
|
||||
.font(.title3.bold())
|
||||
.foregroundColor(.payfritGreen)
|
||||
}
|
||||
if let balanceApplied = result.balanceApplied {
|
||||
HStack {
|
||||
Text("Balance applied:")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
Spacer()
|
||||
Text("$\(balanceApplied)")
|
||||
.font(.caption.weight(.medium))
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(16)
|
||||
.background(Color.payfritGreen.opacity(0.15))
|
||||
.cornerRadius(12)
|
||||
} else if let change = estimatedChangeDue {
|
||||
HStack {
|
||||
Image(systemName: "arrow.uturn.left.circle.fill")
|
||||
.foregroundColor(.payfritGreen)
|
||||
|
|
@ -905,7 +934,7 @@ struct CashCollectionSheet: View {
|
|||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
.tint(Color(red: 0.13, green: 0.55, blue: 0.13))
|
||||
.disabled(!isValid || isProcessing)
|
||||
.disabled(!isValid || isProcessing || completionResult != nil)
|
||||
.padding(.bottom, 16)
|
||||
}
|
||||
.padding(.horizontal, 24)
|
||||
|
|
@ -927,7 +956,14 @@ struct CashCollectionSheet: View {
|
|||
|
||||
Task {
|
||||
do {
|
||||
try await APIService.shared.completeTask(taskId: taskId, cashReceivedCents: cents)
|
||||
let result = try await APIService.shared.completeTask(taskId: taskId, cashReceivedCents: cents)
|
||||
if let result = result {
|
||||
// Show backend-authoritative change before dismissing
|
||||
completionResult = result
|
||||
isProcessing = false
|
||||
// Brief pause so worker can see the confirmed change amount
|
||||
try? await Task.sleep(nanoseconds: 2_000_000_000)
|
||||
}
|
||||
dismiss()
|
||||
onComplete()
|
||||
} catch {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue