Feat Varios 3

This commit is contained in:
2026-01-06 10:34:06 -03:00
parent 0fa77e4a98
commit 9fa21ebec3
65 changed files with 2897 additions and 373 deletions

View File

@@ -37,4 +37,19 @@ public class AuditRepository
ORDER BY a.CreatedAt DESC";
return await conn.QueryAsync<AuditLog>(sql, new { UserId = userId, Limit = limit });
}
public async Task<IEnumerable<AuditLog>> GetFilteredLogsAsync(DateTime from, DateTime to, int? userId = null)
{
using var conn = _db.CreateConnection();
var sql = @"SELECT a.*, u.Username
FROM AuditLogs a
JOIN Users u ON a.UserId = u.Id
WHERE a.CreatedAt >= @From AND a.CreatedAt <= @To";
if (userId.HasValue) sql += " AND a.UserId = @UserId";
sql += " ORDER BY a.CreatedAt DESC";
return await conn.QueryAsync<AuditLog>(sql, new { From = from, To = to, UserId = userId });
}
}

View File

@@ -13,59 +13,63 @@ public class ClientRepository
_db = db;
}
// Búsqueda inteligente con protección de nulos
// Búsqueda inteligente redireccionada a Users
public async Task<IEnumerable<Client>> SearchAsync(string query)
{
using var conn = _db.CreateConnection();
var sql = @"
SELECT TOP 10
Id,
ISNULL(Name, 'Sin Nombre') as Name,
ISNULL(DniOrCuit, '') as DniOrCuit,
Email, Phone, Address
FROM Clients
WHERE Name LIKE @Query OR DniOrCuit LIKE @Query
ORDER BY Name";
ISNULL(BillingName, Username) as Name,
ISNULL(BillingTaxId, '') as DniOrCuit,
Email, Phone, BillingAddress as Address
FROM Users
WHERE BillingName LIKE @Query OR BillingTaxId LIKE @Query OR Username LIKE @Query
ORDER BY BillingName";
return await conn.QueryAsync<Client>(sql, new { Query = $"%{query}%" });
}
// Asegurar existencia (Upsert)
// Asegurar existencia (Upsert en la tabla Users)
public async Task<int> EnsureClientExistsAsync(string name, string dni)
{
using var conn = _db.CreateConnection();
var existingId = await conn.ExecuteScalarAsync<int?>(
"SELECT Id FROM Clients WHERE DniOrCuit = @Dni", new { Dni = dni });
"SELECT Id FROM Users WHERE BillingTaxId = @Dni", new { Dni = dni });
if (existingId.HasValue)
{
await conn.ExecuteAsync("UPDATE Clients SET Name = @Name WHERE Id = @Id", new { Name = name, Id = existingId });
await conn.ExecuteAsync("UPDATE Users SET BillingName = @Name WHERE Id = @Id", new { Name = name, Id = existingId });
return existingId.Value;
}
else
{
// Si no existe, creamos un usuario con rol Cliente (sin password por ahora, es solo para gestión de mostrador)
var sql = @"
INSERT INTO Clients (Name, DniOrCuit) VALUES (@Name, @Dni);
INSERT INTO Users (Username, Role, BillingName, BillingTaxId, PasswordHash, MustChangePassword)
VALUES (@Username, 'Client', @Name, @Dni, 'N/A', 0);
SELECT CAST(SCOPE_IDENTITY() as int);";
return await conn.QuerySingleAsync<int>(sql, new { Name = name, Dni = dni });
// El username será el DNI para asegurar unicidad si no hay otro dato
return await conn.QuerySingleAsync<int>(sql, new { Username = dni, Name = name, Dni = dni });
}
}
// Obtener todos con estadísticas (ISNULL agregado para seguridad)
// Obtener todos con estadísticas desde Users
public async Task<IEnumerable<dynamic>> GetAllWithStatsAsync()
{
using var conn = _db.CreateConnection();
var sql = @"
SELECT
c.Id as id,
ISNULL(c.Name, 'Sin Nombre') as name,
ISNULL(c.DniOrCuit, 'S/D') as dniOrCuit,
ISNULL(c.Email, 'Sin correo') as email,
ISNULL(c.Phone, 'Sin teléfono') as phone,
(SELECT COUNT(1) FROM Listings l WHERE l.ClientId = c.Id) as totalAds,
ISNULL((SELECT SUM(AdFee) FROM Listings l WHERE l.ClientId = c.Id), 0) as totalSpent
FROM Clients c
ORDER BY c.Name";
u.Id as id,
ISNULL(u.BillingName, u.Username) as name,
ISNULL(u.BillingTaxId, 'S/D') as dniOrCuit,
ISNULL(u.Email, 'Sin correo') as email,
ISNULL(u.Phone, 'Sin teléfono') as phone,
(SELECT COUNT(1) FROM Listings l WHERE l.ClientId = u.Id) as totalAds,
ISNULL((SELECT SUM(AdFee) FROM Listings l WHERE l.ClientId = u.Id), 0) as totalSpent
FROM Users u
WHERE Role IN ('Client', 'User') -- Mostramos tanto clientes puros como usuarios web
ORDER BY name";
return await conn.QueryAsync(sql);
}
@@ -73,12 +77,12 @@ public class ClientRepository
{
using var conn = _db.CreateConnection();
var sql = @"
UPDATE Clients
SET Name = @Name,
DniOrCuit = @DniOrCuit,
UPDATE Users
SET BillingName = @Name,
BillingTaxId = @DniOrCuit,
Email = @Email,
Phone = @Phone,
Address = @Address
BillingAddress = @Address
WHERE Id = @Id";
await conn.ExecuteAsync(sql, client);
}
@@ -88,21 +92,23 @@ public class ClientRepository
using var conn = _db.CreateConnection();
var sql = @"
SELECT
c.Id, c.Name, c.DniOrCuit, c.Email, c.Phone, c.Address,
(SELECT COUNT(1) FROM Listings WHERE ClientId = c.Id) as TotalAds,
ISNULL((SELECT SUM(AdFee) FROM Listings WHERE ClientId = c.Id), 0) as TotalInvested,
(SELECT MAX(CreatedAt) FROM Listings WHERE ClientId = c.Id) as LastAdDate,
(SELECT COUNT(1) FROM Listings WHERE ClientId = c.Id AND Status = 'Published') as ActiveAds,
u.Id,
ISNULL(u.BillingName, u.Username) as Name,
u.BillingTaxId as DniOrCuit, u.Email, u.Phone, u.BillingAddress as Address,
(SELECT COUNT(1) FROM Listings WHERE ClientId = u.Id) as TotalAds,
ISNULL((SELECT SUM(AdFee) FROM Listings WHERE ClientId = u.Id), 0) as TotalInvested,
(SELECT MAX(CreatedAt) FROM Listings WHERE ClientId = u.Id) as LastAdDate,
(SELECT COUNT(1) FROM Listings WHERE ClientId = u.Id AND Status = 'Published') as ActiveAds,
ISNULL((
SELECT TOP 1 cat.Name
FROM Listings l
JOIN Categories cat ON l.CategoryId = cat.Id
WHERE l.ClientId = c.Id
WHERE l.ClientId = u.Id
GROUP BY cat.Name
ORDER BY COUNT(l.Id) DESC
), 'N/A') as PreferredCategory
FROM Clients c
WHERE c.Id = @Id";
FROM Users u
WHERE u.Id = @Id";
return await conn.QuerySingleOrDefaultAsync<dynamic>(sql, new { Id = clientId });
}

