diff --git a/Distribution/Features/Store/IStoreRepository.cs b/Distribution/Features/Store/IStoreRepository.cs index e8d178d..6633023 100644 --- a/Distribution/Features/Store/IStoreRepository.cs +++ b/Distribution/Features/Store/IStoreRepository.cs @@ -1,3 +1,5 @@ +using Distribution.Models; + namespace Distribution.Features.Store; public interface IStoreRepository @@ -5,6 +7,8 @@ public interface IStoreRepository Task GetByIdAsync(Guid id); Task GetByIdWithRegionAsync(Guid id); Task> GetAllAsync(); + Task> GetAllAsync(Guid? routeId, KycStatus? kycStatus, string? search, int page, int pageSize); + Task GetCountAsync(Guid? routeId, KycStatus? kycStatus, string? search); Task AddAsync(Models.Store entity); void Update(Models.Store entity); void Remove(Models.Store entity); diff --git a/Distribution/Features/Store/StoreController.cs b/Distribution/Features/Store/StoreController.cs index 4cadec8..fd5a1b3 100644 --- a/Distribution/Features/Store/StoreController.cs +++ b/Distribution/Features/Store/StoreController.cs @@ -1,4 +1,5 @@ -using Microsoft.AspNetCore.Mvc; +using Distribution.Models; +using Microsoft.AspNetCore.Mvc; namespace Distribution.Features.Store; @@ -7,8 +8,13 @@ namespace Distribution.Features.Store; public class StoreController(StoreService service) : ControllerBase { [HttpGet] - public async Task GetAll() => - Ok(await service.GetAllAsync()); + public async Task GetAll( + [FromQuery] Guid? routeId, + [FromQuery] KycStatus? kycStatus, + [FromQuery] string? search, + [FromQuery] int page = 1, + [FromQuery] int pageSize = 20) => + Ok(await service.GetAllAsync(routeId, kycStatus, search, page, pageSize)); [HttpGet("{id:guid}")] public async Task GetById(Guid id) @@ -100,4 +106,11 @@ public class StoreController(StoreService service) : ControllerBase var result = await service.VerifyKycAsync(id, new VerifyKycRequest(request.KycStatus, null), ct); return result is null ? NotFound() : Ok(result); } + + [HttpPatch("{id:guid}/status")] + public async Task PatchStatus(Guid id, PatchStoreStatusRequest request, CancellationToken ct) + { + var result = await service.SetActiveStatusAsync(id, request.IsActive, ct); + return result is null ? NotFound() : Ok(result); + } } diff --git a/Distribution/Features/Store/StoreDTOs.cs b/Distribution/Features/Store/StoreDTOs.cs index 5969f93..3609c34 100644 --- a/Distribution/Features/Store/StoreDTOs.cs +++ b/Distribution/Features/Store/StoreDTOs.cs @@ -20,7 +20,8 @@ public record UpdateStoreRequest( decimal Latitude, decimal Longitude, decimal OneTimeAmount, - decimal CreditAmount); + decimal CreditAmount, + bool IsActive = true); public record StoreResponse( Guid Id, @@ -55,4 +56,6 @@ public record SchemeEligibilityResponse(bool Eligible, string? Reason); public record PatchKycStatusRequest(KycStatus KycStatus); +public record PatchStoreStatusRequest(bool IsActive); + public record StoreDashboardResponse(Guid StoreId, string Period, int OrderCount, decimal TotalOrderValue, DateTime? LastOrderDate); diff --git a/Distribution/Features/Store/StoreRepository.cs b/Distribution/Features/Store/StoreRepository.cs index 9c44537..6e28d24 100644 --- a/Distribution/Features/Store/StoreRepository.cs +++ b/Distribution/Features/Store/StoreRepository.cs @@ -1,4 +1,5 @@ using Distribution.Data; +using Distribution.Models; using Microsoft.EntityFrameworkCore; namespace Distribution.Features.Store; @@ -16,6 +17,25 @@ public class StoreRepository(AppDBContext context) : IStoreRepository public async Task> GetAllAsync() => await context.Stores.ToListAsync(); + private static IQueryable ApplyFilters( + IQueryable query, Guid? routeId, KycStatus? kycStatus, string? search) => + query + .Where(s => routeId == null || s.FK_RouteId == routeId) + .Where(s => kycStatus == null || s.KycStatus == kycStatus) + .Where(s => string.IsNullOrWhiteSpace(search) + || s.StoreName.Contains(search) + || s.OwnerName.Contains(search)); + + public async Task> GetAllAsync(Guid? routeId, KycStatus? kycStatus, string? search, int page, int pageSize) => + await ApplyFilters(context.Stores, routeId, kycStatus, search) + .OrderBy(s => s.StoreName) + .Skip((page - 1) * pageSize) + .Take(pageSize) + .ToListAsync(); + + public async Task GetCountAsync(Guid? routeId, KycStatus? kycStatus, string? search) => + await ApplyFilters(context.Stores, routeId, kycStatus, search).CountAsync(); + public async Task AddAsync(Models.Store entity) => await context.Stores.AddAsync(entity); diff --git a/Distribution/Features/Store/StoreService.cs b/Distribution/Features/Store/StoreService.cs index 9b71e77..bcb1c72 100644 --- a/Distribution/Features/Store/StoreService.cs +++ b/Distribution/Features/Store/StoreService.cs @@ -1,4 +1,5 @@ using Distribution.Features.Scheme; +using Distribution.Features.Stock; using Distribution.Infra.Authorization; using Distribution.Infra.UoW; using Distribution.Models; @@ -23,21 +24,20 @@ public class StoreService(IStoreRepository repo, IUnitOfWork uow, ICurrentUserSe 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> GetAllAsync() + public async Task> GetAllAsync( + Guid? routeId, KycStatus? kycStatus, string? search, int page, int pageSize) { - 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(); + // 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, kycStatus, search); + var stores = await repo.GetAllAsync(routeId, kycStatus, search, page, pageSize); + return new PagedResponse(page, pageSize, total, stores.Select(ToResponse).ToList()); } public async Task GetByIdAsync(Guid id) { var store = await repo.GetByIdAsync(id); - if (store is null || !store.IsActive) return null; + if (store is null) return null; //if (!await HasStoreScopeAsync(id)) return null; return ToResponse(store); } @@ -72,7 +72,7 @@ public class StoreService(IStoreRepository repo, IUnitOfWork uow, ICurrentUserSe public async Task UpdateAsync(Guid id, UpdateStoreRequest request, CancellationToken ct = default) { var store = await repo.GetByIdAsync(id); - if (store is null || !store.IsActive) return null; + if (store is null) return null; if (!await HasStoreScopeAsync(id)) return null; store.FK_RouteId = request.RouteId; store.OwnerName = request.OwnerName; @@ -82,6 +82,7 @@ public class StoreService(IStoreRepository repo, IUnitOfWork uow, ICurrentUserSe store.Longitude = request.Longitude; store.OneTimeAmount = request.OneTimeAmount; store.CreditAmount = request.CreditAmount; + store.IsActive = request.IsActive; await uow.BeginAsync(ct); try { @@ -205,10 +206,32 @@ public class StoreService(IStoreRepository repo, IUnitOfWork uow, ICurrentUserSe return ToResponse(store); } + public async Task 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?> GetVisitHistoryAsync(Guid id) { var store = await repo.GetByIdAsync(id); - if (store is null || !store.IsActive) return null; + if (store is null) return null; if (!await HasStoreScopeAsync(id)) return null; var visits = await routeRepo.GetVisitsByStoreIdAsync(id);