This commit is contained in:
Daniel Arantes Loverde
2026-07-07 15:11:31 -03:00
parent 8b9ebdcb44
commit e51c99973f
293 changed files with 457 additions and 4919 deletions

View File

@@ -0,0 +1,886 @@
import SwiftUI
struct OrdersView: View {
@Binding var appState: AppState
@Environment(\.dismiss) var dismiss
@State var isLoading = false
@State var errorMessage: String? = nil
@State var orders: [AppOrderSummary] = []
@State var hasLoadedOnce = false
@State var storeRatingByStoreId: [String: Double] = [:]
@State var storeRatingByStoreName: [String: Double] = [:]
@State var storeLogoByStoreId: [String: String] = [:]
@State var storeLogoByStoreName: [String: String] = [:]
@State var selectedOrderRoute: OrderRouteContext? = nil
var body: some View {
ScrollView(showsIndicators: false) {
VStack(alignment: .leading, spacing: 14) {
screenHeader(title: "Meus Pedidos", onBack: { dismiss() })
if isLoading {
ProgressView()
.frame(maxWidth: .infinity)
.padding(.top, 24)
} else if let errorMessage, errorMessage.isEmpty == false {
Text(errorMessage)
.font(AppTypography.body)
.foregroundStyle(Color.red)
.multilineTextAlignment(.center)
.frame(maxWidth: .infinity)
.padding(.top, 24)
} else if orders.isEmpty {
Text("Nenhum pedido encontrado.")
.font(AppTypography.body)
.foregroundStyle(AppColors.textMuted)
.padding(.top, 24)
} else {
ForEach(orders) { order in
orderCard(order)
}
}
}
.padding(.horizontal, 20)
.padding(.top, 18)
.padding(.bottom, UIDevice.bottomNotch + 18)
}
.background(AppColors.backgroundLight)
.navigationBarBackButtonHidden(true)
.appHiddenNavigationBar()
.task {
await loadOrdersIfNeeded()
await refreshStoreRatings()
}
.refreshable {
// Decoupled from .refreshable's own cancellable wrapping Task
// see StoreDetailView's .refreshable for why.
await Task {
await loadOrders(force: true)
await refreshStoreRatings()
}.value
}
.navigationDestination(item: $selectedOrderRoute) { context in
OrderEntryDestinationView(
orderId: context.orderId,
initialShortId: context.shortId,
fallbackPaymentMethod: context.paymentMethod,
fallbackTotal: context.total,
routeIntent: context.intent,
appState: $appState
)
}
}
private func screenHeader(title: String, onBack: @escaping () -> Void) -> some View {
ZStack {
Text(title)
.font(AppTypography.heading2)
.foregroundStyle(AppColors.textPrimary)
HStack {
Button(action: onBack) {
Image(systemName: "chevron.left")
.font(.system(size: 24, weight: .semibold))
.foregroundStyle(AppColors.textPrimary)
.frame(width: 52, height: 52)
.background(AppColors.surface)
.clipShape(Circle())
.shadow(color: .black.opacity(0.06), radius: 8, y: 2)
}
.buttonStyle(.plain)
Spacer()
}
}
}
private func orderCard(_ order: AppOrderSummary) -> some View {
let status = orderVisualStatus(for: order)
let detailsRoute = OrderRouteContext(
orderId: trackingOrderId(for: order),
shortId: order.shortId,
paymentMethod: order.paymentMethod,
total: order.total,
intent: .details
)
let trackingRoute = OrderRouteContext(
orderId: trackingOrderId(for: order),
shortId: order.shortId,
paymentMethod: order.paymentMethod,
total: order.total,
intent: .tracking
)
return VStack(alignment: .leading, spacing: 14) {
HStack(spacing: 12) {
AsyncStoreImage(imageURL: resolvedMediaURL(storeLogoURL(for: order)))
.frame(width: 80, height: 80)
.clipShape(RoundedRectangle(cornerRadius: 20, style: .continuous))
VStack(alignment: .leading, spacing: 5) {
HStack(spacing: 6) {
Text(order.storeName?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false ? (order.storeName ?? "Pedido") : "Pedido #\(order.shortId ?? order.id)")
.font(AppTypography.heading2)
.minimumScaleFactor(0.01)
.foregroundStyle(AppColors.textPrimary)
.lineLimit(1)
Spacer()
Text(status.badgeTitle)
.font(AppTypography.caption)
.minimumScaleFactor(0.01)
.foregroundStyle(status.badgeForeground)
.padding(.horizontal, 10)
.padding(.vertical, 6)
.background(status.badgeBackground)
.clipShape(Capsule())
}
HStack(spacing: 6) {
Text(orderMetaText(order))
.font(AppTypography.body)
.minimumScaleFactor(0.01)
.foregroundStyle(AppColors.textMuted)
.lineLimit(1)
if let rating = storeRating(for: order) {
Text("")
.font(AppTypography.body)
.minimumScaleFactor(0.01)
.foregroundStyle(AppColors.textMuted)
Image(systemName: "star.fill")
.font(.system(size: 11, weight: .bold))
.minimumScaleFactor(0.01)
.foregroundStyle(Color(hex: "#7CF02A"))
Text(String(format: "%.1f", rating).replacingOccurrences(of: ".", with: ","))
.font(AppTypography.body)
.minimumScaleFactor(0.01)
.foregroundStyle(AppColors.textMuted)
}
}
}
}
Divider()
HStack(spacing: 12) {
Button(status.isCanceled ? "Ajuda" : "Ver Detalhes") {
selectedOrderRoute = detailsRoute
}
.font(AppTypography.heading3)
.foregroundStyle(AppColors.textMuted)
.lineLimit(1)
.minimumScaleFactor(0.5)
.buttonStyle(.plain)
.appLayoutPriority(0)
Spacer(minLength: 8)
Button {
if status.isInProgress {
selectedOrderRoute = trackingRoute
return
}
SnackbarCenter.shared.show(
title: "Recompra será integrada com o catálogo em breve.",
style: .info,
icon: "cart.badge.plus",
duration: 2.0
)
} label: {
HStack(spacing: 8) {
Image(systemName: status.isInProgress ? "truck.box.fill" : "arrow.clockwise")
Text(status.isInProgress ? "Acompanhar" : "Pedir Novamente")
.font(AppTypography.heading3)
.lineLimit(1)
.minimumScaleFactor(0.5)
}
.lineLimit(1)
//.frame(minWidth: status.isInProgress ? 136 : 184)
.foregroundStyle(status.actionForeground)
.padding(.horizontal, 14)
.padding(.vertical, 11)
.background(status.actionBackground)
.clipShape(Capsule())
}
.buttonStyle(.plain)
.appLayoutPriority(2)
}
}
.padding(18)
.background(AppColors.surface)
.overlay(alignment: .leading) {
if status.isInProgress {
RoundedRectangle(cornerRadius: 3, style: .continuous)
.fill(Color(hex: "#C8F06E"))
.frame(width: 5)
.padding(.vertical, 20)
}
}
.clipShape(RoundedRectangle(cornerRadius: 28, style: .continuous))
}
private func orderVisualStatus(for order: AppOrderSummary) -> OrderRowStatusStyle {
let rawDetailed = (order.statusDetailed ?? "").trimmingCharacters(in: .whitespacesAndNewlines).uppercased()
let rawStatus = (order.status ?? "").trimmingCharacters(in: .whitespacesAndNewlines).uppercased()
let technical = [rawDetailed, rawStatus].joined(separator: "|")
if technical.contains("CANCEL") || technical.contains("REFUND") {
return .canceled
}
if technical.contains("COMPLETED") || technical.contains("DELIVERED") {
return .delivered
}
if technical.contains("IN_DELIVERY")
|| technical.contains("DELIVERING")
|| technical.contains("OUT_FOR_DELIVERY")
|| technical.contains("PENDING")
|| technical.contains("ACCEPTED")
|| technical.contains("PREPAR")
|| technical.contains("READY") {
return .inProgress
}
let label = (order.statusLabel ?? "").trimmingCharacters(in: .whitespacesAndNewlines).uppercased()
if label.contains("CANCEL") {
return .canceled
}
if label.contains("CONCLU") || label.contains("ENTREGUE") {
return .delivered
}
return .inProgress
}
private func formatCurrency(_ value: Double) -> String {
String(format: "R$ %.2f", value).replacingOccurrences(of: ".", with: ",")
}
private func orderMetaText(_ order: AppOrderSummary) -> String {
let dateText = formatOrderDate(order.createdAt) ?? "Agora"
let totalText = formatCurrency(order.total ?? 0)
return "\(dateText)\(totalText)"
}
private func formatOrderDate(_ isoValue: String?) -> String? {
guard let isoValue, isoValue.isEmpty == false else { return nil }
let iso = ISO8601DateFormatter()
iso.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
var date = iso.date(from: isoValue)
if date == nil {
iso.formatOptions = [.withInternetDateTime]
date = iso.date(from: isoValue)
}
guard let date else { return nil }
let formatter = DateFormatter()
formatter.locale = Locale(identifier: "pt_BR")
formatter.dateFormat = "dd MMM, HH:mm"
return formatter.string(from: date)
}
private func trackingOrderId(for order: AppOrderSummary) -> String {
let orderCandidate = (order.orderId ?? "").trimmingCharacters(in: .whitespacesAndNewlines)
if orderCandidate.isEmpty == false {
return orderCandidate
}
let candidate = (order.realId ?? "").trimmingCharacters(in: .whitespacesAndNewlines)
if candidate.isEmpty == false {
return candidate
}
return order.id
}
private func normalizedOrderId(_ value: String?) -> String {
(value ?? "").trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
}
private func normalizedStoreName(_ value: String?) -> String {
(value ?? "")
.trimmingCharacters(in: .whitespacesAndNewlines)
.folding(options: [.diacriticInsensitive, .caseInsensitive], locale: .current)
.lowercased()
}
@MainActor
private func loadOrdersIfNeeded() async {
guard hasLoadedOnce == false else { return }
await loadOrders(force: false)
}
@MainActor
private func loadOrders(force: Bool) async {
if isLoading { return }
if force == false, hasLoadedOnce { return }
isLoading = true
errorMessage = nil
let previousOrders = orders
var trackedMapped: [AppOrderSummary] = []
let cachedTracked = SessionStateStore.loadTrackedOrders()
if cachedTracked.isEmpty == false {
trackedMapped = cachedTracked.map {
AppOrderSummary.fromTracked($0)
}
if hasLoadedOnce == false, previousOrders.isEmpty {
orders = mergeOrders(apiOrders: [], trackedOrders: trackedMapped)
}
}
do {
let response = try await ApiService().listOrders(forceRefresh: force)
if response.error {
if previousOrders.isEmpty == false {
orders = previousOrders
}
errorMessage = response.message ?? "Não foi possível carregar os pedidos."
} else {
let remote = response.result ?? []
orders = mergeOrders(apiOrders: remote, trackedOrders: trackedMapped)
}
} catch {
if isCancelledRequest(error) {
if previousOrders.isEmpty == false {
orders = previousOrders
}
isLoading = false
return
}
if orders.isEmpty {
errorMessage = "Não foi possível carregar os pedidos."
}
}
isLoading = false
hasLoadedOnce = true
}
private func mergeOrders(apiOrders: [AppOrderSummary], trackedOrders: [AppOrderSummary]) -> [AppOrderSummary] {
var map: [String: AppOrderSummary] = [:]
var sourceRank: [String: Int] = [:]
for (index, item) in trackedOrders.enumerated() {
let key = identityKey(for: item)
map[key] = item
if sourceRank[key] == nil {
sourceRank[key] = 10_000 + index
}
}
for (index, item) in apiOrders.enumerated() {
let key = identityKey(for: item)
map[key] = item
sourceRank[key] = index
}
return map.values.sorted { lhs, rhs in
let leftDate = orderDateSortValue(lhs)
let rightDate = orderDateSortValue(rhs)
if leftDate != rightDate {
return leftDate > rightDate
}
let leftRank = sourceRank[identityKey(for: lhs)] ?? Int.max
let rightRank = sourceRank[identityKey(for: rhs)] ?? Int.max
if leftRank != rightRank {
return leftRank < rightRank
}
let leftNumericId = Int(lhs.id)
let rightNumericId = Int(rhs.id)
if let leftNumericId, let rightNumericId, leftNumericId != rightNumericId {
return leftNumericId > rightNumericId
}
return lhs.id.localizedCompare(rhs.id) == .orderedDescending
}
}
private func identityKey(for order: AppOrderSummary) -> String {
let raw = trackingOrderId(for: order)
return raw.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
}
private func orderDateSortValue(_ order: AppOrderSummary) -> Date {
parseDateForSort(order.updatedAt)
?? parseDateForSort(order.createdAt)
?? .distantPast
}
private func parseDateForSort(_ rawValue: String?) -> Date? {
guard let rawValue else { return nil }
let value = rawValue.trimmingCharacters(in: .whitespacesAndNewlines)
if value.isEmpty { return nil }
let iso = ISO8601DateFormatter()
iso.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
if let date = iso.date(from: value) { return date }
iso.formatOptions = [.withInternetDateTime]
if let date = iso.date(from: value) { return date }
let formatter = DateFormatter()
formatter.locale = Locale(identifier: "pt_BR")
let formats = [
"yyyy-MM-dd'T'HH:mm:ss.SSSXXXXX",
"yyyy-MM-dd'T'HH:mm:ssXXXXX",
"yyyy-MM-dd HH:mm:ss",
"yyyy-MM-dd'T'HH:mm:ss",
"yyyy-MM-dd'T'HH:mm:ss.SSSZ",
"yyyy-MM-dd HH:mm:ss Z",
"dd/MM/yyyy HH:mm:ss",
"dd/MM/yyyy HH:mm",
"dd/MM/yyyy"
]
for format in formats {
formatter.dateFormat = format
if let date = formatter.date(from: value) {
return date
}
}
return nil
}
private func isCancelledRequest(_ error: Error) -> Bool {
if error is CancellationError {
return true
}
if let networkError = error as? NetworkError,
case .transportError(let message) = networkError {
let normalized = message.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
return normalized.contains("cancel")
}
if let networkError = error as? NetworkError,
case .cancelled = networkError {
return true
}
return error.localizedDescription.lowercased().contains("cancel")
}
@MainActor
private func refreshStoreRatings() async {
var storeList: [StoreSummary] = AppContentCache.shared.value(
for: AppCacheKey.homeStoresLatestSnapshot,
as: [StoreSummary].self
) ?? []
if storeList.isEmpty {
let response = try? await ApiService().listStores()
storeList = response?.result ?? []
}
var byId: [String: Double] = [:]
var byName: [String: Double] = [:]
var logoById: [String: String] = [:]
var logoByName: [String: String] = [:]
for store in storeList {
let storeId = normalizedOrderId(store.id)
let nameKey = normalizedStoreName(store.name)
if let rating = store.rating, rating > 0 {
if storeId.isEmpty == false { byId[storeId] = rating }
if nameKey.isEmpty == false { byName[nameKey] = rating }
}
if let logo = store.logo, logo.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false {
if storeId.isEmpty == false { logoById[storeId] = logo }
if nameKey.isEmpty == false { logoByName[nameKey] = logo }
}
}
storeRatingByStoreId = byId
storeRatingByStoreName = byName
storeLogoByStoreId = logoById
storeLogoByStoreName = logoByName
}
private func storeRating(for order: AppOrderSummary) -> Double? {
let storeId = normalizedOrderId(order.storeId)
if storeId.isEmpty == false, let fromId = storeRatingByStoreId[storeId] {
return fromId
}
let nameKey = normalizedStoreName(order.storeName)
if nameKey.isEmpty == false, let fromName = storeRatingByStoreName[nameKey] {
return fromName
}
return nil
}
private func storeLogoURL(for order: AppOrderSummary) -> String? {
if let url = order.storeLogoURL, url.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false {
return url
}
let storeId = normalizedOrderId(order.storeId)
if storeId.isEmpty == false, let logo = storeLogoByStoreId[storeId] { return logo }
let nameKey = normalizedStoreName(order.storeName)
if nameKey.isEmpty == false, let logo = storeLogoByStoreName[nameKey] { return logo }
return nil
}
private func resolvedMediaURL(_ raw: String?) -> String? {
ImageSourceResolver.resolve(raw)
}
}
struct OrderRouteContext: Identifiable, Hashable {
var id: String { "\(orderId)|\(intent.rawValue)" }
let orderId: String
let shortId: String?
let paymentMethod: String?
let total: Double?
let intent: OrderRouteIntent
}
enum OrderRouteIntent: String, Hashable {
case details
case tracking
case auto
}
private enum OrderRowStatusStyle {
case delivered
case inProgress
case canceled
var badgeTitle: String {
switch self {
case .delivered: return "Entregue"
case .inProgress: return "Em andamento"
case .canceled: return "Cancelado"
}
}
var badgeForeground: Color {
switch self {
case .delivered: return Color(hex: "#16843B")
case .inProgress: return Color(hex: "#B06A28")
case .canceled: return Color(hex: "#D62828")
}
}
var badgeBackground: Color {
switch self {
case .delivered: return Color(hex: "#E8F7E9")
case .inProgress: return Color(hex: "#FFF2E5")
case .canceled: return Color(hex: "#FDECEC")
}
}
var actionForeground: Color {
switch self {
case .inProgress: return .white
case .delivered, .canceled: return Color(hex: "#0E1A06")
}
}
var actionBackground: Color {
switch self {
case .inProgress: return Color(hex: "#111216")
case .delivered, .canceled: return Color(hex: "#C8F06E")
}
}
var isInProgress: Bool { self == .inProgress }
var isCanceled: Bool { self == .canceled }
}
extension AppOrderSummary {
static func fromTracked(_ tracked: PublicOrderResult) -> AppOrderSummary {
AppOrderSummary(
id: tracked.id,
orderId: tracked.realId ?? tracked.id,
realId: tracked.realId,
storeId: nil,
shortId: tracked.shortId,
total: tracked.total,
status: tracked.status,
statusDetailed: nil,
statusLabel: nil,
nextAction: nil,
paymentStatus: tracked.paymentStatus,
paymentMethod: tracked.paymentMethod,
deliveryType: tracked.deliveryType,
storeName: tracked.storeName,
storePhone: tracked.storePhone,
storeLogoURL: tracked.storeLogoURL,
createdAt: tracked.createdAt,
updatedAt: tracked.updatedAt
)
}
init(
id: String,
orderId: String?,
realId: String?,
storeId: String?,
shortId: String?,
total: Double?,
status: String?,
statusDetailed: String?,
statusLabel: String?,
nextAction: String?,
paymentStatus: String?,
paymentMethod: String?,
deliveryType: String?,
storeName: String?,
storePhone: String?,
storeLogoURL: String?,
createdAt: String?,
updatedAt: String?
) {
self.id = id
self.orderId = orderId
self.realId = realId
self.storeId = storeId
self.shortId = shortId
self.total = total
self.status = status
self.statusDetailed = statusDetailed
self.statusLabel = statusLabel
self.nextAction = nextAction
self.paymentStatus = paymentStatus
self.paymentMethod = paymentMethod
self.deliveryType = deliveryType
self.storeName = storeName
self.storePhone = storePhone
self.storeLogoURL = storeLogoURL
self.createdAt = createdAt
self.updatedAt = updatedAt
}
}
struct OrderEntryDestinationView: View {
let orderId: String
let initialShortId: String?
let fallbackPaymentMethod: String?
let fallbackTotal: Double?
let routeIntent: OrderRouteIntent
@Binding var appState: AppState
@State var isResolvingRoute = true
@State var didResolve = false
@State var pixContext: PixPaymentContext? = nil
@State var orderTrackingContext: OrderTrackingContext? = nil
@State var orderDetails: PublicOrderResult? = nil
var body: some View {
Group {
if isResolvingRoute {
VStack(spacing: 10) {
ProgressView()
Text("Carregando pedido...")
.font(AppTypography.body)
.foregroundStyle(AppColors.textMuted)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(AppColors.backgroundLight)
} else if let pixContext {
PaymentPixView(
context: pixContext,
appState: $appState,
onPaymentConfirmed: {
orderTrackingContext = OrderTrackingContext(orderId: pixContext.orderId, shortId: pixContext.shortId)
},
onOpenTracking: {
orderTrackingContext = OrderTrackingContext(orderId: pixContext.orderId, shortId: pixContext.shortId)
}
)
} else if let orderDetails {
OrderDetailsView(order: orderDetails, orderId: orderId, initialShortId: initialShortId, appState: $appState)
} else {
OrderTrackingView(orderId: orderId, initialShortId: initialShortId)
}
}
.navigationDestination(item: $orderTrackingContext) { context in
OrderTrackingView(orderId: context.orderId, initialShortId: context.shortId)
}
.task {
guard didResolve == false else { return }
didResolve = true
await resolveRoute()
}
}
@MainActor
func resolveRoute() async {
defer { isResolvingRoute = false }
let order = await fetchOrderForRouting()
guard let order else { return }
if routeIntent == .details {
orderDetails = order
return
}
// For both .tracking and .auto: show payment screen if payment is still pending.
// Timeline only shows once payment is confirmed or method is off-app.
if shouldOpenPaymentScreen(for: order) {
let normalizedMethod = normalizePaymentMethod(order)
if normalizedMethod.contains("PIX") {
let pixFromPayment = order.payment?.pix
let pixFromPayload = order.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
let storeId = order.storeId ?? ""
pixContext = PixPaymentContext(
id: order.id,
orderId: order.id,
shortId: order.shortId ?? initialShortId,
storeId: storeId,
copyPaste: copyPaste?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false
? (copyPaste ?? "")
: "Código PIX indisponível no momento. Aguarde e tente novamente.",
qrCodeImageBase64: qrCodeImage,
expirationDate: expirationDate,
total: order.total ?? 0,
profileName: "",
profileEmail: "",
profilePhone: "",
addressZip: nil,
addressNumber: nil,
deliveryType: order.deliveryType ?? "DELIVERY",
itemsJSON: "[]"
)
return
}
if normalizedMethod == "CREDIT_CARD" || normalizedMethod == "DEBIT_CARD" {
orderTrackingContext = OrderTrackingContext(orderId: order.id, shortId: order.shortId ?? initialShortId)
return
}
}
// For .auto only: route terminal/canceled orders to details instead of timeline.
if routeIntent == .auto && shouldOpenOrderDetails(for: order) {
orderDetails = order
}
// .tracking (and .auto fallthrough) nil states body renders OrderTrackingView
}
@MainActor
func fetchOrderForRouting() async -> PublicOrderResult? {
logger.info("OrderEntry fetch route orderId=\(orderId)")
do {
let response = try await ApiService().publicOrder(orderId: orderId)
if response.error == false, let result = response.result {
logger.info("OrderEntry fetch route success orderId=\(orderId) status=\(result.status ?? "nil") paymentStatus=\(result.paymentStatus ?? "nil")")
return result
}
logger.error("OrderEntry fetch route API error orderId=\(orderId) message=\(response.message ?? "unknown")")
} catch {
logger.error("OrderEntry fetch route failure orderId=\(orderId) error=\(error.localizedDescription)")
}
return nil
}
func isOnlinePaymentMethod(_ order: PublicOrderResult) -> Bool {
let method = normalizePaymentMethod(order)
return method == "PIX" || method == "CREDIT_CARD" || method == "DEBIT_CARD"
}
func shouldOpenOrderDetails(for order: PublicOrderResult) -> Bool {
let status = normalize(order.status)
if status.contains("CANCEL") { return true }
if status.contains("COMPLETED") || status.contains("DELIVERED") { return true }
return false
}
func shouldOpenPaymentScreen(for order: PublicOrderResult) -> Bool {
guard order.isPaymentConfirmed == false else { return false }
guard isOnlinePaymentMethod(order) else { return false }
guard isPaymentPending(order) else { return false }
guard isInStorePayment(order) == false else { return false }
let status = normalize(order.status)
if status.contains("PREPAR") ||
status.contains("READY") ||
status.contains("DELIVER") ||
status.contains("ROTA") ||
status.contains("COMPLETED") ||
status.contains("CANCEL") ||
status.contains("REFUND") {
return false
}
let paymentStatus = normalize(order.paymentStatus)
if paymentStatus.contains("CONFIRM") ||
paymentStatus.contains("PAID") ||
paymentStatus.contains("RECEIV") ||
paymentStatus.contains("APPROV") {
return false
}
let method = normalizePaymentMethod(order)
if method == "PIX" {
return hasPixPayload(order)
}
return method == "CREDIT_CARD" || method == "DEBIT_CARD"
}
func hasPixPayload(_ order: PublicOrderResult) -> Bool {
let fromPayment = (order.payment?.pix?.copyPaste ?? "")
.trimmingCharacters(in: .whitespacesAndNewlines)
if fromPayment.isEmpty == false { return true }
let fromPayload = (order.paymentPayload?.copyPaste ?? "")
.trimmingCharacters(in: .whitespacesAndNewlines)
return fromPayload.isEmpty == false
}
func isPaymentPending(_ order: PublicOrderResult) -> Bool {
let status = normalize(order.status)
let paymentStatus = normalize(order.paymentStatus)
let nextAction = normalize(order.nextAction)
if status.contains("PAYMENT_PENDING") {
return true
}
if paymentStatus.contains("PENDING") {
return true
}
if nextAction.contains("PAY") || nextAction.contains("PAYMENT") {
return true
}
return false
}
func isInStorePayment(_ order: PublicOrderResult) -> Bool {
let nextAction = normalize(order.nextAction)
if nextAction.contains("TRACK") || nextAction.contains("DELIVER") {
return true
}
let status = normalize(order.status)
if status.contains("PREPAR") ||
status.contains("READY") ||
status.contains("DELIVER") ||
status.contains("ROTA") ||
status.contains("OUT_FOR_DELIVERY") {
return true
}
return false
}
func normalizePaymentMethod(_ order: PublicOrderResult) -> String {
let first = normalize(order.paymentMethodCode)
if first.isEmpty == false {
return first
}
let second = normalize(order.paymentMethod)
if second.isEmpty == false {
return second
}
return normalize(fallbackPaymentMethod)
}
func normalize(_ value: String?) -> String {
(value ?? "")
.trimmingCharacters(in: .whitespacesAndNewlines)
.uppercased()
}
}