Manos Chorianopoulos

added getMerchantCategories request

This diff is collapsed. Click to expand it.
......@@ -2194,8 +2194,88 @@ public final class WarplySDK {
}
}
// MARK: - Profile
// MARK: - Merchant Categories
/// Get merchant categories
/// - Parameters:
/// - language: Language for the categories (optional, defaults to applicationLocale)
/// - completion: Completion handler with merchant categories array
/// - failureCallback: Failure callback with error code
public func getMerchantCategories(
language: String? = nil,
completion: @escaping ([MerchantCategoryModel]?) -> Void,
failureCallback: @escaping (Int) -> Void
) {
let finalLanguage = language ?? self.applicationLocale
Task {
do {
let endpoint = Endpoint.getMerchantCategories(language: finalLanguage)
let response = try await networkService.requestRaw(endpoint)
await MainActor.run {
if response["status"] as? Int == 1 {
// Success analytics
let dynatraceEvent = LoyaltySDKDynatraceEventModel()
dynatraceEvent._eventName = "custom_success_get_merchant_categories_loyalty"
dynatraceEvent._parameters = nil
self.postFrameworkEvent("dynatrace", sender: dynatraceEvent)
var categories: [MerchantCategoryModel] = []
// Parse from context.MAPP_SHOPS.result structure
if let mappShops = response["MAPP_SHOPS"] as? [String: Any],
let result = mappShops["result"] as? [[String: Any]] {
for categoryDict in result {
let category = MerchantCategoryModel(dictionary: categoryDict)
categories.append(category)
}
print("✅ [WarplySDK] Retrieved \(categories.count) merchant categories")
completion(categories)
} else {
print("⚠️ [WarplySDK] No merchant categories found in response")
completion([])
}
} else {
// Error analytics
let dynatraceEvent = LoyaltySDKDynatraceEventModel()
dynatraceEvent._eventName = "custom_error_get_merchant_categories_loyalty"
dynatraceEvent._parameters = nil
self.postFrameworkEvent("dynatrace", sender: dynatraceEvent)
failureCallback(-1)
}
}
} catch {
await MainActor.run {
self.handleError(error, context: "getMerchantCategories", endpoint: "getMerchantCategories", failureCallback: failureCallback)
}
}
}
}
/// Get merchant categories (async/await variant)
/// - Parameter language: Language for the categories (optional, defaults to applicationLocale)
/// - Returns: Array of merchant categories
/// - Throws: WarplyError if the request fails
public func getMerchantCategories(language: String? = nil) async throws -> [MerchantCategoryModel] {
return try await withCheckedThrowingContinuation { continuation in
getMerchantCategories(language: language, completion: { categories in
if let categories = categories {
continuation.resume(returning: categories)
} else {
continuation.resume(throwing: WarplyError.networkError)
}
}, failureCallback: { errorCode in
continuation.resume(throwing: WarplyError.unknownError(errorCode))
})
}
}
// MARK: - Profile
/// Get user profile details
/// - Parameters:
/// - completion: Completion handler with profile model
......
......@@ -71,6 +71,7 @@ public enum Endpoint {
// Market & Merchants
case getMarketPassDetails
case getMerchants(language: String, categories: [String], defaultShown: Bool, center: Double, tags: [String], uuid: String, distance: Int, parentUuids: [String])
case getMerchantCategories(language: String)
// Card Management
case addCard(cardNumber: String, cardIssuer: String, cardHolder: String, expirationMonth: String, expirationYear: String)
......@@ -126,7 +127,7 @@ public enum Endpoint {
return "/user/v5/{appUUID}/logout"
// Standard Context endpoints - /api/mobile/v2/{appUUID}/context/
case .getCampaigns, .getAvailableCoupons, .getCouponSets:
case .getCampaigns, .getAvailableCoupons, .getCouponSets, .getMerchantCategories:
return "/api/mobile/v2/{appUUID}/context/"
// Authenticated Context endpoints - /oauth/{appUUID}/context
......@@ -159,7 +160,7 @@ public enum Endpoint {
switch self {
case .register, .changePassword, .resetPassword, .requestOtp, .verifyTicket, .refreshToken, .logout, .getCampaigns, .getCampaignsPersonalized,
.getCoupons, .getCouponSets, .getAvailableCoupons,
.getMarketPassDetails, .getProfile, .addCard, .getCards, .deleteCard, .getTransactionHistory, .getPointsHistory, .validateCoupon, .redeemCoupon, .getMerchants, .sendEvent, .sendDeviceInfo, .getCosmoteUser:
.getMarketPassDetails, .getProfile, .addCard, .getCards, .deleteCard, .getTransactionHistory, .getPointsHistory, .validateCoupon, .redeemCoupon, .getMerchants, .getMerchantCategories, .sendEvent, .sendDeviceInfo, .getCosmoteUser:
return .POST
case .getSingleCampaign, .getNetworkStatus:
return .GET
......@@ -379,6 +380,15 @@ public enum Endpoint {
]
]
// Merchant Categories - using shops structure for DEI API
case .getMerchantCategories(let language):
return [
"shops": [
"language": language,
"action": "retrieve_categories"
]
]
// Analytics endpoints - events structure
case .sendEvent(let eventName, let priority):
return [
......@@ -434,7 +444,7 @@ public enum Endpoint {
return .userManagement
// Standard Context - /api/mobile/v2/{appUUID}/context/
case .getCampaigns, .getAvailableCoupons, .getCouponSets:
case .getCampaigns, .getAvailableCoupons, .getCouponSets, .getMerchantCategories:
return .standardContext
// Authenticated Context - /oauth/{appUUID}/context
......@@ -476,7 +486,7 @@ public enum Endpoint {
// Standard Authentication (loyalty headers only)
case .register, .resetPassword, .requestOtp, .getCampaigns, .getAvailableCoupons, .getCouponSets, .refreshToken, .logout,
.verifyTicket, .getSingleCampaign, .sendEvent, .sendDeviceInfo,
.getMerchants, .getNetworkStatus:
.getMerchants, .getMerchantCategories, .getNetworkStatus:
return .standard
// Bearer Token Authentication (loyalty headers + Authorization: Bearer)
......
......@@ -962,6 +962,22 @@ extension NetworkService {
return response
}
// MARK: - Merchant Categories Methods
/// Get merchant categories
/// - Parameter language: Language for the categories
/// - Returns: Response dictionary containing merchant categories
/// - Throws: NetworkError if request fails
public func getMerchantCategories(language: String) async throws -> [String: Any] {
print("🔄 [NetworkService] Getting merchant categories for language: \(language)")
let endpoint = Endpoint.getMerchantCategories(language: language)
let response = try await requestRaw(endpoint)
print("✅ [NetworkService] Get merchant categories request completed")
return response
}
// MARK: - Coupon Operations Methods
/// Validate a coupon for the user
......
//
// MerchantCategoryModel.swift
// SwiftWarplyFramework
//
// Created by Warply on 28/07/2025.
// Copyright © 2025 Warply. All rights reserved.
//
import Foundation
// MARK: - Merchant Category Model
public class MerchantCategoryModel: NSObject {
private var uuid: String?
private var admin_name: String?
private var image: String?
private var parent: String?
private var fields: String?
private var children: [Any]?
private var count: Int?
private var name: String?
public init() {
self.uuid = ""
self.admin_name = ""
self.image = ""
self.parent = ""
self.fields = ""
self.children = []
self.count = 0
self.name = ""
}
public init(dictionary: [String: Any]) {
self.uuid = dictionary["uuid"] as? String ?? ""
self.admin_name = dictionary["admin_name"] as? String ?? ""
self.image = dictionary["image"] as? String ?? ""
self.parent = dictionary["parent"] as? String
self.fields = dictionary["fields"] as? String ?? ""
self.children = dictionary["children"] as? [Any] ?? []
self.count = dictionary["count"] as? Int ?? 0
self.name = dictionary["name"] as? String
}
// MARK: - Public Accessors
public var _uuid: String {
get { return self.uuid ?? "" }
set(newValue) { self.uuid = newValue }
}
public var _admin_name: String {
get { return self.admin_name ?? "" }
set(newValue) { self.admin_name = newValue }
}
public var _image: String {
get { return self.image ?? "" }
set(newValue) { self.image = newValue }
}
public var _parent: String? {
get { return self.parent }
set(newValue) { self.parent = newValue }
}
public var _fields: String {
get { return self.fields ?? "" }
set(newValue) { self.fields = newValue }
}
public var _children: [Any] {
get { return self.children ?? [] }
set(newValue) { self.children = newValue }
}
public var _count: Int {
get { return self.count ?? 0 }
set(newValue) { self.count = newValue }
}
public var _name: String? {
get { return self.name }
set(newValue) { self.name = newValue }
}
// MARK: - Computed Properties
/// Display name for the category - uses name if available, otherwise falls back to admin_name
public var displayName: String {
return self.name ?? self.admin_name ?? ""
}
/// Clean image URL with whitespace trimmed
public var cleanImageUrl: String {
return self.image?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
}
/// Check if this category has a parent category
public var hasParent: Bool {
return self.parent != nil && !(self.parent?.isEmpty ?? true)
}
/// Check if this category has child categories
public var hasChildren: Bool {
return !self.children.isEmpty
}
}
// MARK: - Codable Support
extension MerchantCategoryModel: Codable {
private enum CodingKeys: String, CodingKey {
case uuid
case admin_name
case image
case parent
case fields
case children
case count
case name
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(uuid, forKey: .uuid)
try container.encode(admin_name, forKey: .admin_name)
try container.encode(image, forKey: .image)
try container.encodeIfPresent(parent, forKey: .parent)
try container.encode(fields, forKey: .fields)
try container.encode(count, forKey: .count)
try container.encodeIfPresent(name, forKey: .name)
// Note: children is [Any] so we skip encoding it for now
}
public required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.uuid = try container.decodeIfPresent(String.self, forKey: .uuid)
self.admin_name = try container.decodeIfPresent(String.self, forKey: .admin_name)
self.image = try container.decodeIfPresent(String.self, forKey: .image)
self.parent = try container.decodeIfPresent(String.self, forKey: .parent)
self.fields = try container.decodeIfPresent(String.self, forKey: .fields)
self.count = try container.decodeIfPresent(Int.self, forKey: .count)
self.name = try container.decodeIfPresent(String.self, forKey: .name)
self.children = [] // Default empty array for children
}
}
// MARK: - Debug Description
extension MerchantCategoryModel {
public override var description: String {
return """
MerchantCategoryModel {
uuid: \(_uuid)
displayName: \(displayName)
admin_name: \(_admin_name)
image: \(cleanImageUrl)
parent: \(_parent ?? "nil")
count: \(_count)
hasChildren: \(hasChildren)
}
"""
}
}
......@@ -46,6 +46,9 @@ import UIKit
// Merchants data
var merchants: [MerchantModel] = []
// Merchant categories data
var merchantCategories: [MerchantCategoryModel] = []
// Profile data
var profileModel: ProfileModel?
var profileSection: SectionModel?
......@@ -176,9 +179,8 @@ import UIKit
self.merchants = merchants
print("✅ [MyRewardsViewController] Loaded \(merchants.count) merchants")
// For now, create the coupon sets section without filtering
// Later this will be enhanced to filter by merchant categories
self.createCouponSetsSection()
// Load merchant categories after merchants success
self.loadMerchantCategories()
} failureCallback: { [weak self] errorCode in
print("Failed to load merchants: \(errorCode)")
......@@ -187,8 +189,58 @@ import UIKit
}
}
// 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
}
self.merchantCategories = categories
print("✅ [MyRewardsViewController] Loaded \(categories.count) merchant categories")
// TODO: Implement category-based filtering for coupon sets sections
// For now, create the standard coupon sets section
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() {
// Create coupon sets section with real data
// TODO: IMPLEMENT CATEGORY-BASED FILTERING
//
// Current logic: Creates one section with all coupon sets
//
// Future enhancement: Filter coupon sets into different sections based on categories
// Logic:
// 1. For each couponset, get its merchant_uuid
// 2. Find the merchant with that merchant_uuid in self.merchants
// 3. Get the merchant's category_uuid
// 4. Find the category with that category_uuid in self.merchantCategories
// 5. Group coupon sets by category
// 6. Create separate sections for each category
//
// Example structure after filtering:
// - Section: "Εκπαίδευση" (Education) - coupon sets from education merchants
// - Section: "Ψυχαγωγία" (Entertainment) - coupon sets from entertainment merchants
// - etc.
//
// Implementation steps:
// 1. Create a dictionary to group coupon sets by category: [String: [CouponSetItemModel]]
// 2. Iterate through self.couponSets
// 3. For each coupon set, find its merchant and category
// 4. Add coupon set to the appropriate category group
// 5. Create a SectionModel for each category group
// 6. Sort sections by category name or priority
// Current implementation (temporary):
if !self.couponSets.isEmpty {
let couponSetsSection = SectionModel(
sectionType: .myRewardsHorizontalCouponsets,
......