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?> GetByIdAsync(Guid id);
Task<Models.Store?> GetByIdWithRegionAsync(Guid id); Task<Models.Store?> GetByIdWithRegionAsync(Guid id);
Task<List<Models.Store>> GetAllAsync(); Task<List<Models.Store>> GetAllAsync();
Task<List<Models.Store>> GetAllAsync(Guid? routeId, KycStatus? kycStatus, string? search, int page, int pageSize); Task<List<Models.Store>> GetAllAsync(Guid? routeId, string? search, int page, int pageSize);
Task<int> GetCountAsync(Guid? routeId, KycStatus? kycStatus, string? search); Task<int> GetCountAsync(Guid? routeId, string? search);
Task AddAsync(Models.Store entity); Task AddAsync(Models.Store entity);
void Update(Models.Store entity); void Update(Models.Store entity);
void Remove(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] [HttpGet]
public async Task<IActionResult> GetAll( public async Task<IActionResult> GetAll(
[FromQuery] Guid? routeId, [FromQuery] Guid? routeId,
[FromQuery] KycStatus? kycStatus,
[FromQuery] string? search, [FromQuery] string? search,
[FromQuery] int page = 1, [FromQuery] int page = 1,
[FromQuery] int pageSize = 20) => [FromQuery] int pageSize = 20) =>
Ok(await service.GetAllAsync(routeId, kycStatus, search, page, pageSize)); Ok(await service.GetAllAsync(routeId, search, page, pageSize));
[HttpGet("{id:guid}")] [HttpGet("{id:guid}")]
public async Task<IActionResult> GetById(Guid id) public async Task<IActionResult> GetById(Guid id)
@@ -65,13 +64,6 @@ public class StoreController(StoreService service) : ControllerBase
return result is null ? NotFound() : Ok(result); 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")] [HttpPut("{id:guid}/segment")]
public async Task<IActionResult> ClassifySegment(Guid id, ClassifySegmentRequest request, CancellationToken ct) 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); 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")] [HttpPatch("{id:guid}/status")]
public async Task<IActionResult> PatchStatus(Guid id, PatchStoreStatusRequest request, CancellationToken ct) 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( public record CreateStoreRequest(
Guid RouteId, Guid RouteId,
string OwnerName, string? OwnerName,
string StoreName,
string StoreAddress,
decimal Latitude,
decimal Longitude,
decimal OneTimeAmount,
decimal CreditAmount);
public record UpdateStoreRequest(
Guid RouteId,
string OwnerName,
string StoreName, string StoreName,
string StoreAddress, string StoreAddress,
decimal Latitude, decimal Latitude,
decimal Longitude, decimal Longitude,
decimal OneTimeAmount, decimal OneTimeAmount,
decimal CreditAmount, 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( public record StoreResponse(
Guid Id, Guid Id,
Guid RouteId, Guid RouteId,
string OwnerName, string? OwnerName,
string? ContactNumber,
string StoreName, string StoreName,
string StoreAddress, string StoreAddress,
decimal Latitude, decimal Latitude,
@@ -36,7 +41,6 @@ public record StoreResponse(
decimal CreditLimit, decimal CreditLimit,
decimal OutstandingBalance, decimal OutstandingBalance,
string? Segment, string? Segment,
KycStatus KycStatus,
bool IsActive, bool IsActive,
DateTime CreatedAt); DateTime CreatedAt);
@@ -44,8 +48,6 @@ public record UpdateCreditLimitRequest(decimal NewLimit, Guid ApproverId);
public record UpdateOutstandingBalanceRequest(decimal NewBalance); public record UpdateOutstandingBalanceRequest(decimal NewBalance);
public record VerifyKycRequest(KycStatus Status, string? Notes);
public record ClassifySegmentRequest(string Segment); public record ClassifySegmentRequest(string Segment);
public record OutstandingBalanceResponse(Guid StoreId, decimal CreditLimit, decimal OutstandingBalance, decimal AvailableCredit); 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 SchemeEligibilityResponse(bool Eligible, string? Reason);
public record PatchKycStatusRequest(KycStatus KycStatus);
public record PatchStoreStatusRequest(bool IsActive); public record PatchStoreStatusRequest(bool IsActive);
public record StoreDashboardResponse(Guid StoreId, string Period, int OrderCount, decimal TotalOrderValue, DateTime? LastOrderDate); 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(); await context.Stores.ToListAsync();
private static IQueryable<Models.Store> ApplyFilters( 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 query
.Where(s => routeId == null || s.FK_RouteId == routeId) .Where(s => routeId == null || s.FK_RouteId == routeId)
.Where(s => kycStatus == null || s.KycStatus == kycStatus)
.Where(s => string.IsNullOrWhiteSpace(search) .Where(s => string.IsNullOrWhiteSpace(search)
|| s.StoreName.Contains(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) => public async Task<List<Models.Store>> GetAllAsync(Guid? routeId, string? search, int page, int pageSize) =>
await ApplyFilters(context.Stores, routeId, kycStatus, search) await ApplyFilters(context.Stores, routeId, search)
.OrderBy(s => s.StoreName) .OrderBy(s => s.StoreName)
.Skip((page - 1) * pageSize) .Skip((page - 1) * pageSize)
.Take(pageSize) .Take(pageSize)
.ToListAsync(); .ToListAsync();
public async Task<int> GetCountAsync(Guid? routeId, KycStatus? kycStatus, string? search) => public async Task<int> GetCountAsync(Guid? routeId, string? search) =>
await ApplyFilters(context.Stores, routeId, kycStatus, search).CountAsync(); await ApplyFilters(context.Stores, routeId, search).CountAsync();
public async Task AddAsync(Models.Store entity) => public async Task AddAsync(Models.Store entity) =>
await context.Stores.AddAsync(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) => 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, 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.KycStatus, s.IsActive, s.CreatedAt); s.CreditLimit, s.OutstandingBalance, s.Segment, s.IsActive, s.CreatedAt);
public async Task<PagedResponse<StoreResponse>> GetAllAsync( 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 // 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). // removes the row, only marks it (same rule as Orders, see testbug.md #42).
var total = await repo.GetCountAsync(routeId, kycStatus, search); var total = await repo.GetCountAsync(routeId, search);
var stores = await repo.GetAllAsync(routeId, kycStatus, search, page, pageSize); var stores = await repo.GetAllAsync(routeId, search, page, pageSize);
return new PagedResponse<StoreResponse>(page, pageSize, total, stores.Select(ToResponse).ToList()); 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, FK_RouteId = request.RouteId,
OwnerName = request.OwnerName, OwnerName = request.OwnerName,
ContactNumber = request.ContactNumber,
StoreName = request.StoreName, StoreName = request.StoreName,
StoreAddress = request.StoreAddress, StoreAddress = request.StoreAddress,
Latitude = request.Latitude, Latitude = request.Latitude,
Longitude = request.Longitude, Longitude = request.Longitude,
OneTimeAmount = request.OneTimeAmount, OneTimeAmount = request.OneTimeAmount,
CreditAmount = request.CreditAmount CreditAmount = request.CreditAmount,
CreditLimit = request.CreditLimit
}; };
await uow.BeginAsync(ct); await uow.BeginAsync(ct);
try try
@@ -76,12 +78,14 @@ public class StoreService(IStoreRepository repo, IUnitOfWork uow, ICurrentUserSe
if (!await HasStoreScopeAsync(id)) return null; if (!await HasStoreScopeAsync(id)) return null;
store.FK_RouteId = request.RouteId; store.FK_RouteId = request.RouteId;
store.OwnerName = request.OwnerName; store.OwnerName = request.OwnerName;
store.ContactNumber = request.ContactNumber;
store.StoreName = request.StoreName; store.StoreName = request.StoreName;
store.StoreAddress = request.StoreAddress; store.StoreAddress = request.StoreAddress;
store.Latitude = request.Latitude; store.Latitude = request.Latitude;
store.Longitude = request.Longitude; store.Longitude = request.Longitude;
store.OneTimeAmount = request.OneTimeAmount; store.OneTimeAmount = request.OneTimeAmount;
store.CreditAmount = request.CreditAmount; store.CreditAmount = request.CreditAmount;
if (request.CreditLimit is not null) store.CreditLimit = request.CreditLimit.Value;
store.IsActive = request.IsActive; store.IsActive = request.IsActive;
await uow.BeginAsync(ct); await uow.BeginAsync(ct);
try try
@@ -165,27 +169,6 @@ public class StoreService(IStoreRepository repo, IUnitOfWork uow, ICurrentUserSe
if (!await HasStoreScopeAsync(id)) return null; if (!await HasStoreScopeAsync(id)) return null;
return new OutstandingBalanceResponse(store.Id, store.CreditLimit, store.OutstandingBalance, store.CreditLimit - store.OutstandingBalance); 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) public async Task<StoreResponse?> ClassifySegmentAsync(Guid id, ClassifySegmentRequest request, CancellationToken ct = default)
{ {
var store = await repo.GetByIdAsync(id); var store = await repo.GetByIdAsync(id);
-1
View File
@@ -4,7 +4,6 @@ namespace Distribution.Models
{ Warehouse, Vehicle, External } { Warehouse, Vehicle, External }
public enum KycStatus { Pending, Verified, Rejected }
public enum MovementType { Sale, CustomerReturn, Transfer, Damage, Adjustment, Load, PurchaseReceipt, PurchaseReturn } 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 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 StoreName { get; set; }
public String StoreAddress { get; set; } public String StoreAddress { get; set; }
public decimal Latitude { get; set; } = 0; public decimal Latitude { get; set; } = 0;
@@ -14,7 +15,6 @@ namespace Distribution.Models
public decimal CreditLimit { get; set; } = 0; public decimal CreditLimit { get; set; } = 0;
public decimal OutstandingBalance { get; set; } = 0; public decimal OutstandingBalance { get; set; } = 0;
public String? Segment { get; set; } //Gold ,silver,key Account,high risk public String? Segment { get; set; } //Gold ,silver,key Account,high risk
public KycStatus KycStatus { get; set; } = KycStatus.Pending;
public Guid FK_RouteId { get; set; } public Guid FK_RouteId { get; set; }
[ForeignKey("FK_RouteId")] [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 { import type {
PaginatedResponse, PaginatedResponse,
StoreResponse, StoreResponse,
KycDocumentResponse,
OutstandingBalanceResponse, OutstandingBalanceResponse,
StoreDashboardResponse, StoreDashboardResponse,
StoreVisitHistoryItem, StoreVisitHistoryItem,
KycStatus,
} from '@/types/api' } from '@/types/api'
import type { PageParams } from './master' import type { PageParams } from './master'
@@ -15,7 +13,8 @@ import type { PageParams } from './master'
export interface StoreBody { export interface StoreBody {
routeId: string routeId: string
ownerName: string ownerName?: string | null
contactNumber?: string | null
storeName: string storeName: string
storeAddress: string storeAddress: string
latitude: number latitude: number
@@ -28,7 +27,6 @@ export interface StoreBody {
export interface StoreListParams extends PageParams { export interface StoreListParams extends PageParams {
routeId?: string routeId?: string
kycStatus?: KycStatus
search?: string search?: string
} }
@@ -53,16 +51,6 @@ export const stores = {
dashboard: (id: string, period?: string) => dashboard: (id: string, period?: string) =>
api.get<StoreDashboardResponse>(`/stores/${id}/dashboard`, { params: { period } }).then(r => r.data), 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) => visitHistory: (id: string) =>
api.get<StoreVisitHistoryItem[]>(`/stores/${id}/visit-history`).then(r => r.data), api.get<StoreVisitHistoryItem[]>(`/stores/${id}/visit-history`).then(r => r.data),
} }