Manos Chorianopoulos

added getMerchantCategories request

This diff is collapsed. Click to expand it.
...@@ -2194,8 +2194,88 @@ public final class WarplySDK { ...@@ -2194,8 +2194,88 @@ public final class WarplySDK {
2194 } 2194 }
2195 } 2195 }
2196 2196
2197 - // MARK: - Profile 2197 + // MARK: - Merchant Categories
2198 +
2199 + /// Get merchant categories
2200 + /// - Parameters:
2201 + /// - language: Language for the categories (optional, defaults to applicationLocale)
2202 + /// - completion: Completion handler with merchant categories array
2203 + /// - failureCallback: Failure callback with error code
2204 + public func getMerchantCategories(
2205 + language: String? = nil,
2206 + completion: @escaping ([MerchantCategoryModel]?) -> Void,
2207 + failureCallback: @escaping (Int) -> Void
2208 + ) {
2209 + let finalLanguage = language ?? self.applicationLocale
2210 +
2211 + Task {
2212 + do {
2213 + let endpoint = Endpoint.getMerchantCategories(language: finalLanguage)
2214 + let response = try await networkService.requestRaw(endpoint)
2215 +
2216 + await MainActor.run {
2217 + if response["status"] as? Int == 1 {
2218 + // Success analytics
2219 + let dynatraceEvent = LoyaltySDKDynatraceEventModel()
2220 + dynatraceEvent._eventName = "custom_success_get_merchant_categories_loyalty"
2221 + dynatraceEvent._parameters = nil
2222 + self.postFrameworkEvent("dynatrace", sender: dynatraceEvent)
2223 +
2224 + var categories: [MerchantCategoryModel] = []
2225 +
2226 + // Parse from context.MAPP_SHOPS.result structure
2227 + if let mappShops = response["MAPP_SHOPS"] as? [String: Any],
2228 + let result = mappShops["result"] as? [[String: Any]] {
2229 +
2230 + for categoryDict in result {
2231 + let category = MerchantCategoryModel(dictionary: categoryDict)
2232 + categories.append(category)
2233 + }
2234 +
2235 + print("✅ [WarplySDK] Retrieved \(categories.count) merchant categories")
2236 + completion(categories)
2237 + } else {
2238 + print("⚠️ [WarplySDK] No merchant categories found in response")
2239 + completion([])
2240 + }
2241 + } else {
2242 + // Error analytics
2243 + let dynatraceEvent = LoyaltySDKDynatraceEventModel()
2244 + dynatraceEvent._eventName = "custom_error_get_merchant_categories_loyalty"
2245 + dynatraceEvent._parameters = nil
2246 + self.postFrameworkEvent("dynatrace", sender: dynatraceEvent)
2247 +
2248 + failureCallback(-1)
2249 + }
2250 + }
2251 + } catch {
2252 + await MainActor.run {
2253 + self.handleError(error, context: "getMerchantCategories", endpoint: "getMerchantCategories", failureCallback: failureCallback)
2254 + }
2255 + }
2256 + }
2257 + }
2198 2258
2259 + /// Get merchant categories (async/await variant)
2260 + /// - Parameter language: Language for the categories (optional, defaults to applicationLocale)
2261 + /// - Returns: Array of merchant categories
2262 + /// - Throws: WarplyError if the request fails
2263 + public func getMerchantCategories(language: String? = nil) async throws -> [MerchantCategoryModel] {
2264 + return try await withCheckedThrowingContinuation { continuation in
2265 + getMerchantCategories(language: language, completion: { categories in
2266 + if let categories = categories {
2267 + continuation.resume(returning: categories)
2268 + } else {
2269 + continuation.resume(throwing: WarplyError.networkError)
2270 + }
2271 + }, failureCallback: { errorCode in
2272 + continuation.resume(throwing: WarplyError.unknownError(errorCode))
2273 + })
2274 + }
2275 + }
2276 +
2277 + // MARK: - Profile
2278 +
2199 /// Get user profile details 2279 /// Get user profile details
2200 /// - Parameters: 2280 /// - Parameters:
2201 /// - completion: Completion handler with profile model 2281 /// - completion: Completion handler with profile model
......
...@@ -71,6 +71,7 @@ public enum Endpoint { ...@@ -71,6 +71,7 @@ public enum Endpoint {
71 // Market & Merchants 71 // Market & Merchants
72 case getMarketPassDetails 72 case getMarketPassDetails
73 case getMerchants(language: String, categories: [String], defaultShown: Bool, center: Double, tags: [String], uuid: String, distance: Int, parentUuids: [String]) 73 case getMerchants(language: String, categories: [String], defaultShown: Bool, center: Double, tags: [String], uuid: String, distance: Int, parentUuids: [String])
74 + case getMerchantCategories(language: String)
74 75
75 // Card Management 76 // Card Management
76 case addCard(cardNumber: String, cardIssuer: String, cardHolder: String, expirationMonth: String, expirationYear: String) 77 case addCard(cardNumber: String, cardIssuer: String, cardHolder: String, expirationMonth: String, expirationYear: String)
...@@ -126,7 +127,7 @@ public enum Endpoint { ...@@ -126,7 +127,7 @@ public enum Endpoint {
126 return "/user/v5/{appUUID}/logout" 127 return "/user/v5/{appUUID}/logout"
127 128
128 // Standard Context endpoints - /api/mobile/v2/{appUUID}/context/ 129 // Standard Context endpoints - /api/mobile/v2/{appUUID}/context/
129 - case .getCampaigns, .getAvailableCoupons, .getCouponSets: 130 + case .getCampaigns, .getAvailableCoupons, .getCouponSets, .getMerchantCategories:
130 return "/api/mobile/v2/{appUUID}/context/" 131 return "/api/mobile/v2/{appUUID}/context/"
131 132
132 // Authenticated Context endpoints - /oauth/{appUUID}/context 133 // Authenticated Context endpoints - /oauth/{appUUID}/context
...@@ -159,7 +160,7 @@ public enum Endpoint { ...@@ -159,7 +160,7 @@ public enum Endpoint {
159 switch self { 160 switch self {
160 case .register, .changePassword, .resetPassword, .requestOtp, .verifyTicket, .refreshToken, .logout, .getCampaigns, .getCampaignsPersonalized, 161 case .register, .changePassword, .resetPassword, .requestOtp, .verifyTicket, .refreshToken, .logout, .getCampaigns, .getCampaignsPersonalized,
161 .getCoupons, .getCouponSets, .getAvailableCoupons, 162 .getCoupons, .getCouponSets, .getAvailableCoupons,
162 - .getMarketPassDetails, .getProfile, .addCard, .getCards, .deleteCard, .getTransactionHistory, .getPointsHistory, .validateCoupon, .redeemCoupon, .getMerchants, .sendEvent, .sendDeviceInfo, .getCosmoteUser: 163 + .getMarketPassDetails, .getProfile, .addCard, .getCards, .deleteCard, .getTransactionHistory, .getPointsHistory, .validateCoupon, .redeemCoupon, .getMerchants, .getMerchantCategories, .sendEvent, .sendDeviceInfo, .getCosmoteUser:
163 return .POST 164 return .POST
164 case .getSingleCampaign, .getNetworkStatus: 165 case .getSingleCampaign, .getNetworkStatus:
165 return .GET 166 return .GET
...@@ -379,6 +380,15 @@ public enum Endpoint { ...@@ -379,6 +380,15 @@ public enum Endpoint {
379 ] 380 ]
380 ] 381 ]
381 382
383 + // Merchant Categories - using shops structure for DEI API
384 + case .getMerchantCategories(let language):
385 + return [
386 + "shops": [
387 + "language": language,
388 + "action": "retrieve_categories"
389 + ]
390 + ]
391 +
382 // Analytics endpoints - events structure 392 // Analytics endpoints - events structure
383 case .sendEvent(let eventName, let priority): 393 case .sendEvent(let eventName, let priority):
384 return [ 394 return [
...@@ -434,7 +444,7 @@ public enum Endpoint { ...@@ -434,7 +444,7 @@ public enum Endpoint {
434 return .userManagement 444 return .userManagement
435 445
436 // Standard Context - /api/mobile/v2/{appUUID}/context/ 446 // Standard Context - /api/mobile/v2/{appUUID}/context/
437 - case .getCampaigns, .getAvailableCoupons, .getCouponSets: 447 + case .getCampaigns, .getAvailableCoupons, .getCouponSets, .getMerchantCategories:
438 return .standardContext 448 return .standardContext
439 449
440 // Authenticated Context - /oauth/{appUUID}/context 450 // Authenticated Context - /oauth/{appUUID}/context
...@@ -476,7 +486,7 @@ public enum Endpoint { ...@@ -476,7 +486,7 @@ public enum Endpoint {
476 // Standard Authentication (loyalty headers only) 486 // Standard Authentication (loyalty headers only)
477 case .register, .resetPassword, .requestOtp, .getCampaigns, .getAvailableCoupons, .getCouponSets, .refreshToken, .logout, 487 case .register, .resetPassword, .requestOtp, .getCampaigns, .getAvailableCoupons, .getCouponSets, .refreshToken, .logout,
478 .verifyTicket, .getSingleCampaign, .sendEvent, .sendDeviceInfo, 488 .verifyTicket, .getSingleCampaign, .sendEvent, .sendDeviceInfo,
479 - .getMerchants, .getNetworkStatus: 489 + .getMerchants, .getMerchantCategories, .getNetworkStatus:
480 return .standard 490 return .standard
481 491
482 // Bearer Token Authentication (loyalty headers + Authorization: Bearer) 492 // Bearer Token Authentication (loyalty headers + Authorization: Bearer)
......
...@@ -962,6 +962,22 @@ extension NetworkService { ...@@ -962,6 +962,22 @@ extension NetworkService {
962 return response 962 return response
963 } 963 }
964 964
965 + // MARK: - Merchant Categories Methods
966 +
967 + /// Get merchant categories
968 + /// - Parameter language: Language for the categories
969 + /// - Returns: Response dictionary containing merchant categories
970 + /// - Throws: NetworkError if request fails
971 + public func getMerchantCategories(language: String) async throws -> [String: Any] {
972 + print("🔄 [NetworkService] Getting merchant categories for language: \(language)")
973 + let endpoint = Endpoint.getMerchantCategories(language: language)
974 + let response = try await requestRaw(endpoint)
975 +
976 + print("✅ [NetworkService] Get merchant categories request completed")
977 +
978 + return response
979 + }
980 +
965 // MARK: - Coupon Operations Methods 981 // MARK: - Coupon Operations Methods
966 982
967 /// Validate a coupon for the user 983 /// Validate a coupon for the user
......
1 +//
2 +// MerchantCategoryModel.swift
3 +// SwiftWarplyFramework
4 +//
5 +// Created by Warply on 28/07/2025.
6 +// Copyright © 2025 Warply. All rights reserved.
7 +//
8 +
9 +import Foundation
10 +
11 +// MARK: - Merchant Category Model
12 +
13 +public class MerchantCategoryModel: NSObject {
14 + private var uuid: String?
15 + private var admin_name: String?
16 + private var image: String?
17 + private var parent: String?
18 + private var fields: String?
19 + private var children: [Any]?
20 + private var count: Int?
21 + private var name: String?
22 +
23 + public init() {
24 + self.uuid = ""
25 + self.admin_name = ""
26 + self.image = ""
27 + self.parent = ""
28 + self.fields = ""
29 + self.children = []
30 + self.count = 0
31 + self.name = ""
32 + }
33 +
34 + public init(dictionary: [String: Any]) {
35 + self.uuid = dictionary["uuid"] as? String ?? ""
36 + self.admin_name = dictionary["admin_name"] as? String ?? ""
37 + self.image = dictionary["image"] as? String ?? ""
38 + self.parent = dictionary["parent"] as? String
39 + self.fields = dictionary["fields"] as? String ?? ""
40 + self.children = dictionary["children"] as? [Any] ?? []
41 + self.count = dictionary["count"] as? Int ?? 0
42 + self.name = dictionary["name"] as? String
43 + }
44 +
45 + // MARK: - Public Accessors
46 +
47 + public var _uuid: String {
48 + get { return self.uuid ?? "" }
49 + set(newValue) { self.uuid = newValue }
50 + }
51 +
52 + public var _admin_name: String {
53 + get { return self.admin_name ?? "" }
54 + set(newValue) { self.admin_name = newValue }
55 + }
56 +
57 + public var _image: String {
58 + get { return self.image ?? "" }
59 + set(newValue) { self.image = newValue }
60 + }
61 +
62 + public var _parent: String? {
63 + get { return self.parent }
64 + set(newValue) { self.parent = newValue }
65 + }
66 +
67 + public var _fields: String {
68 + get { return self.fields ?? "" }
69 + set(newValue) { self.fields = newValue }
70 + }
71 +
72 + public var _children: [Any] {
73 + get { return self.children ?? [] }
74 + set(newValue) { self.children = newValue }
75 + }
76 +
77 + public var _count: Int {
78 + get { return self.count ?? 0 }
79 + set(newValue) { self.count = newValue }
80 + }
81 +
82 + public var _name: String? {
83 + get { return self.name }
84 + set(newValue) { self.name = newValue }
85 + }
86 +
87 + // MARK: - Computed Properties
88 +
89 + /// Display name for the category - uses name if available, otherwise falls back to admin_name
90 + public var displayName: String {
91 + return self.name ?? self.admin_name ?? ""
92 + }
93 +
94 + /// Clean image URL with whitespace trimmed
95 + public var cleanImageUrl: String {
96 + return self.image?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
97 + }
98 +
99 + /// Check if this category has a parent category
100 + public var hasParent: Bool {
101 + return self.parent != nil && !(self.parent?.isEmpty ?? true)
102 + }
103 +
104 + /// Check if this category has child categories
105 + public var hasChildren: Bool {
106 + return !self.children.isEmpty
107 + }
108 +}
109 +
110 +// MARK: - Codable Support
111 +
112 +extension MerchantCategoryModel: Codable {
113 + private enum CodingKeys: String, CodingKey {
114 + case uuid
115 + case admin_name
116 + case image
117 + case parent
118 + case fields
119 + case children
120 + case count
121 + case name
122 + }
123 +
124 + public func encode(to encoder: Encoder) throws {
125 + var container = encoder.container(keyedBy: CodingKeys.self)
126 + try container.encode(uuid, forKey: .uuid)
127 + try container.encode(admin_name, forKey: .admin_name)
128 + try container.encode(image, forKey: .image)
129 + try container.encodeIfPresent(parent, forKey: .parent)
130 + try container.encode(fields, forKey: .fields)
131 + try container.encode(count, forKey: .count)
132 + try container.encodeIfPresent(name, forKey: .name)
133 + // Note: children is [Any] so we skip encoding it for now
134 + }
135 +
136 + public required init(from decoder: Decoder) throws {
137 + let container = try decoder.container(keyedBy: CodingKeys.self)
138 + self.uuid = try container.decodeIfPresent(String.self, forKey: .uuid)
139 + self.admin_name = try container.decodeIfPresent(String.self, forKey: .admin_name)
140 + self.image = try container.decodeIfPresent(String.self, forKey: .image)
141 + self.parent = try container.decodeIfPresent(String.self, forKey: .parent)
142 + self.fields = try container.decodeIfPresent(String.self, forKey: .fields)
143 + self.count = try container.decodeIfPresent(Int.self, forKey: .count)
144 + self.name = try container.decodeIfPresent(String.self, forKey: .name)
145 + self.children = [] // Default empty array for children
146 + }
147 +}
148 +
149 +// MARK: - Debug Description
150 +
151 +extension MerchantCategoryModel {
152 + public override var description: String {
153 + return """
154 + MerchantCategoryModel {
155 + uuid: \(_uuid)
156 + displayName: \(displayName)
157 + admin_name: \(_admin_name)
158 + image: \(cleanImageUrl)
159 + parent: \(_parent ?? "nil")
160 + count: \(_count)
161 + hasChildren: \(hasChildren)
162 + }
163 + """
164 + }
165 +}
...@@ -46,6 +46,9 @@ import UIKit ...@@ -46,6 +46,9 @@ import UIKit
46 // Merchants data 46 // Merchants data
47 var merchants: [MerchantModel] = [] 47 var merchants: [MerchantModel] = []
48 48
49 + // Merchant categories data
50 + var merchantCategories: [MerchantCategoryModel] = []
51 +
49 // Profile data 52 // Profile data
50 var profileModel: ProfileModel? 53 var profileModel: ProfileModel?
51 var profileSection: SectionModel? 54 var profileSection: SectionModel?
...@@ -176,9 +179,8 @@ import UIKit ...@@ -176,9 +179,8 @@ import UIKit
176 self.merchants = merchants 179 self.merchants = merchants
177 print("✅ [MyRewardsViewController] Loaded \(merchants.count) merchants") 180 print("✅ [MyRewardsViewController] Loaded \(merchants.count) merchants")
178 181
179 - // For now, create the coupon sets section without filtering 182 + // Load merchant categories after merchants success
180 - // Later this will be enhanced to filter by merchant categories 183 + self.loadMerchantCategories()
181 - self.createCouponSetsSection()
182 184
183 } failureCallback: { [weak self] errorCode in 185 } failureCallback: { [weak self] errorCode in
184 print("Failed to load merchants: \(errorCode)") 186 print("Failed to load merchants: \(errorCode)")
...@@ -187,8 +189,58 @@ import UIKit ...@@ -187,8 +189,58 @@ import UIKit
187 } 189 }
188 } 190 }
189 191
192 + // MARK: - Merchant Categories Loading
193 + private func loadMerchantCategories() {
194 + // Load merchant categories from WarplySDK
195 + WarplySDK.shared.getMerchantCategories { [weak self] categories in
196 + guard let self = self, let categories = categories else {
197 + // If categories fail to load, still create coupon sets section without filtering
198 + self?.createCouponSetsSection()
199 + return
200 + }
201 +
202 + self.merchantCategories = categories
203 + print("✅ [MyRewardsViewController] Loaded \(categories.count) merchant categories")
204 +
205 + // TODO: Implement category-based filtering for coupon sets sections
206 + // For now, create the standard coupon sets section
207 + self.createCouponSetsSection()
208 +
209 + } failureCallback: { [weak self] errorCode in
210 + print("Failed to load merchant categories: \(errorCode)")
211 + // If categories fail, still show coupon sets without filtering
212 + self?.createCouponSetsSection()
213 + }
214 + }
215 +
190 private func createCouponSetsSection() { 216 private func createCouponSetsSection() {
191 - // Create coupon sets section with real data 217 + // TODO: IMPLEMENT CATEGORY-BASED FILTERING
218 + //
219 + // Current logic: Creates one section with all coupon sets
220 + //
221 + // Future enhancement: Filter coupon sets into different sections based on categories
222 + // Logic:
223 + // 1. For each couponset, get its merchant_uuid
224 + // 2. Find the merchant with that merchant_uuid in self.merchants
225 + // 3. Get the merchant's category_uuid
226 + // 4. Find the category with that category_uuid in self.merchantCategories
227 + // 5. Group coupon sets by category
228 + // 6. Create separate sections for each category
229 + //
230 + // Example structure after filtering:
231 + // - Section: "Εκπαίδευση" (Education) - coupon sets from education merchants
232 + // - Section: "Ψυχαγωγία" (Entertainment) - coupon sets from entertainment merchants
233 + // - etc.
234 + //
235 + // Implementation steps:
236 + // 1. Create a dictionary to group coupon sets by category: [String: [CouponSetItemModel]]
237 + // 2. Iterate through self.couponSets
238 + // 3. For each coupon set, find its merchant and category
239 + // 4. Add coupon set to the appropriate category group
240 + // 5. Create a SectionModel for each category group
241 + // 6. Sort sections by category name or priority
242 +
243 + // Current implementation (temporary):
192 if !self.couponSets.isEmpty { 244 if !self.couponSets.isEmpty {
193 let couponSetsSection = SectionModel( 245 let couponSetsSection = SectionModel(
194 sectionType: .myRewardsHorizontalCouponsets, 246 sectionType: .myRewardsHorizontalCouponsets,
......