import Foundation enum ReviewPlatform: String, Encodable { case ios case android case web static var current: ReviewPlatform { #if os(iOS) return .ios #else return .web #endif } } struct SubmitOrderReviewPayload: Encodable { let rate: Int let message: String let orderRate: Int let orderComment: String let orderPositiveTags: [String] let orderImprovementTags: [String] let deliverySentiment: String let deliveryPositiveTags: [String] let deliveryNegativeTags: [String] let appNps: Int let platform: String } struct ReviewTagItem: Decodable, Hashable, Identifiable { let id: String let label: String } struct ReviewOrderTagRules: Decodable { let positiveAllowedWhenRateGte: Int? let improvementAllowedWhenRateLte: Int? } struct ReviewOrderTagsCatalog: Decodable { let positive: [ReviewTagItem] let improvement: [ReviewTagItem] let rules: ReviewOrderTagRules? } struct ReviewDeliverySentimentRule: Decodable { let id: String let allowedTags: [String] } struct ReviewDeliveryTagsCatalog: Decodable { let sentiments: [ReviewDeliverySentimentRule] let positive: [ReviewTagItem] let negative: [ReviewTagItem] } struct ReviewNpsCatalog: Decodable { let min: Int? let max: Int? } struct ReviewAppTagsCatalog: Decodable { let nps: ReviewNpsCatalog? let platforms: [String]? } struct ReviewTagsCatalog: Decodable { let version: String? let order: ReviewOrderTagsCatalog? let delivery: ReviewDeliveryTagsCatalog? let app: ReviewAppTagsCatalog? } struct SubmitOrderReviewResult: Decodable { let id: String? let storeId: String? let userId: String? let clientName: String? let rate: Int? let message: String? let orderRate: Int? let orderComment: String? let deliverySentiment: String? let orderPositiveTags: [String]? let orderImprovementTags: [String]? let deliveryPositiveTags: [String]? let deliveryNegativeTags: [String]? let appNps: Int? let platform: String? let orderId: String? let date: String? let editableUntil: String? let storeReplyUntil: String? let reviewWindowExpiresAt: String? let storeReplyMessage: String? let storeReplyAt: String? enum CodingKeys: String, CodingKey { case id case storeId case userId case clientName case rate case message case orderRate case orderComment case deliverySentiment case deliveryFeedback case itemFeedback case improvementFeedback case orderPositiveTags case orderImprovementTags case deliveryPositiveTags case deliveryNegativeTags case appNps case app_nps case platform case orderId case date case editableUntil case storeReplyUntil case reviewWindowExpiresAt case storeReply case store_response case storeResponse case reply case storeReplyMessage case storeReplyAt } init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) id = ApiService.decodeFlexibleString(from: container, keys: [.id]) storeId = ApiService.decodeFlexibleString(from: container, keys: [.storeId]) userId = ApiService.decodeFlexibleString(from: container, keys: [.userId]) clientName = ApiService.decodeFlexibleString(from: container, keys: [.clientName]) rate = ApiService.decodeFlexibleInt(from: container, keys: [.rate]) message = ApiService.decodeFlexibleString(from: container, keys: [.message]) orderRate = ApiService.decodeFlexibleInt(from: container, keys: [.orderRate, .rate]) orderComment = ApiService.decodeFlexibleString(from: container, keys: [.orderComment, .message]) deliverySentiment = ApiService.decodeFlexibleString(from: container, keys: [.deliverySentiment, .deliveryFeedback]) orderPositiveTags = Self.decodeStringList(from: container, keys: [.orderPositiveTags, .itemFeedback]) orderImprovementTags = Self.decodeStringList(from: container, keys: [.orderImprovementTags, .improvementFeedback]) deliveryPositiveTags = (try? container.decode([String].self, forKey: .deliveryPositiveTags)) ?? nil deliveryNegativeTags = (try? container.decode([String].self, forKey: .deliveryNegativeTags)) ?? nil appNps = Self.decodeNps(from: container, keys: [.appNps, .app_nps]) platform = ApiService.decodeFlexibleString(from: container, keys: [.platform]) orderId = ApiService.decodeFlexibleString(from: container, keys: [.orderId]) date = ApiService.decodeFlexibleString(from: container, keys: [.date]) editableUntil = ApiService.decodeFlexibleString(from: container, keys: [.editableUntil]) storeReplyUntil = ApiService.decodeFlexibleString(from: container, keys: [.storeReplyUntil]) reviewWindowExpiresAt = ApiService.decodeFlexibleString(from: container, keys: [.reviewWindowExpiresAt]) storeReplyMessage = Self.decodeReplyMessage(from: container) storeReplyAt = Self.decodeReplyDate(from: container) } private static func decodeStringList( from container: KeyedDecodingContainer, keys: [CodingKeys] ) -> [String]? { for key in keys { if let list = try? container.decode([String].self, forKey: key) { return list } if let single = try? container.decode(String.self, forKey: key) { let normalized = single.trimmingCharacters(in: .whitespacesAndNewlines) if normalized.isEmpty == false { return [normalized] } } } return nil } private static func decodeNps( from container: KeyedDecodingContainer, keys: [CodingKeys] ) -> Int? { for key in keys { if let value = try? container.decode(Int.self, forKey: key) { return value } if let value = try? container.decode(Double.self, forKey: key) { return Int(value.rounded()) } if let raw = try? container.decode(String.self, forKey: key) { let trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines) if trimmed.isEmpty { continue } if let asInt = Int(trimmed) { return asInt } let normalized = trimmed.replacingOccurrences(of: ",", with: ".") if let asDouble = Double(normalized) { return Int(asDouble.rounded()) } } } return nil } private static func decodeReplyMessage(from container: KeyedDecodingContainer) -> String? { if let value = ApiService.decodeFlexibleString(from: container, keys: [.storeReplyMessage]) { return value } if let value = ApiService.decodeFlexibleString(from: container, keys: [.storeReply, .store_response, .storeResponse, .reply]) { return value } for key in [CodingKeys.storeReply, .store_response, .storeResponse, .reply] { if let object = try? container.decode([String: String].self, forKey: key) { let candidates = ["message", "text", "reply", "content", "body"] for candidate in candidates { let value = (object[candidate] ?? "").trimmingCharacters(in: .whitespacesAndNewlines) if value.isEmpty == false { return value } } } } return nil } private static func decodeReplyDate(from container: KeyedDecodingContainer) -> String? { if let value = ApiService.decodeFlexibleString(from: container, keys: [.storeReplyAt]) { return value } for key in [CodingKeys.storeReply, .store_response, .storeResponse, .reply] { if let object = try? container.decode([String: String].self, forKey: key) { let candidates = ["date", "createdAt", "updatedAt", "repliedAt", "replyAt"] for candidate in candidates { let value = (object[candidate] ?? "").trimmingCharacters(in: .whitespacesAndNewlines) if value.isEmpty == false { return value } } } } return nil } } struct PublicStoreReviewsResult: Decodable { let reviews: [SubmitOrderReviewResult] enum CodingKeys: String, CodingKey { case reviews case data case items } init(from decoder: Decoder) throws { if let list = try? [SubmitOrderReviewResult](from: decoder) { reviews = list return } let container = try decoder.container(keyedBy: CodingKeys.self) if let list = try? container.decode([SubmitOrderReviewResult].self, forKey: .reviews) { reviews = list return } if let list = try? container.decode([SubmitOrderReviewResult].self, forKey: .data) { reviews = list return } if let list = try? container.decode([SubmitOrderReviewResult].self, forKey: .items) { reviews = list return } reviews = [] } }