Merge pull request 'fix: Backend-authoritative cash change calculation' (#3) from schwifty/cash-change-backend-authoritative into main
This commit is contained in:
commit
88a3a9d6e0
2 changed files with 74 additions and 6 deletions
|
|
@ -56,6 +56,22 @@ struct ChatMessagesResult {
|
||||||
let chatClosed: Bool
|
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
|
// MARK: - API Service
|
||||||
|
|
||||||
actor APIService {
|
actor APIService {
|
||||||
|
|
@ -474,7 +490,9 @@ actor APIService {
|
||||||
return arr.map { WorkTask(json: $0) }
|
return arr.map { WorkTask(json: $0) }
|
||||||
}
|
}
|
||||||
|
|
||||||
func completeTask(taskId: Int, workerRating: [String: Bool]? = nil, cashReceivedCents: Int? = nil, cancelOrder: Bool = false) async throws {
|
/// Complete a task. Returns CashCompletionResult when cashReceivedCents is provided.
|
||||||
|
@discardableResult
|
||||||
|
func completeTask(taskId: Int, workerRating: [String: Bool]? = nil, cashReceivedCents: Int? = nil, cancelOrder: Bool = false) async throws -> CashCompletionResult? {
|
||||||
var payload: [String: Any] = [
|
var payload: [String: Any] = [
|
||||||
"TaskID": taskId,
|
"TaskID": taskId,
|
||||||
"UserID": userId ?? 0
|
"UserID": userId ?? 0
|
||||||
|
|
@ -497,6 +515,20 @@ actor APIService {
|
||||||
}
|
}
|
||||||
throw APIError.serverError("Failed to complete task: \(errorMsg)")
|
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 {
|
func closeChat(taskId: Int) async throws {
|
||||||
|
|
|
||||||
|
|
@ -834,6 +834,7 @@ struct CashCollectionSheet: View {
|
||||||
@State private var cashReceivedText = ""
|
@State private var cashReceivedText = ""
|
||||||
@State private var isProcessing = false
|
@State private var isProcessing = false
|
||||||
@State private var errorMessage: String?
|
@State private var errorMessage: String?
|
||||||
|
@State private var completionResult: CashCompletionResult?
|
||||||
|
|
||||||
private var isAdmin: Bool { roleId >= 2 }
|
private var isAdmin: Bool { roleId >= 2 }
|
||||||
|
|
||||||
|
|
@ -844,7 +845,8 @@ struct CashCollectionSheet: View {
|
||||||
return Int(round(dollars * 100))
|
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 }
|
guard let receivedCents = cashReceivedCents else { return nil }
|
||||||
let change = Double(receivedCents - orderTotalCents) / 100
|
let change = Double(receivedCents - orderTotalCents) / 100
|
||||||
return change >= 0 ? change : nil
|
return change >= 0 ? change : nil
|
||||||
|
|
@ -888,8 +890,35 @@ struct CashCollectionSheet: View {
|
||||||
.cornerRadius(12)
|
.cornerRadius(12)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Change display
|
// Change display — backend-authoritative after completion, estimate before
|
||||||
if let change = changeDue {
|
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 {
|
HStack {
|
||||||
Image(systemName: "arrow.uturn.left.circle.fill")
|
Image(systemName: "arrow.uturn.left.circle.fill")
|
||||||
.foregroundColor(.payfritGreen)
|
.foregroundColor(.payfritGreen)
|
||||||
|
|
@ -954,7 +983,7 @@ struct CashCollectionSheet: View {
|
||||||
}
|
}
|
||||||
.buttonStyle(.borderedProminent)
|
.buttonStyle(.borderedProminent)
|
||||||
.tint(Color(red: 0.13, green: 0.55, blue: 0.13))
|
.tint(Color(red: 0.13, green: 0.55, blue: 0.13))
|
||||||
.disabled(!isValid || isProcessing)
|
.disabled(!isValid || isProcessing || completionResult != nil)
|
||||||
.padding(.bottom, 16)
|
.padding(.bottom, 16)
|
||||||
}
|
}
|
||||||
.padding(.horizontal, 24)
|
.padding(.horizontal, 24)
|
||||||
|
|
@ -976,7 +1005,14 @@ struct CashCollectionSheet: View {
|
||||||
|
|
||||||
Task {
|
Task {
|
||||||
do {
|
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()
|
dismiss()
|
||||||
onComplete()
|
onComplete()
|
||||||
} catch {
|
} catch {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue