Feat Backend ERP 1
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
103
src/SIGCM.Infrastructure/Repositories/ClientProfileRepository.cs
Normal file
103
src/SIGCM.Infrastructure/Repositories/ClientProfileRepository.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
101
src/SIGCM.Infrastructure/Repositories/OrderRepository.cs
Normal file
101
src/SIGCM.Infrastructure/Repositories/OrderRepository.cs
Normal 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 });
|
||||
}
|
||||
}
|
||||
129
src/SIGCM.Infrastructure/Repositories/ProductRepository.cs
Normal file
129
src/SIGCM.Infrastructure/Repositories/ProductRepository.cs
Normal 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 });
|
||||
}
|
||||
}
|
||||
78
src/SIGCM.Infrastructure/Repositories/SellerRepository.cs
Normal file
78
src/SIGCM.Infrastructure/Repositories/SellerRepository.cs
Normal 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 });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user