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 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 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 } 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 { 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 } let payloadBuildResult = buildCreateOrderPayload(paymentMethod: effectivePaymentMethod) 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) 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 let isInAppMethod = availableInAppPaymentMethods.contains(effectivePaymentMethod) if useInAppPayment == false || isInAppMethod == false { appState.cart.clear() SessionStateStore.clearPendingCartOrder() orderTrackingContext = OrderTrackingContext(orderId: orderId, shortId: result.shortId) return } if orderSnapshot.isPaymentConfirmed { appState.cart.clear() SessionStateStore.clearPendingCartOrder() orderTrackingContext = OrderTrackingContext(orderId: orderId, shortId: result.shortId) return } SessionStateStore.savePendingCartOrderId(orderId) if effectivePaymentMethod == .creditCard { cardPaymentContext = CardPaymentContext( orderId: orderId, shortId: result.shortId, total: totalValue ) return } 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 guard let copyPaste, copyPaste.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false else { SnackbarCenter.shared.show(title: "Código PIX não retornado pela API.", style: .error, icon: "xmark.octagon.fill", duration: 3.0) return } pixPaymentContext = PixPaymentContext( id: orderId, orderId: orderId, shortId: result.shortId, copyPaste: copyPaste, qrCodeImageBase64: qrCodeImage, expirationDate: expirationDate ) } 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) -> Result { 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 ) ) } }