View File

@@ -0,0 +1,63 @@
using Dapper;
using SIGCM.Domain.Entities;
using SIGCM.Domain.Interfaces;
using SIGCM.Infrastructure.Data;
namespace SIGCM.Infrastructure.Repositories;
public class CouponRepository : ICouponRepository
{
private readonly IDbConnectionFactory _connectionFactory;
public CouponRepository(IDbConnectionFactory connectionFactory)
{
_connectionFactory = connectionFactory;
}
public async Task<IEnumerable<Coupon>> GetAllAsync()
{
using var conn = _connectionFactory.CreateConnection();
return await conn.QueryAsync<Coupon>("SELECT * FROM Coupons ORDER BY CreatedAt DESC");
}
public async Task<int> CreateAsync(Coupon coupon)
{
using var conn = _connectionFactory.CreateConnection();
var sql = @"
INSERT INTO Coupons (Code, DiscountType, DiscountValue, ExpiryDate, MaxUsages, MaxUsagesPerUser, IsActive, CreatedAt)
VALUES (@Code, @DiscountType, @DiscountValue, @ExpiryDate, @MaxUsages, @MaxUsagesPerUser, @IsActive, GETUTCDATE());
SELECT CAST(SCOPE_IDENTITY() as int);";
return await conn.QuerySingleAsync<int>(sql, coupon);
}
public async Task DeleteAsync(int id)
{
using var conn = _connectionFactory.CreateConnection();
await conn.ExecuteAsync("DELETE FROM Coupons WHERE Id = @Id", new { Id = id });
}
public async Task<Coupon?> GetByCodeAsync(string code)
{
using var conn = _connectionFactory.CreateConnection();
return await conn.QuerySingleOrDefaultAsync<Coupon>(
"SELECT * FROM Coupons WHERE Code = @Code AND IsActive = 1",
new { Code = code });
}
public async Task IncrementUsageAsync(int id)
{
using var conn = _connectionFactory.CreateConnection();
await conn.ExecuteAsync(
"UPDATE Coupons SET UsageCount = UsageCount + 1 WHERE Id = @Id",
new { Id = id });
}
public async Task<int> CountUserUsageAsync(int userId, string couponCode)
{
using var conn = _connectionFactory.CreateConnection();
// Check listings for this user with this coupon code
// Note: CouponCode column was added to Listings
var sql = "SELECT COUNT(1) FROM Listings WHERE UserId = @UserId AND CouponCode = @CouponCode";
return await conn.ExecuteScalarAsync<int>(sql, new { UserId = userId, CouponCode = couponCode });
}
}

