import Foundation enum StoreCatalogNormalizer { static func sanitize(categories: [StoreCatalogCategory], storeId: String) -> [StoreCatalogCategory] { var seenCategoryIds: Set = [] return categories.enumerated().map { categoryIndex, category in let categoryId = makeUniqueId( rawValue: category.id, fallback: "\(storeId)-category-\(categoryIndex)", seenIds: &seenCategoryIds ) let normalizedPizzaConfig = sanitize( pizzaConfig: category.pizzaConfig, categoryId: categoryId ) var seenProductIds: Set = [] let normalizedProducts = category.products.enumerated().map { productIndex, product in sanitize( product: product, categoryId: categoryId, productIndex: productIndex, seenProductIds: &seenProductIds ) } return StoreCatalogCategory( id: categoryId, name: category.name, isPizzaCategory: category.isPizzaCategory, pizzaConfig: normalizedPizzaConfig, products: normalizedProducts ) } } static func preferredCategoryId( from categories: [StoreCatalogCategory], preferredId: String? ) -> String? { guard let preferredId, preferredId.isEmpty == false else { return categories.first?.id } if categories.contains(where: { $0.id == preferredId }) { return preferredId } return categories.first?.id } private static func sanitize( pizzaConfig: StorePizzaConfig?, categoryId: String ) -> StorePizzaConfig? { guard let pizzaConfig else { return nil } var seenSizeIds: Set = [] let normalizedSizes = pizzaConfig.sizes.enumerated().map { index, size in StorePizzaSize( id: makeUniqueId( rawValue: size.id, fallback: "\(categoryId)-size-\(index)", seenIds: &seenSizeIds ), name: size.name, slices: size.slices, maxFlavors: size.maxFlavors ) } var seenDoughIds: Set = [] let normalizedDoughs = pizzaConfig.doughs.enumerated().map { index, dough in StorePizzaDough( id: makeUniqueId( rawValue: dough.id, fallback: "\(categoryId)-dough-\(index)", seenIds: &seenDoughIds ), name: dough.name, active: dough.active ) } var seenCrustIds: Set = [] let normalizedCrusts = pizzaConfig.crusts.enumerated().map { index, crust in StorePizzaCrust( id: makeUniqueId( rawValue: crust.id, fallback: "\(categoryId)-crust-\(index)", seenIds: &seenCrustIds ), name: crust.name, active: crust.active, priceModifier: crust.priceModifier ) } return StorePizzaConfig( sizes: normalizedSizes, doughs: normalizedDoughs, crusts: normalizedCrusts ) } private static func sanitize( product: StoreCatalogProduct, categoryId: String, productIndex: Int, seenProductIds: inout Set ) -> StoreCatalogProduct { let productId = makeUniqueId( rawValue: product.id, fallback: "\(categoryId)-product-\(productIndex)", seenIds: &seenProductIds ) var seenGroupIds: Set = [] var seenAddonItemIds: Set = [] let normalizedAddonGroups = product.addonGroups.enumerated().map { groupIndex, group in let groupId = makeUniqueId( rawValue: group.id, fallback: "\(productId)-group-\(groupIndex)", seenIds: &seenGroupIds ) let normalizedItems = group.items.enumerated().map { itemIndex, item in StoreAddonItem( id: makeUniqueId( rawValue: item.id, fallback: "\(groupId)-item-\(itemIndex)", seenIds: &seenAddonItemIds ), name: item.name, price: item.price ) } return StoreAddonGroup( id: groupId, name: group.name, minSelectors: group.minSelectors, maxSelectors: group.maxSelectors, items: normalizedItems ) } return StoreCatalogProduct( id: productId, type: product.type, name: product.name, description: product.description, image: product.image, price: product.price, originalPrice: product.originalPrice, pizzaPrices: product.pizzaPrices, addonGroups: normalizedAddonGroups ) } private static func makeUniqueId( rawValue: String, fallback: String, seenIds: inout Set ) -> String { let trimmedRawValue = rawValue.trimmingCharacters(in: .whitespacesAndNewlines) let baseId = trimmedRawValue.isEmpty ? fallback : trimmedRawValue if seenIds.contains(baseId) == false { seenIds.insert(baseId) return baseId } var suffix = 1 while true { let candidate = "\(baseId)-\(suffix)" if seenIds.contains(candidate) == false { seenIds.insert(candidate) return candidate } suffix += 1 } } } extension StoreCatalogCategory { init( id: String, name: String, isPizzaCategory: Bool, pizzaConfig: StorePizzaConfig?, products: [StoreCatalogProduct] ) { self.id = id self.name = name self.isPizzaCategory = isPizzaCategory self.pizzaConfig = pizzaConfig self.products = products } } extension StoreCatalogProduct { init( id: String, type: String?, name: String, description: String?, image: String?, price: Double?, originalPrice: Double?, pizzaPrices: [String: Double], addonGroups: [StoreAddonGroup] ) { self.id = id self.type = type self.name = name self.description = description self.image = image self.price = price self.originalPrice = originalPrice self.pizzaPrices = pizzaPrices self.addonGroups = addonGroups } } extension StoreAddonGroup { init( id: String, name: String, minSelectors: Int?, maxSelectors: Int?, items: [StoreAddonItem] ) { self.id = id self.name = name self.minSelectors = minSelectors self.maxSelectors = maxSelectors self.items = items } } extension StoreAddonItem { init( id: String, name: String, price: Double? ) { self.id = id self.name = name self.price = price } } extension StorePizzaConfig { init( sizes: [StorePizzaSize], doughs: [StorePizzaDough], crusts: [StorePizzaCrust] ) { self.sizes = sizes self.doughs = doughs self.crusts = crusts } } extension StorePizzaCrust { init( id: String, name: String?, active: Bool?, priceModifier: Double? ) { self.id = id self.name = name self.active = active self.priceModifier = priceModifier } }