fix store profile fields

This commit is contained in:
2026-09-24 14:18:52 +05:30
parent 80db899091
commit 96e12972b9
14 changed files with 41 additions and 219 deletions
@@ -7,8 +7,8 @@ public interface IStoreRepository
Task<Models.Store?> GetByIdAsync(Guid id);
Task<Models.Store?> GetByIdWithRegionAsync(Guid id);
Task<List<Models.Store>> GetAllAsync();
Task<List<Models.Store>> GetAllAsync(Guid? routeId, KycStatus? kycStatus, string? search, int page, int pageSize);
Task<int> GetCountAsync(Guid? routeId, KycStatus? kycStatus, string? search);
Task<List<Models.Store>> GetAllAsync(Guid? routeId, string? search, int page, int pageSize);
Task<int> GetCountAsync(Guid? routeId, string? search);
Task AddAsync(Models.Store entity);
void Update(Models.Store entity);
void Remove(Models.Store entity);
@@ -1,7 +0,0 @@
namespace Distribution.Features.Store.Kyc;
public interface IStoreKycDocumentRepository
{
Task<List<Models.StoreKycDocument>> GetByStoreIdAsync(Guid storeId);
Task AddAsync(Models.StoreKycDocument entity);
}
@@ -1,22 +0,0 @@
using Microsoft.AspNetCore.Mvc;
namespace Distribution.Features.Store.Kyc;
[ApiController]
[Route("api/stores/{storeId:guid}/kyc-documents")]
public class StoreKycDocumentController(StoreKycDocumentService service) : ControllerBase
{
[HttpGet]
public async Task<IActionResult> GetByStoreId(Guid storeId)
{
var result = await service.GetByStoreIdAsync(storeId);
return result is null ? NotFound() : Ok(result);
}
[HttpPost]
public async Task<IActionResult> Upload(Guid storeId, [FromForm] UploadKycDocumentRequest request, CancellationToken ct)
{
var result = await service.UploadAsync(storeId, request, ct);
return result is null ? NotFound() : Ok(result);
}
}
@@ -1,5 +0,0 @@
namespace Distribution.Features.Store.Kyc;
public record UploadKycDocumentRequest(string DocumentType, IFormFile File);
public record StoreKycDocumentResponse(Guid Id, Guid StoreId, string DocumentType, string FileReference, DateTime UploadedAt);
@@ -1,13 +0,0 @@
using Distribution.Data;
using Microsoft.EntityFrameworkCore;
namespace Distribution.Features.Store.Kyc;
public class StoreKycDocumentRepository(AppDBContext context) : IStoreKycDocumentRepository
{
public async Task<List<Models.StoreKycDocument>> GetByStoreIdAsync(Guid storeId) =>
await context.StoreKycDocuments.Where(d => d.FK_StoreId == storeId && d.IsActive).ToListAsync();
public async Task AddAsync(Models.StoreKycDocument entity) =>
await context.StoreKycDocuments.AddAsync(entity);
}
@@ -1,71 +0,0 @@
using Distribution.Infra.Authorization;
using Distribution.Infra.UoW;
namespace Distribution.Features.Store.Kyc;
public class StoreKycDocumentService(
IStoreKycDocumentRepository repo,
IStoreRepository storeRepo,
IUnitOfWork uow,
ICurrentUserService currentUser,
IScopeAccessService scopeAccess,
IWebHostEnvironment env)
{
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 static StoreKycDocumentResponse ToResponse(Models.StoreKycDocument d) =>
new(d.Id, d.FK_StoreId, d.DocumentType, d.FileReference, d.UploadedAt);
public async Task<List<StoreKycDocumentResponse>?> GetByStoreIdAsync(Guid storeId)
{
if (!await HasStoreScopeAsync(storeId)) return null;
var docs = await repo.GetByStoreIdAsync(storeId);
return docs.Select(ToResponse).ToList();
}
public async Task<StoreKycDocumentResponse?> UploadAsync(Guid storeId, UploadKycDocumentRequest request, CancellationToken ct = default)
{
var store = await storeRepo.GetByIdAsync(storeId);
if (store is null || !store.IsActive) return null;
if (!await HasStoreScopeAsync(storeId)) return null;
var uploadsRoot = Path.Combine(env.ContentRootPath, "wwwroot", "uploads", "kyc", storeId.ToString());
Directory.CreateDirectory(uploadsRoot);
var fileName = $"{Guid.NewGuid()}{Path.GetExtension(request.File.FileName)}";
var filePath = Path.Combine(uploadsRoot, fileName);
await using (var stream = new FileStream(filePath, FileMode.Create))
{
await request.File.CopyToAsync(stream, ct);
}
var fileReference = $"uploads/kyc/{storeId}/{fileName}";
var doc = new Models.StoreKycDocument
{
FK_StoreId = storeId,
DocumentType = request.DocumentType,
FileReference = fileReference,
UploadedAt = DateTime.Now
};
await uow.BeginAsync(ct);
try
{
await repo.AddAsync(doc);
await uow.CommitAsync(ct);
}
catch
{
await uow.RollbackAsync(ct);
throw;
}
return ToResponse(doc);
}
}
+1 -16
View File
@@ -10,11 +10,10 @@ public class StoreController(StoreService service) : ControllerBase
[HttpGet]
public async Task<IActionResult> 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));
Ok(await service.GetAllAsync(routeId, search, page, pageSize));
[HttpGet("{id:guid}")]
public async Task<IActionResult> GetById(Guid id)
@@ -65,13 +64,6 @@ public class StoreController(StoreService service) : ControllerBase
return result is null ? NotFound() : Ok(result);
}
[HttpPut("{id:guid}/kyc/verify")]
public async Task<IActionResult> VerifyKyc(Guid id, VerifyKycRequest request, CancellationToken ct)
{
var result = await service.VerifyKycAsync(id, request, ct);
return result is null ? NotFound() : Ok(result);
}
[HttpPut("{id:guid}/segment")]
public async Task<IActionResult> ClassifySegment(Guid id, ClassifySegmentRequest request, CancellationToken ct)
{
@@ -100,13 +92,6 @@ public class StoreController(StoreService service) : ControllerBase
return result is null ? NotFound() : Ok(result);
}
[HttpPatch("{id:guid}/kyc-status")]
public async Task<IActionResult> PatchKycStatus(Guid id, PatchKycStatusRequest request, CancellationToken ct)
{
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<IActionResult> PatchStatus(Guid id, PatchStoreStatusRequest request, CancellationToken ct)
{
+18 -18
View File
@@ -4,29 +4,34 @@ namespace Distribution.Features.Store;
public record CreateStoreRequest(
Guid RouteId,
string OwnerName,
string StoreName,
string StoreAddress,
decimal Latitude,
decimal Longitude,
decimal OneTimeAmount,
decimal CreditAmount);
public record UpdateStoreRequest(
Guid RouteId,
string OwnerName,
string? OwnerName,
string StoreName,
string StoreAddress,
decimal Latitude,
decimal Longitude,
decimal OneTimeAmount,
decimal CreditAmount,
bool IsActive = true);
decimal CreditLimit = 0,
string? ContactNumber = null);
public record UpdateStoreRequest(
Guid RouteId,
string? OwnerName,
string StoreName,
string StoreAddress,
decimal Latitude,
decimal Longitude,
decimal OneTimeAmount,
decimal CreditAmount,
bool IsActive = true,
decimal? CreditLimit = null,
string? ContactNumber = null);
public record StoreResponse(
Guid Id,
Guid RouteId,
string OwnerName,
string? OwnerName,
string? ContactNumber,
string StoreName,
string StoreAddress,
decimal Latitude,
@@ -36,7 +41,6 @@ public record StoreResponse(
decimal CreditLimit,
decimal OutstandingBalance,
string? Segment,
KycStatus KycStatus,
bool IsActive,
DateTime CreatedAt);
@@ -44,8 +48,6 @@ public record UpdateCreditLimitRequest(decimal NewLimit, Guid ApproverId);
public record UpdateOutstandingBalanceRequest(decimal NewBalance);
public record VerifyKycRequest(KycStatus Status, string? Notes);
public record ClassifySegmentRequest(string Segment);
public record OutstandingBalanceResponse(Guid StoreId, decimal CreditLimit, decimal OutstandingBalance, decimal AvailableCredit);
@@ -54,8 +56,6 @@ public record StoreVisitHistoryItem(Guid VisitId, DateTime VisitDate, Guid RepUs
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);
@@ -18,23 +18,22 @@ public class StoreRepository(AppDBContext context) : IStoreRepository
await context.Stores.ToListAsync();
private static IQueryable<Models.Store> ApplyFilters(
IQueryable<Models.Store> query, Guid? routeId, KycStatus? kycStatus, string? search) =>
IQueryable<Models.Store> query, Guid? routeId, 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));
|| (s.OwnerName != null && s.OwnerName.Contains(search)));
public async Task<List<Models.Store>> GetAllAsync(Guid? routeId, KycStatus? kycStatus, string? search, int page, int pageSize) =>
await ApplyFilters(context.Stores, routeId, kycStatus, search)
public async Task<List<Models.Store>> GetAllAsync(Guid? routeId, string? search, int page, int pageSize) =>
await ApplyFilters(context.Stores, routeId, search)
.OrderBy(s => s.StoreName)
.Skip((page - 1) * pageSize)
.Take(pageSize)
.ToListAsync();
public async Task<int> GetCountAsync(Guid? routeId, KycStatus? kycStatus, string? search) =>
await ApplyFilters(context.Stores, routeId, kycStatus, search).CountAsync();
public async Task<int> GetCountAsync(Guid? routeId, string? search) =>
await ApplyFilters(context.Stores, routeId, search).CountAsync();
public async Task AddAsync(Models.Store entity) =>
await context.Stores.AddAsync(entity);
+10 -27
View File
@@ -21,16 +21,16 @@ public class StoreService(IStoreRepository repo, IUnitOfWork uow, ICurrentUserSe
}
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);
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, KycStatus? kycStatus, string? search, int page, int pageSize)
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, kycStatus, search);
var stores = await repo.GetAllAsync(routeId, kycStatus, search, page, pageSize);
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());
}
@@ -48,12 +48,14 @@ public class StoreService(IStoreRepository repo, IUnitOfWork uow, ICurrentUserSe
{
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
CreditAmount = request.CreditAmount,
CreditLimit = request.CreditLimit
};
await uow.BeginAsync(ct);
try
@@ -76,12 +78,14 @@ public class StoreService(IStoreRepository repo, IUnitOfWork uow, ICurrentUserSe
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
@@ -165,27 +169,6 @@ public class StoreService(IStoreRepository repo, IUnitOfWork uow, ICurrentUserSe
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);
-1
View File
@@ -4,7 +4,6 @@ namespace Distribution.Models
{ Warehouse, Vehicle, External }
public enum KycStatus { Pending, Verified, Rejected }
public enum MovementType { Sale, CustomerReturn, Transfer, Damage, Adjustment, Load, PurchaseReceipt, PurchaseReturn }
+2 -2
View File
@@ -4,7 +4,8 @@ namespace Distribution.Models
{
public class Store : Template
{
public String OwnerName { get; set; }
public String? OwnerName { get; set; }
public String? ContactNumber { get; set; }
public String StoreName { get; set; }
public String StoreAddress { get; set; }
public decimal Latitude { get; set; } = 0;
@@ -14,7 +15,6 @@ namespace Distribution.Models
public decimal CreditLimit { get; set; } = 0;
public decimal OutstandingBalance { get; set; } = 0;
public String? Segment { get; set; } //Gold ,silver,key Account,high risk
public KycStatus KycStatus { get; set; } = KycStatus.Pending;
public Guid FK_RouteId { get; set; }
[ForeignKey("FK_RouteId")]
-14
View File
@@ -1,14 +0,0 @@
using System.ComponentModel.DataAnnotations.Schema;
namespace Distribution.Models
{
public class StoreKycDocument : Template
{
public Guid FK_StoreId { get; set; }
[ForeignKey("FK_StoreId")]
public Store Store { get; set; }
public String DocumentType { get; set; }
public String FileReference { get; set; }
public DateTime UploadedAt { get; set; } = DateTime.Now;
}
}
+2 -14
View File
@@ -2,11 +2,9 @@ import { api } from '@/lib/api'
import type {
PaginatedResponse,
StoreResponse,
KycDocumentResponse,
OutstandingBalanceResponse,
StoreDashboardResponse,
StoreVisitHistoryItem,
KycStatus,
} from '@/types/api'
import type { PageParams } from './master'
@@ -15,7 +13,8 @@ import type { PageParams } from './master'
export interface StoreBody {
routeId: string
ownerName: string
ownerName?: string | null
contactNumber?: string | null
storeName: string
storeAddress: string
latitude: number
@@ -28,7 +27,6 @@ export interface StoreBody {
export interface StoreListParams extends PageParams {
routeId?: string
kycStatus?: KycStatus
search?: string
}
@@ -53,16 +51,6 @@ export const stores = {
dashboard: (id: string, period?: string) =>
api.get<StoreDashboardResponse>(`/stores/${id}/dashboard`, { params: { period } }).then(r => r.data),
kycDocuments: (id: string) =>
api.get<KycDocumentResponse[]>(`/stores/${id}/kyc-documents`).then(r => r.data),
uploadKycDocument: (id: string, documentType: string, fileReference: string) =>
api.post<KycDocumentResponse>(`/stores/${id}/kyc-documents`, { documentType, fileReference }).then(r => r.data),
updateKycStatus: (id: string, kycStatus: KycStatus) =>
api.patch<StoreResponse>(`/stores/${id}/kyc-status`, { kycStatus }).then(r => r.data),
visitHistory: (id: string) =>
api.get<StoreVisitHistoryItem[]>(`/stores/${id}/visit-history`).then(r => r.data),
}