View File

@@ -0,0 +1,72 @@
using Dapper;
using SIGCM.Domain.Entities;
using SIGCM.Domain.Interfaces;
using SIGCM.Infrastructure.Data;
namespace SIGCM.Infrastructure.Repositories;
public class ListingNoteRepository : IListingNoteRepository
{
private readonly IDbConnectionFactory _connectionFactory;
public ListingNoteRepository(IDbConnectionFactory connectionFactory)
{
_connectionFactory = connectionFactory;
}
public async Task<int> CreateAsync(ListingNote note)
{
using var conn = _connectionFactory.CreateConnection();
var sql = @"
INSERT INTO ListingNotes (ListingId, SenderId, IsFromModerator, Message, CreatedAt, IsRead)
VALUES (@ListingId, @SenderId, @IsFromModerator, @Message, GETUTCDATE(), 0);
SELECT CAST(SCOPE_IDENTITY() as int);";
return await conn.QuerySingleAsync<int>(sql, note);
}
public async Task<IEnumerable<ListingNote>> GetByListingIdAsync(int listingId)
{
using var conn = _connectionFactory.CreateConnection();
var sql = "SELECT * FROM ListingNotes WHERE ListingId = @ListingId ORDER BY CreatedAt ASC";
return await conn.QueryAsync<ListingNote>(sql, new { ListingId = listingId });
}
public async Task<IEnumerable<ListingNote>> GetByUserIdAsync(int userId)
{
using var conn = _connectionFactory.CreateConnection();
var sql = @"
SELECT ln.*
FROM ListingNotes ln
INNER JOIN Listings l ON ln.ListingId = l.Id
WHERE l.UserId = @UserId
ORDER BY ln.CreatedAt DESC";
return await conn.QueryAsync<ListingNote>(sql, new { UserId = userId });
}
public async Task MarkAsReadAsync(int noteId)
{
using var conn = _connectionFactory.CreateConnection();
await conn.ExecuteAsync("UPDATE ListingNotes SET IsRead = 1 WHERE Id = @Id", new { Id = noteId });
}
public async Task<int> GetUnreadCountAsync(int userId, bool isForModerator)
{
using var conn = _connectionFactory.CreateConnection();
var sql = "";
if (isForModerator)
{
// Para moderadores: Mensajes que NO son de moderador y NO están leídos
sql = @"SELECT COUNT(*) FROM ListingNotes WHERE IsFromModerator = 0 AND IsRead = 0";
}
else
{
// Para usuarios: Mensajes que SON de moderador, para sus avisos, y NO están leídos
sql = @"
SELECT COUNT(*)
FROM ListingNotes ln
INNER JOIN Listings l ON ln.ListingId = l.Id
WHERE l.UserId = @UserId AND ln.IsFromModerator = 1 AND ln.IsRead = 0";
}
return await conn.ExecuteScalarAsync<int>(sql, new { UserId = userId });
}
}

