Feat Backend ERP 1

This commit is contained in:
2026-01-07 11:34:25 -03:00
parent 81aea41e69
commit fdb221d0fa
29 changed files with 1364 additions and 1 deletions

View File

@@ -0,0 +1,61 @@
using Dapper;
using SIGCM.Domain.Entities;
using SIGCM.Domain.Interfaces;
using SIGCM.Infrastructure.Data;
namespace SIGCM.Infrastructure.Repositories;
public class AdvertisingRepository : IAdvertisingRepository
{
private readonly IDbConnectionFactory _db;
public AdvertisingRepository(IDbConnectionFactory db) => _db = db;
// --- GRÁFICA ---
public async Task<IEnumerable<GraphicGrid>> GetGridsByCompanyAsync(int companyId)
{
using var conn = _db.CreateConnection();
return await conn.QueryAsync<GraphicGrid>(
"SELECT * FROM GraphicGrids WHERE CompanyId = @CompanyId AND IsActive = 1",
new { CompanyId = companyId });
}
public async Task<GraphicGrid?> GetGridByIdAsync(int id)
{
using var conn = _db.CreateConnection();
return await conn.QuerySingleOrDefaultAsync<GraphicGrid>(
"SELECT * FROM GraphicGrids WHERE Id = @Id", new { Id = id });
}
public async Task<int> CreateGridAsync(GraphicGrid grid)
{
using var conn = _db.CreateConnection();
var sql = @"
INSERT INTO GraphicGrids
(CompanyId, Name, ColumnWidthMm, ModuleHeightMm, PricePerModule, MaxColumns, MaxModules, ColorSurchargePercentage, IsActive)
VALUES
(@CompanyId, @Name, @ColumnWidthMm, @ModuleHeightMm, @PricePerModule, @MaxColumns, @MaxModules, @ColorSurchargePercentage, @IsActive);
SELECT CAST(SCOPE_IDENTITY() as int);";
return await conn.QuerySingleAsync<int>(sql, grid);
}
// --- RADIO ---
public async Task<IEnumerable<RadioTariff>> GetRadioTariffsByCompanyAsync(int companyId)
{
using var conn = _db.CreateConnection();
return await conn.QueryAsync<RadioTariff>(
"SELECT * FROM RadioTariffs WHERE CompanyId = @CompanyId AND IsActive = 1",
new { CompanyId = companyId });
}
public async Task<int> CreateRadioTariffAsync(RadioTariff tariff)
{
using var conn = _db.CreateConnection();
var sql = @"
INSERT INTO RadioTariffs
(CompanyId, ProgramName, TimeSlotStart, TimeSlotEnd, PricePerSecond, PricePerSpot, IsActive)
VALUES
(@CompanyId, @ProgramName, @TimeSlotStart, @TimeSlotEnd, @PricePerSecond, @PricePerSpot, @IsActive);
SELECT CAST(SCOPE_IDENTITY() as int);";
return await conn.QuerySingleAsync<int>(sql, tariff);
}
}

View File

