Manos Chorianopoulos

new getCouponFilters request and MyRewardsViewController sections handling

......@@ -438,6 +438,7 @@ private final class SDKState {
var carouselList: [CampaignItemModel] = []
var marketPassDetails: MarketPassDetailsModel?
var supermarketCampaign: CampaignItemModel?
var couponFilters: CouponFiltersDataModel?
private init() {}
}
......@@ -2545,6 +2546,77 @@ public final class WarplySDK {
}
}
/// Get coupon filters via new endpoint
/// - Parameters:
/// - language: Optional language code (defaults to applicationLocale)
/// - completion: Returns the raw response dictionary on success
/// - failureCallback: Returns error code on failure
public func getCouponFilters(
language: String? = nil,
completion: @escaping (CouponFiltersDataModel?) -> Void,
failureCallback: @escaping (Int) -> Void
) {
let finalLanguage = language ?? self.applicationLocale
Task {
do {
let endpoint = Endpoint.getCouponFilters(language: finalLanguage)
let response = try await networkService.requestRaw(endpoint)
await MainActor.run {
print("📥 getCouponFilters response: \(response)")
let dynatraceEvent = LoyaltySDKDynatraceEventModel()
dynatraceEvent._eventName = "custom_success_couponfilters_loyalty"
dynatraceEvent._parameters = nil
self.postFrameworkEvent("dynatrace", sender: dynatraceEvent)
if let resultData = response["result"] as? [String: Any] {
let filterModel = CouponFiltersDataModel(dictionary: resultData)
self.setCouponFilters(filterModel)
completion(filterModel)
} else {
completion(nil)
}
}
} catch {
await MainActor.run {
let dynatraceEvent = LoyaltySDKDynatraceEventModel()
dynatraceEvent._eventName = "custom_error_couponfilters_loyalty"
dynatraceEvent._parameters = nil
self.postFrameworkEvent("dynatrace", sender: dynatraceEvent)
if let networkError = error as? NetworkError {
failureCallback(networkError.code)
} else {
failureCallback(-1)
}
}
}
}
}
/// Get coupon filters (async/await variant)
/// - Parameters:
/// - language: Language code for localized content (optional, defaults to applicationLocale)
/// - Returns: Parsed filters model
/// - Throws: WarplyError if the request fails
public func getCouponFilters(
language: String? = nil
) async throws -> CouponFiltersDataModel {
return try await withCheckedThrowingContinuation { continuation in
getCouponFilters(language: language, completion: { response in
if let response = response {
continuation.resume(returning: response)
} else {
continuation.resume(throwing: WarplyError.networkError)
}
}, failureCallback: { errorCode in
continuation.resume(throwing: WarplyError.unknownError(errorCode))
})
}
}
/// Get available coupons (async/await variant)
/// - Returns: Dictionary of coupon availability data
/// - Throws: WarplyError if the request fails
......@@ -4344,6 +4416,16 @@ public final class WarplySDK {
return state.couponSets
}
/// Set coupon filters data
public func setCouponFilters(_ filters: CouponFiltersDataModel) {
state.couponFilters = filters
}
/// Get coupon filters data
public func getCouponFilters() -> CouponFiltersDataModel? {
return state.couponFilters
}
/// Set seasonal list
public func setSeasonalList(_ seasonalCoupons: [LoyaltyGiftsForYouPackage]) {
state.seasonalList = seasonalCoupons
......
......@@ -68,6 +68,7 @@ public enum Endpoint {
case getCoupons(language: String, couponsetType: String)
case getCouponSets(language: String, active: Bool, visible: Bool, uuids: [String]?)
case getCouponSetsNew(language: String, active: Bool, visible: Bool, region: String?, offerCategory: String?)
case getCouponFilters(language: String)
case getAvailableCoupons
// Market & Merchants
......@@ -143,7 +144,7 @@ public enum Endpoint {
return "/api/mobile/v2/{appUUID}/context/"
// Authenticated Context endpoints - /oauth/{appUUID}/context
case .getCampaignsPersonalized, .getCoupons, .getMarketPassDetails, .getProfile, .addCard, .getCards, .deleteCard, .getTransactionHistory, .getPointsHistory, .validateCoupon, .redeemCoupon, .retrieveCoupon, .getCarouselContent, .getCouponSetsNew:
case .getCampaignsPersonalized, .getCoupons, .getMarketPassDetails, .getProfile, .addCard, .getCards, .deleteCard, .getTransactionHistory, .getPointsHistory, .validateCoupon, .redeemCoupon, .retrieveCoupon, .getCarouselContent, .getCouponSetsNew, .getCouponFilters:
return "/oauth/{appUUID}/context"
// Session endpoints - /api/session/{sessionUuid}
......@@ -171,7 +172,7 @@ public enum Endpoint {
public var method: HTTPMethod {
switch self {
case .register, .changePassword, .resetPassword, .requestOtp, .verifyTicket, .refreshToken, .logout, .getCampaigns, .getCampaignsPersonalized,
.getCoupons, .getCouponSets, .getCouponSetsNew, .getAvailableCoupons,
.getCoupons, .getCouponSets, .getCouponSetsNew, .getCouponFilters, .getAvailableCoupons,
.getMarketPassDetails, .getProfile, .addCard, .getCards, .deleteCard, .getTransactionHistory, .getPointsHistory, .validateCoupon, .redeemCoupon, .retrieveCoupon, .getMerchants, .getMerchantCategories, .getStores, .getArticles, .sendEvent, .sendDeviceInfo, .getCosmoteUser, .deiLogin, .getCarouselContent:
return .POST
case .getSingleCampaign, .getNetworkStatus:
......@@ -314,6 +315,14 @@ public enum Endpoint {
"coupon": couponParams
]
case .getCouponFilters(let language):
return [
"coupon": [
"action": "get_filters",
"language": language
]
]
case .getAvailableCoupons:
return [
"coupon": [
......@@ -523,7 +532,7 @@ public enum Endpoint {
return .standardContext
// Authenticated Context - /oauth/{appUUID}/context
case .getCampaignsPersonalized, .getCoupons, .getMarketPassDetails, .getProfile, .addCard, .getCards, .deleteCard, .getTransactionHistory, .getPointsHistory, .validateCoupon, .redeemCoupon, .retrieveCoupon, .getCarouselContent, .getCouponSetsNew:
case .getCampaignsPersonalized, .getCoupons, .getMarketPassDetails, .getProfile, .addCard, .getCards, .deleteCard, .getTransactionHistory, .getPointsHistory, .validateCoupon, .redeemCoupon, .retrieveCoupon, .getCarouselContent, .getCouponSetsNew, .getCouponFilters:
return .authenticatedContext
// Authentication - /oauth/{appUUID}/login, /oauth/{appUUID}/token
......@@ -565,7 +574,7 @@ public enum Endpoint {
return .standard
// Bearer Token Authentication (loyalty headers + Authorization: Bearer)
case .changePassword, .getCampaignsPersonalized, .getCoupons, .getMarketPassDetails, .getProfile, .addCard, .getCards, .deleteCard, .getTransactionHistory, .getPointsHistory, .validateCoupon, .redeemCoupon, .retrieveCoupon, .getCarouselContent, .getCouponSetsNew:
case .changePassword, .getCampaignsPersonalized, .getCoupons, .getMarketPassDetails, .getProfile, .addCard, .getCards, .deleteCard, .getTransactionHistory, .getPointsHistory, .validateCoupon, .redeemCoupon, .retrieveCoupon, .getCarouselContent, .getCouponSetsNew, .getCouponFilters:
return .bearerToken
// Basic Authentication (loyalty headers + Authorization: Basic)
......
......@@ -659,6 +659,72 @@ public class RedeemedSMHistoryModel {
}
}
// MARK: - Coupon Filters Models
public class CouponFiltersDataModel {
private var offer_categories: [CouponOfferCategoryModel]?
private var regions: [String]?
public init(dictionary: [String: Any]) {
if let regionsArray = dictionary["regions"] as? [Any] {
self.regions = regionsArray.compactMap { $0 as? String }
} else {
self.regions = []
}
if let categoriesArray = dictionary["offer_categories"] as? [[String: Any]] {
var tempCategories: [CouponOfferCategoryModel] = []
for catDict in categoriesArray {
tempCategories.append(CouponOfferCategoryModel(dictionary: catDict))
}
self.offer_categories = tempCategories
} else {
self.offer_categories = []
}
}
public var _offer_categories: [CouponOfferCategoryModel]? { get { return self.offer_categories } }
public var _regions: [String]? { get { return self.regions } }
}
public class CouponOfferCategoryModel {
private var uuid: String?
private var admin_name: String?
private var name: String?
private var image: String?
private var parent: String?
private var children: [CouponOfferCategoryModel]?
public init(dictionary: [String: Any]) {
self.uuid = dictionary["uuid"] as? String
self.admin_name = dictionary["admin_name"] as? String
self.name = dictionary["name"] as? String
if let imgString = dictionary["image"] as? String {
self.image = imgString.trimmingCharacters(in: .whitespacesAndNewlines)
}
self.parent = dictionary["parent"] as? String
if let childrenArray = dictionary["children"] as? [[String: Any]] {
var tempChildren: [CouponOfferCategoryModel] = []
for childDict in childrenArray {
tempChildren.append(CouponOfferCategoryModel(dictionary: childDict))
}
self.children = tempChildren
} else {
self.children = []
}
}
public var _uuid: String { get { return self.uuid ?? "" } }
public var _admin_name: String { get { return self.admin_name ?? "" } }
public var _name: String { get { return self.name ?? "" } }
public var _image: String { get { return self.image ?? "" } }
public var _parent: String { get { return self.parent ?? "" } }
public var _children: [CouponOfferCategoryModel]? { get { return self.children } }
}
// MARK: - String Extension for HTML
// extension String {
......
......@@ -53,6 +53,7 @@ import UIKit
// Coupon sets data
var couponSets: [CouponSetItemModel] = []
var couponFilters: CouponFiltersDataModel?
// Merchants data
var merchants: [MerchantModel] = []
......@@ -91,7 +92,6 @@ import UIKit
// Load data
loadProfile() // Load Profile
loadCarouselContent()
// loadCampaigns() // Load campaigns
loadCouponSets() // Load couponsets
loadCoupons()
......@@ -287,26 +287,12 @@ import UIKit
// }
private func loadCouponSets() {
// Load coupon sets from WarplySDK
// WarplySDK.shared.getCouponSets { [weak self] couponSets in
// guard let self = self, let couponSets = couponSets else { return }
// self.couponSets = couponSets
// // Load merchants after getting coupon sets
// self.loadMerchants()
// } failureCallback: { [weak self] errorCode in
// print("Failed to load coupon sets: \(errorCode)")
// // No sections added on failure - table will be empty
// }
WarplySDK.shared.getCouponSetsNew(
completion: { [weak self] couponSets in
guard let self = self, let couponSets = couponSets else { return }
self.couponSets = couponSets
self?.createCouponSetsSection()
self.loadCouponFilters()
},
failureCallback: { errorCode in
......@@ -315,6 +301,19 @@ import UIKit
)
}
private func loadCouponFilters() {
WarplySDK.shared.getCouponFilters { [weak self] couponFilters in
guard let self = self, let couponFilters = couponFilters else { return }
self.couponFilters = couponFilters
self.createCouponSetsSection()
} failureCallback: { errorCode in
print("=== getCouponFilters Error: \(errorCode)")
self.createCouponSetsSection()
}
}
// MARK: - Coupons Loading
private func loadCoupons() {
WarplySDK.shared.getCouponsUniversal({ [weak self] couponsData in
......@@ -368,89 +367,60 @@ import UIKit
// }
// TODO: DELETE loadMerchants - No matching needed
private func loadMerchants() {
// Load merchants from WarplySDK (using enhanced getMerchants method)
WarplySDK.shared.getMerchants { [weak self] merchants in
guard let self = self, let merchants = merchants else {
// If merchants fail to load, still create coupon sets section without filtering
self?.createCouponSetsSection()
return
}
// private func loadMerchants() {
// // Load merchants from WarplySDK (using enhanced getMerchants method)
// WarplySDK.shared.getMerchants { [weak self] merchants in
// guard let self = self, let merchants = merchants else {
// // If merchants fail to load, still create coupon sets section without filtering
// self?.createCouponSetsSection()
// return
// }
self.merchants = merchants
print("✅ [MyRewardsViewController] Loaded \(merchants.count) merchants")
// self.merchants = merchants
// print("✅ [MyRewardsViewController] Loaded \(merchants.count) merchants")
// Load merchant categories after merchants success
self.loadMerchantCategories()
// // Load merchant categories after merchants success
// self.loadMerchantCategories()
} failureCallback: { [weak self] errorCode in
print("Failed to load merchants: \(errorCode)")
// If merchants fail, still show coupon sets without filtering
self?.createCouponSetsSection()
}
}
// } failureCallback: { [weak self] errorCode in
// print("Failed to load merchants: \(errorCode)")
// // If merchants fail, still show coupon sets without filtering
// self?.createCouponSetsSection()
// }
// }
// MARK: - Merchant Categories Loading
private func loadMerchantCategories() {
// Load merchant categories from WarplySDK
WarplySDK.shared.getMerchantCategories { [weak self] categories in
guard let self = self, let categories = categories else {
// If categories fail to load, still create coupon sets section without filtering
self?.createCouponSetsSection()
return
}
// private func loadMerchantCategories() {
// // Load merchant categories from WarplySDK
// WarplySDK.shared.getMerchantCategories { [weak self] categories in
// guard let self = self, let categories = categories else {
// // If categories fail to load, still create coupon sets section without filtering
// self?.createCouponSetsSection()
// return
// }
self.merchantCategories = categories
print("✅ [MyRewardsViewController] Loaded \(categories.count) merchant categories")
// self.merchantCategories = categories
// print("✅ [MyRewardsViewController] Loaded \(categories.count) merchant categories")
// Create coupon sets sections with category-based filtering
self.createCouponSetsSection()
// // Create coupon sets sections with category-based filtering
// self.createCouponSetsSection()
} failureCallback: { [weak self] errorCode in
print("Failed to load merchant categories: \(errorCode)")
// If categories fail, still show coupon sets without filtering
self?.createCouponSetsSection()
}
}
// } failureCallback: { [weak self] errorCode in
// print("Failed to load merchant categories: \(errorCode)")
// // If categories fail, still show coupon sets without filtering
// self?.createCouponSetsSection()
// }
// }
private func createCouponSetsSection() {
// TODO: no merchant match any more
print("🔍 [MyRewardsViewController] Starting coupon filtering:")
print("🔍 [MyRewardsViewController] Starting coupon filtering by new offer_categories:")
print(" - Coupon Sets: \(couponSets.count)")
print(" - Merchants: \(merchants.count)")
print(" - Categories: \(merchantCategories.count)")
// Check if we have all required data for filtering
guard !couponSets.isEmpty, !merchants.isEmpty, !merchantCategories.isEmpty else {
print("⚠️ [MyRewardsViewController] Missing data for filtering - using fallback single section")
print(" - Coupon Sets Empty: \(couponSets.isEmpty)")
print(" - Merchants Empty: \(merchants.isEmpty)")
print(" - Categories Empty: \(merchantCategories.isEmpty)")
// Fallback: Create single section with all coupon sets
createSingleCouponSetsSection()
return
}
// Group coupon sets by merchant category
var categorySections: [SectionModel] = []
var processedCouponSets: Set<String> = [] // Track processed coupon sets to avoid duplicates
// Extract promoted couponsets for "Top offers" section
// 1. Top offers (promoted)
let promotedCouponSets = couponSets.filter { $0._promoted }
print("🌟 [MyRewardsViewController] Found \(promotedCouponSets.count) promoted couponsets")
// Create "Top offers" section if we have promoted couponsets
if !promotedCouponSets.isEmpty {
// Bind merchant data to promoted couponsets
for couponSet in promotedCouponSets {
if let merchant = merchants.first(where: { $0._uuid == couponSet._merchant_uuid }) {
couponSet._merchant = merchant
print(" 🔗 Bound merchant '\(merchant._name)' to promoted coupon set '\(couponSet._name)'")
}
}
let topOffersSection = SectionModel(
sectionType: .myRewardsHorizontalCouponsets,
title: "Top offers",
......@@ -461,128 +431,106 @@ import UIKit
print(" ✅ Created 'Top offers' section with \(promotedCouponSets.count) items")
}
print("🔄 [MyRewardsViewController] Processing categories for filtering...")
// 2. Group by parent CouponFilter
var groupedCouponSets: [String: [CouponSetItemModel]] = [:]
var unmatchedCouponSets: [CouponSetItemModel] = []
let filtersData = self.couponFilters?._offer_categories ?? []
for category in merchantCategories {
// Find merchants in this category
let categoryMerchants = merchants.filter { merchant in
merchant._category_uuid == category._uuid
for couponSet in couponSets {
let offerCategory = couponSet._offer_category
if offerCategory.isEmpty {
unmatchedCouponSets.append(couponSet)
continue
}
print(" - Category '\(category.displayName)' has \(categoryMerchants.count) merchants")
var matchedParentName: String? = nil
// Find coupon sets from merchants in this category
let categoryCouponSets = couponSets.filter { couponSet in
// Skip if already processed (avoid duplicates)
guard !processedCouponSets.contains(couponSet._uuid) else { return false }
for parentCategory in filtersData {
let parentName = parentCategory._name.isEmpty ? parentCategory._admin_name : parentCategory._name
// Check if this coupon set belongs to any merchant in this category
let belongsToCategory = categoryMerchants.contains { merchant in
merchant._uuid == couponSet._merchant_uuid
// Check parent
if parentCategory._uuid == offerCategory || parentCategory._admin_name == offerCategory || parentCategory._name == offerCategory {
matchedParentName = parentName
break
}
if belongsToCategory {
processedCouponSets.insert(couponSet._uuid)
// BIND MERCHANT DATA: Find and bind the merchant to this coupon set
if let merchant = categoryMerchants.first(where: { $0._uuid == couponSet._merchant_uuid }) {
couponSet._merchant = merchant
print(" 🔗 Bound merchant '\(merchant._name)' to coupon set '\(couponSet._name)'")
// Check children
if let children = parentCategory._children {
for child in children {
if child._uuid == offerCategory || child._admin_name == offerCategory || child._name == offerCategory {
matchedParentName = parentName
break
}
}
return belongsToCategory
}
print(" - Category '\(category.displayName)' has \(categoryCouponSets.count) coupon sets")
// Create section if we have coupon sets for this category
if !categoryCouponSets.isEmpty {
let section = SectionModel(
sectionType: .myRewardsHorizontalCouponsets,
title: category.displayName,
items: categoryCouponSets,
itemType: .couponSets
)
categorySections.append(section)
print(" ✅ Created section for '\(category.displayName)' with \(categoryCouponSets.count) items")
if matchedParentName != nil {
break
}
}
// COMMENTED OUT: Don't show unmatched couponsets - only show categorized ones
/*
// Handle any remaining unmatched coupon sets
let unmatchedCouponSets = couponSets.filter { couponSet in
!processedCouponSets.contains(couponSet._uuid)
if let parentName = matchedParentName {
if groupedCouponSets[parentName] == nil {
groupedCouponSets[parentName] = []
}
if !unmatchedCouponSets.isEmpty {
print(" ⚠️ Found \(unmatchedCouponSets.count) unmatched coupon sets - adding to 'Άλλες Προσφορές' section")
// BIND MERCHANT DATA for unmatched coupon sets too
for couponSet in unmatchedCouponSets {
if let merchant = merchants.first(where: { $0._uuid == couponSet._merchant_uuid }) {
couponSet._merchant = merchant
print(" 🔗 Bound merchant '\(merchant._name)' to unmatched coupon set '\(couponSet._name)'")
groupedCouponSets[parentName]?.append(couponSet)
} else {
unmatchedCouponSets.append(couponSet)
}
}
let unmatchedSection = SectionModel(
// 3. Create Sections & Sort
var dynamicSections: [SectionModel] = []
for (parentName, items) in groupedCouponSets {
let section = SectionModel(
sectionType: .myRewardsHorizontalCouponsets,
title: "Άλλες Προσφορές", // "Other Offers"
items: unmatchedCouponSets,
title: parentName,
items: items,
itemType: .couponSets
)
categorySections.append(unmatchedSection)
dynamicSections.append(section)
}
*/
// Sort sections by title for consistent ordering, but keep "Top offers" at the top
categorySections.sort { section1, section2 in
let title1 = section1.title ?? ""
let title2 = section2.title ?? ""
// Put "Top offers" at the beginning
if title1 == "Top offers" { return true }
if title2 == "Top offers" { return false }
// All other sections sorted alphabetically
return title1.localizedCaseInsensitiveCompare(title2) == .orderedAscending
}
let priorityCategories = ["Αγορές", "Φαγητό και καφές"]
print("✅ [MyRewardsViewController] Created \(categorySections.count) category sections:")
for section in categorySections {
print(" - '\(section.title ?? "Unknown")': \(section.itemCount) coupon sets")
}
dynamicSections.sort { s1, s2 in
let title1 = s1.title ?? ""
let title2 = s2.title ?? ""
// Add category sections to main sections array
self.sections.append(contentsOf: categorySections)
let idx1 = priorityCategories.firstIndex(of: title1)
let idx2 = priorityCategories.firstIndex(of: title2)
// Reload table view with new sections
DispatchQueue.main.async {
self.tableView.reloadData()
if let i1 = idx1, let i2 = idx2 {
return i1 < i2
} else if idx1 != nil {
return true
} else if idx2 != nil {
return false
} else {
return title1.localizedCaseInsensitiveCompare(title2) == .orderedAscending
}
}
private func createSingleCouponSetsSection() {
print("📦 [MyRewardsViewController] Creating single fallback coupon sets section")
categorySections.append(contentsOf: dynamicSections)
// Fallback: Single section with all coupon sets
if !self.couponSets.isEmpty {
let couponSetsSection = SectionModel(
// 4. Other Offers
if !unmatchedCouponSets.isEmpty {
let otherSection = SectionModel(
sectionType: .myRewardsHorizontalCouponsets,
title: "Προσφορές", // "Offers"
items: self.couponSets,
title: "Άλλες Προσφορές",
items: unmatchedCouponSets,
itemType: .couponSets
)
self.sections.append(couponSetsSection)
print("✅ [MyRewardsViewController] Created fallback section with \(self.couponSets.count) coupon sets")
} else {
print("⚠️ [MyRewardsViewController] No coupon sets available - no section created")
categorySections.append(otherSection)
print(" ✅ Created 'Άλλες Προσφορές' section with \(unmatchedCouponSets.count) items")
}
// Add category sections to main sections array
self.sections.append(contentsOf: categorySections)
// Reload table view with new sections
DispatchQueue.main.async {
self.tableView.reloadData()
}
......