View File

@@ -29,12 +29,14 @@ public class ListingRepository : IListingRepository
INSERT INTO Listings (
CategoryId, OperationId, Title, Description, Price, Currency,
CreatedAt, Status, UserId, PrintText, PrintStartDate, PrintDaysCount,
IsBold, IsFrame, PrintFontSize, PrintAlignment, AdFee, ClientId
IsBold, IsFrame, PrintFontSize, PrintAlignment, AdFee, ClientId,
PublicationStartDate, IsFeatured, FeaturedExpiry, AllowContact, CouponCode, Origin
)
VALUES (
@CategoryId, @OperationId, @Title, @Description, @Price, @Currency,
@CreatedAt, @Status, @UserId, @PrintText, @PrintStartDate, @PrintDaysCount,
@IsBold, @IsFrame, @PrintFontSize, @PrintAlignment, @AdFee, @ClientId
@IsBold, @IsFrame, @PrintFontSize, @PrintAlignment, @AdFee, @ClientId,
@PublicationStartDate, @IsFeatured, @FeaturedExpiry, @AllowContact, @CouponCode, @Origin
);
SELECT CAST(SCOPE_IDENTITY() as int);";
@@ -101,11 +103,15 @@ public class ListingRepository : IListingRepository
using var conn = _connectionFactory.CreateConnection();
var sql = @"
SELECT
l.*, c.Name as CategoryName, cl.Name as ClientName, cl.DniOrCuit as ClientDni,
l.*,
c.Name as CategoryName,
p.Name as ParentCategoryName,
cl.BillingName as ClientName, cl.BillingTaxId as ClientDni,
u.Username as CashierName
FROM Listings l
LEFT JOIN Categories c ON l.CategoryId = c.Id
LEFT JOIN Clients cl ON l.ClientId = cl.Id
LEFT JOIN Categories p ON c.ParentId = p.Id
LEFT JOIN Users cl ON l.ClientId = cl.Id
LEFT JOIN Users u ON l.UserId = u.Id
WHERE l.Id = @Id;
@@ -142,10 +148,16 @@ public class ListingRepository : IListingRepository
CreatedAt = listingData.CreatedAt,
UserId = listingData.UserId,
CategoryName = listingData.CategoryName,
ParentCategoryName = listingData.ParentCategoryName,
PrintDaysCount = (int)(listingData.PrintDaysCount ?? 0),
PrintText = listingData.PrintText,
IsBold = listingData.IsBold != null && Convert.ToBoolean(listingData.IsBold),
IsFrame = listingData.IsFrame != null && Convert.ToBoolean(listingData.IsFrame),
PublicationStartDate = listingData.PublicationStartDate,
ApprovedAt = listingData.ApprovedAt,
IsFeatured = listingData.IsFeatured != null && Convert.ToBoolean(listingData.IsFeatured),
FeaturedExpiry = listingData.FeaturedExpiry,
AllowContact = listingData.AllowContact != null && Convert.ToBoolean(listingData.AllowContact),
},
Attributes = attributes,
Images = images,
@@ -162,6 +174,8 @@ public class ListingRepository : IListingRepository
FROM Listings l
JOIN Categories c ON l.CategoryId = c.Id
WHERE l.Status = 'Published'
AND (l.PublicationStartDate IS NULL OR l.PublicationStartDate <= GETUTCDATE())
AND (l.PrintDaysCount = 0 OR DATEADD(day, l.PrintDaysCount, COALESCE(l.PublicationStartDate, l.ApprovedAt, l.CreatedAt)) >= GETUTCDATE())
ORDER BY l.CreatedAt DESC";
return await conn.QueryAsync<Listing>(sql);
@@ -190,7 +204,7 @@ public class ListingRepository : IListingRepository
public async Task<IEnumerable<Listing>> SearchAsync(string? query, int? categoryId)
{
return await SearchFacetedAsync(query, categoryId, null);
return await SearchFacetedAsync(query, categoryId, null, null, null, null, null, true);
}
// Búsqueda Avanzada Facetada
@@ -201,23 +215,24 @@ public class ListingRepository : IListingRepository
DateTime? from = null,
DateTime? to = null,
string? origin = null,
string? status = null)
string? status = null,
bool onlyActive = false)
{
using var conn = _connectionFactory.CreateConnection();
var parameters = new DynamicParameters();
string sql = @"
SELECT l.*, c.Name as CategoryName, cl.Name as ClientName, cl.DniOrCuit as ClientDni,
SELECT l.*, c.Name as CategoryName, cl.BillingName as ClientName, cl.BillingTaxId as ClientDni,
(SELECT TOP 1 Url FROM ListingImages li WHERE li.ListingId = l.Id ORDER BY IsMainInfo DESC, DisplayOrder ASC) as MainImageUrl
FROM Listings l
JOIN Categories c ON l.CategoryId = c.Id
LEFT JOIN Clients cl ON l.ClientId = cl.Id
LEFT JOIN Users cl ON l.ClientId = cl.Id
WHERE 1=1";
// --- FILTROS EXISTENTES ---
if (!string.IsNullOrEmpty(query))
{
sql += " AND (l.Title LIKE @Query OR l.Description LIKE @Query OR cl.DniOrCuit = @ExactQuery)";
sql += " AND (l.Title LIKE @Query OR l.Description LIKE @Query OR cl.BillingTaxId = @ExactQuery OR cl.BillingName LIKE @Query)";
parameters.Add("Query", $"%{query}%");
parameters.Add("ExactQuery", query);
}
@@ -253,7 +268,16 @@ public class ListingRepository : IListingRepository
parameters.Add("Status", status);
}
sql += " ORDER BY l.CreatedAt DESC";
// --- FILTRO DE VISIBILIDAD (Solo para búsquedas públicas) ---
if (onlyActive)
{
// Solo avisos publicados y dentro de su rango de vigencia
sql += @" AND l.Status = 'Published'
AND (l.PublicationStartDate IS NULL OR l.PublicationStartDate <= GETUTCDATE())
AND (l.PrintDaysCount = 0 OR DATEADD(day, l.PrintDaysCount, COALESCE(l.PublicationStartDate, l.ApprovedAt, l.CreatedAt)) >= GETUTCDATE())";
}
sql += " ORDER BY l.IsFeatured DESC, l.CreatedAt DESC";
return await conn.QueryAsync<Listing>(sql, parameters);
}
@@ -277,15 +301,28 @@ public class ListingRepository : IListingRepository
{
using var conn = _connectionFactory.CreateConnection();
// Avisos que NO están publicados ni rechazados ni borrados.
// Asumimos 'Pending' o 'Draft' si vienen del Wizard y requieren revisión.
// Para este ejemplo, buscamos 'Pending'.
return await conn.QueryAsync<Listing>("SELECT * FROM Listings WHERE Status = 'Pending' ORDER BY CreatedAt ASC");
// Asumimos 'Pending'. Incluimos conteo de notas no leídas del usuario.
var sql = @"
SELECT l.*,
(SELECT COUNT(*) FROM ListingNotes ln WHERE ln.ListingId = l.Id AND ln.IsFromModerator = 0 AND ln.IsRead = 0) as UnreadNotesCount
FROM Listings l
WHERE l.Status = 'Pending'
ORDER BY l.CreatedAt ASC";
return await conn.QueryAsync<Listing>(sql);
}
public async Task UpdateStatusAsync(int id, string status)
{
using var conn = _connectionFactory.CreateConnection();
await conn.ExecuteAsync("UPDATE Listings SET Status = @Status WHERE Id = @Id", new { Id = id, Status = status });
var sql = @"
UPDATE Listings
SET Status = @Status,
ApprovedAt = CASE
WHEN (@Status = 'Published' OR @Status = 'Approved') AND ApprovedAt IS NULL THEN GETUTCDATE()
ELSE ApprovedAt
END
WHERE Id = @Id";
await conn.ExecuteAsync(sql, new { Id = id, Status = status });
}
public async Task<int> CountByCategoryIdAsync(int categoryId)
@@ -562,10 +599,11 @@ public class ListingRepository : IListingRepository
SELECT
l.Id, l.CreatedAt as Date, l.Title,
c.Name as Category, u.Username as Cashier, l.AdFee as Amount,
l.Origin as Source
l.Origin as Source, cl.BillingName as ClientName
FROM Listings l
JOIN Categories c ON l.CategoryId = c.Id
LEFT JOIN Users u ON l.UserId = u.Id
LEFT JOIN Users cl ON l.ClientId = cl.Id
WHERE CAST(l.CreatedAt AS DATE) BETWEEN @Start AND @End
AND l.Status = 'Published'
AND (@UserId IS NULL OR l.UserId = @UserId)