@@ -0,0 +1,103 @@
using Dapper;
using SIGCM.Domain.Entities;
using SIGCM.Domain.Interfaces;
using SIGCM.Infrastructure.Data;
namespace SIGCM.Infrastructure.Repositories;
public class ClientProfileRepository : IClientProfileRepository
{
private readonly IDbConnectionFactory _db;
public ClientProfileRepository(IDbConnectionFactory db) => _db = db;
public async Task<ClientProfile?> GetProfileAsync(int userId)
{
using var conn = _db.CreateConnection();
var sql = @"
SELECT cp.*, u.BillingName as ClientName
FROM ClientProfiles cp
JOIN Users u ON cp.UserId = u.Id
WHERE cp.UserId = @UserId";
return await conn.QuerySingleOrDefaultAsync<ClientProfile>(sql, new { UserId = userId });
}
public async Task UpsertProfileAsync(ClientProfile profile)
{
using var conn = _db.CreateConnection();
var exists = await conn.ExecuteScalarAsync<int>(
"SELECT COUNT(1) FROM ClientProfiles WHERE UserId = @UserId", new { profile.UserId });
if (exists > 0)
{
var sql = @"
UPDATE ClientProfiles
SET CreditLimit = @CreditLimit,
PaymentTermsDays = @PaymentTermsDays,
IsCreditBlocked = @IsCreditBlocked,
BlockReason = @BlockReason,
LastCreditCheckAt = GETUTCDATE()
WHERE UserId = @UserId";
await conn.ExecuteAsync(sql, profile);
}
else
{
var sql = @"
INSERT INTO ClientProfiles (UserId, CreditLimit, PaymentTermsDays, IsCreditBlocked, BlockReason)
VALUES (@UserId, @CreditLimit, @PaymentTermsDays, @IsCreditBlocked, @BlockReason)";
await conn.ExecuteAsync(sql, profile);
}
}
public async Task<decimal> CalculateCurrentDebtAsync(int userId)
{
using var conn = _db.CreateConnection();
// Sumamos el total de órdenes que NO están pagadas ('Paid') ni canceladas ('Cancelled')
// Asumimos deuda total por simplicidad. En un sistema más complejo sumaríamos (TotalAmount - PagosParciales).
var sql = @"
SELECT ISNULL(SUM(TotalAmount), 0)
FROM Orders
WHERE ClientId = @UserId
AND PaymentStatus NOT IN ('Paid', 'Cancelled')";
return await conn.ExecuteScalarAsync<decimal>(sql, new { UserId = userId });
}
public async Task<IEnumerable<ClientProfile>> GetDebtorsAsync()
{
using var conn = _db.CreateConnection();
// ESTRATEGIA OPTIMIZADA:
// 1. Hacemos JOIN de Perfiles, Usuarios y Órdenes.
// 2. Filtramos solo órdenes impagas.
// 3. Agrupamos por Cliente.
// 4. Filtramos (HAVING) solo aquellos cuya suma de deuda sea > 0.
// 5. Esto se ejecuta en el motor de base de datos, no en memoria.
var sql = @"
SELECT
cp.UserId,
cp.CreditLimit,
cp.PaymentTermsDays,
cp.IsCreditBlocked,
cp.BlockReason,
cp.LastCreditCheckAt,
u.BillingName as ClientName,
SUM(o.TotalAmount) as CurrentDebt
FROM ClientProfiles cp
INNER JOIN Users u ON cp.UserId = u.Id
INNER JOIN Orders o ON o.ClientId = cp.UserId
WHERE o.PaymentStatus NOT IN ('Paid', 'Cancelled') -- Solo deuda activa
GROUP BY
cp.UserId,
cp.CreditLimit,
cp.PaymentTermsDays,
cp.IsCreditBlocked,
cp.BlockReason,
cp.LastCreditCheckAt,
u.BillingName
HAVING SUM(o.TotalAmount) > 0
ORDER BY CurrentDebt DESC";
return await conn.QueryAsync<ClientProfile>(sql);
}
}

View File

@@ -0,0 +1,101 @@
using Dapper;
using SIGCM.Domain.Entities;
using SIGCM.Domain.Interfaces;
using SIGCM.Infrastructure.Data;
namespace SIGCM.Infrastructure.Repositories;
public class OrderRepository : IOrderRepository
{
private readonly IDbConnectionFactory _db;
public OrderRepository(IDbConnectionFactory db) => _db = db;
public async Task<int> CreateOrderAsync(Order order, IEnumerable<OrderItem> items)
{
using var conn = _db.CreateConnection();
conn.Open();
using var transaction = conn.BeginTransaction();
try
{
// 1. Obtener siguiente número de secuencia de forma atómica
// Formato: ORD-{AÑO}-{SECUENCIA} (Ej: ORD-2026-000015)
var nextVal = await conn.ExecuteScalarAsync<int>(
"SELECT NEXT VALUE FOR OrderNumberSeq", transaction: transaction);
var orderNumber = $"ORD-{DateTime.UtcNow.Year}-{nextVal:D6}";
order.OrderNumber = orderNumber;
// 2. Insertar Cabecera
var sqlOrder = @"
INSERT INTO Orders
(OrderNumber, ClientId, SellerId, CreatedAt, DueDate, TotalNet, TotalTax, TotalAmount, PaymentStatus, FulfillmentStatus, Notes)
VALUES
(@OrderNumber, @ClientId, @SellerId, GETUTCDATE(), @DueDate, @TotalNet, @TotalTax, @TotalAmount, @PaymentStatus, @FulfillmentStatus, @Notes);
SELECT CAST(SCOPE_IDENTITY() as int);";
var orderId = await conn.QuerySingleAsync<int>(sqlOrder, order, transaction);
// 3. Insertar Items (Código existente...)
var sqlItem = @"
INSERT INTO OrderItems
(OrderId, ProductId, CompanyId, RelatedEntityId, RelatedEntityType, Quantity, UnitPrice, TaxRate, SubTotal, CommissionPercentage, CommissionAmount)
VALUES
(@OrderId, @ProductId, @CompanyId, @RelatedEntityId, @RelatedEntityType, @Quantity, @UnitPrice, @TaxRate, @SubTotal, @CommissionPercentage, @CommissionAmount)";
foreach (var item in items)
{
item.OrderId = orderId;
await conn.ExecuteAsync(sqlItem, item, transaction);
}
transaction.Commit();
return orderId;
}
catch
{
transaction.Rollback();
throw;
}
}
public async Task<Order?> GetByIdAsync(int id)
{
using var conn = _db.CreateConnection();
var sql = @"
SELECT o.*, u.Username as SellerName, c.BillingName as ClientName
FROM Orders o
LEFT JOIN Users u ON o.SellerId = u.Id
LEFT JOIN Users c ON o.ClientId = c.Id
WHERE o.Id = @Id";
return await conn.QuerySingleOrDefaultAsync<Order>(sql, new { Id = id });
}
public async Task<IEnumerable<Order>> GetByClientIdAsync(int clientId)
{
using var conn = _db.CreateConnection();
return await conn.QueryAsync<Order>(
"SELECT * FROM Orders WHERE ClientId = @ClientId ORDER BY CreatedAt DESC",
new { ClientId = clientId });
}
public async Task<IEnumerable<OrderItem>> GetItemsByOrderIdAsync(int orderId)
{
using var conn = _db.CreateConnection();
var sql = @"
SELECT oi.*, p.Name as ProductName
FROM OrderItems oi
JOIN Products p ON oi.ProductId = p.Id
WHERE oi.OrderId = @OrderId";
return await conn.QueryAsync<OrderItem>(sql, new { OrderId = orderId });
}
public async Task UpdatePaymentStatusAsync(int orderId, string status)
{
using var conn = _db.CreateConnection();
await conn.ExecuteAsync(
"UPDATE Orders SET PaymentStatus = @Status WHERE Id = @Id",
new { Id = orderId, Status = status });
}
}

