CheckPoint: Avances Varios

This commit is contained in:
2025-12-18 13:32:50 -03:00
parent 8f535f3a6e
commit 32663e6324
92 changed files with 12629 additions and 195 deletions

View File

@@ -1,6 +0,0 @@
namespace SIGCM.Infrastructure;
public class Class1
{
}

View File

@@ -1,4 +1,6 @@
using Dapper;
// Asegúrate de que BCrypt.Net esté instalado en este proyecto o referenciado
// Si no te reconoce BCrypt, avísame, pero debería funcionar porque ya se usa en AuthService.
namespace SIGCM.Infrastructure.Data;
@@ -14,60 +16,7 @@ public class DbInitializer
public async Task InitializeAsync()
{
using var connection = _connectionFactory.CreateConnection();
var sql = @"
IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'Users')
BEGIN
CREATE TABLE Users (
Id INT IDENTITY(1,1) PRIMARY KEY,
Username NVARCHAR(50) NOT NULL UNIQUE,
PasswordHash NVARCHAR(255) NOT NULL,
Role NVARCHAR(20) NOT NULL,
Email NVARCHAR(100) NULL,
CreatedAt DATETIME DEFAULT GETUTCDATE()
);
-- Seed generic admin (password: admin123)
-- Hash created with BCrypt
INSERT INTO Users (Username, PasswordHash, Role)
VALUES ('admin', '$2a$11$u.w..ExampleHashPlaceholder...', 'Admin');
END
IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'Categories')
BEGIN
CREATE TABLE Categories (
Id INT IDENTITY(1,1) PRIMARY KEY,
ParentId INT NULL,
Name NVARCHAR(100) NOT NULL,
Slug NVARCHAR(100) NOT NULL,
Active BIT DEFAULT 1,
FOREIGN KEY (ParentId) REFERENCES Categories(Id)
);
END
IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'Operations')
BEGIN
CREATE TABLE Operations (
Id INT IDENTITY(1,1) PRIMARY KEY,
Name NVARCHAR(50) NOT NULL UNIQUE
);
END
IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'CategoryOperations')
BEGIN
CREATE TABLE CategoryOperations (
CategoryId INT NOT NULL,
OperationId INT NOT NULL,
PRIMARY KEY (CategoryId, OperationId),
FOREIGN KEY (CategoryId) REFERENCES Categories(Id) ON DELETE CASCADE,
FOREIGN KEY (OperationId) REFERENCES Operations(Id) ON DELETE CASCADE
);
END
";
// Fixing the placeholder hash to a valid one might be necessary if I want to login immediately.
// I will update the hash command later or create a small utility to generate one.
// For now, I'll remove the INSERT or comment it out until I can generate a real hash in C#.
var schemaSql = @"
IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'Users')
BEGIN
@@ -167,6 +116,22 @@ BEGIN
);
END
";
// Ejecutar creación de tablas
await connection.ExecuteAsync(schemaSql);
// --- SEED DE DATOS (Usuario Admin) ---
var adminCount = await connection.ExecuteScalarAsync<int>("SELECT COUNT(1) FROM Users WHERE Username = 'admin'");
if (adminCount == 0)
{
// Creamos el hash válido para la clave del usuario usando la librería del proyecto
var passwordHash = BCrypt.Net.BCrypt.HashPassword("Diagonal423");
var insertAdminSql = @"
INSERT INTO Users (Username, PasswordHash, Role, Email)
VALUES ('admin', @PasswordHash, 'Admin', 'admin@sigcm.com')";
await connection.ExecuteAsync(insertAdminSql, new { PasswordHash = passwordHash });
}
}
}
}

View File

