Add store management workflows and KYC order gating
This commit is contained in:
@@ -1,11 +1,13 @@
|
||||
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)
|
||||
{
|
||||
private const int GlobalScopeRoleLevel = 50;
|
||||
private const int CreditApproverRoleLevel = 20;
|
||||
|
||||
private bool HasGlobalScope => currentUser.RoleLevel >= GlobalScopeRoleLevel;
|
||||
|
||||
@@ -15,6 +17,10 @@ public class StoreService(IStoreRepository repo, IUnitOfWork uow, ICurrentUserSe
|
||||
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();
|
||||
@@ -23,7 +29,7 @@ public class StoreService(IStoreRepository repo, IUnitOfWork uow, ICurrentUserSe
|
||||
var accessibleStoreIds = (await scopeAccess.GetAccessibleStoreIdsAsync(currentUser.UserId.Value)).ToHashSet();
|
||||
stores = stores.Where(s => accessibleStoreIds.Contains(s.Id)).ToList();
|
||||
}
|
||||
return stores.Select(s => new StoreResponse(s.Id, s.FK_RouteId, s.OwnerName, s.StoreName, s.StoreAddress, s.Latitude, s.Longitude, s.OneTimeAmount, s.CreditAmount, s.IsActive, s.CreatedAt)).ToList();
|
||||
return stores.Select(ToResponse).ToList();
|
||||
}
|
||||
|
||||
public async Task<StoreResponse?> GetByIdAsync(Guid id)
|
||||
@@ -31,7 +37,7 @@ public class StoreService(IStoreRepository repo, IUnitOfWork uow, ICurrentUserSe
|
||||
var store = await repo.GetByIdAsync(id);
|
||||
if (store is null || !store.IsActive) return null;
|
||||
if (!await HasStoreScopeAsync(id)) return null;
|
||||
return new StoreResponse(store.Id, store.FK_RouteId, store.OwnerName, store.StoreName, store.StoreAddress, store.Latitude, store.Longitude, store.OneTimeAmount, store.CreditAmount, store.IsActive, store.CreatedAt);
|
||||
return ToResponse(store);
|
||||
}
|
||||
|
||||
public async Task<StoreResponse> CreateAsync(CreateStoreRequest request, CancellationToken ct = default)
|
||||
@@ -58,7 +64,7 @@ public class StoreService(IStoreRepository repo, IUnitOfWork uow, ICurrentUserSe
|
||||
await uow.RollbackAsync(ct);
|
||||
throw;
|
||||
}
|
||||
return new StoreResponse(store.Id, store.FK_RouteId, store.OwnerName, store.StoreName, store.StoreAddress, store.Latitude, store.Longitude, store.OneTimeAmount, store.CreditAmount, store.IsActive, store.CreatedAt);
|
||||
return ToResponse(store);
|
||||
}
|
||||
|
||||
public async Task<StoreResponse?> UpdateAsync(Guid id, UpdateStoreRequest request, CancellationToken ct = default)
|
||||
@@ -85,7 +91,7 @@ public class StoreService(IStoreRepository repo, IUnitOfWork uow, ICurrentUserSe
|
||||
await uow.RollbackAsync(ct);
|
||||
throw;
|
||||
}
|
||||
return new StoreResponse(store.Id, store.FK_RouteId, store.OwnerName, store.StoreName, store.StoreAddress, store.Latitude, store.Longitude, store.OneTimeAmount, store.CreditAmount, store.IsActive, store.CreatedAt);
|
||||
return ToResponse(store);
|
||||
}
|
||||
|
||||
public async Task<bool> DeleteAsync(Guid id, CancellationToken ct = default)
|
||||
@@ -106,4 +112,131 @@ public class StoreService(IStoreRepository repo, IUnitOfWork uow, ICurrentUserSe
|
||||
}
|
||||
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;
|
||||
// Wired to Step 17 (Promotions & Schemes) once scheme rules exist.
|
||||
return new SchemeEligibilityResponse(true, null);
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user