View File

@@ -0,0 +1,129 @@
using Dapper;
using SIGCM.Domain.Entities;
using SIGCM.Domain.Interfaces;
using SIGCM.Infrastructure.Data;
namespace SIGCM.Infrastructure.Repositories;
public class ProductRepository : IProductRepository
{
private readonly IDbConnectionFactory _db;
public ProductRepository(IDbConnectionFactory db) => _db = db;
public async Task<IEnumerable<Product>> GetAllAsync()
{
using var conn = _db.CreateConnection();
var sql = @"
SELECT p.*, c.Name as CompanyName, pt.Code as TypeCode
FROM Products p
JOIN Companies c ON p.CompanyId = c.Id
JOIN ProductTypes pt ON p.ProductTypeId = pt.Id
WHERE p.IsActive = 1";
return await conn.QueryAsync<Product>(sql);
}
public async Task<Product?> GetByIdAsync(int id)
{
using var conn = _db.CreateConnection();
var sql = @"
SELECT p.*, c.Name as CompanyName, pt.Code as TypeCode
FROM Products p
JOIN Companies c ON p.CompanyId = c.Id
JOIN ProductTypes pt ON p.ProductTypeId = pt.Id
WHERE p.Id = @Id";
return await conn.QuerySingleOrDefaultAsync<Product>(sql, new { Id = id });
}
public async Task<IEnumerable<Product>> GetByCompanyIdAsync(int companyId)
{
using var conn = _db.CreateConnection();
return await conn.QueryAsync<Product>(
"SELECT * FROM Products WHERE CompanyId = @Id AND IsActive = 1",
new { Id = companyId });
}
public async Task<int> CreateAsync(Product product)
{
using var conn = _db.CreateConnection();
var sql = @"
INSERT INTO Products (CompanyId, ProductTypeId, Name, Description, SKU, BasePrice, TaxRate, IsActive)
VALUES (@CompanyId, @ProductTypeId, @Name, @Description, @SKU, @BasePrice, @TaxRate, @IsActive);
SELECT CAST(SCOPE_IDENTITY() as int);";
return await conn.QuerySingleAsync<int>(sql, product);
}
public async Task UpdateAsync(Product product)
{
using var conn = _db.CreateConnection();
var sql = @"
UPDATE Products
SET Name = @Name, Description = @Description, BasePrice = @BasePrice, TaxRate = @TaxRate, IsActive = @IsActive
WHERE Id = @Id";
await conn.ExecuteAsync(sql, product);
}
public async Task<IEnumerable<Company>> GetAllCompaniesAsync()
{
using var conn = _db.CreateConnection();
return await conn.QueryAsync<Company>("SELECT * FROM Companies WHERE IsActive = 1");
}
public async Task<IEnumerable<ProductBundle>> GetBundleComponentsAsync(int parentProductId)
{
using var conn = _db.CreateConnection();
var sql = @"
SELECT pb.*, p.*
FROM ProductBundles pb
JOIN Products p ON pb.ChildProductId = p.Id
WHERE pb.ParentProductId = @ParentProductId";
// Usamos Dapper Multi-Mapping para llenar el objeto ChildProduct
return await conn.QueryAsync<ProductBundle, Product, ProductBundle>(
sql,
(bundle, product) =>
{
bundle.ChildProduct = product;
return bundle;
},
new { ParentProductId = parentProductId },
splitOn: "Id" // Asumiendo que p.Id es la columna donde empieza el split
);
}
public async Task AddComponentToBundleAsync(ProductBundle bundle)
{
using var conn = _db.CreateConnection();
// 1. Validar que no exista ya esa relación para evitar duplicados
var exists = await conn.ExecuteScalarAsync<int>(
@"SELECT COUNT(1) FROM ProductBundles
WHERE ParentProductId = @ParentProductId AND ChildProductId = @ChildProductId",
bundle);
if (exists > 0)
{
// En producción, actualizamos la cantidad en lugar de fallar o duplicar
var updateSql = @"
UPDATE ProductBundles
SET Quantity = @Quantity, FixedAllocationAmount = @FixedAllocationAmount
WHERE ParentProductId = @ParentProductId AND ChildProductId = @ChildProductId";
await conn.ExecuteAsync(updateSql, bundle);
}
else
{
// Insertar nueva relación
var insertSql = @"
INSERT INTO ProductBundles (ParentProductId, ChildProductId, Quantity, FixedAllocationAmount)
VALUES (@ParentProductId, @ChildProductId, @Quantity, @FixedAllocationAmount)";
await conn.ExecuteAsync(insertSql, bundle);
}
}
public async Task RemoveComponentFromBundleAsync(int bundleId, int childProductId)
{
using var conn = _db.CreateConnection();
await conn.ExecuteAsync(
"DELETE FROM ProductBundles WHERE ParentProductId = @ParentId AND ChildProductId = @ChildId",
new { ParentId = bundleId, ChildId = childProductId });
}
}

