Feat Varios 3
This commit is contained in:
@@ -116,9 +116,181 @@ BEGIN
|
||||
);
|
||||
END
|
||||
";
|
||||
// Ejecutar creación de tablas
|
||||
// Ejecutar creación de tablas base
|
||||
await connection.ExecuteAsync(schemaSql);
|
||||
|
||||
// --- MIGRACIONES (Schema Update) ---
|
||||
var migrationSql = @"
|
||||
-- Listings Columns
|
||||
IF NOT EXISTS(SELECT * FROM sys.columns WHERE Name = N'PublicationStartDate' AND Object_ID = Object_ID(N'Listings'))
|
||||
ALTER TABLE Listings ADD PublicationStartDate DATETIME2 NULL;
|
||||
IF NOT EXISTS(SELECT * FROM sys.columns WHERE Name = N'ApprovedAt' AND Object_ID = Object_ID(N'Listings'))
|
||||
ALTER TABLE Listings ADD ApprovedAt DATETIME2 NULL;
|
||||
IF NOT EXISTS(SELECT * FROM sys.columns WHERE Name = N'IsFeatured' AND Object_ID = Object_ID(N'Listings'))
|
||||
ALTER TABLE Listings ADD IsFeatured BIT NOT NULL DEFAULT 0;
|
||||
IF NOT EXISTS(SELECT * FROM sys.columns WHERE Name = N'FeaturedExpiry' AND Object_ID = Object_ID(N'Listings'))
|
||||
ALTER TABLE Listings ADD FeaturedExpiry DATETIME2 NULL;
|
||||
IF NOT EXISTS(SELECT * FROM sys.columns WHERE Name = N'AllowContact' AND Object_ID = Object_ID(N'Listings'))
|
||||
ALTER TABLE Listings ADD AllowContact BIT NOT NULL DEFAULT 1;
|
||||
|
||||
-- Users Columns (Security & Profiles)
|
||||
IF NOT EXISTS(SELECT * FROM sys.columns WHERE Name = N'BillingName' AND Object_ID = Object_ID(N'Users'))
|
||||
ALTER TABLE Users ADD BillingName NVARCHAR(200) NULL;
|
||||
IF NOT EXISTS(SELECT * FROM sys.columns WHERE Name = N'BillingAddress' AND Object_ID = Object_ID(N'Users'))
|
||||
ALTER TABLE Users ADD BillingAddress NVARCHAR(500) NULL;
|
||||
IF NOT EXISTS(SELECT * FROM sys.columns WHERE Name = N'BillingTaxId' AND Object_ID = Object_ID(N'Users'))
|
||||
ALTER TABLE Users ADD BillingTaxId NVARCHAR(50) NULL;
|
||||
IF NOT EXISTS(SELECT * FROM sys.columns WHERE Name = N'BillingTaxType' AND Object_ID = Object_ID(N'Users'))
|
||||
ALTER TABLE Users ADD BillingTaxType NVARCHAR(50) NULL;
|
||||
IF NOT EXISTS(SELECT * FROM sys.columns WHERE Name = N'ClientId' AND Object_ID = Object_ID(N'Users'))
|
||||
ALTER TABLE Users ADD ClientId INT NULL;
|
||||
|
||||
-- Audit & Security Columns for Users
|
||||
IF NOT EXISTS(SELECT * FROM sys.columns WHERE Name = N'FailedLoginAttempts' AND Object_ID = Object_ID(N'Users'))
|
||||
ALTER TABLE Users ADD FailedLoginAttempts INT NOT NULL DEFAULT 0;
|
||||
IF NOT EXISTS(SELECT * FROM sys.columns WHERE Name = N'LockoutEnd' AND Object_ID = Object_ID(N'Users'))
|
||||
ALTER TABLE Users ADD LockoutEnd DATETIME NULL;
|
||||
IF NOT EXISTS(SELECT * FROM sys.columns WHERE Name = N'MustChangePassword' AND Object_ID = Object_ID(N'Users'))
|
||||
ALTER TABLE Users ADD MustChangePassword BIT NOT NULL DEFAULT 1;
|
||||
IF NOT EXISTS(SELECT * FROM sys.columns WHERE Name = N'IsActive' AND Object_ID = Object_ID(N'Users'))
|
||||
ALTER TABLE Users ADD IsActive BIT NOT NULL DEFAULT 1;
|
||||
IF NOT EXISTS(SELECT * FROM sys.columns WHERE Name = N'LastLogin' AND Object_ID = Object_ID(N'Users'))
|
||||
ALTER TABLE Users ADD LastLogin DATETIME NULL;
|
||||
IF NOT EXISTS(SELECT * FROM sys.columns WHERE Name = N'GoogleId' AND Object_ID = Object_ID(N'Users'))
|
||||
ALTER TABLE Users ADD GoogleId NVARCHAR(255) NULL;
|
||||
IF NOT EXISTS(SELECT * FROM sys.columns WHERE Name = N'IsMfaEnabled' AND Object_ID = Object_ID(N'Users'))
|
||||
ALTER TABLE Users ADD IsMfaEnabled BIT NOT NULL DEFAULT 0;
|
||||
IF NOT EXISTS(SELECT * FROM sys.columns WHERE Name = N'MfaSecret' AND Object_ID = Object_ID(N'Users'))
|
||||
ALTER TABLE Users ADD MfaSecret NVARCHAR(255) NULL;
|
||||
IF NOT EXISTS(SELECT * FROM sys.columns WHERE Name = N'Phone' AND Object_ID = Object_ID(N'Users'))
|
||||
ALTER TABLE Users ADD Phone NVARCHAR(50) NULL;
|
||||
|
||||
-- Coupons Table
|
||||
IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'Coupons')
|
||||
BEGIN
|
||||
CREATE TABLE Coupons (
|
||||
Id INT IDENTITY(1,1) PRIMARY KEY,
|
||||
Code NVARCHAR(50) NOT NULL,
|
||||
DiscountType NVARCHAR(20) NOT NULL,
|
||||
DiscountValue DECIMAL(18,2) NOT NULL,
|
||||
ExpiryDate DATETIME2 NULL,
|
||||
UsageCount INT NOT NULL DEFAULT 0,
|
||||
MaxUsages INT NULL,
|
||||
IsActive BIT NOT NULL DEFAULT 1,
|
||||
CreatedAt DATETIME2 NOT NULL DEFAULT GETUTCDATE()
|
||||
);
|
||||
CREATE UNIQUE INDEX IX_Coupons_Code ON Coupons(Code);
|
||||
|
||||
-- Seed Default Coupon
|
||||
INSERT INTO Coupons (Code, DiscountType, DiscountValue) VALUES ('WELCOME2025', 'Percentage', 10.00);
|
||||
END
|
||||
|
||||
-- Listings CouponCode
|
||||
IF NOT EXISTS(SELECT * FROM sys.columns WHERE Name = N'CouponCode' AND Object_ID = Object_ID(N'Listings'))
|
||||
ALTER TABLE Listings ADD CouponCode NVARCHAR(50) NULL;
|
||||
|
||||
-- Coupons MaxUsagesPerUser
|
||||
IF NOT EXISTS(SELECT * FROM sys.columns WHERE Name = N'MaxUsagesPerUser' AND Object_ID = Object_ID(N'Coupons'))
|
||||
ALTER TABLE Coupons ADD MaxUsagesPerUser INT NULL;
|
||||
|
||||
-- Tabla de Notas/Mensajes de Avisos
|
||||
IF NOT EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'ListingNotes') AND type in (N'U'))
|
||||
BEGIN
|
||||
CREATE TABLE ListingNotes (
|
||||
Id INT IDENTITY(1,1) PRIMARY KEY,
|
||||
ListingId INT NOT NULL,
|
||||
SenderId INT NOT NULL,
|
||||
IsFromModerator BIT NOT NULL DEFAULT 1,
|
||||
Message NVARCHAR(MAX) NOT NULL,
|
||||
CreatedAt DATETIME2 NOT NULL DEFAULT GETUTCDATE(),
|
||||
IsRead BIT NOT NULL DEFAULT 0,
|
||||
FOREIGN KEY (ListingId) REFERENCES Listings(Id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (SenderId) REFERENCES Users(Id)
|
||||
);
|
||||
END
|
||||
ELSE
|
||||
BEGIN
|
||||
-- Migración gradual si la tabla ya existía con ModeratorId
|
||||
IF EXISTS(SELECT * FROM sys.columns WHERE Name = N'ModeratorId' AND Object_ID = Object_ID(N'ListingNotes'))
|
||||
BEGIN
|
||||
EXEC sp_rename 'ListingNotes.ModeratorId', 'SenderId', 'COLUMN';
|
||||
END
|
||||
|
||||
IF NOT EXISTS(SELECT * FROM sys.columns WHERE Name = N'IsFromModerator' AND Object_ID = Object_ID(N'ListingNotes'))
|
||||
BEGIN
|
||||
ALTER TABLE ListingNotes ADD IsFromModerator BIT NOT NULL DEFAULT 1;
|
||||
END
|
||||
END
|
||||
";
|
||||
await connection.ExecuteAsync(migrationSql);
|
||||
|
||||
// --- MIGRACIÓN DE DATOS (Post-Schema Update) ---
|
||||
var dataMigrationSql = @"
|
||||
-- Data Migration: Clients to Users
|
||||
IF EXISTS (SELECT * FROM sys.tables WHERE name = 'Clients')
|
||||
BEGIN
|
||||
-- Verificamos si la tabla Clients tiene la columna Phone para evitar errores
|
||||
DECLARE @phoneColumnExists BIT = 0;
|
||||
IF EXISTS(SELECT * FROM sys.columns WHERE Name = N'Phone' AND Object_ID = Object_ID(N'Clients'))
|
||||
SET @phoneColumnExists = 1;
|
||||
|
||||
IF @phoneColumnExists = 1
|
||||
BEGIN
|
||||
INSERT INTO Users (Username, PasswordHash, Role, Email, BillingName, BillingTaxId, BillingAddress, Phone, MustChangePassword, IsActive)
|
||||
SELECT
|
||||
ISNULL(DniOrCuit, Name) as Username,
|
||||
'N/A' as PasswordHash,
|
||||
'Client' as Role,
|
||||
Email,
|
||||
Name as BillingName,
|
||||
DniOrCuit as BillingTaxId,
|
||||
Address as BillingAddress,
|
||||
Phone,
|
||||
0 as MustChangePassword,
|
||||
1 as IsActive
|
||||
FROM Clients
|
||||
WHERE DniOrCuit NOT IN (SELECT BillingTaxId FROM Users WHERE BillingTaxId IS NOT NULL);
|
||||
END
|
||||
ELSE
|
||||
BEGIN
|
||||
INSERT INTO Users (Username, PasswordHash, Role, Email, BillingName, BillingTaxId, BillingAddress, MustChangePassword, IsActive)
|
||||
SELECT
|
||||
ISNULL(DniOrCuit, Name) as Username,
|
||||
'N/A' as PasswordHash,
|
||||
'Client' as Role,
|
||||
Email,
|
||||
Name as BillingName,
|
||||
DniOrCuit as BillingTaxId,
|
||||
Address as BillingAddress,
|
||||
0 as MustChangePassword,
|
||||
1 as IsActive
|
||||
FROM Clients
|
||||
WHERE DniOrCuit NOT IN (SELECT BillingTaxId FROM Users WHERE BillingTaxId IS NOT NULL);
|
||||
END
|
||||
|
||||
-- 1. Actualizar Listings para que apunten a Users antes de romper nada (opcional pero recomendado si hay datos)
|
||||
-- Solo lo hacemos para los que ya existen en Users por BillingTaxId
|
||||
UPDATE l
|
||||
SET l.ClientId = u.Id
|
||||
FROM Listings l
|
||||
JOIN Clients c ON l.ClientId = c.Id
|
||||
JOIN Users u ON c.DniOrCuit = u.BillingTaxId;
|
||||
|
||||
-- 2. Eliminar todas las llaves foráneas que apuntan a Clients
|
||||
DECLARE @sql NVARCHAR(MAX) = N'';
|
||||
SELECT @sql += 'ALTER TABLE ' + QUOTENAME(OBJECT_SCHEMA_NAME(parent_object_id)) + '.' + QUOTENAME(OBJECT_NAME(parent_object_id)) +
|
||||
' DROP CONSTRAINT ' + QUOTENAME(name) + ';'
|
||||
FROM sys.foreign_keys
|
||||
WHERE referenced_object_id = OBJECT_ID('Clients');
|
||||
|
||||
IF @sql <> '' EXEC sp_executesql @sql;
|
||||
|
||||
-- 3. Finalmente borrar la tabla
|
||||
DROP TABLE Clients;
|
||||
END
|
||||
";
|
||||
await connection.ExecuteAsync(dataMigrationSql);
|
||||
|
||||
// --- SEED DE DATOS (Usuario Admin) ---
|
||||
var adminCount = await connection.ExecuteScalarAsync<int>("SELECT COUNT(1) FROM Users WHERE Username = 'admin'");
|
||||
|
||||
|
||||
69
src/SIGCM.Infrastructure/Data/UPDATE_SCHEMA.sql
Normal file
69
src/SIGCM.Infrastructure/Data/UPDATE_SCHEMA.sql
Normal file
@@ -0,0 +1,69 @@
|
||||
-- Run this script to update the database schema
|
||||
|
||||
-- Update Listings table
|
||||
ALTER TABLE Listings ADD PublicationStartDate DATETIME2 NULL;
|
||||
ALTER TABLE Listings ADD ApprovedAt DATETIME2 NULL;
|
||||
ALTER TABLE Listings ADD IsFeatured BIT NOT NULL DEFAULT 0;
|
||||
ALTER TABLE Listings ADD FeaturedExpiry DATETIME2 NULL;
|
||||
ALTER TABLE Listings ADD AllowContact BIT NOT NULL DEFAULT 1;
|
||||
|
||||
-- Update Users table
|
||||
ALTER TABLE Users ADD BillingName NVARCHAR(200) NULL;
|
||||
ALTER TABLE Users ADD BillingAddress NVARCHAR(500) NULL;
|
||||
ALTER TABLE Users ADD BillingTaxId NVARCHAR(50) NULL;
|
||||
ALTER TABLE Users ADD BillingTaxType NVARCHAR(50) NULL;
|
||||
ALTER TABLE Users ADD ClientId INT NULL;
|
||||
ALTER TABLE Users ADD FailedLoginAttempts INT NOT NULL DEFAULT 0;
|
||||
ALTER TABLE Users ADD LockoutEnd DATETIME NULL;
|
||||
ALTER TABLE Users ADD MustChangePassword BIT NOT NULL DEFAULT 1;
|
||||
ALTER TABLE Users ADD IsActive BIT NOT NULL DEFAULT 1;
|
||||
ALTER TABLE Users ADD LastLogin DATETIME NULL;
|
||||
ALTER TABLE Users ADD GoogleId NVARCHAR(255) NULL;
|
||||
ALTER TABLE Users ADD IsMfaEnabled BIT NOT NULL DEFAULT 0;
|
||||
ALTER TABLE Users ADD MfaSecret NVARCHAR(255) NULL;
|
||||
ALTER TABLE Users ADD Phone NVARCHAR(50) NULL;
|
||||
|
||||
-- Create Coupons table
|
||||
IF NOT EXISTS (SELECT * FROM sysobjects WHERE name='Coupons' and xtype='U')
|
||||
BEGIN
|
||||
CREATE TABLE Coupons (
|
||||
Id INT IDENTITY(1,1) PRIMARY KEY,
|
||||
Code NVARCHAR(50) NOT NULL,
|
||||
DiscountType NVARCHAR(20) NOT NULL,
|
||||
DiscountValue DECIMAL(18,2) NOT NULL,
|
||||
ExpiryDate DATETIME2 NULL,
|
||||
UsageCount INT NOT NULL DEFAULT 0,
|
||||
MaxUsages INT NULL,
|
||||
IsActive BIT NOT NULL DEFAULT 1,
|
||||
CreatedAt DATETIME2 NOT NULL DEFAULT GETUTCDATE()
|
||||
);
|
||||
CREATE UNIQUE INDEX IX_Coupons_Code ON Coupons(Code);
|
||||
END
|
||||
|
||||
-- Data Migration and Consolidation
|
||||
-- Copy clients to users if they don't exist
|
||||
INSERT INTO Users (Username, PasswordHash, Role, Email, BillingName, BillingTaxId, BillingAddress, Phone, MustChangePassword, IsActive)
|
||||
SELECT
|
||||
ISNULL(DniOrCuit, Name), 'N/A', 'Client', Email, Name, DniOrCuit, Address, Phone, 0, 1
|
||||
FROM Clients
|
||||
WHERE DniOrCuit NOT IN (SELECT BillingTaxId FROM Users WHERE BillingTaxId IS NOT NULL);
|
||||
|
||||
-- Update Listings to point to new User IDs
|
||||
UPDATE l
|
||||
SET l.ClientId = u.Id
|
||||
FROM Listings l
|
||||
JOIN Clients c ON l.ClientId = c.Id
|
||||
JOIN Users u ON c.DniOrCuit = u.BillingTaxId;
|
||||
|
||||
-- Drop foreign keys referencing Clients
|
||||
DECLARE @sql NVARCHAR(MAX) = N'';
|
||||
SELECT @sql += 'ALTER TABLE ' + QUOTENAME(OBJECT_SCHEMA_NAME(parent_object_id)) + '.' + QUOTENAME(OBJECT_NAME(parent_object_id)) +
|
||||
' DROP CONSTRAINT ' + QUOTENAME(name) + ';'
|
||||
FROM sys.foreign_keys
|
||||
WHERE referenced_object_id = OBJECT_ID('Clients');
|
||||
|
||||
IF @sql <> '' EXEC sp_executesql @sql;
|
||||
|
||||
-- DROP obsolete table
|
||||
IF EXISTS (SELECT * FROM sysobjects WHERE name='Clients' and xtype='U')
|
||||
DROP TABLE Clients;
|
||||
@@ -32,6 +32,8 @@ public static class DependencyInjection
|
||||
services.AddScoped<ImageOptimizationService>();
|
||||
services.AddScoped<IClaimRepository, ClaimRepository>();
|
||||
services.AddScoped<NotificationRepository>();
|
||||
services.AddScoped<ICouponRepository, CouponRepository>();
|
||||
services.AddScoped<IListingNoteRepository, ListingNoteRepository>();
|
||||
|
||||
// Registro de MercadoPagoService configurado desde IConfiguration (appsettings o env vars)
|
||||
services.AddScoped<MercadoPagoService>(sp =>
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
63
src/SIGCM.Infrastructure/Repositories/CouponRepository.cs
Normal file
63
src/SIGCM.Infrastructure/Repositories/CouponRepository.cs
Normal 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 });
|
||||
}
|
||||
}
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ public class AuthService : IAuthService
|
||||
// Si MFA está activo, no devolver token aún, pedir verificación
|
||||
if (user.IsMfaEnabled)
|
||||
{
|
||||
return new AuthResult { Success = true, RequiresMfa = true };
|
||||
return new AuthResult { Success = true, RequiresMfa = true, UserId = user.Id };
|
||||
}
|
||||
|
||||
// Éxito: Reiniciar intentos y generar token
|
||||
@@ -59,7 +59,8 @@ public class AuthService : IAuthService
|
||||
{
|
||||
Success = true,
|
||||
Token = _tokenService.GenerateToken(user),
|
||||
RequiresPasswordChange = user.MustChangePassword
|
||||
RequiresPasswordChange = user.MustChangePassword,
|
||||
UserId = user.Id
|
||||
};
|
||||
}
|
||||
|
||||
@@ -83,7 +84,7 @@ public class AuthService : IAuthService
|
||||
};
|
||||
|
||||
await _userRepo.CreateAsync(user);
|
||||
return new AuthResult { Success = true, Token = _tokenService.GenerateToken(user) };
|
||||
return new AuthResult { Success = true, Token = _tokenService.GenerateToken(user), UserId = user.Id };
|
||||
}
|
||||
|
||||
// Login mediante Google OAuth
|
||||
@@ -117,9 +118,9 @@ public class AuthService : IAuthService
|
||||
await _userRepo.UpdateAsync(user);
|
||||
}
|
||||
|
||||
if (user.IsMfaEnabled) return new AuthResult { Success = true, RequiresMfa = true };
|
||||
if (user.IsMfaEnabled) return new AuthResult { Success = true, RequiresMfa = true, UserId = user.Id };
|
||||
|
||||
return new AuthResult { Success = true, Token = _tokenService.GenerateToken(user) };
|
||||
return new AuthResult { Success = true, Token = _tokenService.GenerateToken(user), UserId = user.Id };
|
||||
}
|
||||
catch (InvalidJwtException)
|
||||
{
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using SIGCM.Application.DTOs;
|
||||
using SIGCM.Domain.Entities;
|
||||
using SIGCM.Infrastructure.Repositories;
|
||||
using SIGCM.Domain.Interfaces;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace SIGCM.Infrastructure.Services;
|
||||
@@ -8,10 +9,12 @@ namespace SIGCM.Infrastructure.Services;
|
||||
public class PricingService
|
||||
{
|
||||
private readonly PricingRepository _repo;
|
||||
private readonly ICouponRepository _couponRepo;
|
||||
|
||||
public PricingService(PricingRepository repo)
|
||||
public PricingService(PricingRepository repo, ICouponRepository couponRepo)
|
||||
{
|
||||
_repo = repo;
|
||||
_couponRepo = couponRepo;
|
||||
}
|
||||
|
||||
public async Task<CalculatePriceResponse> CalculateAsync(CalculatePriceRequest request)
|
||||
@@ -53,41 +56,45 @@ public class PricingService
|
||||
|
||||
currentCost += extraWordCost + specialCharCost;
|
||||
|
||||
// 4. Estilos (Negrita / Recuadro) - Se suman al precio unitario diario
|
||||
// 4. Estilos (Negrita / Recuadro / Destacado)
|
||||
if (request.IsBold) currentCost += pricing.BoldSurcharge;
|
||||
if (request.IsFrame) currentCost += pricing.FrameSurcharge;
|
||||
|
||||
// Costo Destacado (Hardcoded por ahora o agregar a regla)
|
||||
decimal featuredSurcharge = 0;
|
||||
if (request.IsFeatured)
|
||||
{
|
||||
featuredSurcharge = 500m; // Valor ejemplo por día
|
||||
currentCost += featuredSurcharge;
|
||||
}
|
||||
|
||||
// 5. Multiplicar por Días
|
||||
decimal totalBeforeDiscount = currentCost * request.Days;
|
||||
|
||||
// 6. Motor de Promociones
|
||||
// 6. Motor de Promociones y Cupones
|
||||
var promotions = await _repo.GetActivePromotionsAsync();
|
||||
decimal totalDiscount = 0;
|
||||
List<string> appliedPromos = new();
|
||||
|
||||
// Lógica de promociones automáticas
|
||||
foreach (var promo in promotions)
|
||||
{
|
||||
// Filtro por Categoría
|
||||
if (promo.CategoryId.HasValue && promo.CategoryId != request.CategoryId) continue;
|
||||
|
||||
// Filtro por Cantidad de Días
|
||||
if (promo.MinDays > 0 && request.Days < promo.MinDays) continue;
|
||||
|
||||
// Filtro por Días de la Semana
|
||||
|
||||
// Verificar si aplica según el día de la semana
|
||||
if (!string.IsNullOrEmpty(promo.DaysOfWeek))
|
||||
{
|
||||
var targetDays = promo.DaysOfWeek.Split(',').Select(int.Parse).ToList();
|
||||
bool hitsDay = false;
|
||||
// Revisamos cada día que durará el aviso
|
||||
for (int i = 0; i < request.Days; i++)
|
||||
{
|
||||
var currentDay = (int)request.StartDate.AddDays(i).DayOfWeek;
|
||||
if (targetDays.Contains(currentDay)) hitsDay = true;
|
||||
var currentDay = (int)request.StartDate.AddDays(i).DayOfWeek;
|
||||
if (targetDays.Contains(currentDay)) hitsDay = true;
|
||||
}
|
||||
if (!hitsDay) continue; // No cae en ningún día de promo
|
||||
if (!hitsDay) continue;
|
||||
}
|
||||
|
||||
// Aplicar Descuento
|
||||
if (promo.DiscountPercentage > 0)
|
||||
{
|
||||
decimal discountVal = totalBeforeDiscount * (promo.DiscountPercentage / 100m);
|
||||
@@ -101,16 +108,38 @@ public class PricingService
|
||||
}
|
||||
}
|
||||
|
||||
// Lógica de Cupón
|
||||
if (!string.IsNullOrEmpty(request.CouponCode))
|
||||
{
|
||||
var coupon = await _couponRepo.GetByCodeAsync(request.CouponCode);
|
||||
if (coupon != null && (!coupon.ExpiryDate.HasValue || coupon.ExpiryDate > DateTime.UtcNow)
|
||||
&& (!coupon.MaxUsages.HasValue || coupon.UsageCount < coupon.MaxUsages))
|
||||
{
|
||||
if (coupon.DiscountType == "Percentage")
|
||||
{
|
||||
decimal val = totalBeforeDiscount * (coupon.DiscountValue / 100m);
|
||||
totalDiscount += val;
|
||||
appliedPromos.Add($"CUPÓN {coupon.Code} (-{coupon.DiscountValue}%)");
|
||||
}
|
||||
else if (coupon.DiscountType == "Fixed")
|
||||
{
|
||||
totalDiscount += coupon.DiscountValue;
|
||||
appliedPromos.Add($"CUPÓN {coupon.Code} (-${coupon.DiscountValue})");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new CalculatePriceResponse
|
||||
{
|
||||
TotalPrice = Math.Max(0, totalBeforeDiscount - totalDiscount),
|
||||
BaseCost = pricing.BasePrice * request.Days,
|
||||
ExtraCost = (extraWordCost + specialCharCost) * request.Days,
|
||||
Surcharges = ((request.IsBold ? pricing.BoldSurcharge : 0) + (request.IsFrame ? pricing.FrameSurcharge : 0)) * request.Days,
|
||||
Surcharges = ((request.IsBold ? pricing.BoldSurcharge : 0) + (request.IsFrame ? pricing.FrameSurcharge : 0) + (request.IsFeatured ? featuredSurcharge : 0)) * request.Days,
|
||||
Discount = totalDiscount,
|
||||
WordCount = realWordCount,
|
||||
SpecialCharCount = specialCharCount,
|
||||
Details = $"Tarifa Diaria: ${currentCost} x {request.Days} días. (Extras diarios: ${extraWordCost + specialCharCost})"
|
||||
Details = $"Tarifa Diaria: ${currentCost} x {request.Days} días. (Extras diarios: ${extraWordCost + specialCharCost}). {string.Join(", ", appliedPromos)}",
|
||||
AppliedPromotion = string.Join(", ", appliedPromos)
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user