@@ -3,6 +3,7 @@ using SIGCM.Domain.Interfaces;
using SIGCM.Application.Interfaces;
using SIGCM.Infrastructure.Data;
using SIGCM.Infrastructure.Repositories;
using SIGCM.Infrastructure.Services;
namespace SIGCM.Infrastructure;
@@ -15,11 +16,13 @@ public static class DependencyInjection
services.AddScoped<ICategoryRepository, CategoryRepository>();
services.AddScoped<IOperationRepository, OperationRepository>();
services.AddScoped<IUserRepository, UserRepository>();
services.AddScoped<ITokenService, Services.TokenService>();
services.AddScoped<IAuthService, Services.AuthService>();
services.AddScoped<ITokenService, TokenService>();
services.AddScoped<IAuthService, AuthService>();
services.AddScoped<IAttributeDefinitionRepository, AttributeDefinitionRepository>();
services.AddScoped<IListingRepository, ListingRepository>();
services.AddScoped<IImageRepository, ImageRepository>();
services.AddScoped<PricingRepository>();
services.AddScoped<PricingService>();
return services;
}
}

View File

@@ -25,8 +25,16 @@ public class ListingRepository : IListingRepository
try
{
var sqlListing = @"
INSERT INTO Listings (CategoryId, OperationId, Title, Description, Price, Currency, CreatedAt, Status, UserId)
VALUES (@CategoryId, @OperationId, @Title, @Description, @Price, @Currency, @CreatedAt, @Status, @UserId);
INSERT INTO Listings (
CategoryId, OperationId, Title, Description, Price, Currency,
CreatedAt, Status, UserId, PrintText, PrintStartDate, PrintDaysCount,
IsBold, IsFrame, PrintFontSize, PrintAlignment
)
VALUES (
@CategoryId, @OperationId, @Title, @Description, @Price, @Currency,
@CreatedAt, @Status, @UserId, @PrintText, @PrintStartDate, @PrintDaysCount,
@IsBold, @IsFrame, @PrintFontSize, @PrintAlignment
);
SELECT CAST(SCOPE_IDENTITY() as int);";
var listingId = await conn.QuerySingleAsync<int>(sqlListing, listing, transaction);
@@ -59,15 +67,90 @@ public class ListingRepository : IListingRepository
return await conn.QuerySingleOrDefaultAsync<Listing>("SELECT * FROM Listings WHERE Id = @Id", new { Id = id });
}
public async Task<SIGCM.Domain.Models.ListingDetail?> GetDetailByIdAsync(int id)
{
using var conn = _connectionFactory.CreateConnection();
var sql = @"
SELECT * FROM Listings WHERE 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;
";
using var multi = await conn.QueryMultipleAsync(sql, new { Id = id });
var listing = await multi.ReadSingleOrDefaultAsync<Listing>();
if (listing == null) return null;
var attributes = await multi.ReadAsync<SIGCM.Domain.Models.ListingAttributeValueWithName>();
var images = await multi.ReadAsync<ListingImage>();
return new SIGCM.Domain.Models.ListingDetail
{
Listing = listing,
Attributes = attributes,
Images = images
};
}
public async Task<IEnumerable<Listing>> GetAllAsync()
{
using var conn = _connectionFactory.CreateConnection();
// A simple query for now
// Subquery para obtener la imagen principal
var sql = @"
SELECT l.*
SELECT TOP 20 l.*,
(SELECT TOP 1 Url FROM ListingImages li WHERE li.ListingId = l.Id ORDER BY IsMainInfo DESC, DisplayOrder ASC) as MainImageUrl
FROM Listings l
ORDER BY l.CreatedAt DESC";
return await conn.QueryAsync<Listing>(sql);
}
}
public async Task<IEnumerable<Listing>> SearchAsync(string? query, int? categoryId)
{
using var conn = _connectionFactory.CreateConnection();
var sql = @"
SELECT l.*,
(SELECT TOP 1 Url FROM ListingImages li WHERE li.ListingId = l.Id ORDER BY IsMainInfo DESC, DisplayOrder ASC) as MainImageUrl
FROM Listings l
WHERE 1=1";
var parameters = new DynamicParameters();
if (!string.IsNullOrEmpty(query))
{
sql += " AND (l.Title LIKE @Query OR l.Description LIKE @Query)";
parameters.Add("Query", $"%{query}%");
}
if (categoryId.HasValue)
{
sql += " AND l.CategoryId = @CategoryId";
parameters.Add("CategoryId", categoryId);
}
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();
// La lógica: El aviso debe haber empezado antes o en la fecha target
// Y la fecha target debe ser menor a la fecha de inicio + duración
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"; // Ordenado por Rubro y luego alfabético
return await conn.QueryAsync<Listing>(sql, new { TargetDate = targetDate.Date });
}
}

