Manos Chorianopoulos

new getCouponFilters request and MyRewardsViewController sections handling

...@@ -438,6 +438,7 @@ private final class SDKState { ...@@ -438,6 +438,7 @@ private final class SDKState {
438 var carouselList: [CampaignItemModel] = [] 438 var carouselList: [CampaignItemModel] = []
439 var marketPassDetails: MarketPassDetailsModel? 439 var marketPassDetails: MarketPassDetailsModel?
440 var supermarketCampaign: CampaignItemModel? 440 var supermarketCampaign: CampaignItemModel?
441 + var couponFilters: CouponFiltersDataModel?
441 442
442 private init() {} 443 private init() {}
443 } 444 }
...@@ -2545,6 +2546,77 @@ public final class WarplySDK { ...@@ -2545,6 +2546,77 @@ public final class WarplySDK {
2545 } 2546 }
2546 } 2547 }
2547 2548
2549 + /// Get coupon filters via new endpoint
2550 + /// - Parameters:
2551 + /// - language: Optional language code (defaults to applicationLocale)
2552 + /// - completion: Returns the raw response dictionary on success
2553 + /// - failureCallback: Returns error code on failure
2554 + public func getCouponFilters(
2555 + language: String? = nil,
2556 + completion: @escaping (CouponFiltersDataModel?) -> Void,
2557 + failureCallback: @escaping (Int) -> Void
2558 + ) {
2559 + let finalLanguage = language ?? self.applicationLocale
2560 +
2561 + Task {
2562 + do {
2563 + let endpoint = Endpoint.getCouponFilters(language: finalLanguage)
2564 + let response = try await networkService.requestRaw(endpoint)
2565 +
2566 + await MainActor.run {
2567 + print("📥 getCouponFilters response: \(response)")
2568 +
2569 + let dynatraceEvent = LoyaltySDKDynatraceEventModel()
2570 + dynatraceEvent._eventName = "custom_success_couponfilters_loyalty"
2571 + dynatraceEvent._parameters = nil
2572 + self.postFrameworkEvent("dynatrace", sender: dynatraceEvent)
2573 +
2574 + if let resultData = response["result"] as? [String: Any] {
2575 + let filterModel = CouponFiltersDataModel(dictionary: resultData)
2576 + self.setCouponFilters(filterModel)
2577 + completion(filterModel)
2578 + } else {
2579 + completion(nil)
2580 + }
2581 + }
2582 + } catch {
2583 + await MainActor.run {
2584 + let dynatraceEvent = LoyaltySDKDynatraceEventModel()
2585 + dynatraceEvent._eventName = "custom_error_couponfilters_loyalty"
2586 + dynatraceEvent._parameters = nil
2587 + self.postFrameworkEvent("dynatrace", sender: dynatraceEvent)
2588 +
2589 + if let networkError = error as? NetworkError {
2590 + failureCallback(networkError.code)
2591 + } else {
2592 + failureCallback(-1)
2593 + }
2594 + }
2595 + }
2596 + }
2597 + }
2598 +
2599 + /// Get coupon filters (async/await variant)
2600 + /// - Parameters:
2601 + /// - language: Language code for localized content (optional, defaults to applicationLocale)
2602 + /// - Returns: Parsed filters model
2603 + /// - Throws: WarplyError if the request fails
2604 + public func getCouponFilters(
2605 + language: String? = nil
2606 + ) async throws -> CouponFiltersDataModel {
2607 + return try await withCheckedThrowingContinuation { continuation in
2608 + getCouponFilters(language: language, completion: { response in
2609 + if let response = response {
2610 + continuation.resume(returning: response)
2611 + } else {
2612 + continuation.resume(throwing: WarplyError.networkError)
2613 + }
2614 + }, failureCallback: { errorCode in
2615 + continuation.resume(throwing: WarplyError.unknownError(errorCode))
2616 + })
2617 + }
2618 + }
2619 +
2548 /// Get available coupons (async/await variant) 2620 /// Get available coupons (async/await variant)
2549 /// - Returns: Dictionary of coupon availability data 2621 /// - Returns: Dictionary of coupon availability data
2550 /// - Throws: WarplyError if the request fails 2622 /// - Throws: WarplyError if the request fails
...@@ -4344,6 +4416,16 @@ public final class WarplySDK { ...@@ -4344,6 +4416,16 @@ public final class WarplySDK {
4344 return state.couponSets 4416 return state.couponSets
4345 } 4417 }
4346 4418
4419 + /// Set coupon filters data
4420 + public func setCouponFilters(_ filters: CouponFiltersDataModel) {
4421 + state.couponFilters = filters
4422 + }
4423 +
4424 + /// Get coupon filters data
4425 + public func getCouponFilters() -> CouponFiltersDataModel? {
4426 + return state.couponFilters
4427 + }
4428 +
4347 /// Set seasonal list 4429 /// Set seasonal list
4348 public func setSeasonalList(_ seasonalCoupons: [LoyaltyGiftsForYouPackage]) { 4430 public func setSeasonalList(_ seasonalCoupons: [LoyaltyGiftsForYouPackage]) {
4349 state.seasonalList = seasonalCoupons 4431 state.seasonalList = seasonalCoupons
......
...@@ -68,6 +68,7 @@ public enum Endpoint { ...@@ -68,6 +68,7 @@ public enum Endpoint {
68 case getCoupons(language: String, couponsetType: String) 68 case getCoupons(language: String, couponsetType: String)
69 case getCouponSets(language: String, active: Bool, visible: Bool, uuids: [String]?) 69 case getCouponSets(language: String, active: Bool, visible: Bool, uuids: [String]?)
70 case getCouponSetsNew(language: String, active: Bool, visible: Bool, region: String?, offerCategory: String?) 70 case getCouponSetsNew(language: String, active: Bool, visible: Bool, region: String?, offerCategory: String?)
71 + case getCouponFilters(language: String)
71 case getAvailableCoupons 72 case getAvailableCoupons
72 73
73 // Market & Merchants 74 // Market & Merchants
...@@ -143,7 +144,7 @@ public enum Endpoint { ...@@ -143,7 +144,7 @@ public enum Endpoint {
143 return "/api/mobile/v2/{appUUID}/context/" 144 return "/api/mobile/v2/{appUUID}/context/"
144 145
145 // Authenticated Context endpoints - /oauth/{appUUID}/context 146 // Authenticated Context endpoints - /oauth/{appUUID}/context
146 - case .getCampaignsPersonalized, .getCoupons, .getMarketPassDetails, .getProfile, .addCard, .getCards, .deleteCard, .getTransactionHistory, .getPointsHistory, .validateCoupon, .redeemCoupon, .retrieveCoupon, .getCarouselContent, .getCouponSetsNew: 147 + case .getCampaignsPersonalized, .getCoupons, .getMarketPassDetails, .getProfile, .addCard, .getCards, .deleteCard, .getTransactionHistory, .getPointsHistory, .validateCoupon, .redeemCoupon, .retrieveCoupon, .getCarouselContent, .getCouponSetsNew, .getCouponFilters:
147 return "/oauth/{appUUID}/context" 148 return "/oauth/{appUUID}/context"
148 149
149 // Session endpoints - /api/session/{sessionUuid} 150 // Session endpoints - /api/session/{sessionUuid}
...@@ -171,7 +172,7 @@ public enum Endpoint { ...@@ -171,7 +172,7 @@ public enum Endpoint {
171 public var method: HTTPMethod { 172 public var method: HTTPMethod {
172 switch self { 173 switch self {
173 case .register, .changePassword, .resetPassword, .requestOtp, .verifyTicket, .refreshToken, .logout, .getCampaigns, .getCampaignsPersonalized, 174 case .register, .changePassword, .resetPassword, .requestOtp, .verifyTicket, .refreshToken, .logout, .getCampaigns, .getCampaignsPersonalized,
174 - .getCoupons, .getCouponSets, .getCouponSetsNew, .getAvailableCoupons, 175 + .getCoupons, .getCouponSets, .getCouponSetsNew, .getCouponFilters, .getAvailableCoupons,
175 .getMarketPassDetails, .getProfile, .addCard, .getCards, .deleteCard, .getTransactionHistory, .getPointsHistory, .validateCoupon, .redeemCoupon, .retrieveCoupon, .getMerchants, .getMerchantCategories, .getStores, .getArticles, .sendEvent, .sendDeviceInfo, .getCosmoteUser, .deiLogin, .getCarouselContent: 176 .getMarketPassDetails, .getProfile, .addCard, .getCards, .deleteCard, .getTransactionHistory, .getPointsHistory, .validateCoupon, .redeemCoupon, .retrieveCoupon, .getMerchants, .getMerchantCategories, .getStores, .getArticles, .sendEvent, .sendDeviceInfo, .getCosmoteUser, .deiLogin, .getCarouselContent:
176 return .POST 177 return .POST
177 case .getSingleCampaign, .getNetworkStatus: 178 case .getSingleCampaign, .getNetworkStatus:
...@@ -314,6 +315,14 @@ public enum Endpoint { ...@@ -314,6 +315,14 @@ public enum Endpoint {
314 "coupon": couponParams 315 "coupon": couponParams
315 ] 316 ]
316 317
318 + case .getCouponFilters(let language):
319 + return [
320 + "coupon": [
321 + "action": "get_filters",
322 + "language": language
323 + ]
324 + ]
325 +
317 case .getAvailableCoupons: 326 case .getAvailableCoupons:
318 return [ 327 return [
319 "coupon": [ 328 "coupon": [
...@@ -523,7 +532,7 @@ public enum Endpoint { ...@@ -523,7 +532,7 @@ public enum Endpoint {
523 return .standardContext 532 return .standardContext
524 533
525 // Authenticated Context - /oauth/{appUUID}/context 534 // Authenticated Context - /oauth/{appUUID}/context
526 - case .getCampaignsPersonalized, .getCoupons, .getMarketPassDetails, .getProfile, .addCard, .getCards, .deleteCard, .getTransactionHistory, .getPointsHistory, .validateCoupon, .redeemCoupon, .retrieveCoupon, .getCarouselContent, .getCouponSetsNew: 535 + case .getCampaignsPersonalized, .getCoupons, .getMarketPassDetails, .getProfile, .addCard, .getCards, .deleteCard, .getTransactionHistory, .getPointsHistory, .validateCoupon, .redeemCoupon, .retrieveCoupon, .getCarouselContent, .getCouponSetsNew, .getCouponFilters:
527 return .authenticatedContext 536 return .authenticatedContext
528 537
529 // Authentication - /oauth/{appUUID}/login, /oauth/{appUUID}/token 538 // Authentication - /oauth/{appUUID}/login, /oauth/{appUUID}/token
...@@ -565,7 +574,7 @@ public enum Endpoint { ...@@ -565,7 +574,7 @@ public enum Endpoint {
565 return .standard 574 return .standard
566 575
567 // Bearer Token Authentication (loyalty headers + Authorization: Bearer) 576 // Bearer Token Authentication (loyalty headers + Authorization: Bearer)
568 - case .changePassword, .getCampaignsPersonalized, .getCoupons, .getMarketPassDetails, .getProfile, .addCard, .getCards, .deleteCard, .getTransactionHistory, .getPointsHistory, .validateCoupon, .redeemCoupon, .retrieveCoupon, .getCarouselContent, .getCouponSetsNew: 577 + case .changePassword, .getCampaignsPersonalized, .getCoupons, .getMarketPassDetails, .getProfile, .addCard, .getCards, .deleteCard, .getTransactionHistory, .getPointsHistory, .validateCoupon, .redeemCoupon, .retrieveCoupon, .getCarouselContent, .getCouponSetsNew, .getCouponFilters:
569 return .bearerToken 578 return .bearerToken
570 579
571 // Basic Authentication (loyalty headers + Authorization: Basic) 580 // Basic Authentication (loyalty headers + Authorization: Basic)
......
...@@ -659,6 +659,72 @@ public class RedeemedSMHistoryModel { ...@@ -659,6 +659,72 @@ public class RedeemedSMHistoryModel {
659 } 659 }
660 } 660 }
661 661
662 +// MARK: - Coupon Filters Models
663 +
664 +public class CouponFiltersDataModel {
665 + private var offer_categories: [CouponOfferCategoryModel]?
666 + private var regions: [String]?
667 +
668 + public init(dictionary: [String: Any]) {
669 + if let regionsArray = dictionary["regions"] as? [Any] {
670 + self.regions = regionsArray.compactMap { $0 as? String }
671 + } else {
672 + self.regions = []
673 + }
674 +
675 + if let categoriesArray = dictionary["offer_categories"] as? [[String: Any]] {
676 + var tempCategories: [CouponOfferCategoryModel] = []
677 + for catDict in categoriesArray {
678 + tempCategories.append(CouponOfferCategoryModel(dictionary: catDict))
679 + }
680 + self.offer_categories = tempCategories
681 + } else {
682 + self.offer_categories = []
683 + }
684 + }
685 +
686 + public var _offer_categories: [CouponOfferCategoryModel]? { get { return self.offer_categories } }
687 + public var _regions: [String]? { get { return self.regions } }
688 +}
689 +
690 +public class CouponOfferCategoryModel {
691 + private var uuid: String?
692 + private var admin_name: String?
693 + private var name: String?
694 + private var image: String?
695 + private var parent: String?
696 + private var children: [CouponOfferCategoryModel]?
697 +
698 + public init(dictionary: [String: Any]) {
699 + self.uuid = dictionary["uuid"] as? String
700 + self.admin_name = dictionary["admin_name"] as? String
701 + self.name = dictionary["name"] as? String
702 +
703 + if let imgString = dictionary["image"] as? String {
704 + self.image = imgString.trimmingCharacters(in: .whitespacesAndNewlines)
705 + }
706 +
707 + self.parent = dictionary["parent"] as? String
708 +
709 + if let childrenArray = dictionary["children"] as? [[String: Any]] {
710 + var tempChildren: [CouponOfferCategoryModel] = []
711 + for childDict in childrenArray {
712 + tempChildren.append(CouponOfferCategoryModel(dictionary: childDict))
713 + }
714 + self.children = tempChildren
715 + } else {
716 + self.children = []
717 + }
718 + }
719 +
720 + public var _uuid: String { get { return self.uuid ?? "" } }
721 + public var _admin_name: String { get { return self.admin_name ?? "" } }
722 + public var _name: String { get { return self.name ?? "" } }
723 + public var _image: String { get { return self.image ?? "" } }
724 + public var _parent: String { get { return self.parent ?? "" } }
725 + public var _children: [CouponOfferCategoryModel]? { get { return self.children } }
726 +}
727 +
662 // MARK: - String Extension for HTML 728 // MARK: - String Extension for HTML
663 729
664 // extension String { 730 // extension String {
......
...@@ -53,6 +53,7 @@ import UIKit ...@@ -53,6 +53,7 @@ import UIKit
53 53
54 // Coupon sets data 54 // Coupon sets data
55 var couponSets: [CouponSetItemModel] = [] 55 var couponSets: [CouponSetItemModel] = []
56 + var couponFilters: CouponFiltersDataModel?
56 57
57 // Merchants data 58 // Merchants data
58 var merchants: [MerchantModel] = [] 59 var merchants: [MerchantModel] = []
...@@ -91,7 +92,6 @@ import UIKit ...@@ -91,7 +92,6 @@ import UIKit
91 // Load data 92 // Load data
92 loadProfile() // Load Profile 93 loadProfile() // Load Profile
93 loadCarouselContent() 94 loadCarouselContent()
94 - // loadCampaigns() // Load campaigns
95 loadCouponSets() // Load couponsets 95 loadCouponSets() // Load couponsets
96 loadCoupons() 96 loadCoupons()
97 97
...@@ -287,26 +287,12 @@ import UIKit ...@@ -287,26 +287,12 @@ import UIKit
287 // } 287 // }
288 288
289 private func loadCouponSets() { 289 private func loadCouponSets() {
290 - // Load coupon sets from WarplySDK
291 - // WarplySDK.shared.getCouponSets { [weak self] couponSets in
292 - // guard let self = self, let couponSets = couponSets else { return }
293 -
294 - // self.couponSets = couponSets
295 -
296 - // // Load merchants after getting coupon sets
297 - // self.loadMerchants()
298 -
299 - // } failureCallback: { [weak self] errorCode in
300 - // print("Failed to load coupon sets: \(errorCode)")
301 - // // No sections added on failure - table will be empty
302 - // }
303 -
304 WarplySDK.shared.getCouponSetsNew( 290 WarplySDK.shared.getCouponSetsNew(
305 completion: { [weak self] couponSets in 291 completion: { [weak self] couponSets in
306 guard let self = self, let couponSets = couponSets else { return } 292 guard let self = self, let couponSets = couponSets else { return }
307 293
308 self.couponSets = couponSets 294 self.couponSets = couponSets
309 - self?.createCouponSetsSection() 295 + self.loadCouponFilters()
310 296
311 }, 297 },
312 failureCallback: { errorCode in 298 failureCallback: { errorCode in
...@@ -314,6 +300,19 @@ import UIKit ...@@ -314,6 +300,19 @@ import UIKit
314 } 300 }
315 ) 301 )
316 } 302 }
303 +
304 + private func loadCouponFilters() {
305 + WarplySDK.shared.getCouponFilters { [weak self] couponFilters in
306 + guard let self = self, let couponFilters = couponFilters else { return }
307 +
308 + self.couponFilters = couponFilters
309 + self.createCouponSetsSection()
310 +
311 + } failureCallback: { errorCode in
312 + print("=== getCouponFilters Error: \(errorCode)")
313 + self.createCouponSetsSection()
314 + }
315 + }
317 316
318 // MARK: - Coupons Loading 317 // MARK: - Coupons Loading
319 private func loadCoupons() { 318 private func loadCoupons() {
...@@ -368,89 +367,60 @@ import UIKit ...@@ -368,89 +367,60 @@ import UIKit
368 // } 367 // }
369 368
370 // TODO: DELETE loadMerchants - No matching needed 369 // TODO: DELETE loadMerchants - No matching needed
371 - private func loadMerchants() { 370 + // private func loadMerchants() {
372 - // Load merchants from WarplySDK (using enhanced getMerchants method) 371 + // // Load merchants from WarplySDK (using enhanced getMerchants method)
373 - WarplySDK.shared.getMerchants { [weak self] merchants in 372 + // WarplySDK.shared.getMerchants { [weak self] merchants in
374 - guard let self = self, let merchants = merchants else { 373 + // guard let self = self, let merchants = merchants else {
375 - // If merchants fail to load, still create coupon sets section without filtering 374 + // // If merchants fail to load, still create coupon sets section without filtering
376 - self?.createCouponSetsSection() 375 + // self?.createCouponSetsSection()
377 - return 376 + // return
378 - } 377 + // }
379 378
380 - self.merchants = merchants 379 + // self.merchants = merchants
381 - print("✅ [MyRewardsViewController] Loaded \(merchants.count) merchants") 380 + // print("✅ [MyRewardsViewController] Loaded \(merchants.count) merchants")
382 381
383 - // Load merchant categories after merchants success 382 + // // Load merchant categories after merchants success
384 - self.loadMerchantCategories() 383 + // self.loadMerchantCategories()
385 384
386 - } failureCallback: { [weak self] errorCode in 385 + // } failureCallback: { [weak self] errorCode in
387 - print("Failed to load merchants: \(errorCode)") 386 + // print("Failed to load merchants: \(errorCode)")
388 - // If merchants fail, still show coupon sets without filtering 387 + // // If merchants fail, still show coupon sets without filtering
389 - self?.createCouponSetsSection() 388 + // self?.createCouponSetsSection()
390 - } 389 + // }
391 - } 390 + // }
392 391
393 // MARK: - Merchant Categories Loading 392 // MARK: - Merchant Categories Loading
394 - private func loadMerchantCategories() { 393 + // private func loadMerchantCategories() {
395 - // Load merchant categories from WarplySDK 394 + // // Load merchant categories from WarplySDK
396 - WarplySDK.shared.getMerchantCategories { [weak self] categories in 395 + // WarplySDK.shared.getMerchantCategories { [weak self] categories in
397 - guard let self = self, let categories = categories else { 396 + // guard let self = self, let categories = categories else {
398 - // If categories fail to load, still create coupon sets section without filtering 397 + // // If categories fail to load, still create coupon sets section without filtering
399 - self?.createCouponSetsSection() 398 + // self?.createCouponSetsSection()
400 - return 399 + // return
401 - } 400 + // }
402 401
403 - self.merchantCategories = categories 402 + // self.merchantCategories = categories
404 - print("✅ [MyRewardsViewController] Loaded \(categories.count) merchant categories") 403 + // print("✅ [MyRewardsViewController] Loaded \(categories.count) merchant categories")
405 404
406 - // Create coupon sets sections with category-based filtering 405 + // // Create coupon sets sections with category-based filtering
407 - self.createCouponSetsSection() 406 + // self.createCouponSetsSection()
408 407
409 - } failureCallback: { [weak self] errorCode in 408 + // } failureCallback: { [weak self] errorCode in
410 - print("Failed to load merchant categories: \(errorCode)") 409 + // print("Failed to load merchant categories: \(errorCode)")
411 - // If categories fail, still show coupon sets without filtering 410 + // // If categories fail, still show coupon sets without filtering
412 - self?.createCouponSetsSection() 411 + // self?.createCouponSetsSection()
413 - } 412 + // }
414 - } 413 + // }
415 414
416 private func createCouponSetsSection() { 415 private func createCouponSetsSection() {
417 - // TODO: no merchant match any more 416 + print("🔍 [MyRewardsViewController] Starting coupon filtering by new offer_categories:")
418 -
419 - print("🔍 [MyRewardsViewController] Starting coupon filtering:")
420 print(" - Coupon Sets: \(couponSets.count)") 417 print(" - Coupon Sets: \(couponSets.count)")
421 - print(" - Merchants: \(merchants.count)")
422 - print(" - Categories: \(merchantCategories.count)")
423 -
424 - // Check if we have all required data for filtering
425 - guard !couponSets.isEmpty, !merchants.isEmpty, !merchantCategories.isEmpty else {
426 - print("⚠️ [MyRewardsViewController] Missing data for filtering - using fallback single section")
427 - print(" - Coupon Sets Empty: \(couponSets.isEmpty)")
428 - print(" - Merchants Empty: \(merchants.isEmpty)")
429 - print(" - Categories Empty: \(merchantCategories.isEmpty)")
430 -
431 - // Fallback: Create single section with all coupon sets
432 - createSingleCouponSetsSection()
433 - return
434 - }
435 418
436 - // Group coupon sets by merchant category
437 var categorySections: [SectionModel] = [] 419 var categorySections: [SectionModel] = []
438 - var processedCouponSets: Set<String> = [] // Track processed coupon sets to avoid duplicates
439 420
440 - // Extract promoted couponsets for "Top offers" section 421 + // 1. Top offers (promoted)
441 let promotedCouponSets = couponSets.filter { $0._promoted } 422 let promotedCouponSets = couponSets.filter { $0._promoted }
442 - print("🌟 [MyRewardsViewController] Found \(promotedCouponSets.count) promoted couponsets")
443 -
444 - // Create "Top offers" section if we have promoted couponsets
445 if !promotedCouponSets.isEmpty { 423 if !promotedCouponSets.isEmpty {
446 - // Bind merchant data to promoted couponsets
447 - for couponSet in promotedCouponSets {
448 - if let merchant = merchants.first(where: { $0._uuid == couponSet._merchant_uuid }) {
449 - couponSet._merchant = merchant
450 - print(" 🔗 Bound merchant '\(merchant._name)' to promoted coupon set '\(couponSet._name)'")
451 - }
452 - }
453 -
454 let topOffersSection = SectionModel( 424 let topOffersSection = SectionModel(
455 sectionType: .myRewardsHorizontalCouponsets, 425 sectionType: .myRewardsHorizontalCouponsets,
456 title: "Top offers", 426 title: "Top offers",
...@@ -461,128 +431,106 @@ import UIKit ...@@ -461,128 +431,106 @@ import UIKit
461 print(" ✅ Created 'Top offers' section with \(promotedCouponSets.count) items") 431 print(" ✅ Created 'Top offers' section with \(promotedCouponSets.count) items")
462 } 432 }
463 433
464 - print("🔄 [MyRewardsViewController] Processing categories for filtering...") 434 + // 2. Group by parent CouponFilter
435 + var groupedCouponSets: [String: [CouponSetItemModel]] = [:]
436 + var unmatchedCouponSets: [CouponSetItemModel] = []
465 437
466 - for category in merchantCategories { 438 + let filtersData = self.couponFilters?._offer_categories ?? []
467 - // Find merchants in this category 439 +
468 - let categoryMerchants = merchants.filter { merchant in 440 + for couponSet in couponSets {
469 - merchant._category_uuid == category._uuid 441 + let offerCategory = couponSet._offer_category
442 +
443 + if offerCategory.isEmpty {
444 + unmatchedCouponSets.append(couponSet)
445 + continue
470 } 446 }
471 447
472 - print(" - Category '\(category.displayName)' has \(categoryMerchants.count) merchants") 448 + var matchedParentName: String? = nil
473 449
474 - // Find coupon sets from merchants in this category 450 + for parentCategory in filtersData {
475 - let categoryCouponSets = couponSets.filter { couponSet in 451 + let parentName = parentCategory._name.isEmpty ? parentCategory._admin_name : parentCategory._name
476 - // Skip if already processed (avoid duplicates)
477 - guard !processedCouponSets.contains(couponSet._uuid) else { return false }
478 452
479 - // Check if this coupon set belongs to any merchant in this category 453 + // Check parent
480 - let belongsToCategory = categoryMerchants.contains { merchant in 454 + if parentCategory._uuid == offerCategory || parentCategory._admin_name == offerCategory || parentCategory._name == offerCategory {
481 - merchant._uuid == couponSet._merchant_uuid 455 + matchedParentName = parentName
456 + break
482 } 457 }
483 458
484 - if belongsToCategory { 459 + // Check children
485 - processedCouponSets.insert(couponSet._uuid) 460 + if let children = parentCategory._children {
486 - 461 + for child in children {
487 - // BIND MERCHANT DATA: Find and bind the merchant to this coupon set 462 + if child._uuid == offerCategory || child._admin_name == offerCategory || child._name == offerCategory {
488 - if let merchant = categoryMerchants.first(where: { $0._uuid == couponSet._merchant_uuid }) { 463 + matchedParentName = parentName
489 - couponSet._merchant = merchant 464 + break
490 - print(" 🔗 Bound merchant '\(merchant._name)' to coupon set '\(couponSet._name)'") 465 + }
491 } 466 }
492 } 467 }
493 468
494 - return belongsToCategory 469 + if matchedParentName != nil {
470 + break
471 + }
495 } 472 }
496 473
497 - print(" - Category '\(category.displayName)' has \(categoryCouponSets.count) coupon sets") 474 + if let parentName = matchedParentName {
498 - 475 + if groupedCouponSets[parentName] == nil {
499 - // Create section if we have coupon sets for this category 476 + groupedCouponSets[parentName] = []
500 - if !categoryCouponSets.isEmpty { 477 + }
501 - let section = SectionModel( 478 + groupedCouponSets[parentName]?.append(couponSet)
502 - sectionType: .myRewardsHorizontalCouponsets, 479 + } else {
503 - title: category.displayName, 480 + unmatchedCouponSets.append(couponSet)
504 - items: categoryCouponSets,
505 - itemType: .couponSets
506 - )
507 - categorySections.append(section)
508 -
509 - print(" ✅ Created section for '\(category.displayName)' with \(categoryCouponSets.count) items")
510 } 481 }
511 } 482 }
512 483
513 - // COMMENTED OUT: Don't show unmatched couponsets - only show categorized ones 484 + // 3. Create Sections & Sort
514 - /* 485 + var dynamicSections: [SectionModel] = []
515 - // Handle any remaining unmatched coupon sets 486 + for (parentName, items) in groupedCouponSets {
516 - let unmatchedCouponSets = couponSets.filter { couponSet in 487 + let section = SectionModel(
517 - !processedCouponSets.contains(couponSet._uuid)
518 - }
519 -
520 - if !unmatchedCouponSets.isEmpty {
521 - print(" ⚠️ Found \(unmatchedCouponSets.count) unmatched coupon sets - adding to 'Άλλες Προσφορές' section")
522 -
523 - // BIND MERCHANT DATA for unmatched coupon sets too
524 - for couponSet in unmatchedCouponSets {
525 - if let merchant = merchants.first(where: { $0._uuid == couponSet._merchant_uuid }) {
526 - couponSet._merchant = merchant
527 - print(" 🔗 Bound merchant '\(merchant._name)' to unmatched coupon set '\(couponSet._name)'")
528 - }
529 - }
530 -
531 - let unmatchedSection = SectionModel(
532 sectionType: .myRewardsHorizontalCouponsets, 488 sectionType: .myRewardsHorizontalCouponsets,
533 - title: "Άλλες Προσφορές", // "Other Offers" 489 + title: parentName,
534 - items: unmatchedCouponSets, 490 + items: items,
535 itemType: .couponSets 491 itemType: .couponSets
536 ) 492 )
537 - categorySections.append(unmatchedSection) 493 + dynamicSections.append(section)
538 } 494 }
539 - */
540 495
541 - // Sort sections by title for consistent ordering, but keep "Top offers" at the top 496 + let priorityCategories = ["Αγορές", "Φαγητό και καφές"]
542 - categorySections.sort { section1, section2 in 497 +
543 - let title1 = section1.title ?? "" 498 + dynamicSections.sort { s1, s2 in
544 - let title2 = section2.title ?? "" 499 + let title1 = s1.title ?? ""
500 + let title2 = s2.title ?? ""
545 501
546 - // Put "Top offers" at the beginning 502 + let idx1 = priorityCategories.firstIndex(of: title1)
547 - if title1 == "Top offers" { return true } 503 + let idx2 = priorityCategories.firstIndex(of: title2)
548 - if title2 == "Top offers" { return false }
549 504
550 - // All other sections sorted alphabetically 505 + if let i1 = idx1, let i2 = idx2 {
551 - return title1.localizedCaseInsensitiveCompare(title2) == .orderedAscending 506 + return i1 < i2
552 - } 507 + } else if idx1 != nil {
553 - 508 + return true
554 - print("✅ [MyRewardsViewController] Created \(categorySections.count) category sections:") 509 + } else if idx2 != nil {
555 - for section in categorySections { 510 + return false
556 - print(" - '\(section.title ?? "Unknown")': \(section.itemCount) coupon sets") 511 + } else {
512 + return title1.localizedCaseInsensitiveCompare(title2) == .orderedAscending
513 + }
557 } 514 }
558 515
559 - // Add category sections to main sections array 516 + categorySections.append(contentsOf: dynamicSections)
560 - self.sections.append(contentsOf: categorySections)
561 -
562 - // Reload table view with new sections
563 - DispatchQueue.main.async {
564 - self.tableView.reloadData()
565 - }
566 - }
567 -
568 - private func createSingleCouponSetsSection() {
569 - print("📦 [MyRewardsViewController] Creating single fallback coupon sets section")
570 517
571 - // Fallback: Single section with all coupon sets 518 + // 4. Other Offers
572 - if !self.couponSets.isEmpty { 519 + if !unmatchedCouponSets.isEmpty {
573 - let couponSetsSection = SectionModel( 520 + let otherSection = SectionModel(
574 sectionType: .myRewardsHorizontalCouponsets, 521 sectionType: .myRewardsHorizontalCouponsets,
575 - title: "Προσφορές", // "Offers" 522 + title: "Άλλες Προσφορές",
576 - items: self.couponSets, 523 + items: unmatchedCouponSets,
577 itemType: .couponSets 524 itemType: .couponSets
578 ) 525 )
579 - self.sections.append(couponSetsSection) 526 + categorySections.append(otherSection)
580 - 527 + print(" ✅ Created 'Άλλες Προσφορές' section with \(unmatchedCouponSets.count) items")
581 - print("✅ [MyRewardsViewController] Created fallback section with \(self.couponSets.count) coupon sets")
582 - } else {
583 - print("⚠️ [MyRewardsViewController] No coupon sets available - no section created")
584 } 528 }
585 529
530 + // Add category sections to main sections array
531 + self.sections.append(contentsOf: categorySections)
532 +
533 + // Reload table view with new sections
586 DispatchQueue.main.async { 534 DispatchQueue.main.async {
587 self.tableView.reloadData() 535 self.tableView.reloadData()
588 } 536 }
......