Files
SIG-CM/src/SIGCM.Infrastructure/Repositories/ListingRepository.cs
2026-01-05 10:30:04 -03:00

640 lines
26 KiB
C#

// src/SIGCM.Infrastructure/Repositories/ListingRepository.cs
using Dapper;
using SIGCM.Application.DTOs;
using SIGCM.Domain.Entities;
using SIGCM.Domain.Interfaces;
using SIGCM.Domain.Models;
using SIGCM.Infrastructure.Data;
namespace SIGCM.Infrastructure.Repositories;
public class ListingRepository : IListingRepository
{
private readonly IDbConnectionFactory _connectionFactory;
public ListingRepository(IDbConnectionFactory connectionFactory)
{
_connectionFactory = connectionFactory;
}
public async Task<int> CreateAsync(Listing listing, Dictionary<int, string> attributes, List<Payment>? payments = null)
{
using var conn = _connectionFactory.CreateConnection();
conn.Open();
using var transaction = conn.BeginTransaction();
try
{
var sqlListing = @"
INSERT INTO Listings (
CategoryId, OperationId, Title, Description, Price, Currency,
CreatedAt, Status, UserId, PrintText, PrintStartDate, PrintDaysCount,
IsBold, IsFrame, PrintFontSize, PrintAlignment, AdFee, ClientId
)
VALUES (
@CategoryId, @OperationId, @Title, @Description, @Price, @Currency,
@CreatedAt, @Status, @UserId, @PrintText, @PrintStartDate, @PrintDaysCount,
@IsBold, @IsFrame, @PrintFontSize, @PrintAlignment, @AdFee, @ClientId
);
SELECT CAST(SCOPE_IDENTITY() as int);";
var listingId = await conn.QuerySingleAsync<int>(sqlListing, listing, transaction);
if (attributes != null && attributes.Any())
{
var sqlAttr = @"
INSERT INTO ListingAttributeValues (ListingId, AttributeDefinitionId, Value)
VALUES (@ListingId, @AttributeDefinitionId, @Value)";
foreach (var attr in attributes)
{
await conn.ExecuteAsync(sqlAttr, new { ListingId = listingId, AttributeDefinitionId = attr.Key, Value = attr.Value }, transaction);
}
}
if (listing.ImagesToClone != null && listing.ImagesToClone.Any())
{
foreach (var url in listing.ImagesToClone)
{
// Validamos que la URL sea válida antes de insertar
if (string.IsNullOrEmpty(url)) continue;
var sqlImg = @"INSERT INTO ListingImages (ListingId, Url, IsMainInfo, DisplayOrder)
VALUES (@listingId, @url, 0, 0)";
// Usamos el ID del nuevo aviso (listingId) y la URL del viejo
await conn.ExecuteAsync(sqlImg, new { listingId, url }, transaction);
}
}
if (payments != null && payments.Any())
{
var sqlPayment = @"
INSERT INTO Payments (ListingId, Amount, PaymentMethod, CardPlan, Surcharge, PaymentDate)
VALUES (@ListingId, @Amount, @PaymentMethod, @CardPlan, @Surcharge, @PaymentDate)";
foreach (var pay in payments)
{
pay.ListingId = listingId;
await conn.ExecuteAsync(sqlPayment, pay, transaction);
}
}
transaction.Commit();
return listingId;
}
catch
{
transaction.Rollback();
throw;
}
}
public async Task<Listing?> GetByIdAsync(int id)
{
using var conn = _connectionFactory.CreateConnection();
return await conn.QuerySingleOrDefaultAsync<Listing>("SELECT * FROM Listings WHERE Id = @Id", new { Id = id });
}
public async Task<ListingDetail?> GetDetailByIdAsync(int id)
{
using var conn = _connectionFactory.CreateConnection();
var sql = @"
SELECT
l.*, c.Name as CategoryName, cl.Name as ClientName, cl.DniOrCuit 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 Users u ON l.UserId = u.Id
WHERE l.Id = @Id;
SELECT lav.*, ad.Name as AttributeName
FROM ListingAttributeValues lav
JOIN AttributeDefinitions ad ON lav.AttributeDefinitionId = ad.Id
WHERE lav.ListingId = @Id;
SELECT * FROM ListingImages WHERE ListingId = @Id ORDER BY DisplayOrder;
SELECT * FROM Payments WHERE ListingId = @Id;
";
using var multi = await conn.QueryMultipleAsync(sql, new { Id = id });
var listingData = await multi.ReadSingleOrDefaultAsync<dynamic>();
if (listingData == null) return null;
var attributes = await multi.ReadAsync<ListingAttributeValueWithName>();
var images = await multi.ReadAsync<ListingImage>();
var payments = await multi.ReadAsync<Payment>();
return new ListingDetail
{
Listing = new Listing
{
Id = (int)listingData.Id,
CategoryId = (int)(listingData.CategoryId ?? 0),
OperationId = (int)(listingData.OperationId ?? 0),
Title = listingData.Title ?? "",
Description = listingData.Description ?? "",
Price = listingData.Price ?? 0m,
AdFee = listingData.AdFee ?? 0m,
Status = listingData.Status ?? "Draft",
CreatedAt = listingData.CreatedAt,
UserId = listingData.UserId,
CategoryName = listingData.CategoryName,
PrintDaysCount = (int)(listingData.PrintDaysCount ?? 0),
PrintText = listingData.PrintText,
IsBold = listingData.IsBold != null && Convert.ToBoolean(listingData.IsBold),
IsFrame = listingData.IsFrame != null && Convert.ToBoolean(listingData.IsFrame),
},
Attributes = attributes,
Images = images,
Payments = payments
};
}
public async Task<IEnumerable<Listing>> GetAllAsync()
{
using var conn = _connectionFactory.CreateConnection();
var sql = @"
SELECT l.*, c.Name as CategoryName,
(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
WHERE l.Status = 'Published'
ORDER BY l.CreatedAt DESC";
return await conn.QueryAsync<Listing>(sql);
}
// Implementación de incremento
public async Task IncrementViewCountAsync(int id)
{
using var conn = _connectionFactory.CreateConnection();
await conn.ExecuteAsync("UPDATE Listings SET ViewCount = ViewCount + 1 WHERE Id = @Id", new { Id = id });
}
public async Task<IEnumerable<Listing>> GetByUserIdAsync(int userId)
{
using var conn = _connectionFactory.CreateConnection();
var sql = @"
SELECT l.*, c.Name as CategoryName,
(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
WHERE l.UserId = @UserId
ORDER BY l.CreatedAt DESC";
return await conn.QueryAsync<Listing>(sql, new { UserId = userId });
}
public async Task<IEnumerable<Listing>> SearchAsync(string? query, int? categoryId)
{
return await SearchFacetedAsync(query, categoryId, null);
}
// Búsqueda Avanzada Facetada
public async Task<IEnumerable<Listing>> SearchFacetedAsync(
string? query,
int? categoryId,
Dictionary<string, string>? attributes,
DateTime? from = null,
DateTime? to = null,
string? origin = null,
string? status = null)
{
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 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
WHERE 1=1";
// --- FILTROS EXISTENTES ---
if (!string.IsNullOrEmpty(query))
{
sql += " AND (l.Title LIKE @Query OR l.Description LIKE @Query OR cl.DniOrCuit = @ExactQuery)";
parameters.Add("Query", $"%{query}%");
parameters.Add("ExactQuery", query);
}
if (categoryId.HasValue && categoryId.Value > 0)
{
sql += " AND l.CategoryId = @CategoryId";
parameters.Add("CategoryId", categoryId);
}
if (from.HasValue)
{
sql += " AND l.CreatedAt >= @From";
parameters.Add("From", from.Value.Date);
}
if (to.HasValue)
{
sql += " AND l.CreatedAt <= @To";
parameters.Add("To", to.Value.Date.AddDays(1).AddSeconds(-1));
}
if (!string.IsNullOrEmpty(origin) && origin != "All")
{
sql += " AND l.Origin = @Origin";
parameters.Add("Origin", origin);
}
// --- FILTRO DE ESTADO ---
if (!string.IsNullOrEmpty(status))
{
sql += " AND l.Status = @Status";
parameters.Add("Status", status);
}
sql += " ORDER BY l.CreatedAt DESC";
return await conn.QueryAsync<Listing>(sql, parameters);
}
public async Task<IEnumerable<Listing>> GetListingsForPrintAsync(DateTime targetDate)
{
using var conn = _connectionFactory.CreateConnection();
var sql = @"
SELECT l.*, c.Name as CategoryName
FROM Listings l
JOIN Categories c ON l.CategoryId = c.Id
WHERE l.PrintStartDate IS NOT NULL
AND @TargetDate >= CAST(l.PrintStartDate AS DATE)
AND @TargetDate < DATEADD(day, l.PrintDaysCount, CAST(l.PrintStartDate AS DATE))
ORDER BY c.Name, l.Title";
return await conn.QueryAsync<Listing>(sql, new { TargetDate = targetDate.Date });
}
public async Task<IEnumerable<Listing>> GetPendingModerationAsync()
{
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");
}
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 });
}
public async Task<int> CountByCategoryIdAsync(int categoryId)
{
using var conn = _connectionFactory.CreateConnection();
return await conn.ExecuteScalarAsync<int>("SELECT COUNT(1) FROM Listings WHERE CategoryId = @Id", new { Id = categoryId });
}
public async Task MoveListingsAsync(int sourceCategoryId, int targetCategoryId)
{
using var conn = _connectionFactory.CreateConnection();
// Solo movemos los avisos, no tocamos la estructura de categorías
await conn.ExecuteAsync(
"UPDATE Listings SET CategoryId = @TargetId WHERE CategoryId = @SourceId",
new { SourceId = sourceCategoryId, TargetId = targetCategoryId });
}
public async Task<IEnumerable<CategorySalesReportDto>> GetSalesByRootCategoryAsync(DateTime startDate, DateTime endDate)
{
using var conn = _connectionFactory.CreateConnection();
// SQL con CTE Recursiva:
// 1. Mapeamos TODA la jerarquía para saber cuál es el ID raíz de cada subcategoría.
// 2. Unimos con Listings y sumamos.
var sql = @"
WITH CategoryHierarchy AS (
-- Caso base: Categorías Raíz (donde ParentId es NULL)
SELECT Id, Id as RootId, Name
FROM Categories
WHERE ParentId IS NULL
UNION ALL
-- Caso recursivo: Hijos que se vinculan a su padre
SELECT c.Id, ch.RootId, ch.Name
FROM Categories c
INNER JOIN CategoryHierarchy ch ON c.ParentId = ch.Id
)
SELECT
ch.RootId as CategoryId,
ch.Name as CategoryName,
SUM(l.Price) as TotalSales,
COUNT(l.Id) as AdCount
FROM Listings l
INNER JOIN CategoryHierarchy ch ON l.CategoryId = ch.Id
WHERE l.CreatedAt >= @StartDate AND l.CreatedAt <= @EndDate
AND l.Status = 'Published'
GROUP BY ch.RootId, ch.Name
ORDER BY TotalSales DESC";
return await conn.QueryAsync<CategorySalesReportDto>(sql, new { StartDate = startDate, EndDate = endDate });
}
public async Task<int> GetPendingCountAsync()
{
using var conn = _connectionFactory.CreateConnection();
// Contamos solo avisos en estado 'Pending'
return await conn.ExecuteScalarAsync<int>("SELECT COUNT(1) FROM Listings WHERE Status = 'Pending'");
}
public async Task<DashboardStats> GetDashboardStatsAsync(DateTime startDate, DateTime endDate)
{
using var conn = _connectionFactory.CreateConnection();
var stats = new DashboardStats();
// 1. KPIs del periodo seleccionado
var kpisSql = @"
SELECT
CAST(ISNULL(SUM(AdFee), 0) AS DECIMAL(18,2)) as RevenueToday,
COUNT(Id) as AdsToday
FROM Listings
WHERE CAST(CreatedAt AS DATE) >= @StartDate
AND CAST(CreatedAt AS DATE) <= @EndDate
AND Status = 'Published'";
var kpis = await conn.QueryFirstOrDefaultAsync(kpisSql, new { StartDate = startDate.Date, EndDate = endDate.Date });
stats.RevenueToday = kpis?.RevenueToday ?? 0;
stats.AdsToday = kpis?.AdsToday ?? 0;
stats.TicketAverage = stats.AdsToday > 0 ? stats.RevenueToday / stats.AdsToday : 0;
// 2. Occupation (based on the last day of the range)
stats.PaperOccupation = Math.Min(100, (stats.AdsToday * 100.0) / 100.0);
// 3. Tendencia del periodo
// Si el rango es mayor a 10 días, agrupamos diferente, pero por ahora mantenemos ddd
var trendSql = @"
SELECT
FORMAT(CreatedAt, 'dd/MM') as Day,
SUM(AdFee) as Amount
FROM Listings
WHERE CAST(CreatedAt AS DATE) >= @StartDate
AND CAST(CreatedAt AS DATE) <= @EndDate
AND Status = 'Published'
GROUP BY FORMAT(CreatedAt, 'dd/MM'), CAST(CreatedAt AS DATE)
ORDER BY CAST(CreatedAt AS DATE) ASC";
var trendResult = await conn.QueryAsync<DailyRevenue>(trendSql, new { StartDate = startDate.Date, EndDate = endDate.Date });
stats.WeeklyTrend = trendResult.ToList();
// 4. Mix de Canales del periodo
var channelsSql = @"
SELECT
CASE WHEN UserId IS NULL THEN 'Web' ELSE 'Mostrador' END as Name,
COUNT(Id) as Value
FROM Listings
WHERE CAST(CreatedAt AS DATE) >= @StartDate
AND CAST(CreatedAt AS DATE) <= @EndDate
AND Status = 'Published'
GROUP BY CASE WHEN UserId IS NULL THEN 'Web' ELSE 'Mostrador' END";
var channelResult = await conn.QueryAsync<ChannelStat>(channelsSql, new { StartDate = startDate.Date, EndDate = endDate.Date });
stats.ChannelMix = channelResult.ToList();
return stats;
}
public async Task<AdvancedAnalyticsDto> GetAdvancedAnalyticsAsync(DateTime startDate, DateTime endDate)
{
using var conn = _connectionFactory.CreateConnection();
var analytics = new AdvancedAnalyticsDto();
// 1. Calcular Periodo Anterior para comparación
var duration = endDate - startDate;
var prevStart = startDate.Add(-duration);
var prevEnd = startDate.AddSeconds(-1);
// 2. KPIs del Periodo Actual
var currentKpiSql = @"
SELECT
CAST(ISNULL(SUM(AdFee), 0) AS DECIMAL(18,2)) as Revenue,
COUNT(Id) as Ads
FROM Listings
WHERE CAST(CreatedAt AS DATE) BETWEEN @Start AND @End
AND Status = 'Published'";
var currentKpis = await conn.QueryFirstOrDefaultAsync(currentKpiSql, new { Start = startDate.Date, End = endDate.Date });
analytics.TotalRevenue = currentKpis?.Revenue ?? 0;
analytics.TotalAds = currentKpis?.Ads ?? 0;
// 3. KPIs del Periodo Anterior
var prevKpiSql = @"
SELECT
CAST(ISNULL(SUM(AdFee), 0) AS DECIMAL(18,2)) as Revenue,
COUNT(Id) as Ads
FROM Listings
WHERE CAST(CreatedAt AS DATE) BETWEEN @PrevStart AND @PrevEnd
AND Status = 'Published'";
var prevKpis = await conn.QueryFirstOrDefaultAsync(prevKpiSql, new { PrevStart = prevStart.Date, PrevEnd = prevEnd.Date });
analytics.PreviousPeriodRevenue = prevKpis?.Revenue ?? 0;
analytics.PreviousPeriodAds = prevKpis?.Ads ?? 0;
// 4. Calcular Crecimiento
if (analytics.PreviousPeriodRevenue > 0)
analytics.RevenueGrowth = (double)((analytics.TotalRevenue - analytics.PreviousPeriodRevenue) / analytics.PreviousPeriodRevenue) * 100;
if (analytics.PreviousPeriodAds > 0)
analytics.AdsGrowth = (double)(analytics.TotalAds - analytics.PreviousPeriodAds) / analytics.PreviousPeriodAds * 100;
// 5. Distribución de Pagos (Real)
var paymentsSql = @"
SELECT
p.PaymentMethod as Method,
SUM(p.Amount + p.Surcharge) as Total,
COUNT(p.Id) as Count
FROM Payments p
INNER JOIN Listings l ON p.ListingId = l.Id
WHERE CAST(l.CreatedAt AS DATE) BETWEEN @Start AND @End
AND l.Status = 'Published'
GROUP BY p.PaymentMethod";
analytics.PaymentsDistribution = (await conn.QueryAsync<PaymentMethodStat>(paymentsSql, new { Start = startDate.Date, End = endDate.Date })).ToList();
// 6. Rendimiento por Categoría
var categorySql = @"
SELECT
c.Name as CategoryName,
SUM(l.AdFee) as Revenue,
COUNT(l.Id) as AdsCount
FROM Listings l
JOIN Categories c ON l.CategoryId = c.Id
WHERE CAST(l.CreatedAt AS DATE) BETWEEN @Start AND @End
AND l.Status = 'Published'
GROUP BY c.Name
ORDER BY Revenue DESC";
var catPerf = (await conn.QueryAsync<CategoryPerformanceStat>(categorySql, new { Start = startDate.Date, End = endDate.Date })).ToList();
foreach (var cp in catPerf)
{
cp.Share = analytics.TotalRevenue > 0 ? (double)(cp.Revenue / analytics.TotalRevenue) * 100 : 0;
}
analytics.CategoryPerformance = catPerf;
// 7. Análisis Horario (Peak Hours)
var hourlySql = @"
SELECT
DATEPART(HOUR, CreatedAt) as Hour,
COUNT(Id) as Count
FROM Listings
WHERE CAST(CreatedAt AS DATE) BETWEEN @Start AND @End
AND Status = 'Published'
GROUP BY DATEPART(HOUR, CreatedAt)
ORDER BY Hour";
analytics.HourlyActivity = (await conn.QueryAsync<HourlyStat>(hourlySql, new { Start = startDate.Date, End = endDate.Date })).ToList();
// 8. Tendencia Diaria
var dailySql = @"
SELECT
FORMAT(CreatedAt, 'dd/MM') as Day,
SUM(AdFee) as Amount
FROM Listings
WHERE CAST(CreatedAt AS DATE) BETWEEN @Start AND @End
AND Status = 'Published'
GROUP BY FORMAT(CreatedAt, 'dd/MM'), CAST(CreatedAt AS DATE)
ORDER BY CAST(CreatedAt AS DATE) ASC";
analytics.DailyTrends = (await conn.QueryAsync<DailyRevenue>(dailySql, new { Start = startDate.Date, End = endDate.Date })).ToList();
var sourceSql = @"
SELECT
Origin,
COUNT(Id) as Count
FROM Listings
WHERE CAST(CreatedAt AS DATE) BETWEEN @Start AND @End
AND Status = 'Published'
GROUP BY Origin";
var sources = await conn.QueryAsync<dynamic>(sourceSql, new { Start = startDate.Date, End = endDate.Date });
int total = 0;
int web = 0;
int mostrador = 0;
foreach (var s in sources)
{
if (s.Origin == "Web") web = (int)s.Count;
if (s.Origin == "Mostrador") mostrador = (int)s.Count;
total += (int)s.Count;
}
analytics.SourceMix = new SourceMixDto
{
MostradorCount = mostrador,
WebCount = web,
MostradorPercent = total > 0 ? (mostrador * 100.0 / total) : 0,
WebPercent = total > 0 ? (web * 100.0 / total) : 0
};
return analytics;
}
public async Task<CashierDashboardDto?> GetCashierStatsAsync(int userId, DateTime startDate, DateTime endDate)
{
using var conn = _connectionFactory.CreateConnection();
// Filtramos tanto la recaudación como los pendientes por el rango seleccionado
var sql = @"
SELECT
(SELECT ISNULL(SUM(Amount + Surcharge), 0) FROM Payments p INNER JOIN Listings l ON p.ListingId = l.Id
WHERE l.UserId = @UserId AND CAST(l.CreatedAt AS DATE) BETWEEN @Start AND @End AND l.Status = 'Published') as MyRevenue,
(SELECT COUNT(1) FROM Listings WHERE UserId = @UserId AND Status = 'Pending') as MyPendingAds,
(SELECT COUNT(1) FROM Listings WHERE UserId = @UserId AND CAST(CreatedAt AS DATE) BETWEEN @Start AND @End AND Status = 'Published') as MyAdsCount";
return await conn.QueryFirstOrDefaultAsync<CashierDashboardDto>(sql, new { UserId = userId, Start = startDate.Date, End = endDate.Date });
}
public async Task<GlobalReportDto> GetDetailedReportAsync(DateTime start, DateTime end, int? userId = null)
{
using var conn = _connectionFactory.CreateConnection();
var report = new GlobalReportDto { FromDate = start, ToDate = end };
var sql = @"
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
FROM Listings l
JOIN Categories c ON l.CategoryId = c.Id
LEFT JOIN Users u ON l.UserId = u.Id
WHERE CAST(l.CreatedAt AS DATE) BETWEEN @Start AND @End
AND l.Status = 'Published'
AND (@UserId IS NULL OR l.UserId = @UserId)
ORDER BY l.CreatedAt ASC";
var items = await conn.QueryAsync<ReportItemDto>(sql, new { Start = start.Date, End = end.Date, UserId = userId });
report.Items = items.ToList();
// TOTALES FÍSICOS (Solo lo que entró por caja)
var totalsSql = @"
SELECT
p.PaymentMethod,
SUM(p.Amount + p.Surcharge) as Total
FROM Payments p
INNER JOIN Listings l ON p.ListingId = l.Id
WHERE CAST(l.CreatedAt AS DATE) BETWEEN @Start AND @End
AND l.Status = 'Published'
AND l.Origin = 'Mostrador'
AND (@UserId IS NULL OR l.UserId = @UserId)
GROUP BY p.PaymentMethod";
var paymentTotals = await conn.QueryAsync<dynamic>(totalsSql, new { Start = start.Date, End = end.Date, UserId = userId });
// Reiniciar totales para el reporte
report.TotalCash = 0; report.TotalDebit = 0; report.TotalCredit = 0; report.TotalTransfer = 0;
foreach (var p in paymentTotals)
{
string method = p.PaymentMethod;
decimal total = (decimal)p.Total;
switch (method)
{
case "Cash": report.TotalCash = total; break;
case "Debit": report.TotalDebit = total; break;
case "Credit": report.TotalCredit = total; break;
case "Transfer": report.TotalTransfer = total; break;
}
}
report.TotalRevenue = report.TotalCash + report.TotalDebit + report.TotalCredit + report.TotalTransfer;
return report;
}
public async Task AddPaymentAsync(Payment payment)
{
using var conn = _connectionFactory.CreateConnection();
var sql = @"
INSERT INTO Payments (ListingId, Amount, PaymentMethod, CardPlan, Surcharge, PaymentDate, ExternalReference, ExternalId, Status)
VALUES (@ListingId, @Amount, @PaymentMethod, @CardPlan, @Surcharge, @PaymentDate, @ExternalReference, @ExternalId, @Status)";
await conn.ExecuteAsync(sql, payment);
}
public async Task<IEnumerable<dynamic>> GetActiveCashiersAsync()
{
using var conn = _connectionFactory.CreateConnection();
// Traemos usuarios que tengan el rol adecuado para filtrar en el historial
var sql = @"SELECT Id, Username FROM Users
WHERE Role IN ('Cajero', 'Admin')
AND IsActive = 1
ORDER BY Username ASC";
return await conn.QueryAsync(sql);
}
public async Task UpdateOverlayStatusAsync(int id, int userId, string? status)
{
using var conn = _connectionFactory.CreateConnection();
// Seguridad: Filtramos por Id del aviso Y Id del usuario dueño
var sql = "UPDATE Listings SET OverlayStatus = @status WHERE Id = @id AND UserId = @userId";
await conn.ExecuteAsync(sql, new { id, userId, status });
}
}