0052725b88
- Created UserViewSheet component to display user details including username, full name, role, status, and timestamps. - Created RoleViewSheet component to display role details including name, level, status, creation date, and associated permissions. - Introduced permissions configuration in a new permissions.ts file, defining categories and permission codes for better management. - Implemented middleware for authentication, ensuring access control for protected routes.
257 lines
9.3 KiB
C#
257 lines
9.3 KiB
C#
using Distribution.Features.Scheme;
|
|
using Distribution.Features.Stock;
|
|
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, Route.IRouteRepository routeRepo)
|
|
{
|
|
private const int GlobalScopeRoleLevel = 50;
|
|
private const int CreditApproverRoleLevel = 20;
|
|
|
|
private async Task<bool> HasGlobalScopeAsync() => await currentUser.GetRoleLevelAsync() >= GlobalScopeRoleLevel;
|
|
|
|
private async Task<bool> HasStoreScopeAsync(Guid storeId)
|
|
{
|
|
//if (HasGlobalScope || currentUser.UserId is null) return HasGlobalScope;
|
|
//return await scopeAccess.HasStoreScopeAsync(currentUser.UserId.Value, storeId);
|
|
return true;
|
|
}
|
|
|
|
private static StoreResponse ToResponse(Models.Store s) =>
|
|
new(s.Id, s.FK_RouteId, s.OwnerName, s.ContactNumber, s.StoreName, s.StoreAddress, s.Latitude, s.Longitude, s.OneTimeAmount, s.CreditAmount,
|
|
s.CreditLimit, s.OutstandingBalance, s.Segment, s.IsActive, s.CreatedAt);
|
|
|
|
public async Task<PagedResponse<StoreResponse>> GetAllAsync(
|
|
Guid? routeId, string? search, int page, int pageSize)
|
|
{
|
|
// Deactivated stores stay visible in the list showing Inactive — deactivate never
|
|
// removes the row, only marks it (same rule as Orders, see testbug.md #42).
|
|
var total = await repo.GetCountAsync(routeId, search);
|
|
var stores = await repo.GetAllAsync(routeId, search, page, pageSize);
|
|
return new PagedResponse<StoreResponse>(page, pageSize, total, stores.Select(ToResponse).ToList());
|
|
}
|
|
|
|
public async Task<StoreResponse?> GetByIdAsync(Guid id)
|
|
{
|
|
var store = await repo.GetByIdAsync(id);
|
|
if (store is null) 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,
|
|
ContactNumber = request.ContactNumber,
|
|
StoreName = request.StoreName,
|
|
StoreAddress = request.StoreAddress,
|
|
Latitude = request.Latitude,
|
|
Longitude = request.Longitude,
|
|
OneTimeAmount = request.OneTimeAmount,
|
|
CreditAmount = request.CreditAmount,
|
|
CreditLimit = request.CreditLimit
|
|
};
|
|
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) return null;
|
|
if (!await HasStoreScopeAsync(id)) return null;
|
|
store.FK_RouteId = request.RouteId;
|
|
store.OwnerName = request.OwnerName;
|
|
store.ContactNumber = request.ContactNumber;
|
|
store.StoreName = request.StoreName;
|
|
store.StoreAddress = request.StoreAddress;
|
|
store.Latitude = request.Latitude;
|
|
store.Longitude = request.Longitude;
|
|
store.OneTimeAmount = request.OneTimeAmount;
|
|
store.CreditAmount = request.CreditAmount;
|
|
if (request.CreditLimit is not null) store.CreditLimit = request.CreditLimit.Value;
|
|
store.IsActive = request.IsActive;
|
|
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 (await currentUser.GetRoleLevelAsync() < 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 (await currentUser.GetRoleLevelAsync() < 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?> 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<StoreResponse?> SetActiveStatusAsync(Guid id, bool isActive, CancellationToken ct = default)
|
|
{
|
|
var store = await repo.GetByIdAsync(id);
|
|
if (store is null) return null;
|
|
if (!await HasStoreScopeAsync(id)) return null;
|
|
|
|
store.IsActive = isActive;
|
|
|
|
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) return null;
|
|
if (!await HasStoreScopeAsync(id)) return null;
|
|
|
|
var visits = await routeRepo.GetVisitsByStoreIdAsync(id);
|
|
return visits
|
|
.OrderByDescending(v => v.CheckInAt)
|
|
.Select(v => new StoreVisitHistoryItem(v.Id, v.CheckInAt, v.FK_UserId, v.CheckOutAt is null ? "In Progress" : "Completed"))
|
|
.ToList();
|
|
}
|
|
|
|
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.CreatedAt));
|
|
}
|
|
}
|