View File

@@ -0,0 +1,78 @@
using Dapper;
using SIGCM.Domain.Entities;
using SIGCM.Domain.Interfaces;
using SIGCM.Infrastructure.Data;
namespace SIGCM.Infrastructure.Repositories;
public class SellerRepository : ISellerRepository
{
private readonly IDbConnectionFactory _db;
public SellerRepository(IDbConnectionFactory db) => _db = db;
public async Task<SellerProfile?> GetProfileAsync(int userId)
{
using var conn = _db.CreateConnection();
var sql = @"
SELECT sp.*, u.Username
FROM SellerProfiles sp
JOIN Users u ON sp.UserId = u.Id
WHERE sp.UserId = @UserId";
return await conn.QuerySingleOrDefaultAsync<SellerProfile>(sql, new { UserId = userId });
}
public async Task UpsertProfileAsync(SellerProfile profile)
{
using var conn = _db.CreateConnection();
var exists = await conn.ExecuteScalarAsync<int>(
"SELECT COUNT(1) FROM SellerProfiles WHERE UserId = @UserId", new { profile.UserId });
if (exists > 0)
{
var sql = @"
UPDATE SellerProfiles
SET SellerCode = @SellerCode,
BaseCommissionPercentage = @BaseCommissionPercentage,
IsActive = @IsActive
WHERE UserId = @UserId";
await conn.ExecuteAsync(sql, profile);
}
else
{
var sql = @"
INSERT INTO SellerProfiles (UserId, SellerCode, BaseCommissionPercentage, IsActive)
VALUES (@UserId, @SellerCode, @BaseCommissionPercentage, @IsActive)";
await conn.ExecuteAsync(sql, profile);
}
}
public async Task<IEnumerable<dynamic>> GetAllActiveSellersAsync()
{
using var conn = _db.CreateConnection();
// Traemos usuarios que tienen perfil de vendedor O que tienen rol de Vendedor/Cajero
var sql = @"
SELECT u.Id, u.Username, sp.SellerCode, ISNULL(sp.BaseCommissionPercentage, 0) as CommissionRate
FROM Users u
LEFT JOIN SellerProfiles sp ON u.Id = sp.UserId
WHERE u.IsActive = 1
AND (sp.IsActive = 1 OR u.Role IN ('Vendedor', 'Cajero'))
ORDER BY u.Username";
return await conn.QueryAsync(sql);
}
public async Task<decimal> GetMonthlySalesAsync(int sellerId, DateTime date)
{
using var conn = _db.CreateConnection();
var start = new DateTime(date.Year, date.Month, 1);
var end = start.AddMonths(1).AddSeconds(-1);
var sql = @"
SELECT ISNULL(SUM(TotalAmount), 0)
FROM Orders
WHERE SellerId = @SellerId
AND CreatedAt BETWEEN @Start AND @End
AND PaymentStatus = 'Paid'"; // Solo ventas cobradas suman para comisión (regla común)
return await conn.ExecuteScalarAsync<decimal>(sql, new { SellerId = sellerId, Start = start, End = end });
}
}