View File

@@ -46,8 +46,10 @@ public class UserRepository : IUserRepository
{
using var conn = _connectionFactory.CreateConnection();
var sql = @"
INSERT INTO Users (Username, PasswordHash, Role, Email, FailedLoginAttempts, LockoutEnd, MustChangePassword, IsActive, LastLogin, GoogleId, IsMfaEnabled, MfaSecret)
VALUES (@Username, @PasswordHash, @Role, @Email, @FailedLoginAttempts, @LockoutEnd, @MustChangePassword, @IsActive, @LastLogin, @GoogleId, @IsMfaEnabled, @MfaSecret);
INSERT INTO Users (Username, PasswordHash, Role, Email, FailedLoginAttempts, LockoutEnd, MustChangePassword, IsActive, LastLogin, GoogleId, IsMfaEnabled, MfaSecret,
BillingName, BillingAddress, BillingTaxId, BillingTaxType, ClientId, Phone)
VALUES (@Username, @PasswordHash, @Role, @Email, @FailedLoginAttempts, @LockoutEnd, @MustChangePassword, @IsActive, @LastLogin, @GoogleId, @IsMfaEnabled, @MfaSecret,
@BillingName, @BillingAddress, @BillingTaxId, @BillingTaxType, @ClientId, @Phone);
SELECT CAST(SCOPE_IDENTITY() as int);";
return await conn.QuerySingleAsync<int>(sql, user);
}
@@ -75,7 +77,9 @@ public class UserRepository : IUserRepository
SET Username = @Username, Role = @Role, Email = @Email, PasswordHash = @PasswordHash,
FailedLoginAttempts = @FailedLoginAttempts, LockoutEnd = @LockoutEnd,
MustChangePassword = @MustChangePassword, IsActive = @IsActive, LastLogin = @LastLogin,
GoogleId = @GoogleId, IsMfaEnabled = @IsMfaEnabled, MfaSecret = @MfaSecret
GoogleId = @GoogleId, IsMfaEnabled = @IsMfaEnabled, MfaSecret = @MfaSecret,
BillingName = @BillingName, BillingAddress = @BillingAddress, BillingTaxId = @BillingTaxId, BillingTaxType = @BillingTaxType,
ClientId = @ClientId, Phone = @Phone
WHERE Id = @Id";
await conn.ExecuteAsync(sql, user);
}