View File

@@ -0,0 +1,94 @@
using Dapper;
using SIGCM.Domain.Entities;
using SIGCM.Infrastructure.Data;
namespace SIGCM.Infrastructure.Repositories;
public class PricingRepository
{
private readonly IDbConnectionFactory _db;
public PricingRepository(IDbConnectionFactory db)
{
_db = db;
}
public async Task<CategoryPricing?> GetByCategoryIdAsync(int categoryId)
{
using var conn = _db.CreateConnection();
return await conn.QuerySingleOrDefaultAsync<CategoryPricing>(
"SELECT * FROM CategoryPricing WHERE CategoryId = @Id", new { Id = categoryId });
}
public async Task UpsertPricingAsync(CategoryPricing pricing)
{
using var conn = _db.CreateConnection();
// Lógica de "Si existe actualiza, sino inserta"
var exists = await conn.ExecuteScalarAsync<int>(
"SELECT COUNT(1) FROM CategoryPricing WHERE CategoryId = @CategoryId", new { pricing.CategoryId });
if (exists > 0)
{
var updateSql = @"
UPDATE CategoryPricing
SET BasePrice = @BasePrice, BaseWordCount = @BaseWordCount,
ExtraWordPrice = @ExtraWordPrice, SpecialChars = @SpecialChars,
SpecialCharPrice = @SpecialCharPrice, BoldSurcharge = @BoldSurcharge,
FrameSurcharge = @FrameSurcharge
WHERE CategoryId = @CategoryId";
await conn.ExecuteAsync(updateSql, pricing);
}
else
{
var insertSql = @"
INSERT INTO CategoryPricing
(CategoryId, BasePrice, BaseWordCount, ExtraWordPrice, SpecialChars, SpecialCharPrice, BoldSurcharge, FrameSurcharge)
VALUES
(@CategoryId, @BasePrice, @BaseWordCount, @ExtraWordPrice, @SpecialChars, @SpecialCharPrice, @BoldSurcharge, @FrameSurcharge)";
await conn.ExecuteAsync(insertSql, pricing);
}
}
public async Task<IEnumerable<Promotion>> GetActivePromotionsAsync()
{
using var conn = _db.CreateConnection();
return await conn.QueryAsync<Promotion>(
"SELECT * FROM Promotions WHERE IsActive = 1");
}
// --- SECCIÓN PROMOCIONES ---
public async Task<IEnumerable<Promotion>> GetAllPromotionsAsync()
{
using var conn = _db.CreateConnection();
return await conn.QueryAsync<Promotion>("SELECT * FROM Promotions ORDER BY Id DESC");
}
public async Task<int> CreatePromotionAsync(Promotion promo)
{
using var conn = _db.CreateConnection();
var sql = @"
INSERT INTO Promotions (Name, CategoryId, MinDays, DaysOfWeek, DiscountPercentage, DiscountFixedAmount, IsActive)
VALUES (@Name, @CategoryId, @MinDays, @DaysOfWeek, @DiscountPercentage, @DiscountFixedAmount, @IsActive);
SELECT CAST(SCOPE_IDENTITY() as int);";
return await conn.QuerySingleAsync<int>(sql, promo);
}
public async Task UpdatePromotionAsync(Promotion promo)
{
using var conn = _db.CreateConnection();
var sql = @"
UPDATE Promotions
SET Name = @Name, CategoryId = @CategoryId, MinDays = @MinDays,
DaysOfWeek = @DaysOfWeek, DiscountPercentage = @DiscountPercentage,
DiscountFixedAmount = @DiscountFixedAmount, IsActive = @IsActive
WHERE Id = @Id";
await conn.ExecuteAsync(sql, promo);
}
public async Task DeletePromotionAsync(int id)
{
using var conn = _db.CreateConnection();
await conn.ExecuteAsync("DELETE FROM Promotions WHERE Id = @Id", new { Id = id });
}
}

