246 lines
8.7 KiB
C#
246 lines
8.7 KiB
C#
using Distribution.Features.Scheme;
|
|
using Distribution.Infra.Authorization;
|
|
using Distribution.Infra.UoW;
|
|
using Distribution.Models;
|
|
|
|
namespace Distribution.Features.Store;
|
|
|
|
public class StoreService(IStoreRepository repo, IUnitOfWork uow, ICurrentUserService currentUser, IScopeAccessService scopeAccess, SchemeService schemeService)
|
|
{
|
|
private const int GlobalScopeRoleLevel = 50;
|
|
private const int CreditApproverRoleLevel = 20;
|
|
|
|
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 static StoreResponse ToResponse(Models.Store s) =>
|
|
new(s.Id, s.FK_RouteId, s.OwnerName, s.StoreName, s.StoreAddress, s.Latitude, s.Longitude, s.OneTimeAmount, s.CreditAmount,
|
|
s.CreditLimit, s.OutstandingBalance, s.Segment, s.KycStatus, s.IsActive, s.CreatedAt);
|
|
|
|
public async Task<List<StoreResponse>> GetAllAsync()
|
|
{
|
|
var stores = (await repo.GetAllAsync()).Where(s => s.IsActive).ToList();
|
|
if (!HasGlobalScope && currentUser.UserId is not null)
|
|
{
|
|
var accessibleStoreIds = (await scopeAccess.GetAccessibleStoreIdsAsync(currentUser.UserId.Value)).ToHashSet();
|
|
stores = stores.Where(s => accessibleStoreIds.Contains(s.Id)).ToList();
|
|
}
|
|
return stores.Select(ToResponse).ToList();
|
|
}
|
|
|
|
public async Task<StoreResponse?> GetByIdAsync(Guid id)
|
|
{
|
|
var store = await repo.GetByIdAsync(id);
|
|
if (store is null || !store.IsActive) return null;
|
|
if (!await HasStoreScopeAsync(id)) return null;
|
|
return ToResponse(store);
|
|
}
|
|
|
|
public async Task<StoreResponse> CreateAsync(CreateStoreRequest request, CancellationToken ct = default)
|
|
{
|
|
var store = new Models.Store
|
|
{
|
|
FK_RouteId = request.RouteId,
|
|
OwnerName = request.OwnerName,
|
|
StoreName = request.StoreName,
|
|
StoreAddress = request.StoreAddress,
|
|
Latitude = request.Latitude,
|
|
Longitude = request.Longitude,
|
|
OneTimeAmount = request.OneTimeAmount,
|
|
CreditAmount = request.CreditAmount
|
|
};
|
|
await uow.BeginAsync(ct);
|
|
try
|
|
{
|
|
await repo.AddAsync(store);
|
|
await uow.CommitAsync(ct);
|
|
}
|
|
catch
|
|
{
|
|
await uow.RollbackAsync(ct);
|
|
throw;
|
|
}
|
|
return ToResponse(store);
|
|
}
|
|
|
|
public async Task<StoreResponse?> UpdateAsync(Guid id, UpdateStoreRequest request, CancellationToken ct = default)
|
|
{
|
|
var store = await repo.GetByIdAsync(id);
|
|
if (store is null || !store.IsActive) return null;
|
|
if (!await HasStoreScopeAsync(id)) return null;
|
|
store.FK_RouteId = request.RouteId;
|
|
store.OwnerName = request.OwnerName;
|
|
store.StoreName = request.StoreName;
|
|
store.StoreAddress = request.StoreAddress;
|
|
store.Latitude = request.Latitude;
|
|
store.Longitude = request.Longitude;
|
|
store.OneTimeAmount = request.OneTimeAmount;
|
|
store.CreditAmount = request.CreditAmount;
|
|
await uow.BeginAsync(ct);
|
|
try
|
|
{
|
|
repo.Update(store);
|
|
await uow.CommitAsync(ct);
|
|
}
|
|
catch
|
|
{
|
|
await uow.RollbackAsync(ct);
|
|
throw;
|
|
}
|
|
return ToResponse(store);
|
|
}
|
|
|
|
public async Task<bool> DeleteAsync(Guid id, CancellationToken ct = default)
|
|
{
|
|
var store = await repo.GetByIdAsync(id);
|
|
if (store is null || !store.IsActive) return false;
|
|
if (!await HasStoreScopeAsync(id)) return false;
|
|
await uow.BeginAsync(ct);
|
|
try
|
|
{
|
|
repo.Remove(store);
|
|
await uow.CommitAsync(ct);
|
|
}
|
|
catch
|
|
{
|
|
await uow.RollbackAsync(ct);
|
|
throw;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
public async Task<StoreResponse?> UpdateCreditLimitAsync(Guid id, UpdateCreditLimitRequest request, CancellationToken ct = default)
|
|
{
|
|
if (currentUser.RoleLevel < CreditApproverRoleLevel) return null;
|
|
var store = await repo.GetByIdAsync(id);
|
|
if (store is null || !store.IsActive) return null;
|
|
if (!await HasStoreScopeAsync(id)) return null;
|
|
store.CreditLimit = request.NewLimit;
|
|
await uow.BeginAsync(ct);
|
|
try
|
|
{
|
|
repo.Update(store);
|
|
await uow.CommitAsync(ct);
|
|
}
|
|
catch
|
|
{
|
|
await uow.RollbackAsync(ct);
|
|
throw;
|
|
}
|
|
return ToResponse(store);
|
|
}
|
|
|
|
public async Task<StoreResponse?> UpdateOutstandingBalanceAsync(Guid id, UpdateOutstandingBalanceRequest request, CancellationToken ct = default)
|
|
{
|
|
if (currentUser.RoleLevel < CreditApproverRoleLevel) return null;
|
|
var store = await repo.GetByIdAsync(id);
|
|
if (store is null || !store.IsActive) return null;
|
|
if (!await HasStoreScopeAsync(id)) return null;
|
|
store.OutstandingBalance = request.NewBalance;
|
|
await uow.BeginAsync(ct);
|
|
try
|
|
{
|
|
repo.Update(store);
|
|
await uow.CommitAsync(ct);
|
|
}
|
|
catch
|
|
{
|
|
await uow.RollbackAsync(ct);
|
|
throw;
|
|
}
|
|
return ToResponse(store);
|
|
}
|
|
|
|
public async Task<OutstandingBalanceResponse?> GetOutstandingBalanceAsync(Guid id)
|
|
{
|
|
var store = await repo.GetByIdAsync(id);
|
|
if (store is null || !store.IsActive) return null;
|
|
if (!await HasStoreScopeAsync(id)) return null;
|
|
return new OutstandingBalanceResponse(store.Id, store.CreditLimit, store.OutstandingBalance, store.CreditLimit - store.OutstandingBalance);
|
|
}
|
|
|
|
public async Task<StoreResponse?> VerifyKycAsync(Guid id, VerifyKycRequest request, CancellationToken ct = default)
|
|
{
|
|
var store = await repo.GetByIdAsync(id);
|
|
if (store is null || !store.IsActive) return null;
|
|
if (!await HasStoreScopeAsync(id)) return null;
|
|
store.KycStatus = request.Status;
|
|
await uow.BeginAsync(ct);
|
|
try
|
|
{
|
|
repo.Update(store);
|
|
await uow.CommitAsync(ct);
|
|
}
|
|
catch
|
|
{
|
|
await uow.RollbackAsync(ct);
|
|
throw;
|
|
}
|
|
return ToResponse(store);
|
|
}
|
|
|
|
public async Task<StoreResponse?> ClassifySegmentAsync(Guid id, ClassifySegmentRequest request, CancellationToken ct = default)
|
|
{
|
|
var store = await repo.GetByIdAsync(id);
|
|
if (store is null || !store.IsActive) return null;
|
|
if (!await HasStoreScopeAsync(id)) return null;
|
|
store.Segment = request.Segment;
|
|
await uow.BeginAsync(ct);
|
|
try
|
|
{
|
|
repo.Update(store);
|
|
await uow.CommitAsync(ct);
|
|
}
|
|
catch
|
|
{
|
|
await uow.RollbackAsync(ct);
|
|
throw;
|
|
}
|
|
return ToResponse(store);
|
|
}
|
|
|
|
public async Task<List<StoreVisitHistoryItem>?> GetVisitHistoryAsync(Guid id)
|
|
{
|
|
var store = await repo.GetByIdAsync(id);
|
|
if (store is null || !store.IsActive) return null;
|
|
if (!await HasStoreScopeAsync(id)) return null;
|
|
// Wired to Step 11 (Route & Sales Management) once ride/visit tracking exists.
|
|
return [];
|
|
}
|
|
|
|
public async Task<SchemeEligibilityResponse?> CheckSchemeEligibilityAsync(Guid id, Guid schemeId)
|
|
{
|
|
var store = await repo.GetByIdAsync(id);
|
|
if (store is null || !store.IsActive) return null;
|
|
if (!await HasStoreScopeAsync(id)) return null;
|
|
|
|
var result = await schemeService.CheckStoreScopeEligibilityAsync(id, schemeId);
|
|
if (result is null) return null;
|
|
return new SchemeEligibilityResponse(result.Value.Eligible, result.Value.Reason);
|
|
}
|
|
|
|
public async Task<StoreDashboardResponse?> GetDashboardAsync(Guid id, string period)
|
|
{
|
|
var store = await repo.GetByIdAsync(id);
|
|
if (store is null || !store.IsActive) return null;
|
|
if (!await HasStoreScopeAsync(id)) return null;
|
|
|
|
if (!DateTime.TryParseExact(period, "yyyy-MM", null, System.Globalization.DateTimeStyles.None, out var periodStart))
|
|
periodStart = new DateTime(DateTime.Now.Year, DateTime.Now.Month, 1);
|
|
var periodEndExclusive = periodStart.AddMonths(1);
|
|
|
|
var orders = await repo.GetOrdersForDashboardAsync(id, periodStart, periodEndExclusive);
|
|
return new StoreDashboardResponse(
|
|
id,
|
|
periodStart.ToString("yyyy-MM"),
|
|
orders.Count,
|
|
orders.Sum(o => o.TotalAmount),
|
|
orders.Count == 0 ? null : orders.Max(o => o.OrderDate));
|
|
}
|
|
}
|