Commit
This commit is contained in:
432
Sources/PediFoods/Views/Main/CheckoutView+Logic.swift
Normal file
432
Sources/PediFoods/Views/Main/CheckoutView+Logic.swift
Normal file
@@ -0,0 +1,432 @@
|
||||
import SwiftUI
|
||||
|
||||
extension CheckoutView {
|
||||
enum CheckoutPayloadValidationError: LocalizedError {
|
||||
case emptyCart
|
||||
case missingCustomerName
|
||||
case missingCustomerEmail
|
||||
case missingCustomerPhone
|
||||
case missingAddressStreet
|
||||
case missingAddressNumber
|
||||
case missingAddressNeighborhood
|
||||
|
||||
var errorDescription: String? {
|
||||
switch self {
|
||||
case .emptyCart: return "Carrinho vazio."
|
||||
case .missingCustomerName: return "Nome do cliente não informado."
|
||||
case .missingCustomerEmail: return "Email do cliente não informado."
|
||||
case .missingCustomerPhone: return "Telefone do cliente não informado."
|
||||
case .missingAddressStreet: return "Rua do endereço não informada."
|
||||
case .missingAddressNumber: return "Número do endereço não informado."
|
||||
case .missingAddressNeighborhood: return "Bairro do endereço não informado."
|
||||
}
|
||||
}
|
||||
}
|
||||
var checkoutAddressWatchKey: String {
|
||||
let selectedId = appState.address.selectedId ?? "nil"
|
||||
let display = appState.address.display.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
|
||||
let lat = appState.address.latitude.map { String(format: "%.5f", $0) } ?? "nil"
|
||||
let lng = appState.address.longitude.map { String(format: "%.5f", $0) } ?? "nil"
|
||||
return "\(selectedId)|\(display)|\(lat)|\(lng)"
|
||||
}
|
||||
|
||||
func formatCurrency(_ value: Double) -> String {
|
||||
String(format: "R$ %.2f", value).replacingOccurrences(of: ".", with: ",")
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func updateMinOrderSnackbar() {
|
||||
guard isBelowMinOrder else {
|
||||
SnackbarCenter.shared.dismissPersistent()
|
||||
return
|
||||
}
|
||||
let missing = formatCurrency(minOrderValue - totalValue)
|
||||
SnackbarCenter.shared.show(
|
||||
title: "Pedido mínimo de \(formatCurrency(minOrderValue)). Faltam \(missing) para finalizar.",
|
||||
style: .warning,
|
||||
icon: "exclamationmark.circle.fill",
|
||||
isPersistent: true
|
||||
)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func loadStoreInfoIfNeeded() async {
|
||||
guard let storeId = appState.cart.storeId, storeId.isEmpty == false else { return }
|
||||
do {
|
||||
let response = try await ApiService().storeInfo(storeId: storeId)
|
||||
if response.error {
|
||||
errorMessage = response.message ?? "Não foi possível carregar opções de checkout."
|
||||
return
|
||||
}
|
||||
storeInfo = response.result
|
||||
if showPaymentModeToggle == false {
|
||||
useInAppPayment = true
|
||||
}
|
||||
errorMessage = nil
|
||||
} catch {
|
||||
errorMessage = "Não foi possível carregar opções de checkout."
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func refreshSelectedCustomerAddress() async {
|
||||
do {
|
||||
let response = try await ApiService().profile()
|
||||
if let customer = response.result {
|
||||
appState.profile.id = customer.id
|
||||
appState.profile.name = customer.name
|
||||
appState.profile.email = customer.email
|
||||
if let phoneNumber = customer.phoneNumber, phoneNumber.isEmpty == false {
|
||||
appState.profile.phone = phoneNumber
|
||||
}
|
||||
appState.profile.profilePicture = customer.profilePicture ?? ""
|
||||
SessionStateStore.setActiveUserKey(
|
||||
SessionStateStore.makeUserKey(profileId: customer.id, email: customer.email)
|
||||
)
|
||||
}
|
||||
|
||||
let addresses = response.result?.addressBook ?? []
|
||||
if let selectedId = appState.address.selectedId, selectedId.isEmpty == false {
|
||||
selectedCustomerAddress = addresses.first(where: { $0.id == selectedId })
|
||||
} else {
|
||||
selectedCustomerAddress = nil
|
||||
}
|
||||
|
||||
if selectedCustomerAddress == nil {
|
||||
let display = appState.address.display
|
||||
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
.lowercased()
|
||||
if display.isEmpty == false, display != "defina seu endereco" {
|
||||
selectedCustomerAddress = addresses.first {
|
||||
($0.label ?? "").trimmingCharacters(in: .whitespacesAndNewlines).lowercased() == display
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if selectedCustomerAddress == nil,
|
||||
let lat = appState.address.latitude, let lng = appState.address.longitude {
|
||||
selectedCustomerAddress = addresses.first { address in
|
||||
guard let addrLat = address.latLong?.first,
|
||||
let addrLng = address.latLong?.dropFirst().first else { return false }
|
||||
return abs(addrLat - lat) < 0.00001 && abs(addrLng - lng) < 0.00001
|
||||
}
|
||||
}
|
||||
|
||||
if selectedCustomerAddress == nil {
|
||||
selectedCustomerAddress = addresses.first
|
||||
}
|
||||
|
||||
if let selected = selectedCustomerAddress {
|
||||
appState.address.selectedId = selected.id
|
||||
let label = (selected.label ?? "").trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if label.isEmpty == false {
|
||||
appState.address.display = label
|
||||
}
|
||||
if let lat = selected.latLong?.first, let lng = selected.latLong?.dropFirst().first {
|
||||
appState.address.latitude = lat
|
||||
appState.address.longitude = lng
|
||||
}
|
||||
SessionStateStore.saveAddress(appState.address)
|
||||
}
|
||||
} catch {
|
||||
selectedCustomerAddress = nil
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func validateDeliveryAddressIfNeeded() async {
|
||||
guard isDeliveryMode else {
|
||||
addressValidationBlocked = false
|
||||
addressValidationMessage = nil
|
||||
baseDeliveryFee = nil
|
||||
return
|
||||
}
|
||||
|
||||
guard let storeId = appState.cart.storeId, storeId.isEmpty == false else { return }
|
||||
|
||||
baseDeliveryFee = nil
|
||||
|
||||
let payload = ValidateDeliveryAddressPayload(
|
||||
address: ValidateDeliveryAddressDataPayload(
|
||||
street: selectedCustomerAddress?.address,
|
||||
number: selectedCustomerAddress?.number,
|
||||
neighborhood: selectedCustomerAddress?.neighborhood,
|
||||
city: selectedCustomerAddress?.city,
|
||||
state: selectedCustomerAddress?.state,
|
||||
zip: selectedCustomerAddress?.zipCode,
|
||||
lat: appState.address.latitude,
|
||||
lng: appState.address.longitude
|
||||
)
|
||||
)
|
||||
|
||||
isValidatingAddress = true
|
||||
defer { isValidatingAddress = false }
|
||||
|
||||
do {
|
||||
let response = try await ApiService().validateDeliveryAddress(storeId: storeId, payload: payload)
|
||||
if response.error {
|
||||
addressValidationBlocked = true
|
||||
addressValidationMessage = response.message ?? "Não foi possível validar o endereço de entrega."
|
||||
baseDeliveryFee = nil
|
||||
return
|
||||
}
|
||||
|
||||
let result = response.result
|
||||
let allowed = result?.deliveryAllowed ?? false
|
||||
addressValidationBlocked = allowed == false
|
||||
addressValidationMessage = result?.reasonMessage
|
||||
|
||||
if allowed {
|
||||
lastAcceptedAddressState = appState.address
|
||||
} else {
|
||||
showAddressNotServedAlert = true
|
||||
baseDeliveryFee = nil
|
||||
}
|
||||
|
||||
if allowed {
|
||||
if let fee = result?.deliveryFee {
|
||||
baseDeliveryFee = fee
|
||||
} else {
|
||||
addressValidationBlocked = true
|
||||
addressValidationMessage = "Não foi possível calcular a taxa de entrega para este endereço."
|
||||
baseDeliveryFee = nil
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
addressValidationBlocked = true
|
||||
addressValidationMessage = "Não foi possível validar o endereço de entrega."
|
||||
baseDeliveryFee = nil
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeSelectedOptions() {
|
||||
if availableDeliveryTypes.contains(deliveryType) == false,
|
||||
let first = availableDeliveryTypes.first {
|
||||
deliveryType = first
|
||||
}
|
||||
|
||||
if useInAppPayment {
|
||||
if availableInAppPaymentMethods.contains(paymentMethod) == false,
|
||||
let first = availableInAppPaymentMethods.first {
|
||||
paymentMethod = first
|
||||
}
|
||||
} else {
|
||||
if availableStoreMachineMethods.contains(paymentMethod) == false,
|
||||
let first = availableStoreMachineMethods.first {
|
||||
paymentMethod = first
|
||||
}
|
||||
}
|
||||
|
||||
if lastAcceptedAddressState == nil {
|
||||
lastAcceptedAddressState = appState.address
|
||||
}
|
||||
}
|
||||
|
||||
func restoreLastAcceptedAddress() {
|
||||
guard let snapshot = lastAcceptedAddressState else { return }
|
||||
isRestoringAddress = true
|
||||
appState.address = snapshot
|
||||
SessionStateStore.saveAddress(snapshot)
|
||||
Task { @MainActor in
|
||||
await refreshSelectedCustomerAddress()
|
||||
addressValidationBlocked = false
|
||||
addressValidationMessage = nil
|
||||
isRestoringAddress = false
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func handleConfirmPaymentTap() async {
|
||||
guard canConfirmPayment else { return }
|
||||
|
||||
guard let storeId = appState.cart.storeId, storeId.isEmpty == false else {
|
||||
SnackbarCenter.shared.show(title: "Loja do pedido não encontrada.", style: .error, icon: "xmark.octagon.fill", duration: 2.0)
|
||||
return
|
||||
}
|
||||
|
||||
await refreshSelectedCustomerAddress()
|
||||
|
||||
let effectivePaymentMethod = paymentMethod
|
||||
if useInAppPayment && availableInAppPaymentMethods.contains(effectivePaymentMethod) == false {
|
||||
SnackbarCenter.shared.show(title: "Método de pagamento não suportado no app.", style: .warning, icon: "exclamationmark.triangle.fill", duration: 2.0)
|
||||
return
|
||||
}
|
||||
|
||||
// Crédito pelo app → abre seleção de cartão antes de criar pedido
|
||||
if useInAppPayment && effectivePaymentMethod == .creditCard {
|
||||
showCardSelectionSheet = true
|
||||
return
|
||||
}
|
||||
|
||||
let payloadBuildResult = buildCreateOrderPayload(paymentMethod: effectivePaymentMethod, savedCardId: nil)
|
||||
guard case .success(let payload) = payloadBuildResult else {
|
||||
let message: String
|
||||
if case .failure(let reason) = payloadBuildResult {
|
||||
message = reason.localizedDescription
|
||||
} else {
|
||||
message = "Dados do pedido incompletos."
|
||||
}
|
||||
SnackbarCenter.shared.show(title: message, style: .error, icon: "xmark.octagon.fill", duration: 2.0)
|
||||
return
|
||||
}
|
||||
|
||||
isSubmittingOrder = true
|
||||
defer { isSubmittingOrder = false }
|
||||
|
||||
do {
|
||||
let response = try await ApiService().createOrder(storeId: storeId, payload: payload)
|
||||
let isInAppMethod = availableInAppPaymentMethods.contains(effectivePaymentMethod)
|
||||
if useInAppPayment == false || isInAppMethod == false {
|
||||
if response.error == false, let result = response.result {
|
||||
let orderId = result.id ?? UUID().uuidString
|
||||
appState.cart.clear()
|
||||
SessionStateStore.clearPendingCartOrder()
|
||||
orderTrackingContext = OrderTrackingContext(orderId: orderId, shortId: result.shortId)
|
||||
} else {
|
||||
SnackbarCenter.shared.show(title: response.message ?? "Não foi possível criar o pedido.", style: .error, icon: "xmark.octagon.fill", duration: 3.0)
|
||||
}
|
||||
return
|
||||
}
|
||||
handleOrderResponse(response, effectivePaymentMethod: effectivePaymentMethod)
|
||||
} catch {
|
||||
SnackbarCenter.shared.show(title: "Não foi possível criar o pedido.", style: .error, icon: "xmark.octagon.fill", duration: 3.0)
|
||||
}
|
||||
}
|
||||
|
||||
func buildCreateOrderPayload(
|
||||
paymentMethod: CheckoutPaymentMethod,
|
||||
savedCardId: String? = nil,
|
||||
creditCard: CreditCardOrderPayload? = nil,
|
||||
creditCardHolderInfo: SaveCardHolderInfoPayload? = nil,
|
||||
clientCpfCnpj: String? = nil
|
||||
) -> Result<CreateOrderPayload, CheckoutPayloadValidationError> {
|
||||
guard appState.cart.items.isEmpty == false else { return .failure(.emptyCart) }
|
||||
|
||||
let profileName = appState.profile.name.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
let profileEmail = appState.profile.email.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
let profilePhone = appState.profile.phone.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard profileName.isEmpty == false else { return .failure(.missingCustomerName) }
|
||||
guard profileEmail.isEmpty == false else { return .failure(.missingCustomerEmail) }
|
||||
guard profilePhone.isEmpty == false else { return .failure(.missingCustomerPhone) }
|
||||
|
||||
let addressPayload: CreateOrderAddressPayload?
|
||||
if isDeliveryMode {
|
||||
let street = selectedCustomerAddress?.address?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
||||
let number = selectedCustomerAddress?.number?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
||||
let neighborhood = selectedCustomerAddress?.neighborhood?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
||||
guard street.isEmpty == false else { return .failure(.missingAddressStreet) }
|
||||
guard number.isEmpty == false else { return .failure(.missingAddressNumber) }
|
||||
guard neighborhood.isEmpty == false else { return .failure(.missingAddressNeighborhood) }
|
||||
addressPayload = CreateOrderAddressPayload(
|
||||
street: street,
|
||||
number: number,
|
||||
neighborhood: neighborhood,
|
||||
city: selectedCustomerAddress?.city,
|
||||
state: selectedCustomerAddress?.state,
|
||||
zip: selectedCustomerAddress?.zipCode,
|
||||
complement: selectedCustomerAddress?.complement
|
||||
)
|
||||
} else {
|
||||
addressPayload = nil
|
||||
}
|
||||
|
||||
return .success(
|
||||
CreateOrderPayload(
|
||||
customer: CreateOrderCustomerPayload(
|
||||
name: profileName,
|
||||
phone: profilePhone,
|
||||
email: profileEmail,
|
||||
asaasId: nil
|
||||
),
|
||||
items: appState.cart.toOrderItemsPayload(),
|
||||
total: totalValue,
|
||||
paymentMethod: paymentMethod.rawValue,
|
||||
deliveryType: deliveryType.rawValue,
|
||||
address: addressPayload,
|
||||
savedCardId: savedCardId,
|
||||
clientCpfCnpj: clientCpfCnpj,
|
||||
creditCard: creditCard,
|
||||
creditCardHolderInfo: creditCardHolderInfo
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func confirmOrderWithSavedCard(cardId: String, storeId: String) async {
|
||||
let payloadResult = buildCreateOrderPayload(paymentMethod: .creditCard, savedCardId: cardId)
|
||||
guard case .success(let payload) = payloadResult else { return }
|
||||
|
||||
isSubmittingOrder = true
|
||||
defer { isSubmittingOrder = false }
|
||||
|
||||
do {
|
||||
let response = try await ApiService().createOrder(storeId: storeId, payload: payload)
|
||||
handleOrderResponse(response, effectivePaymentMethod: .creditCard)
|
||||
} catch {
|
||||
SnackbarCenter.shared.show(title: "Não foi possível criar o pedido.", style: .error, icon: "xmark.octagon.fill", duration: 3.0)
|
||||
}
|
||||
}
|
||||
|
||||
func handleOrderResponse(_ response: ApiEnvelope<CreateOrderResult>, effectivePaymentMethod: CheckoutPaymentMethod) {
|
||||
if response.error {
|
||||
SnackbarCenter.shared.show(
|
||||
title: response.message ?? "Não foi possível criar o pedido.",
|
||||
style: .error,
|
||||
icon: "xmark.octagon.fill",
|
||||
duration: 3.0
|
||||
)
|
||||
return
|
||||
}
|
||||
guard let result = response.result else {
|
||||
SnackbarCenter.shared.show(title: "Resposta de pagamento inválida.", style: .error, icon: "xmark.octagon.fill", duration: 3.0)
|
||||
return
|
||||
}
|
||||
|
||||
let orderSnapshot = result.asPublicOrderResult()
|
||||
SessionStateStore.saveTrackedOrder(orderSnapshot)
|
||||
let orderId = result.id ?? UUID().uuidString
|
||||
|
||||
if orderSnapshot.isPaymentConfirmed {
|
||||
appState.cart.clear()
|
||||
SessionStateStore.clearPendingCartOrder()
|
||||
orderTrackingContext = OrderTrackingContext(orderId: orderId, shortId: result.shortId)
|
||||
return
|
||||
}
|
||||
|
||||
SessionStateStore.savePendingCartOrderId(orderId)
|
||||
|
||||
let pixFromPayment = result.payment?.pix
|
||||
let pixFromPayload = result.paymentPayload
|
||||
let copyPaste = (pixFromPayment?.copyPaste?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false)
|
||||
? pixFromPayment?.copyPaste : pixFromPayload?.copyPaste
|
||||
let qrCodeImage = (pixFromPayment?.qrCodeImage?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false)
|
||||
? pixFromPayment?.qrCodeImage : pixFromPayload?.qrCodeImage
|
||||
let expirationDate = (pixFromPayment?.expirationDate?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false)
|
||||
? pixFromPayment?.expirationDate : pixFromPayload?.expirationDate
|
||||
|
||||
if let copyPaste, copyPaste.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false {
|
||||
let itemsData = (try? JSONEncoder().encode(appState.cart.toOrderItemsPayload())).flatMap { String(data: $0, encoding: .utf8) } ?? "[]"
|
||||
let storeId = appState.cart.storeId ?? ""
|
||||
pixPaymentContext = PixPaymentContext(
|
||||
id: orderId,
|
||||
orderId: orderId,
|
||||
shortId: result.shortId,
|
||||
storeId: storeId,
|
||||
copyPaste: copyPaste,
|
||||
qrCodeImageBase64: qrCodeImage,
|
||||
expirationDate: expirationDate,
|
||||
total: totalValue,
|
||||
profileName: appState.profile.name,
|
||||
profileEmail: appState.profile.email,
|
||||
profilePhone: appState.profile.phone,
|
||||
addressZip: selectedCustomerAddress?.zipCode,
|
||||
addressNumber: selectedCustomerAddress?.number,
|
||||
deliveryType: deliveryType.rawValue,
|
||||
itemsJSON: itemsData
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
orderTrackingContext = OrderTrackingContext(orderId: orderId, shortId: result.shortId)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user