View File

@@ -0,0 +1,111 @@
using SIGCM.Application.DTOs; // Asegúrate de crear este DTO (ver abajo)
using SIGCM.Domain.Entities;
using SIGCM.Infrastructure.Repositories;
using System.Text.RegularExpressions;
namespace SIGCM.Infrastructure.Services;
public class PricingService
{
private readonly PricingRepository _repo;
public PricingService(PricingRepository repo)
{
_repo = repo;
}
public async Task<CalculatePriceResponse> CalculateAsync(CalculatePriceRequest request)
{
// 1. Obtener Reglas
var pricing = await _repo.GetByCategoryIdAsync(request.CategoryId);
// Si no hay configuración para este rubro, devolvemos 0 o un default seguro
if (pricing == null) return new CalculatePriceResponse
{
TotalPrice = 0,
Details = "No hay tarifas configuradas para este rubro."
};
// 2. Análisis del Texto
var words = request.Text.Split(new[] { ' ', '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries);
int realWordCount = words.Length;
// Contar caracteres especiales configurados en BD (ej: "!")
// Escapamos los caracteres por seguridad en Regex
string escapedSpecialChars = Regex.Escape(pricing.SpecialChars ?? "!");
int specialCharCount = Regex.Matches(request.Text, $"[{escapedSpecialChars}]").Count;
// 3. Costo Base y Excedente
decimal currentCost = pricing.BasePrice; // Precio base incluye N palabras
// ¿Cuántas palabras extra cobramos?
// Nota: Los caracteres especiales se cobran aparte según tu requerimiento,
// o suman al conteo de palabras. Aquí implemento: Se cobran APARTE.
int extraWords = Math.Max(0, realWordCount - pricing.BaseWordCount);
decimal extraWordCost = extraWords * pricing.ExtraWordPrice;
decimal specialCharCost = specialCharCount * pricing.SpecialCharPrice;
currentCost += extraWordCost + specialCharCost;
// 4. Estilos (Negrita / Recuadro) - Se suman al precio unitario diario
if (request.IsBold) currentCost += pricing.BoldSurcharge;
if (request.IsFrame) currentCost += pricing.FrameSurcharge;
// 5. Multiplicar por Días
decimal totalBeforeDiscount = currentCost * request.Days;
// 6. Motor de Promociones
var promotions = await _repo.GetActivePromotionsAsync();
decimal totalDiscount = 0;
List<string> appliedPromos = new();
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
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;
}
if (!hitsDay) continue; // No cae en ningún día de promo
}
// Aplicar Descuento
if (promo.DiscountPercentage > 0)
{
decimal discountVal = totalBeforeDiscount * (promo.DiscountPercentage / 100m);
totalDiscount += discountVal;
appliedPromos.Add($"{promo.Name} (-{promo.DiscountPercentage}%)");
}
if (promo.DiscountFixedAmount > 0)
{
totalDiscount += promo.DiscountFixedAmount;
appliedPromos.Add($"{promo.Name} (-${promo.DiscountFixedAmount})");
}
}
return new CalculatePriceResponse
{
TotalPrice = Math.Max(0, totalBeforeDiscount - totalDiscount),
BaseCost = pricing.BasePrice,
ExtraCost = extraWordCost + specialCharCost,
Surcharges = (request.IsBold ? pricing.BoldSurcharge : 0) + (request.IsFrame ? pricing.FrameSurcharge : 0),
Discount = totalDiscount,
WordCount = realWordCount,
SpecialCharCount = specialCharCount,
Details = $"Base: ${pricing.BasePrice} | Extras: ${extraWordCost + specialCharCost} | Desc: -${totalDiscount} ({string.Join(", ", appliedPromos)})"
};
}
}