using System; using System.Globalization; using System.Text; using System.Text.RegularExpressions; namespace SyncEngine.MySql { public static class ProductFieldMapper { public static string MakeSlug(int sqlExpressProductId, string code, string description) { var safeCode = UrlTitle((code ?? string.Empty).Trim().Replace("*", "")); var title = UrlTitle(description ?? string.Empty); var id = sqlExpressProductId.ToString(CultureInfo.InvariantCulture); return id + "-" + safeCode + "-" + title; } public static string UrlTitle(string value) { if (string.IsNullOrWhiteSpace(value)) { return string.Empty; } var normalized = RemoveDiacritics(value.Trim()); normalized = normalized.ToLowerInvariant(); normalized = Regex.Replace(normalized, @"\s+", "-"); normalized = Regex.Replace(normalized, @"[^a-z0-9_-]", ""); normalized = Regex.Replace(normalized, @"-+", "-").Trim('-'); return normalized; } private static string RemoveDiacritics(string value) { var normalized = value.Normalize(NormalizationForm.FormD); var builder = new StringBuilder(normalized.Length); foreach (var ch in normalized) { if (CharUnicodeInfo.GetUnicodeCategory(ch) != UnicodeCategory.NonSpacingMark) { builder.Append(ch); } } return builder.ToString().Normalize(NormalizationForm.FormC); } public static string Truncate(string value, int maxLength) { if (string.IsNullOrEmpty(value) || value.Length <= maxLength) { return value; } return value.Substring(0, maxLength); } /// Trimmed text for required fields (e.g. description). Empty string is preserved. public static string TrimRequiredString(string value) { return (value ?? string.Empty).Trim(); } /// Optional lookup/text fields: whitespace-only becomes null for MySQL NULL semantics. public static string NormalizeOptionalString(string value) { if (string.IsNullOrWhiteSpace(value)) { return null; } return value.Trim(); } public static decimal? ToPrice(object value) { if (value == null || value == DBNull.Value) { return null; } decimal parsed; if (value is decimal decimalValue) { parsed = decimalValue; } else if (value is double doubleValue) { parsed = (decimal)doubleValue; } else if (value is float floatValue) { parsed = (decimal)floatValue; } else if (!decimal.TryParse( Convert.ToString(value, CultureInfo.InvariantCulture), NumberStyles.Any, CultureInfo.InvariantCulture, out parsed)) { return null; } return Math.Round(parsed, 2, MidpointRounding.AwayFromZero); } /// /// Maps Access Yes/No / SQL BIT to bool. Null/DBNull/false/0 → false; any other non-zero (incl. Access -1) → true. /// public static bool ToBool(object value) { if (value == null || value == DBNull.Value) { return false; } if (value is bool boolValue) { return boolValue; } if (value is byte || value is sbyte || value is short || value is ushort || value is int || value is uint || value is long || value is ulong) { return Convert.ToInt64(value, CultureInfo.InvariantCulture) != 0; } if (value is float || value is double || value is decimal) { return Convert.ToDecimal(value, CultureInfo.InvariantCulture) != 0m; } var text = Convert.ToString(value, CultureInfo.InvariantCulture); if (string.IsNullOrWhiteSpace(text)) { return false; } if (bool.TryParse(text, out var parsedBool)) { return parsedBool; } long parsedLong; if (long.TryParse(text, NumberStyles.Any, CultureInfo.InvariantCulture, out parsedLong)) { return parsedLong != 0; } return false; } } public sealed class ShopProductRow { /// SQL Express tblProduct.ProductID (same value as Access AutoNumber after Leg 1 sync). public int? SqlExpressProductId { get; set; } public string Code { get; set; } public string Description { get; set; } public string Category { get; set; } public string SubCategory { get; set; } public string Item { get; set; } public string Gender { get; set; } public string Era { get; set; } public string Barcode { get; set; } public decimal? Price { get; set; } public string Size { get; set; } public string Slug { get; set; } /// From tblProduct.blnCameraReady — source of truth for shop_products.is_visible. public bool IsVisible { get; set; } public string ImageUrl1 { get; set; } public string ImageUrl2 { get; set; } public string ImageUrl3 { get; set; } public string RowHash { get; set; } } }