dc7e7a70fc
Introduced FlatPaymentController under Distribution.Features.Payment, providing GET and POST endpoints at /api/payments. Supports listing, retrieving, and creating payments with filtering and error handling. Business logic is delegated to PaymentService.
254 lines
9.2 KiB
C#
254 lines
9.2 KiB
C#
using Distribution.Features.Item;
|
|
using Distribution.Features.Order;
|
|
using Distribution.Features.PriceList;
|
|
using Distribution.Features.Ride;
|
|
using Distribution.Features.Stock;
|
|
using Distribution.Features.Store;
|
|
using Distribution.Infra.Authorization;
|
|
using Distribution.Infra.UoW;
|
|
using Distribution.Models;
|
|
using Microsoft.Extensions.Configuration;
|
|
|
|
namespace Distribution.Features.Sale;
|
|
|
|
public class SaleService(
|
|
ISaleRepository repo,
|
|
IOrderRepository orderRepo,
|
|
IStoreRepository storeRepo,
|
|
IItemRepository itemRepo,
|
|
PriceListService priceListService,
|
|
IStockRepository stockRepo,
|
|
IRideRepository rideRepo,
|
|
IUnitOfWork uow,
|
|
ICurrentUserService currentUser,
|
|
IScopeAccessService scopeAccess,
|
|
IConfiguration configuration)
|
|
{
|
|
private const int GlobalScopeRoleLevel = 50;
|
|
|
|
private bool HasGlobalScope => currentUser.RoleLevel >= GlobalScopeRoleLevel;
|
|
|
|
private async Task<bool> HasStoreScopeAsync(Guid storeId)
|
|
{
|
|
if (HasGlobalScope || currentUser.UserId is null) return HasGlobalScope;
|
|
return await scopeAccess.HasStoreScopeAsync(currentUser.UserId.Value, storeId);
|
|
}
|
|
|
|
private async Task<decimal?> ResolveUnitPriceAsync(Guid itemId, decimal? requestedUnitPrice, Models.Store store, DateTime saleDate)
|
|
{
|
|
var item = await itemRepo.GetByIdAsync(itemId);
|
|
if (item is null) return null;
|
|
|
|
var regionId = store.Route?.FK_RegionId;
|
|
var resolution = await priceListService.ResolvePriceAsync(itemId, regionId, item.SalePrice, saleDate);
|
|
if (resolution is null) return null;
|
|
|
|
if (!requestedUnitPrice.HasValue) return resolution.Price;
|
|
|
|
var min = resolution.MinPrice ?? resolution.Price;
|
|
var max = resolution.Price;
|
|
if (requestedUnitPrice.Value < min || requestedUnitPrice.Value > max) return null;
|
|
|
|
return requestedUnitPrice.Value;
|
|
}
|
|
|
|
private static SaleResponse ToResponse(Models.Sale sale, Models.Order order, List<Models.OrderItem> items) =>
|
|
new(
|
|
sale.Id,
|
|
sale.SaleDate,
|
|
sale.FK_StoreId,
|
|
sale.CreatedByUserId,
|
|
sale.FK_RideId,
|
|
sale.TotalAmount,
|
|
sale.Status,
|
|
sale.PaymentStatus,
|
|
sale.FulfillImmediately,
|
|
sale.CompletedAt,
|
|
order.Id,
|
|
order.Status,
|
|
order.FulfilledAt,
|
|
items.Select(i => new SaleItemResponse(i.FK_ItemId, i.Quantity, i.QuantityFulfilled, i.UnitPrice)).ToList(),
|
|
sale.IsActive,
|
|
sale.CreatedAt);
|
|
|
|
public async Task<PagedResponse<SaleResponse>> GetAllAsync(Guid? rideId, int page, int pageSize)
|
|
{
|
|
IEnumerable<Guid>? storeIds = null;
|
|
if (!HasGlobalScope && currentUser.UserId is not null)
|
|
storeIds = await scopeAccess.GetAccessibleStoreIdsAsync(currentUser.UserId.Value);
|
|
|
|
var total = await repo.GetCountAsync(rideId, storeIds);
|
|
var sales = await repo.GetAllAsync(rideId, storeIds, page, pageSize);
|
|
|
|
var result = new List<SaleResponse>();
|
|
foreach (var sale in sales)
|
|
{
|
|
var order = await repo.GetOrderBySaleIdAsync(sale.Id);
|
|
if (order is null) continue;
|
|
var items = await orderRepo.GetItemsByOrderIdAsync(order.Id);
|
|
result.Add(ToResponse(sale, order, items));
|
|
}
|
|
return new PagedResponse<SaleResponse>(page, pageSize, total, result);
|
|
}
|
|
|
|
public async Task<SaleResponse?> GetByIdAsync(Guid id)
|
|
{
|
|
var sale = await repo.GetByIdAsync(id);
|
|
if (sale is null || !sale.IsActive) return null;
|
|
if (!await HasStoreScopeAsync(sale.FK_StoreId)) return null;
|
|
|
|
var order = await repo.GetOrderBySaleIdAsync(id);
|
|
if (order is null) return null;
|
|
var items = await orderRepo.GetItemsByOrderIdAsync(order.Id);
|
|
return ToResponse(sale, order, items);
|
|
}
|
|
|
|
public async Task<List<SaleResponse>?> GetByStoreIdAsync(Guid storeId)
|
|
{
|
|
if (!await HasStoreScopeAsync(storeId)) return null;
|
|
|
|
var sales = await repo.GetByStoreIdAsync(storeId);
|
|
var result = new List<SaleResponse>();
|
|
foreach (var sale in sales)
|
|
{
|
|
var order = await repo.GetOrderBySaleIdAsync(sale.Id);
|
|
if (order is null) continue;
|
|
var items = await orderRepo.GetItemsByOrderIdAsync(order.Id);
|
|
result.Add(ToResponse(sale, order, items));
|
|
}
|
|
return result;
|
|
}
|
|
|
|
public async Task<SaleResponse?> CancelAsync(Guid id, CancellationToken ct = default)
|
|
{
|
|
var sale = await repo.GetByIdAsync(id);
|
|
if (sale is null || !sale.IsActive) return null;
|
|
if (!await HasStoreScopeAsync(sale.FK_StoreId)) return null;
|
|
if (sale.Status == SaleStatus.Cancelled)
|
|
throw new InvalidOperationException("Sale is already cancelled");
|
|
|
|
sale.Status = SaleStatus.Cancelled;
|
|
|
|
await uow.BeginAsync(ct);
|
|
try { repo.Update(sale); await uow.CommitAsync(ct); }
|
|
catch { await uow.RollbackAsync(ct); throw; }
|
|
|
|
var order = await repo.GetOrderBySaleIdAsync(id);
|
|
if (order is null) return null;
|
|
var items = await orderRepo.GetItemsByOrderIdAsync(order.Id);
|
|
return ToResponse(sale, order, items);
|
|
}
|
|
|
|
public async Task<SaleResponse?> CreateAsync(CreateSaleRequest request, CancellationToken ct = default)
|
|
{
|
|
if (!await HasStoreScopeAsync(request.StoreId)) return null;
|
|
|
|
var userId = currentUser.UserId ?? throw new UnauthorizedAccessException();
|
|
var store = await storeRepo.GetByIdWithRegionAsync(request.StoreId);
|
|
if (store is null || !store.IsActive) return null;
|
|
|
|
if (store.KycStatus != KycStatus.Verified)
|
|
{
|
|
var threshold = configuration.GetValue<decimal>("StoreManagement:UnverifiedKycOrderThreshold");
|
|
if (request.TotalAmount > threshold) return null;
|
|
}
|
|
|
|
var saleStatus = request.FulfillImmediately ? SaleStatus.Completed : SaleStatus.Pending;
|
|
var orderStatus = request.FulfillImmediately ? OrderStatus.Fulfilled : OrderStatus.Pending;
|
|
var fulfilledAt = request.FulfillImmediately ? DateTime.Now : (DateTime?)null;
|
|
|
|
var sale = new Models.Sale
|
|
{
|
|
FK_StoreId = request.StoreId,
|
|
SaleDate = request.SaleDate,
|
|
CreatedByUserId = userId,
|
|
FK_RideId = request.RideId,
|
|
TotalAmount = request.TotalAmount,
|
|
Status = saleStatus,
|
|
FulfillImmediately = request.FulfillImmediately,
|
|
CompletedAt = request.FulfillImmediately ? DateTime.Now : null
|
|
};
|
|
|
|
var orderItems = new List<Models.OrderItem>();
|
|
foreach (var item in request.Items)
|
|
{
|
|
var unitPrice = await ResolveUnitPriceAsync(item.ItemId, item.UnitPrice, store, request.SaleDate);
|
|
if (unitPrice is null) return null;
|
|
|
|
orderItems.Add(new Models.OrderItem
|
|
{
|
|
FK_ItemId = item.ItemId,
|
|
Quantity = item.Quantity,
|
|
QuantityFulfilled = request.FulfillImmediately ? item.Quantity : 0,
|
|
UnitPrice = unitPrice.Value
|
|
});
|
|
}
|
|
|
|
var order = new Models.Order
|
|
{
|
|
FK_SaleId = sale.Id,
|
|
FK_StoreId = request.StoreId,
|
|
OrderDate = request.SaleDate,
|
|
TotalAmount = request.TotalAmount,
|
|
Status = orderStatus,
|
|
FulfilledAt = fulfilledAt
|
|
};
|
|
|
|
foreach (var orderItem in orderItems)
|
|
orderItem.FK_OrderId = order.Id;
|
|
|
|
Guid? vehicleId = null;
|
|
if (request.FulfillImmediately && request.RideId.HasValue)
|
|
{
|
|
var ride = await rideRepo.GetByIdAsync(request.RideId.Value);
|
|
if (ride is null) return null;
|
|
vehicleId = ride.FK_VehicleId;
|
|
|
|
foreach (var item in orderItems)
|
|
{
|
|
var balance = await stockRepo.GetBalanceAsync(item.FK_ItemId, vehicleId.Value);
|
|
if (balance is null || balance.Quantity < item.Quantity)
|
|
return null;
|
|
}
|
|
}
|
|
|
|
await uow.BeginAsync(ct);
|
|
try
|
|
{
|
|
await repo.AddAsync(sale);
|
|
await orderRepo.AddAsync(order);
|
|
await orderRepo.AddItemsAsync(orderItems);
|
|
|
|
if (vehicleId.HasValue)
|
|
{
|
|
foreach (var item in orderItems)
|
|
{
|
|
var balance = await stockRepo.GetBalanceAsync(item.FK_ItemId, vehicleId.Value);
|
|
balance!.Quantity -= item.Quantity;
|
|
balance.UpdatedAt = DateTime.Now;
|
|
stockRepo.UpdateBalance(balance);
|
|
|
|
await stockRepo.AddMovementAsync(new StockMovement
|
|
{
|
|
FK_ItemId = item.FK_ItemId,
|
|
FromLocationId = vehicleId.Value,
|
|
FromLocationType = LocationType.Vehicle,
|
|
Quantity = item.Quantity,
|
|
Type = MovementType.Sale,
|
|
ReferenceId = order.Id
|
|
});
|
|
}
|
|
}
|
|
|
|
await uow.CommitAsync(ct);
|
|
}
|
|
catch
|
|
{
|
|
await uow.RollbackAsync(ct);
|
|
throw;
|
|
}
|
|
|
|
return ToResponse(sale, order, orderItems);
|
|
}
|
|
}
|