You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
hmg-mohemm-flutter-app/lib/models/offers_and_discounts/wala_plus_retailer_model.dart

1155 lines
46 KiB
Dart

3 weeks ago
import 'dart:convert';
import 'package:easy_localization/easy_localization.dart';
import 'package:mohem_flutter_app/generated/locale_keys.g.dart';
2 weeks ago
import 'package:mohem_flutter_app/models/offers_and_discounts/get_offers_list.dart';
import 'package:mohem_flutter_app/models/offers_and_discounts/wala_plus_category_model.dart';
class WalaPlusRetailersResponse {
int? page;
int? pagesCount;
int? perPage;
List<WalaPlusRetailerModel>? retailers;
2 weeks ago
String? countryNameAr;
String? countryNameEn;
2 weeks ago
WalaPlusRetailersResponse({this.page, this.pagesCount, this.perPage, this.retailers, this.countryNameAr, this.countryNameEn});
factory WalaPlusRetailersResponse.fromJson(Map<String, dynamic> json) {
List<WalaPlusRetailerModel> retailersList = [];
3 weeks ago
dynamic retailersData = json['retailers'] ?? json['Retailers'] ?? json['data'] ?? json['items'];
if (retailersData is String) {
try {
retailersData = jsonDecode(retailersData);
} catch (_) {}
}
if (retailersData is List) {
retailersData.forEach((v) {
if (v is Map<String, dynamic>) {
retailersList.add(WalaPlusRetailerModel.fromJson(v));
} else if (v is Map) {
retailersList.add(WalaPlusRetailerModel.fromJson(Map<String, dynamic>.from(v)));
}
});
}
return WalaPlusRetailersResponse(
3 weeks ago
page: json['page'] is int ? json['page'] : int.tryParse(json['page']?.toString() ?? json['WP_Page']?.toString() ?? '1'),
pagesCount: json['pages_count'] is int ? json['pages_count'] : int.tryParse(json['pages_count']?.toString() ?? json['pagesCount']?.toString() ?? json['totalPages']?.toString() ?? '1'),
perPage: json['per_page'] is int ? json['per_page'] : int.tryParse(json['per_page']?.toString() ?? json['perPage']?.toString() ?? json['WP_Per_page']?.toString() ?? '10'),
retailers: retailersList,
2 weeks ago
countryNameAr: WalaPlusRetailerModel.decodeUnicodeString(json['country_name_ar'] ?? json['countryNameAr'] ?? json['country_ar']),
countryNameEn: WalaPlusRetailerModel.decodeUnicodeString(json['country_name_en'] ?? json['countryNameEn'] ?? json['country_en']),
);
}
Map<String, dynamic> toJson() {
3 weeks ago
Map<String, dynamic> data = <String, dynamic>{};
data['page'] = page;
data['pages_count'] = pagesCount;
data['per_page'] = perPage;
2 weeks ago
data['country_name_ar'] = countryNameAr;
data['country_name_en'] = countryNameEn;
if (retailers != null) {
data['retailers'] = retailers!.map((v) => v.toJson()).toList();
}
return data;
}
}
3 weeks ago
class WalaPlusRetailersByIDResponse {
int? page;
int? pagesCount;
int? perPage;
List<WalaPlusRetailerLocation>? locations;
List<WalaPlusOfferModel>? offers;
List<WalaPlusRetailerModel>? retailer;
3 weeks ago
WalaPlusRetailersByIDResponse({this.page, this.pagesCount, this.perPage, this.locations, this.offers, this.retailer});
3 weeks ago
factory WalaPlusRetailersByIDResponse.fromJson(Map<String, dynamic> json) {
3 weeks ago
List<WalaPlusRetailerLocation> locList = WalaPlusRetailerModel.parseLocationsList(json['locations'] ?? json['Locations'] ?? json['location'] ?? json['Location']);
List<WalaPlusOfferModel> offList = WalaPlusRetailerModel.parseOffersList(json['offers'] ?? json['Offers']);
2 weeks ago
List<WalaPlusCategoryModel> catList = [];
dynamic catData = json['categories'] ?? json['Categories'] ?? json['category'] ?? json['Category'];
if (catData is List) {
for (var c in catData) {
if (c is Map<String, dynamic>) {
catList.add(WalaPlusCategoryModel.fromJson(c));
} else if (c is Map) {
catList.add(WalaPlusCategoryModel.fromJson(Map<String, dynamic>.from(c)));
} else if (c is String && c.trim().isNotEmpty) {
catList.add(WalaPlusCategoryModel(name: c.trim()));
}
}
} else if (catData is Map) {
catList.add(WalaPlusCategoryModel.fromJson(Map<String, dynamic>.from(catData)));
} else if (catData is String && catData.trim().isNotEmpty) {
catList.add(WalaPlusCategoryModel(name: catData.trim()));
}
3 weeks ago
List<WalaPlusRetailerModel> retList = [];
dynamic retData = json['retailer'] ?? json['Retailer'];
if (retData is List) {
for (var r in retData) {
if (r is Map<String, dynamic>) {
var model = WalaPlusRetailerModel.fromJson(r);
if (model.locations == null || model.locations!.isEmpty) model.locations = locList;
if (model.offers == null || model.offers!.isEmpty) model.offers = offList;
2 weeks ago
if ((model.categories == null || model.categories!.isEmpty) && catList.isNotEmpty) model.categories = catList;
2 weeks ago
if ((model.website == null || model.website!.isEmpty || model.website == "null") && (json['website'] != null || json['merchant_website'] != null || json['url'] != null)) {
model.website = (json['website'] ?? json['merchant_website'] ?? json['url'])?.toString();
}
if (model.isFavorite == null && (json.containsKey('isfavorite') || json.containsKey('is_favorite'))) {
var favVal = json['isfavorite'] ?? json['is_favorite'];
model.isFavorite = favVal == true || favVal == 1 || favVal == 'true' || favVal == '1';
}
3 weeks ago
retList.add(model);
} else if (r is Map) {
var model = WalaPlusRetailerModel.fromJson(Map<String, dynamic>.from(r));
if (model.locations == null || model.locations!.isEmpty) model.locations = locList;
if (model.offers == null || model.offers!.isEmpty) model.offers = offList;
2 weeks ago
if ((model.categories == null || model.categories!.isEmpty) && catList.isNotEmpty) model.categories = catList;
2 weeks ago
if ((model.website == null || model.website!.isEmpty || model.website == "null") && (json['website'] != null || json['merchant_website'] != null || json['url'] != null)) {
model.website = (json['website'] ?? json['merchant_website'] ?? json['url'])?.toString();
}
if (model.isFavorite == null && (json.containsKey('isfavorite') || json.containsKey('is_favorite'))) {
var favVal = json['isfavorite'] ?? json['is_favorite'];
model.isFavorite = favVal == true || favVal == 1 || favVal == 'true' || favVal == '1';
}
3 weeks ago
retList.add(model);
}
}
} else if (retData is Map) {
var model = WalaPlusRetailerModel.fromJson(Map<String, dynamic>.from(retData));
if (model.locations == null || model.locations!.isEmpty) model.locations = locList;
if (model.offers == null || model.offers!.isEmpty) model.offers = offList;
2 weeks ago
if ((model.categories == null || model.categories!.isEmpty) && catList.isNotEmpty) model.categories = catList;
2 weeks ago
if ((model.website == null || model.website!.isEmpty || model.website == "null") && (json['website'] != null || json['merchant_website'] != null || json['url'] != null)) {
model.website = (json['website'] ?? json['merchant_website'] ?? json['url'])?.toString();
}
if (model.isFavorite == null && (json.containsKey('isfavorite') || json.containsKey('is_favorite'))) {
var favVal = json['isfavorite'] ?? json['is_favorite'];
model.isFavorite = favVal == true || favVal == 1 || favVal == 'true' || favVal == '1';
}
3 weeks ago
retList.add(model);
}
return WalaPlusRetailersByIDResponse(
3 weeks ago
page: json['page'] is int ? json['page'] : int.tryParse(json['page']?.toString() ?? json['WP_Page']?.toString() ?? ''),
pagesCount: json['pages_count'] is int ? json['pages_count'] : int.tryParse(json['pages_count']?.toString() ?? json['pagesCount']?.toString() ?? ''),
perPage: json['per_page'] is int ? json['per_page'] : int.tryParse(json['per_page']?.toString() ?? json['perPage']?.toString() ?? ''),
3 weeks ago
locations: locList,
offers: offList,
retailer: retList,
);
}
WalaPlusRetailerModel? get primaryRetailer {
if (retailer != null && retailer!.isNotEmpty) {
return retailer!.first;
}
return null;
}
Map<String, dynamic> toJson() {
Map<String, dynamic> data = <String, dynamic>{};
data['page'] = page;
data['pages_count'] = pagesCount;
data['per_page'] = perPage;
if (locations != null) {
data['locations'] = locations!.map((v) => v.toJson()).toList();
}
if (offers != null) {
data['offers'] = offers!.map((v) => v.toJson()).toList();
}
if (retailer != null) {
data['retailer'] = retailer!.map((v) => v.toJson()).toList();
}
return data;
}
}
class WalaPlusRetailerModel {
String? id;
String? name;
String? offerTitle;
String? offerType;
String? description;
String? info;
String? image;
String? imageThumb;
String? logo;
String? logoThumb;
String? website;
String? type;
dynamic usage;
dynamic weight;
dynamic geoId;
String? pin;
String? barcodeStandard;
String? arHowToUse;
String? enHowToUse;
String? howToUse;
WalaPlusRetailerContact? contact;
dynamic offerWeight;
3 weeks ago
List<WalaPlusOfferModel>? offers;
List<WalaPlusRetailerLocation>? locations;
List<WalaPlusCategoryModel>? categories;
2 weeks ago
String? categoryId;
String? categoryName;
3 weeks ago
String? validity;
String? validTo;
String? validUntil;
String? endDate;
String? expiryDate;
String? duration;
String? email;
String? phone;
2 weeks ago
String? countryNameAr;
String? countryNameEn;
2 weeks ago
bool? isFavorite;
2 weeks ago
// Drives the "MAZAYA EXCLUSIVE" badge on the retailer card (Figma node 927:496). NOT parsed
// from the WalaPlus_Retailers/WalaPlus_RetailersByID API - this is a different, separate
// "Exclusive" API/section, so callers set this manually on the model after fetching from there.
bool? isExclusive;
WalaPlusRetailerModel({
this.id,
this.name,
this.offerTitle,
this.offerType,
this.description,
this.info,
this.image,
this.imageThumb,
this.logo,
this.logoThumb,
this.website,
2 weeks ago
this.isFavorite,
2 weeks ago
this.isExclusive,
this.type,
this.usage,
this.weight,
this.geoId,
this.pin,
this.barcodeStandard,
this.arHowToUse,
this.enHowToUse,
this.howToUse,
this.contact,
this.offerWeight,
3 weeks ago
this.offers,
this.locations,
this.categories,
2 weeks ago
this.categoryId,
this.categoryName,
3 weeks ago
this.validity,
this.validTo,
this.validUntil,
this.endDate,
this.expiryDate,
this.duration,
this.email,
this.phone,
2 weeks ago
this.countryNameAr,
this.countryNameEn,
});
3 weeks ago
static List<WalaPlusOfferModel> parseOffersList(data) {
List<WalaPlusOfferModel> list = [];
if (data == null) return list;
if (data is String) {
try {
data = jsonDecode(data);
} catch (_) {}
}
if (data is List) {
for (var v in data) {
if (v is Map<String, dynamic>) {
list.add(WalaPlusOfferModel.fromJson(v));
} else if (v is Map) {
list.add(WalaPlusOfferModel.fromJson(Map<String, dynamic>.from(v)));
}
}
}
return list;
}
static List<WalaPlusRetailerLocation> parseLocationsList(data) {
List<WalaPlusRetailerLocation> list = [];
if (data == null) return list;
if (data is String) {
try {
data = jsonDecode(data);
} catch (_) {}
}
if (data is List) {
for (var v in data) {
if (v is Map<String, dynamic>) {
list.add(WalaPlusRetailerLocation.fromJson(v));
} else if (v is Map) {
list.add(WalaPlusRetailerLocation.fromJson(Map<String, dynamic>.from(v)));
}
}
}
return list;
}
factory WalaPlusRetailerModel.fromJson(Map<String, dynamic> json) {
3 weeks ago
Map<String, dynamic> targetJson = json;
dynamic offersSource = json['offers'] ?? json['Offers'];
dynamic locationsSource = json['locations'] ?? json['Locations'] ?? json['location'] ?? json['Location'];
// If json is the wrapper object from WalaPlus_RetailersByID containing 'retailer' key
if (json['retailer'] != null || json['Retailer'] != null) {
dynamic retailerData = json['retailer'] ?? json['Retailer'];
if (retailerData is List && retailerData.isNotEmpty) {
var first = retailerData.first;
if (first is Map<String, dynamic>) {
targetJson = first;
} else if (first is Map) {
targetJson = Map<String, dynamic>.from(first);
}
} else if (retailerData is Map<String, dynamic>) {
targetJson = retailerData;
} else if (retailerData is Map) {
targetJson = Map<String, dynamic>.from(retailerData);
}
if (locationsSource == null || (locationsSource is List && locationsSource.isEmpty)) {
locationsSource = targetJson['locations'] ?? targetJson['Locations'] ?? targetJson['location'] ?? targetJson['Location'];
}
if (offersSource == null || (offersSource is List && offersSource.isEmpty)) {
offersSource = targetJson['offers'] ?? targetJson['Offers'];
}
}
3 weeks ago
List<WalaPlusOfferModel> offersList = parseOffersList(offersSource ?? targetJson['offers'] ?? targetJson['Offers']);
3 weeks ago
List<WalaPlusRetailerLocation> locationsList = parseLocationsList(locationsSource ?? targetJson['locations'] ?? targetJson['Locations'] ?? targetJson['location'] ?? targetJson['Location']);
3 weeks ago
List<WalaPlusCategoryModel> categoriesList = [];
2 weeks ago
dynamic rawCats = targetJson['categories'] ?? targetJson['Categories'] ?? targetJson['category'] ?? targetJson['Category'];
if (rawCats != null && rawCats is List) {
for (var v in rawCats) {
if (v is Map<String, dynamic>) {
categoriesList.add(WalaPlusCategoryModel.fromJson(v));
} else if (v is Map) {
categoriesList.add(WalaPlusCategoryModel.fromJson(Map<String, dynamic>.from(v)));
} else if (v is String && v.trim().isNotEmpty) {
categoriesList.add(WalaPlusCategoryModel(name: v.trim()));
}
}
} else if (rawCats is Map) {
categoriesList.add(WalaPlusCategoryModel.fromJson(Map<String, dynamic>.from(rawCats)));
} else if (rawCats is String && rawCats.toString().trim().isNotEmpty && rawCats.toString().trim() != "null") {
categoriesList.add(WalaPlusCategoryModel(name: rawCats.toString().trim()));
}
2 weeks ago
String? singleCatName =
(targetJson['category_name'] ??
targetJson['categoryName'] ??
targetJson['cat_name'] ??
targetJson['catName'] ??
targetJson['CategoryName'] ??
targetJson['Category_Name'] ??
targetJson['category_title'] ??
targetJson['categoryTitle'])
?.toString()
.trim();
String? singleCatAr =
(targetJson['category_name_ar'] ??
targetJson['categoryNameAr'] ??
targetJson['cat_name_ar'] ??
targetJson['catNameAr'] ??
targetJson['category_ar'] ??
targetJson['country_name_ar'] ??
targetJson['countryNameAr'])
?.toString()
.trim();
String? singleCatEn =
(targetJson['category_name_en'] ??
targetJson['categoryNameEn'] ??
targetJson['cat_name_en'] ??
targetJson['catNameEn'] ??
targetJson['category_en'] ??
targetJson['country_name_en'] ??
targetJson['countryNameEn'])
?.toString()
.trim();
String? foundCategoryId =
(targetJson['category_id'] ?? targetJson['categoryId'] ?? targetJson['cat_id'] ?? targetJson['catId'] ?? targetJson['WP_CategoryID'] ?? targetJson['WP_CategoryId'])?.toString().trim();
2 weeks ago
String? rawType = targetJson['type']?.toString().trim();
String? lowerType = rawType?.toLowerCase();
2 weeks ago
bool isTypeStoreKeyword =
lowerType == null ||
2 weeks ago
lowerType.isEmpty ||
lowerType == 'online' ||
lowerType == 'instore' ||
lowerType == 'in_store' ||
lowerType == 'in store' ||
lowerType == 'store' ||
lowerType == 'online,instore' ||
lowerType == 'instore,online';
if ((singleCatName == null || singleCatName.isEmpty) && !isTypeStoreKeyword && rawType != null && rawType.isNotEmpty) {
singleCatName = rawType;
}
if (categoriesList.isEmpty) {
if ((singleCatName != null && singleCatName.isNotEmpty && singleCatName != "null") ||
(singleCatAr != null && singleCatAr.isNotEmpty && singleCatAr != "null") ||
(singleCatEn != null && singleCatEn.isNotEmpty && singleCatEn != "null")) {
2 weeks ago
categoriesList.add(
WalaPlusCategoryModel(id: foundCategoryId, name: singleCatName ?? singleCatEn ?? singleCatAr, countryNameAr: singleCatAr ?? singleCatName, countryNameEn: singleCatEn ?? singleCatName),
);
2 weeks ago
}
}
WalaPlusRetailerContact? contactObj;
3 weeks ago
if (targetJson['contact'] != null) {
if (targetJson['contact'] is Map) {
contactObj = WalaPlusRetailerContact.fromJson(Map<String, dynamic>.from(targetJson['contact']));
} else if (targetJson['contact'] is String && targetJson['contact'].toString().trim().isNotEmpty) {
contactObj = WalaPlusRetailerContact(website: targetJson['contact'].toString());
}
}
3 weeks ago
String? foundValidity =
targetJson['validity']?.toString() ??
3 weeks ago
targetJson['valid_to']?.toString() ??
targetJson['valid_until']?.toString() ??
targetJson['end_date']?.toString() ??
targetJson['expiry_date']?.toString() ??
targetJson['expire_date']?.toString() ??
targetJson['duration']?.toString() ??
targetJson['validity_period']?.toString() ??
targetJson['valid_duration']?.toString() ??
targetJson['offer_validity']?.toString();
if (foundValidity == null || foundValidity == "null" || foundValidity.trim().isEmpty) {
for (var entry in targetJson.entries) {
String k = entry.key.toLowerCase();
3 weeks ago
if ((k.contains('valid') || k.contains('expir') || k.contains('duration')) && !k.contains('isverificationcodevalid') && !k.contains('barcode')) {
3 weeks ago
if (entry.value != null && entry.value.toString().trim().isNotEmpty && entry.value.toString() != "null") {
foundValidity = entry.value.toString().trim();
break;
}
}
}
}
2 weeks ago
String? foundWebsite =
targetJson['website']?.toString() ??
2 weeks ago
targetJson['merchant_website']?.toString() ??
targetJson['merchantWebsite']?.toString() ??
targetJson['website_url']?.toString() ??
targetJson['websiteUrl']?.toString() ??
targetJson['web_site']?.toString() ??
targetJson['url']?.toString() ??
targetJson['merchant_url']?.toString() ??
targetJson['merchantUrl']?.toString() ??
targetJson['merchantLink']?.toString() ??
targetJson['merchant_link']?.toString() ??
targetJson['link']?.toString() ??
contactObj?.website;
if (foundWebsite == null || foundWebsite.trim().isEmpty || foundWebsite == "null") {
if (targetJson['merchant'] is Map) {
var m = targetJson['merchant'];
foundWebsite = (m['website'] ?? m['merchant_website'] ?? m['url'] ?? m['link'])?.toString();
}
}
if (foundWebsite == null || foundWebsite.trim().isEmpty || foundWebsite == "null") {
for (var entry in targetJson.entries) {
String k = entry.key.toLowerCase();
if ((k.contains('website') || k.contains('web_site') || k == 'url' || k.contains('merchant_url')) && !k.contains('image') && !k.contains('logo')) {
if (entry.value != null && entry.value.toString().trim().isNotEmpty && entry.value.toString() != "null") {
foundWebsite = entry.value.toString().trim();
break;
}
}
}
}
bool? parsedIsFavorite;
if (targetJson.containsKey('isfavorite')) {
2 weeks ago
parsedIsFavorite = targetJson['isfavorite'] == true || targetJson['isfavorite'] == 1 || targetJson['isfavorite'] == 'true' || targetJson['isfavorite'] == '1';
} else if (targetJson.containsKey('is_favorite')) {
2 weeks ago
parsedIsFavorite = targetJson['is_favorite'] == true || targetJson['is_favorite'] == 1 || targetJson['is_favorite'] == 'true' || targetJson['is_favorite'] == '1';
} else if (targetJson.containsKey('isFavorite')) {
2 weeks ago
parsedIsFavorite = targetJson['isFavorite'] == true || targetJson['isFavorite'] == 1 || targetJson['isFavorite'] == 'true' || targetJson['isFavorite'] == '1';
} else if (targetJson.containsKey('favorite')) {
2 weeks ago
parsedIsFavorite = targetJson['favorite'] == true || targetJson['favorite'] == 1 || targetJson['favorite'] == 'true' || targetJson['favorite'] == '1';
}
return WalaPlusRetailerModel(
3 weeks ago
id:
targetJson['id']?.toString() ??
3 weeks ago
targetJson['WP_RetailerID']?.toString() ??
targetJson['retailer_id']?.toString() ??
targetJson['RetailerID']?.toString() ??
targetJson['retailerId']?.toString(),
name: decodeUnicodeString(targetJson['name']),
offerTitle: decodeUnicodeString(targetJson['offer_title']),
offerType: targetJson['offer_type']?.toString(),
description: decodeUnicodeString(targetJson['description']),
info: decodeUnicodeString(targetJson['info']),
image: targetJson['image']?.toString(),
imageThumb: targetJson['image_thumb']?.toString(),
logo: targetJson['logo']?.toString(),
logoThumb: targetJson['logo_thumb']?.toString(),
2 weeks ago
website: foundWebsite,
isFavorite: parsedIsFavorite,
3 weeks ago
type: targetJson['type']?.toString(),
usage: targetJson['usage'] is String ? decodeUnicodeString(targetJson['usage']) : targetJson['usage'],
weight: targetJson['weight'],
geoId: targetJson['geo_id'],
pin: targetJson['pin']?.toString(),
barcodeStandard: targetJson['barcode_standard']?.toString(),
arHowToUse: decodeUnicodeString(targetJson['ar_how_to_use']),
enHowToUse: decodeUnicodeString(targetJson['en_how_to_use']),
howToUse: decodeUnicodeString(targetJson['how_to_use']),
contact: contactObj,
3 weeks ago
offerWeight: targetJson['offer_weight'],
offers: offersList,
locations: locationsList,
categories: categoriesList,
2 weeks ago
categoryId: foundCategoryId,
categoryName: singleCatName ?? singleCatEn ?? singleCatAr,
3 weeks ago
validity: foundValidity,
validTo: targetJson['valid_to']?.toString() ?? targetJson['validTo']?.toString(),
validUntil: targetJson['valid_until']?.toString() ?? targetJson['validUntil']?.toString(),
endDate: targetJson['end_date']?.toString() ?? targetJson['endDate']?.toString(),
expiryDate: targetJson['expiry_date']?.toString() ?? targetJson['expiryDate']?.toString(),
duration: targetJson['duration']?.toString(),
email: targetJson['email']?.toString() ?? targetJson['contact_email']?.toString() ?? contactObj?.primaryEmail,
phone: targetJson['phone']?.toString() ?? targetJson['contact_phone']?.toString() ?? targetJson['mobile']?.toString() ?? contactObj?.primaryPhone,
2 weeks ago
countryNameAr: decodeUnicodeString(targetJson['country_name_ar'] ?? targetJson['countryNameAr'] ?? targetJson['country_ar']),
countryNameEn: decodeUnicodeString(targetJson['country_name_en'] ?? targetJson['countryNameEn'] ?? targetJson['country_en']),
);
}
2 weeks ago
// Adapter for the Mazaya "Exclusive Offers" section, which reuses the legacy Offers &
// Discounts (ITG) API/implementation (lib/ui/screens/offers_and_discounts) rather than a
// WalaPlus endpoint. Maps whatever overlaps onto this model so the existing grid/card/detail
// screen can render it unchanged - fields with no equivalent on OffersListModel (favorite,
// redeem, locations, contact, ...) are simply left null, and isExclusive hides the actions
// that depend on them (see WalaPlusRetailerCard, RetailerDetailsScreen).
factory WalaPlusRetailerModel.fromExclusiveOffer(OffersListModel offer, {bool isAr = false}) {
String? name = decodeUnicodeString(isAr ? (offer.titleAr ?? offer.titleEn) : (offer.titleEn ?? offer.titleAr));
String? description = decodeUnicodeString(isAr ? (offer.descriptionAr ?? offer.descriptionEn) : (offer.descriptionEn ?? offer.descriptionAr));
String? categoryName = decodeUnicodeString(isAr ? (offer.categoryNameAr ?? offer.categoryNameEn) : (offer.categoryNameEn ?? offer.categoryNameAr));
String? discountText = _formatExclusiveDiscountText(decodeUnicodeString(offer.discountDescription) ?? decodeUnicodeString(offer.discount));
return WalaPlusRetailerModel(
id: "exclusive-${offer.offersDiscountId ?? ''}",
name: name,
// WalaPlusRetailerCard reads offerTitle as the promo subtitle shown under the name (e.g.
// "20% off") - the discount text is the closest equivalent on a legacy offer.
offerTitle: discountText,
description: description,
info: description,
image: offer.bannerImage,
logo: offer.logo,
website: offer.websiteUrl,
endDate: offer.endDate,
validTo: offer.endDate,
categoryName: categoryName,
categories: categoryName != null ? <WalaPlusCategoryModel>[WalaPlusCategoryModel(name: categoryName)] : null,
isExclusive: true,
);
}
// The legacy Offers & Discounts API sometimes returns a bare number ("10", "20") for its
// discount fields rather than a formatted "10% off" string. WalaPlusRetailerCard's badge
// logic (getDiscountBadge) only recognizes a "<digits>%" pattern, so a bare number would
// otherwise render verbatim instead of as a discount. Already-formatted text (containing "%",
// "off", or "خصم") is left untouched.
static String? _formatExclusiveDiscountText(String? raw) {
if (raw == null) return null;
String trimmed = raw.trim();
if (trimmed.isEmpty) return null;
if (trimmed.contains('%') || trimmed.toLowerCase().contains('off') || trimmed.contains('خصم')) {
return trimmed;
}
if (RegExp(r'^\d+(\.\d+)?$').hasMatch(trimmed)) {
return LocaleKeys.offDiscount.tr(args: ['$trimmed%']);
}
return trimmed;
}
3 weeks ago
static String? decodeUnicodeString(Object? val) {
if (val == null) return null;
String s = val.toString().trim();
if (s.isEmpty || s == "null") return null;
while ((s.startsWith('"') && s.endsWith('"')) || (s.startsWith("'") && s.endsWith("'"))) {
if (s.length >= 2) {
s = s.substring(1, s.length - 1).trim();
} else {
break;
}
}
3 weeks ago
s = s.replaceAll(r'\/', '/').replaceAll(r'\"', '"').replaceAll(r"\'", "'");
3 weeks ago
s = s.replaceAllMapped(RegExp(r'\\u([0-9a-fA-F]{4})'), (match) {
try {
var codeUnit = int.parse(match.group(1)!, radix: 16);
return String.fromCharCode(codeUnit);
} catch (_) {
return match.group(0)!;
}
});
s = s
.replaceAll(r'\u201c', '“')
.replaceAll(r'\u201d', '”')
.replaceAll(r'\u2019', '’')
.replaceAll(r'\u2018', '‘')
.replaceAll(r'\u00a0', ' ')
.replaceAll(r'\u2022', '•')
.replaceAll(r'\u2013', '–')
.replaceAll(r'\u2014', '—')
.replaceAll(r'\u200f', '')
.replaceAll(r'\u200e', '')
.replaceAll(r'\u061f', '؟')
.replaceAll(r'\u201', '“')
.replaceAll('&quot;', '"')
.replaceAll('&apos;', "'")
.replaceAll('&#39;', "'")
.replaceAll('&nbsp;', ' ')
.replaceAll('&amp;', '&');
return s;
}
Map<String, dynamic> toJson() {
Map<String, dynamic> data = <String, dynamic>{};
data['id'] = id;
data['name'] = name;
data['offer_title'] = offerTitle;
data['offer_type'] = offerType;
data['description'] = description;
data['info'] = info;
data['image'] = image;
data['image_thumb'] = imageThumb;
data['logo'] = logo;
data['logo_thumb'] = logoThumb;
data['website'] = website;
data['type'] = type;
data['usage'] = usage;
data['weight'] = weight;
data['geo_id'] = geoId;
data['pin'] = pin;
data['barcode_standard'] = barcodeStandard;
data['ar_how_to_use'] = arHowToUse;
data['en_how_to_use'] = enHowToUse;
data['how_to_use'] = howToUse;
data['contact'] = contact?.toJson();
data['offer_weight'] = offerWeight;
3 weeks ago
data['validity'] = validity;
data['valid_to'] = validTo;
data['valid_until'] = validUntil;
data['end_date'] = endDate;
data['expiry_date'] = expiryDate;
data['duration'] = duration;
data['email'] = email;
data['phone'] = phone;
2 weeks ago
data['country_name_ar'] = countryNameAr;
data['country_name_en'] = countryNameEn;
2 weeks ago
data['is_favorite'] = isFavorite;
data['isfavorite'] = isFavorite;
2 weeks ago
data['is_exclusive'] = isExclusive;
3 weeks ago
if (offers != null) {
data['offers'] = offers!.map((v) => v.toJson()).toList();
}
if (locations != null) {
data['locations'] = locations!.map((v) => v.toJson()).toList();
}
if (categories != null) {
data['categories'] = categories!.map((v) => v.toJson()).toList();
}
2 weeks ago
data['category_id'] = categoryId;
data['category_name'] = categoryName;
return data;
}
2 weeks ago
String? getDisplayCategory({bool isAr = false, List<WalaPlusCategoryModel>? fallbackCategories}) {
if (categories != null && categories!.isNotEmpty) {
for (var cat in categories!) {
String? primary = isAr ? cat.countryNameAr : cat.countryNameEn;
String? secondary = isAr ? (cat.name ?? cat.countryNameEn) : (cat.name ?? cat.countryNameAr);
2 weeks ago
String? resolved =
(primary != null && primary.trim().isNotEmpty && primary != "null") ? primary.trim() : (secondary != null && secondary.trim().isNotEmpty && secondary != "null" ? secondary.trim() : null);
2 weeks ago
if (resolved != null && resolved.isNotEmpty) {
return resolved;
}
}
}
if (categoryName != null && categoryName!.trim().isNotEmpty && categoryName != "null") {
return categoryName!.trim();
}
if (fallbackCategories != null && categoryId != null && categoryId!.trim().isNotEmpty) {
for (var cat in fallbackCategories) {
if (cat.id?.toString() == categoryId?.toString()) {
String? primary = isAr ? cat.countryNameAr : cat.countryNameEn;
String? secondary = isAr ? (cat.name ?? cat.countryNameEn) : (cat.name ?? cat.countryNameAr);
2 weeks ago
String? resolved =
(primary != null && primary.trim().isNotEmpty && primary != "null")
? primary.trim()
: (secondary != null && secondary.trim().isNotEmpty && secondary != "null" ? secondary.trim() : null);
2 weeks ago
if (resolved != null && resolved.isNotEmpty) {
return resolved;
}
}
}
}
if (type != null && type!.trim().isNotEmpty) {
String lt = type!.trim().toLowerCase();
if (lt != 'online' && lt != 'instore' && lt != 'store' && lt != 'in_store') {
return type!.trim();
}
}
return null;
}
3 weeks ago
String? get displayImage {
if (image != null && image!.trim().isNotEmpty && image != "null") return image!.trim();
if (imageThumb != null && imageThumb!.trim().isNotEmpty && imageThumb != "null") return imageThumb!.trim();
return null;
}
String? get displayLogo {
if (logo != null && logo!.trim().isNotEmpty && logo != "null") return logo!.trim();
if (logoThumb != null && logoThumb!.trim().isNotEmpty && logoThumb != "null") return logoThumb!.trim();
return null;
}
String? get resolvedWebsite {
3 weeks ago
if (website != null && website!.trim().isNotEmpty && website != "null") {
return website!.trim();
}
2 weeks ago
if (contact?.website != null && contact!.website!.trim().isNotEmpty && contact!.website != "null") {
return contact!.website!.trim();
}
return null;
}
3 weeks ago
bool get hasPhysicalLocation {
if (locations != null && locations!.isNotEmpty) {
for (var loc in locations!) {
3 weeks ago
if ((loc.cityNameEn != null && loc.cityNameEn!.trim().isNotEmpty && loc.cityNameEn != "null") ||
(loc.cityNameAr != null && loc.cityNameAr!.trim().isNotEmpty && loc.cityNameAr != "null") ||
(loc.city != null && loc.city!.trim().isNotEmpty && loc.city != "null")) {
return true;
}
}
}
3 weeks ago
return false;
}
3 weeks ago
// Resolves a single location's city using the same 3-pass priority this used to apply
// globally: specific city name first, then a non-generic-country city, then any city at all.
String? _resolveLocationCity(WalaPlusRetailerLocation loc, bool isAr) {
String? cName = isAr ? loc.cityNameAr : loc.cityNameEn;
if (cName != null && cName.trim().isNotEmpty && cName != "null") {
return cName.trim();
}
if (loc.city != null && loc.city!.trim().isNotEmpty && loc.city != "null") {
String c = loc.city!.trim();
if (c.toLowerCase() != 'saudi arabia' && c != 'المملكة العربية السعودية') {
return c;
}
}
if (loc.city != null && loc.city!.trim().isNotEmpty && loc.city != "null") {
return loc.city!.trim();
}
return null;
}
// Multiple distinct cities across this retailer's locations -> "Multiple Cities" instead of
// just picking the first one found, so the badge doesn't silently hide the other branches.
3 weeks ago
String getDisplayCity({bool isAr = false}) {
3 weeks ago
if (locations == null || locations!.isEmpty) return '';
Set<String> distinctCities = <String>{};
for (var loc in locations!) {
String? resolved = _resolveLocationCity(loc, isAr);
if (resolved != null) distinctCities.add(resolved);
}
3 weeks ago
if (distinctCities.isEmpty) return '';
if (distinctCities.length > 1) return LocaleKeys.multipleCities.tr();
return distinctCities.first;
}
3 weeks ago
String get displayCity => getDisplayCity(isAr: false);
String get displayUsage {
3 weeks ago
if (usage != null) {
String u = usage.toString().trim();
if (int.tryParse(u) != null) {
return 'usage:$u';
}
if (!u.contains('<') && !u.contains('{') && u.isNotEmpty && u.length <= 12 && u.toLowerCase() != 'online' && u.toLowerCase() != 'store') {
return 'usage:$u';
}
}
return '';
}
3 weeks ago
String getDisplayValidity({bool isAr = false}) {
if (validity != null && validity!.trim().isNotEmpty && validity != "null") {
return validity!.trim();
}
if (validTo != null && validTo!.trim().isNotEmpty && validTo != "null") {
return validTo!.trim();
}
if (validUntil != null && validUntil!.trim().isNotEmpty && validUntil != "null") {
return validUntil!.trim();
}
if (endDate != null && endDate!.trim().isNotEmpty && endDate != "null") {
return endDate!.trim();
}
if (expiryDate != null && expiryDate!.trim().isNotEmpty && expiryDate != "null") {
return expiryDate!.trim();
}
if (duration != null && duration!.trim().isNotEmpty && duration != "null") {
return duration!.trim();
}
return LocaleKeys.offerValid.tr();
}
String? get resolvedEmail {
if (email != null && email!.trim().isNotEmpty && email != "null") {
return email!.trim();
}
if (contact?.primaryEmail != null && contact!.primaryEmail!.trim().isNotEmpty && contact!.primaryEmail != "null") {
return contact!.primaryEmail!.trim();
}
if (locations != null && locations!.isNotEmpty) {
for (var loc in locations!) {
if (loc.contact != null && loc.contact!.contains('@')) {
return loc.contact!.trim();
}
}
}
String combined = "${description ?? ''} ${info ?? ''} ${howToUse ?? ''} ${arHowToUse ?? ''} ${enHowToUse ?? ''}";
RegExp emailRegex = RegExp(r'[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}');
Match? match = emailRegex.firstMatch(combined);
if (match != null) {
return match.group(0);
}
return null;
}
String? get resolvedPhone {
if (phone != null && phone!.trim().isNotEmpty && phone != "null") {
return phone!.trim();
}
if (contact?.primaryPhone != null && contact!.primaryPhone!.trim().isNotEmpty && contact!.primaryPhone != "null") {
return contact!.primaryPhone!.trim();
}
if (locations != null && locations!.isNotEmpty) {
for (var loc in locations!) {
3 weeks ago
if (loc.contact != null && loc.contact!.trim().isNotEmpty && loc.contact != "null" && !loc.contact!.contains('@')) {
3 weeks ago
return loc.contact!.trim();
}
}
}
String combined = "${description ?? ''} ${info ?? ''} ${howToUse ?? ''} ${arHowToUse ?? ''} ${enHowToUse ?? ''}";
RegExp phoneRegex = RegExp(r'(?:\+?966|0)?(?:5\d{8}|1\d{7}|9200\d{5}|800\d{6,7})');
Match? match = phoneRegex.firstMatch(combined);
if (match != null) {
return match.group(0);
}
return null;
}
3 weeks ago
// type can be a comma-separated combo (e.g. "online,instore") when a retailer offers both -
// this splits and normalizes every part instead of only ever matching a single exact value,
// so a combined retailer is correctly recognized as both rather than neither/just one.
List<String> get normalizedTypes {
2 weeks ago
Set<String> result = <String>{};
3 weeks ago
if (type != null && type!.trim().isNotEmpty) {
3 weeks ago
for (String part in type!.split(',')) {
String t = part.trim().toLowerCase();
if (t.isEmpty) continue;
if (t == 'online') {
result.add('online');
} else if (t == 'instore' || t == 'in_store' || t == 'in store' || t == 'store') {
result.add('instore');
} else {
2 weeks ago
// Non-online types (e.g. category names like "Physiotherapy") represent
// in-store merchants/services that require physical presence.
result.add('instore');
3 weeks ago
}
}
3 weeks ago
}
if (locations != null && locations!.isNotEmpty) {
2 weeks ago
result.add('instore');
3 weeks ago
}
2 weeks ago
if (result.isEmpty && hasPhysicalLocation) {
result.add('instore');
}
return result.toList();
3 weeks ago
}
3 weeks ago
bool get isOnlineStore => normalizedTypes.contains('online');
bool get isInstore => normalizedTypes.contains('instore');
// Kept for any existing single-value callers - returns the first normalized type found.
String? get normalizedType {
List<String> types = normalizedTypes;
return types.isEmpty ? null : types.first;
}
String _displayTypeLabel(String norm) {
3 weeks ago
if (norm == 'online') return LocaleKeys.online.tr();
2 weeks ago
return LocaleKeys.inStore.tr();
3 weeks ago
}
String? getDisplayType() {
List<String> types = normalizedTypes;
if (types.isNotEmpty) return _displayTypeLabel(types.first);
3 weeks ago
return null;
}
3 weeks ago
// One label per distinct type (e.g. ["Online", "In Store"]) - used to render a separate chip
// for each, instead of collapsing a dual-type retailer down to a single badge.
List<String> getDisplayTypes() {
List<String> types = normalizedTypes;
if (types.isNotEmpty) return types.map(_displayTypeLabel).toList();
return <String>[];
}
}
class WalaPlusRetailerContact {
3 weeks ago
String? email;
List<String>? emails;
3 weeks ago
String? phone;
List<String>? phones;
String? website;
3 weeks ago
String? address;
String? instagram;
String? twitter;
3 weeks ago
WalaPlusRetailerContact({this.email, this.emails, this.phone, this.phones, this.website, this.address, this.instagram, this.twitter});
factory WalaPlusRetailerContact.fromJson(Map<String, dynamic> json) {
List<String> emailsList = [];
if (json['emails'] != null && json['emails'] is List) {
json['emails'].forEach((v) => emailsList.add(v.toString()));
}
3 weeks ago
String? singleEmail = json['email']?.toString() ?? json['email_address']?.toString();
if (singleEmail != null && singleEmail.trim().isNotEmpty && singleEmail != "null") {
if (!emailsList.contains(singleEmail.trim())) {
emailsList.add(singleEmail.trim());
}
}
List<String> phonesList = [];
if (json['phones'] != null && json['phones'] is List) {
json['phones'].forEach((v) => phonesList.add(v.toString()));
}
3 weeks ago
String? singlePhone = json['phone']?.toString() ?? json['mobile']?.toString() ?? json['phone_number']?.toString();
if (singlePhone != null && singlePhone.trim().isNotEmpty && singlePhone != "null") {
if (!phonesList.contains(singlePhone.trim())) {
phonesList.add(singlePhone.trim());
}
}
return WalaPlusRetailerContact(
3 weeks ago
email: singleEmail,
emails: emailsList,
3 weeks ago
phone: singlePhone,
phones: phonesList,
3 weeks ago
website: WalaPlusRetailerModel.decodeUnicodeString(json['website']),
address: WalaPlusRetailerModel.decodeUnicodeString(json['address']),
instagram: json['instagram']?.toString(),
twitter: json['twitter']?.toString(),
);
}
Map<String, dynamic> toJson() {
3 weeks ago
return {'email': email, 'emails': emails, 'phone': phone, 'phones': phones, 'website': website, 'address': address, 'instagram': instagram, 'twitter': twitter};
}
3 weeks ago
String? get primaryEmail {
if (email != null && email!.trim().isNotEmpty && email != "null") return email!.trim();
if (emails != null && emails!.isNotEmpty) {
for (var e in emails!) {
if (e.trim().isNotEmpty && e != "null") return e.trim();
}
}
return null;
}
String? get primaryPhone {
if (phone != null && phone!.trim().isNotEmpty && phone != "null") return phone!.trim();
if (phones != null && phones!.isNotEmpty) {
for (var p in phones!) {
if (p.trim().isNotEmpty && p != "null") return p.trim();
}
}
return null;
}
}
class WalaPlusRetailerLocation {
3 weeks ago
dynamic id;
String? address;
String? city;
String? cityNameAr;
String? cityNameEn;
String? contact;
String? countryNameAr;
String? countryNameEn;
3 weeks ago
String? distance;
double? latitude;
double? longitude;
dynamic placeId;
WalaPlusRetailerLocation({
3 weeks ago
this.id,
this.address,
this.city,
this.cityNameAr,
this.cityNameEn,
this.contact,
this.countryNameAr,
this.countryNameEn,
3 weeks ago
this.distance,
this.latitude,
this.longitude,
this.placeId,
});
factory WalaPlusRetailerLocation.fromJson(Map<String, dynamic> json) {
return WalaPlusRetailerLocation(
3 weeks ago
id: json['id'],
address: WalaPlusRetailerModel.decodeUnicodeString(json['address']),
city: WalaPlusRetailerModel.decodeUnicodeString(json['city']),
cityNameAr: WalaPlusRetailerModel.decodeUnicodeString(json['city_name_ar']),
cityNameEn: WalaPlusRetailerModel.decodeUnicodeString(json['city_name_en']),
contact: json['contact']?.toString(),
3 weeks ago
countryNameAr: WalaPlusRetailerModel.decodeUnicodeString(json['country_name_ar']),
countryNameEn: WalaPlusRetailerModel.decodeUnicodeString(json['country_name_en']),
3 weeks ago
distance: WalaPlusRetailerModel.decodeUnicodeString(json['distance']),
latitude: json['latitude'] is num ? (json['latitude'] as num).toDouble() : double.tryParse(json['latitude']?.toString() ?? ''),
longitude: json['longitude'] is num ? (json['longitude'] as num).toDouble() : double.tryParse(json['longitude']?.toString() ?? ''),
placeId: json['place_id'],
);
}
Map<String, dynamic> toJson() {
3 weeks ago
Map<String, dynamic> data = <String, dynamic>{};
data['id'] = id;
data['address'] = address;
data['city'] = city;
data['city_name_ar'] = cityNameAr;
data['city_name_en'] = cityNameEn;
data['contact'] = contact;
data['country_name_ar'] = countryNameAr;
data['country_name_en'] = countryNameEn;
3 weeks ago
data['distance'] = distance;
data['latitude'] = latitude;
data['longitude'] = longitude;
data['place_id'] = placeId;
return data;
}
3 weeks ago
3 weeks ago
// distance comes from the API as a raw numeric string (e.g. "14.729670498669968") -
// round to max 2 decimal places and append the localized "KM Away" label.
String? getDisplayDistance() {
if (distance == null) return null;
double? km = double.tryParse(distance!.trim());
if (km == null) return null;
String formatted = km.toStringAsFixed(2);
return LocaleKeys.kmAway.tr(args: [formatted]);
}
3 weeks ago
String getLocalizedCity(bool isAr) {
if (isAr) {
if (cityNameAr != null && cityNameAr!.trim().isNotEmpty && cityNameAr != "null") {
return cityNameAr!.trim();
}
} else {
if (cityNameEn != null && cityNameEn!.trim().isNotEmpty && cityNameEn != "null") {
return cityNameEn!.trim();
}
}
if (city != null && city!.trim().isNotEmpty && city != "null") {
return city!.trim();
}
return isAr ? (cityNameAr ?? "") : (cityNameEn ?? "");
}
String getLocalizedCountry(bool isAr) {
if (isAr) {
if (countryNameAr != null && countryNameAr!.trim().isNotEmpty && countryNameAr != "null") {
return countryNameAr!.trim();
}
} else {
if (countryNameEn != null && countryNameEn!.trim().isNotEmpty && countryNameEn != "null") {
return countryNameEn!.trim();
}
}
return "";
}
}
class WalaPlusOfferModel {
String? id;
String? retailerId;
String? title;
String? image;
String? imageThumb;
String? type;
String? description;
dynamic price;
dynamic usage;
String? expiredAt;
3 weeks ago
WalaPlusOfferModel({this.id, this.retailerId, this.title, this.image, this.imageThumb, this.type, this.description, this.price, this.usage, this.expiredAt});
3 weeks ago
factory WalaPlusOfferModel.fromJson(Map<String, dynamic> json) {
return WalaPlusOfferModel(
id: json['id']?.toString(),
retailerId: (json['retailer_id'] ?? json['retailerId'] ?? json['retailerID'])?.toString(),
title: WalaPlusRetailerModel.decodeUnicodeString(json['title']),
image: json['image']?.toString(),
imageThumb: (json['image_thumb'] ?? json['imageThumb'])?.toString(),
type: json['type']?.toString(),
description: WalaPlusRetailerModel.decodeUnicodeString(json['description']),
price: json['price'],
usage: json['usage'],
expiredAt: (json['expired_at'] ?? json['expiredAt'] ?? json['expiry_date'])?.toString(),
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'retailer_id': retailerId,
'title': title,
'image': image,
'image_thumb': imageThumb,
'type': type,
'description': description,
'price': price,
'usage': usage,
'expired_at': expiredAt,
};
}
String? get displayImage {
if (image != null && image!.trim().isNotEmpty && image != "null") return image!.trim();
if (imageThumb != null && imageThumb!.trim().isNotEmpty && imageThumb != "null") return imageThumb!.trim();
return null;
}
}