using System; using System.Collections.Generic; using System.Data; using System.IO; using System.Linq; using System.Threading.Tasks; using Dapper; using Microsoft.Data.SqlClient; using MotoresArgentinosV2.MigrationTool.Models; namespace MotoresArgentinosV2.MigrationTool; public class AdMigrator { private readonly string _connStringAutos; private readonly string _connStringV2; public AdMigrator(string connAutos, string connV2) { _connStringAutos = connAutos; _connStringV2 = connV2; } public async Task ExecuteAsync() { Console.WriteLine("🚗 INICIANDO MIGRACIÓN DE AUTOS..."); using var dbAutos = new SqlConnection(_connStringAutos); using var dbV2 = new SqlConnection(_connStringV2); // 1. CARGAR CACHÉS DE V2 Console.WriteLine(" 📥 Cargando diccionarios de Marcas, Modelos y Usuarios..."); var brandsV2 = (await dbV2.QueryAsync("SELECT BrandID, LegacyID, Name FROM Brands WHERE VehicleTypeID = 1")).ToList(); var usersV2 = (await dbV2.QueryAsync("SELECT UserID, Email, PhoneNumber FROM Users")).ToDictionary(u => (string)u.Email, u => u); // 2. OBTENER AVISOS LEGACY (QUERY MEJORADA PARA ADMINS) Console.WriteLine(" 📥 Leyendo avisos activos del Legacy..."); // Esta query intenta obtener el email de dos fuentes: // 1. De la tabla Particulares (prioridad) // 2. De la tabla de autenticación (aspnet_Membership) si no está en Particulares var queryAds = @" SELECT A.Auto_Id, A.Auto_Fecha_Publicacion, A.Auto_Cant_Visitas, COALESCE(P.Part_Mail, Mem.Email) as EmailVendedor, M.Marca_Detalle, Mo.Modelo_Detalle, A.Auto_Version, An.Año_Detalle, A.Auto_precio, Mon.Mone_Detalle, A.Auto_Kilometros, A.Auto_Detalle as Descripcion, C.Comb_Detalle, Col.Color_detalle, Car.Carroceria_Nombre, CASE WHEN D.DetAut_PapelesALDia = 1 THEN 1 ELSE 0 END as PapelesAlDia, COALESCE(P.Part_Telefono, '') as Part_Telefono, COALESCE(P.Part_Celular, '') as Part_Celular FROM Autos A -- Join original con particulares LEFT JOIN Particulares P ON A.Auto_Usuario_Id = P.Part_Usu_Nombre -- Join adicional para rescatar usuarios admin/internos sin perfil particular LEFT JOIN aspnet_Users U ON A.Auto_Usuario_Id = U.UserName LEFT JOIN aspnet_Membership Mem ON U.UserId = Mem.UserId -- Joins de datos técnicos LEFT JOIN Marca M ON A.Auto_Marca_Id = M.Marca_Id LEFT JOIN Modelo Mo ON A.Auto_Modelo_Id = Mo.Modelo_Id LEFT JOIN Año An ON A.Auto_Año = An.Año_Id LEFT JOIN Combustible C ON A.Auto_Comb_Id = C.Comb_Id LEFT JOIN Color Col ON A.Auto_Color = Col.Color_Id LEFT JOIN Carroceria Car ON A.Auto_Carroceria_Id = Car.Carroceria_Id LEFT JOIN Moneda Mon ON A.Auto_Moneda_Id = Mon.Mone_id LEFT JOIN Detalle_Auto D ON A.Auto_Id = D.DetAut_Auto_Id WHERE A.Auto_Estado = 0 AND A.Auto_Tipo_Id = 1 AND A.Auto_Fecha_Finalizacion >= GETDATE()"; var adsLegacy = (await dbAutos.QueryAsync(queryAds)).ToList(); Console.WriteLine($" ✅ Encontrados {adsLegacy.Count} avisos candidatos."); // 3. OBTENER FOTOS var idsString = string.Join(",", adsLegacy.Select(a => a.Auto_Id)); var photosLegacy = new List(); if (adsLegacy.Any()) { photosLegacy = (await dbAutos.QueryAsync( $"SELECT Foto_Id, Foto_Auto_Id, Foto_Ruta FROM Fotos_Autos WHERE Foto_Auto_Id IN ({idsString})")).ToList(); } int migrados = 0; int omitidosUsuario = 0; foreach (var ad in adsLegacy) { // A. VALIDAR USUARIO string emailKey = ad.EmailVendedor?.ToLower().Trim() ?? ""; if (!usersV2.TryGetValue(emailKey, out var userV2)) { omitidosUsuario++; // Console.WriteLine($" ⚠️ Aviso {ad.Auto_Id} omitido: Usuario {emailKey} no existe en V2."); continue; } // B. RESOLVER MARCA Y MODELO var brandV2 = brandsV2.FirstOrDefault(b => b.Name.ToLower() == (ad.Marca_Detalle ?? "").ToLower()); if (brandV2 == null) { // Si la marca no existe, saltamos por seguridad o podríamos asignar "Otros" continue; } int modelId = await GetOrCreateModelAsync(dbV2, (int)brandV2.BrandID, ad.Modelo_Detalle); // C. CONSTRUIR DATOS int.TryParse(ad.Año_Detalle, out int year); string versionName = $"{ad.Modelo_Detalle} {ad.Auto_Version}".Trim(); if (versionName.Length > 100) versionName = versionName.Substring(0, 100); string desc = ad.Descripcion ?? ""; if (ad.PapelesAlDia == 1) desc += "\n\n✔️ Papeles al día."; // Contacto: Si venía de Particulares, usamos eso. Si no (admins), usamos el del usuario V2 string contactPhone = !string.IsNullOrWhiteSpace(ad.Part_Celular) ? ad.Part_Celular : ad.Part_Telefono; if (string.IsNullOrWhiteSpace(contactPhone)) contactPhone = userV2.PhoneNumber; string currency = (ad.Mone_Detalle != null && ad.Mone_Detalle.Contains("US")) ? "USD" : "ARS"; // D. INSERTAR AVISO var insertAdSql = @" INSERT INTO Ads ( UserID, VehicleTypeID, BrandID, ModelID, VersionName, Year, KM, Price, Currency, Description, IsFeatured, StatusID, CreatedAt, PublishedAt, ExpiresAt, FuelType, Color, Segment, Location, Condition, Transmission, Steering, ContactPhone, ContactEmail, DisplayContactInfo, LegacyAdID, ViewsCounter ) VALUES ( @UserID, @VehicleTypeID, @BrandID, @ModelID, @VersionName, @Year, @KM, @Price, @Currency, @Description, 0, 4, @CreatedAt, @PublishedAt, DATEADD(day, 30, @PublishedAt), @FuelType, @Color, @Segment, 'La Plata', 'No Especificado', 'No Especificado', 'No Especificado', @ContactPhone, @ContactEmail, 1, @LegacyAdID, @ViewsCounter ); SELECT CAST(SCOPE_IDENTITY() as int);"; int newAdId = await dbV2.ExecuteScalarAsync(insertAdSql, new { UserID = userV2.UserID, VehicleTypeID = 1, BrandID = brandV2.BrandID, ModelID = modelId, VersionName = versionName, Year = year, KM = ad.Auto_Kilometros, Price = ad.Auto_precio, Currency = currency, Description = desc, FuelType = ad.Comb_Detalle, Color = ad.Color_detalle, Segment = ad.Carroceria_Nombre, ContactPhone = contactPhone, ContactEmail = userV2.Email, LegacyAdID = ad.Auto_Id, ViewsCounter = ad.Auto_Cant_Visitas, CreatedAt = ad.Auto_Fecha_Publicacion, PublishedAt = ad.Auto_Fecha_Publicacion }); // E. INSERTAR FOTOS var misFotos = photosLegacy .Where(p => p.Foto_Auto_Id == ad.Auto_Id && !p.Foto_Ruta.Contains("nofoto")) .ToList(); int order = 0; foreach (var foto in misFotos) { string fileName = Path.GetFileName(foto.Foto_Ruta); // Aquí podrías agregar la lógica para copiar el archivo físico si tienes acceso // Por ahora solo guardamos la referencia en DB string v2Path = $"/uploads/legacy/{fileName}"; await dbV2.ExecuteAsync(@" INSERT INTO AdPhotos (AdID, FilePath, IsCover, SortOrder) VALUES (@AdID, @FilePath, @IsCover, @SortOrder)", new { AdID = newAdId, FilePath = v2Path, IsCover = (order == 0), SortOrder = order }); order++; } migrados++; } Console.WriteLine("=================================================="); Console.WriteLine($"🏁 MIGRACIÓN DE AUTOS FINALIZADA"); Console.WriteLine($"✅ Insertados: {migrados}"); Console.WriteLine($"⏩ Omitidos (Sin usuario/marca): {omitidosUsuario}"); Console.WriteLine("=================================================="); } public async Task MigrateMotosAsync() { Console.WriteLine("\n🏍️ INICIANDO MIGRACIÓN DE MOTOS..."); using var dbAutos = new SqlConnection(_connStringAutos); using var dbV2 = new SqlConnection(_connStringV2); // 1. CARGAR CACHÉS (Solo Marcas de Motos) Console.WriteLine(" 📥 Cargando diccionarios..."); var brandsV2 = (await dbV2.QueryAsync("SELECT BrandID, LegacyID, Name FROM Brands WHERE VehicleTypeID = 2")).ToList(); // 2 = Motos var usersV2 = (await dbV2.QueryAsync("SELECT UserID, Email, PhoneNumber FROM Users")).ToDictionary(u => (string)u.Email, u => u); // 2. QUERY MOTOS var query = @" SELECT M.Moto_Id, M.Moto_Fecha_Publicacion, M.Moto_Cant_Visitas, COALESCE(P.Part_Mail, Mem.Email) as EmailVendedor, Ma.Marca_Moto_Detalle as Marca_Detalle, Mo.Modelo_Moto_Detalle as Modelo_Detalle, An.Año_Detalle, M.Moto_Precio, Mon.Mone_Detalle, M.Moto_Kilometraje, M.Moto_Detalle as Descripcion, M.Moto_Cilindrada, M.Moto_Tipo_Cuatri_Id, COALESCE(P.Part_Telefono, '') as Part_Telefono, COALESCE(P.Part_Celular, '') as Part_Celular FROM Motos M LEFT JOIN Particulares P ON M.Moto_Usuario_Id = P.Part_Usu_Nombre LEFT JOIN aspnet_Users U ON M.Moto_Usuario_Id = U.UserName LEFT JOIN aspnet_Membership Mem ON U.UserId = Mem.UserId -- JOINS TÉCNICOS LEFT JOIN Marca_Moto Ma ON M.Moto_Marca_Id = Ma.Marca_Moto_Id LEFT JOIN Modelo_Moto Mo ON M.Moto_Modelo_Id = Mo.Modelo_Moto_Id LEFT JOIN Año An ON M.Moto_Año = An.Año_Id LEFT JOIN Moneda Mon ON M.Moto_Moneda_Id = Mon.Mone_id WHERE M.Moto_Estado = 0 AND M.Moto_Fecha_Finalizacion >= GETDATE()"; var adsLegacy = (await dbAutos.QueryAsync(query)).ToList(); Console.WriteLine($" ✅ Encontradas {adsLegacy.Count} motos candidatas."); // 3. FOTOS var idsString = string.Join(",", adsLegacy.Select(a => a.Moto_Id)); var photosLegacy = new List(); if (adsLegacy.Any()) { photosLegacy = (await dbAutos.QueryAsync( $"SELECT Foto_Id, Foto_Moto_Id, Foto_Ruta FROM Fotos_Motos WHERE Foto_Moto_Id IN ({idsString})")).ToList(); } int migrados = 0; int omitidos = 0; foreach (var ad in adsLegacy) { // A. Validar Usuario string emailKey = ad.EmailVendedor?.ToLower().Trim() ?? ""; if (!usersV2.TryGetValue(emailKey, out var userV2)) { omitidos++; continue; } // B. Resolver Marca y Modelo var brandV2 = brandsV2.FirstOrDefault(b => b.Name.ToLower() == (ad.Marca_Detalle ?? "").ToLower()); if (brandV2 == null) continue; // Marca no mapeada, saltar int modelId = await GetOrCreateModelAsync(dbV2, (int)brandV2.BrandID, ad.Modelo_Detalle); // C. Datos int.TryParse(ad.Año_Detalle, out int year); string currency = (ad.Mone_Detalle != null && ad.Mone_Detalle.Contains("US")) ? "USD" : "ARS"; // Nombre Versión: Concatenamos Modelo + Cilindrada si existe string versionName = ad.Modelo_Detalle; // La cilindrada la ponemos en la descripción como pediste, pero a veces queda bien en el titulo. // Lo dejamos en el título solo si es muy corto, si no, description. // Descripción: Concatenar Cilindrada string desc = ad.Descripcion ?? ""; if (!string.IsNullOrEmpty(ad.Moto_Cilindrada) && ad.Moto_Cilindrada != "0") { desc += $"\n\nCilindrada: {ad.Moto_Cilindrada}cc"; } // Segmento: Lógica de Cuatriciclo string segment = ad.Moto_Tipo_Cuatri_Id > 0 ? "Cuatriciclo" : "No Especificado"; // Contacto string contactPhone = !string.IsNullOrWhiteSpace(ad.Part_Celular) ? ad.Part_Celular : ad.Part_Telefono; if (string.IsNullOrWhiteSpace(contactPhone)) contactPhone = userV2.PhoneNumber; // D. Insertar var insertSql = @" INSERT INTO Ads ( UserID, VehicleTypeID, BrandID, ModelID, VersionName, Year, KM, Price, Currency, Description, IsFeatured, StatusID, CreatedAt, PublishedAt, ExpiresAt, FuelType, Color, Segment, Location, Condition, Transmission, Steering, ContactPhone, ContactEmail, DisplayContactInfo, LegacyAdID, ViewsCounter ) VALUES ( @UserID, 2, @BrandID, @ModelID, @VersionName, @Year, @KM, @Price, @Currency, @Description, 0, 4, @CreatedAt, @PublishedAt, DATEADD(day, 30, @PublishedAt), 'Nafta', 'No Especificado', @Segment, 'La Plata', 'No Especificado', 'Manual', 'No Especificado', @ContactPhone, @ContactEmail, 1, @LegacyAdID, @ViewsCounter ); SELECT CAST(SCOPE_IDENTITY() as int);"; int newAdId = await dbV2.ExecuteScalarAsync(insertSql, new { UserID = userV2.UserID, BrandID = brandV2.BrandID, ModelID = modelId, VersionName = versionName, Year = year, KM = ad.Moto_Kilometraje, Price = ad.Moto_Precio, Currency = currency, Description = desc, Segment = segment, ContactPhone = contactPhone, ContactEmail = userV2.Email, LegacyAdID = ad.Moto_Id, ViewsCounter = ad.Moto_Cant_Visitas, CreatedAt = ad.Moto_Fecha_Publicacion, PublishedAt = ad.Moto_Fecha_Publicacion }); // E. Fotos var misFotos = photosLegacy .Where(p => p.Foto_Moto_Id == ad.Moto_Id && !p.Foto_Ruta.Contains("nofoto")) .ToList(); int order = 0; foreach (var foto in misFotos) { string fileName = Path.GetFileName(foto.Foto_Ruta); string v2Path = $"/uploads/legacy/{fileName}"; await dbV2.ExecuteAsync(@" INSERT INTO AdPhotos (AdID, FilePath, IsCover, SortOrder) VALUES (@AdID, @FilePath, @IsCover, @SortOrder)", new { AdID = newAdId, FilePath = v2Path, IsCover = (order == 0), SortOrder = order }); order++; } migrados++; } Console.WriteLine("=================================================="); Console.WriteLine($"🏁 MIGRACIÓN MOTOS FINALIZADA: {migrados} insertados."); Console.WriteLine("=================================================="); } private async Task GetOrCreateModelAsync(SqlConnection db, int brandId, string modelName) { if (string.IsNullOrWhiteSpace(modelName)) modelName = "Modelo Genérico"; var existing = await db.QueryFirstOrDefaultAsync( "SELECT ModelID FROM Models WHERE BrandID = @brandId AND Name = @modelName", new { brandId, modelName }); if (existing.HasValue) return existing.Value; var newId = await db.ExecuteScalarAsync( "INSERT INTO Models (BrandID, Name) VALUES (@brandId, @modelName); SELECT CAST(SCOPE_IDENTITY() as int);", new { brandId, modelName }); return newId; } }