Compare commits

...

8 Commits

Author SHA1 Message Date
DeepnaPooja e3e331a2bb fix the running issue 2026-08-20 13:24:51 +05:30
DeepnaPooja 4e73dd7fb1 update store mngt.md 2026-08-20 13:10:46 +05:30
DeepnaPooja e88668beca Add store management workflows and KYC order gating 2026-08-20 13:08:00 +05:30
DeepnaPooja c1961ebea2 Add store KYC document management 2026-08-20 13:07:47 +05:30
DeepnaPooja cf9d2c2661 Add store credit and KYC data model 2026-08-20 13:07:27 +05:30
Kalana B. Thilakarathna 429a80b1ed Add planning docs for steps 10–23 and update roadmap
Added detailed implementation planning documents for steps 10–23, covering all major feature areas. Updated mainPlan.md to reference these steps, their dependencies, and planning files. No code changes—these documents clarify requirements and guide future development.
2026-08-20 10:04:09 +05:30
Kalana B. Thilakarathna f95fda3806 stock_ledger completed need to test 2026-08-20 09:42:33 +05:30
DeepnaPooja d2390e7b12 Add md file features in region, route, store, fleet, pricing, procurement, SFA, promotions, returns, finance, reporting, notifications, integration, and mobile capabilities 2026-08-19 15:18:31 +05:30
74 changed files with 5522 additions and 3985 deletions
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,6 +1,6 @@
{
"ConnectionStrings": {
"DefaultConnection": "Server=187.127.102.190;Port=3306;Database=distribution_db;User=kalana;Password=kalana@hexdive"
"DefaultConnection": "Server=187.127.102.190;Port=3306;Database=distribution;User=kalana;Password=kalana@hexdive"
},
"Logging": {
"LogLevel": {
@@ -13,7 +13,7 @@ using System.Reflection;
[assembly: System.Reflection.AssemblyCompanyAttribute("Distribution.Tests")]
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+397af8fd597bb7dd2d4372bf717d06c8e49552b7")]
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+aa7ea37ab61a149a38d83283c5dabd76bf5a39b3")]
[assembly: System.Reflection.AssemblyProductAttribute("Distribution.Tests")]
[assembly: System.Reflection.AssemblyTitleAttribute("Distribution.Tests")]
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
@@ -1 +1 @@
569664876b2d87d1ec65add142012e5eae24d860ed90b6cc736c1cb4bae13b9a
5c052f1f25bc37b6a51a3abe0e2b79d99d21422d5ab89151faea450c0ddb9cc0
+7
View File
@@ -27,6 +27,9 @@ public class AppDBContext(DbContextOptions<AppDBContext> options) : DbContext(op
public DbSet<UserRegionScope> UserRegionScopes { get; set; }
public DbSet<UserRouteScope> UserRouteScopes { get; set; }
public DbSet<UserStoreScope> UserStoreScopes { get; set; }
public DbSet<StockBalance> StockBalances { get; set; }
public DbSet<StockMovement> StockMovements { get; set; }
public DbSet<StoreKycDocument> StoreKycDocuments { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
@@ -51,6 +54,10 @@ public class AppDBContext(DbContextOptions<AppDBContext> options) : DbContext(op
.HasForeignKey(u => u.FK_ReportsToUserId)
.OnDelete(DeleteBehavior.Restrict);
modelBuilder.Entity<StockBalance>()
.HasIndex(sb => new { sb.FK_ItemId, sb.LocationId })
.IsUnique();
Seed.RoleAndPermissionSeed.Apply(modelBuilder);
}
}
@@ -35,6 +35,8 @@ public static class RoleAndPermissionSeed
("stores.create", "Create stores"),
("stores.update", "Update stores"),
("stores.delete", "Delete stores"),
("stores.kyc.verify", "Verify or reject store KYC documents"),
("stores.credit.update", "Update store credit limit and outstanding balance"),
("warehouses.view", "View warehouses"),
("warehouses.create", "Create warehouses"),
@@ -98,14 +100,14 @@ public static class RoleAndPermissionSeed
var itemPerms = new[] { "items.view", "items.create", "items.update", "items.delete", "categories.view", "categories.create", "categories.update", "categories.delete" };
var salesRepPerms = new[] { "orders.view", "orders.create", "orders.update", "stores.view", "routes.view" }.Concat(itemViewPerms).ToArray();
var supervisorPerms = salesRepPerms.Concat(["orders.delete", "stores.create", "stores.update"]).ToArray();
var areaManagerPerms = supervisorPerms.Concat(["vehicles.view", "warehouses.view"]).Concat(itemManagePerms).Distinct().ToArray();
var areaSupervisorPerms = areaManagerPerms.Concat(["routes.create", "routes.update"]).Concat(itemPerms).Distinct().ToArray();
var supervisorPerms = salesRepPerms.Concat(["orders.delete", "stores.create", "stores.update", "stores.credit.update"]).ToArray();
var areaManagerPerms = supervisorPerms.Concat(["vehicles.view", "warehouses.view", "stores.kyc.verify"]).Concat(itemManagePerms).Distinct().ToArray();
var areaSupervisorPerms = areaManagerPerms.Concat(["routes.create", "routes.update", "stores.delete"]).Concat(itemPerms).Distinct().ToArray();
var regionalManagerPerms = new[]
{
"orders.view", "orders.create", "orders.update", "orders.delete",
"stores.view", "stores.create", "stores.update", "stores.delete",
"stores.view", "stores.create", "stores.update", "stores.delete", "stores.kyc.verify", "stores.credit.update",
"routes.view", "routes.create", "routes.update", "routes.delete",
"vehicles.view", "vehicles.create", "vehicles.update", "vehicles.delete",
"warehouses.view", "warehouses.create", "warehouses.update", "warehouses.delete",
+18 -1
View File
@@ -1,9 +1,18 @@
using Distribution.Features.Store;
using Distribution.Infra.Authorization;
using Distribution.Infra.UoW;
using Distribution.Models;
using Microsoft.Extensions.Configuration;
namespace Distribution.Features.Order;
public class OrderService(IOrderRepository repo, IUnitOfWork uow, ICurrentUserService currentUser, IScopeAccessService scopeAccess)
public class OrderService(
IOrderRepository repo,
IStoreRepository storeRepo,
IUnitOfWork uow,
ICurrentUserService currentUser,
IScopeAccessService scopeAccess,
IConfiguration configuration)
{
private const int GlobalScopeRoleLevel = 50;
@@ -63,6 +72,14 @@ public class OrderService(IOrderRepository repo, IUnitOfWork uow, ICurrentUserSe
{
if (!await HasStoreScopeAsync(request.StoreId)) return null;
var store = await storeRepo.GetByIdAsync(request.StoreId);
if (store is null || !store.IsActive) return null;
if (store.KycStatus != KycStatus.Verified)
{
var threshold = configuration.GetValue<decimal>("StoreManagement:UnverifiedKycOrderThreshold");
if (request.TotalAmount > threshold) return null;
}
var order = new Models.Order
{
FK_StoreId = request.StoreId,
@@ -0,0 +1,19 @@
using Distribution.Models;
namespace Distribution.Features.Stock;
public interface IStockRepository
{
// Balance
Task<StockBalance?> GetBalanceAsync(Guid itemId, Guid locationId);
Task<List<StockBalance>> GetBalancesAsync(Guid? locationId, LocationType? locationType, Guid? itemId, int page, int pageSize);
Task<int> GetBalancesCountAsync(Guid? locationId, LocationType? locationType, Guid? itemId);
Task<List<StockBalance>> GetBalancesByItemAsync(Guid itemId);
Task AddBalanceAsync(StockBalance balance);
void UpdateBalance(StockBalance balance);
// Movements
Task AddMovementAsync(StockMovement movement);
Task<List<StockMovement>> GetMovementsAsync(Guid? locationId, Guid? itemId, MovementType? type, DateTime? from, DateTime? to, int page, int pageSize);
Task<int> GetMovementsCountAsync(Guid? locationId, Guid? itemId, MovementType? type, DateTime? from, DateTime? to);
}
@@ -0,0 +1,98 @@
using Distribution.Infra.Authorization;
using Distribution.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace Distribution.Features.Stock;
[ApiController]
[Route("api/stock")]
[Authorize]
public class StockController(StockService service, ICurrentUserService currentUser) : ControllerBase
{
// ── Query ──────────────────────────────────────────────────────────────
[HttpGet("balances")]
public async Task<IActionResult> GetBalances(
[FromQuery] Guid? locationId,
[FromQuery] LocationType? locationType,
[FromQuery] Guid? itemId,
[FromQuery] int page = 1,
[FromQuery] int pageSize = 20)
{
var result = await service.GetBalancesAsync(locationId, locationType, itemId, page, pageSize);
return Ok(result);
}
[HttpGet("balances/{itemId}/total")]
public async Task<IActionResult> GetItemTotal(Guid itemId)
{
var result = await service.GetItemTotalAsync(itemId);
if (result is null) return NotFound();
return Ok(result);
}
[HttpGet("movements")]
public async Task<IActionResult> GetMovements(
[FromQuery] Guid? locationId,
[FromQuery] Guid? itemId,
[FromQuery] MovementType? type,
[FromQuery] DateTime? from,
[FromQuery] DateTime? to,
[FromQuery] int page = 1,
[FromQuery] int pageSize = 20)
{
var result = await service.GetMovementsAsync(locationId, itemId, type, from, to, page, pageSize);
return Ok(result);
}
// ── Write ──────────────────────────────────────────────────────────────
[HttpPost("transfer")]
public async Task<IActionResult> Transfer(TransferRequest request, CancellationToken ct)
{
if (currentUser.UserId is null) return Unauthorized();
try
{
var result = await service.TransferAsync(request, currentUser.UserId.Value, ct);
return Ok(result);
}
catch (InvalidOperationException ex) { return BadRequest(ex.Message); }
}
[HttpPost("damage")]
public async Task<IActionResult> RecordDamage(DamageRequest request, CancellationToken ct)
{
if (currentUser.UserId is null) return Unauthorized();
try
{
var result = await service.RecordDamageAsync(request, currentUser.UserId.Value, ct);
return Ok(result);
}
catch (InvalidOperationException ex) { return BadRequest(ex.Message); }
}
[HttpPost("customer-return")]
public async Task<IActionResult> RecordCustomerReturn(CustomerReturnRequest request, CancellationToken ct)
{
if (currentUser.UserId is null) return Unauthorized();
try
{
var result = await service.RecordCustomerReturnAsync(request, currentUser.UserId.Value, ct);
return Ok(result);
}
catch (InvalidOperationException ex) { return BadRequest(ex.Message); }
}
[HttpPost("adjustment")]
public async Task<IActionResult> Adjust(AdjustmentRequest request, CancellationToken ct)
{
if (currentUser.UserId is null) return Unauthorized();
try
{
var result = await service.AdjustAsync(request, currentUser.UserId.Value, ct);
return Ok(result);
}
catch (InvalidOperationException ex) { return BadRequest(ex.Message); }
}
}
+67
View File
@@ -0,0 +1,67 @@
using Distribution.Models;
namespace Distribution.Features.Stock;
// Shared pagination wrapper
public record PagedResponse<T>(int Page, int PageSize, int Total, List<T> Items);
// Balance
public record StockBalanceResponse(
Guid ItemId,
string ItemName,
Guid LocationId,
LocationType LocationType,
decimal Quantity,
DateTime UpdatedAt);
public record StockTotalResponse(
Guid ItemId,
string ItemName,
decimal TotalQuantity,
List<StockBalanceResponse> Breakdown);
// Movement
public record StockMovementResponse(
Guid Id,
Guid ItemId,
string ItemName,
Guid? FromLocationId,
LocationType? FromLocationType,
Guid? ToLocationId,
LocationType? ToLocationType,
decimal Quantity,
MovementType Type,
string? ReasonCode,
Guid? ReferenceId,
DateTime CreatedAt,
Guid CreatedBy);
// Write requests
public record TransferRequest(
Guid ItemId,
Guid FromLocationId,
LocationType FromLocationType,
Guid ToLocationId,
LocationType ToLocationType,
decimal Quantity,
Guid? ReferenceId);
public record DamageRequest(
Guid ItemId,
Guid LocationId,
LocationType LocationType,
decimal Quantity,
string ReasonCode);
public record CustomerReturnRequest(
Guid ItemId,
Guid VehicleId,
decimal Quantity,
Guid? ReferenceId);
public record AdjustmentRequest(
Guid ItemId,
Guid LocationId,
LocationType LocationType,
decimal Quantity,
string ReasonCode);
@@ -0,0 +1,70 @@
using Distribution.Data;
using Distribution.Models;
using Microsoft.EntityFrameworkCore;
namespace Distribution.Features.Stock;
public class StockRepository(AppDBContext db) : IStockRepository
{
public Task<StockBalance?> GetBalanceAsync(Guid itemId, Guid locationId) =>
db.StockBalances
.Include(b => b.Item)
.FirstOrDefaultAsync(b => b.FK_ItemId == itemId && b.LocationId == locationId);
public Task<List<StockBalance>> GetBalancesAsync(Guid? locationId, LocationType? locationType, Guid? itemId, int page, int pageSize)
{
var query = db.StockBalances.Include(b => b.Item).AsQueryable();
if (locationId.HasValue) query = query.Where(b => b.LocationId == locationId.Value);
if (locationType.HasValue) query = query.Where(b => b.LocationType == locationType.Value);
if (itemId.HasValue) query = query.Where(b => b.FK_ItemId == itemId.Value);
return query.Skip((page - 1) * pageSize).Take(pageSize).ToListAsync();
}
public Task<int> GetBalancesCountAsync(Guid? locationId, LocationType? locationType, Guid? itemId)
{
var query = db.StockBalances.AsQueryable();
if (locationId.HasValue) query = query.Where(b => b.LocationId == locationId.Value);
if (locationType.HasValue) query = query.Where(b => b.LocationType == locationType.Value);
if (itemId.HasValue) query = query.Where(b => b.FK_ItemId == itemId.Value);
return query.CountAsync();
}
public Task<List<StockBalance>> GetBalancesByItemAsync(Guid itemId) =>
db.StockBalances
.Include(b => b.Item)
.Where(b => b.FK_ItemId == itemId)
.ToListAsync();
public async Task AddBalanceAsync(StockBalance balance) =>
await db.StockBalances.AddAsync(balance);
public void UpdateBalance(StockBalance balance) =>
db.StockBalances.Update(balance);
public async Task AddMovementAsync(StockMovement movement) =>
await db.StockMovements.AddAsync(movement);
public Task<List<StockMovement>> GetMovementsAsync(Guid? locationId, Guid? itemId, MovementType? type, DateTime? from, DateTime? to, int page, int pageSize)
{
var query = db.StockMovements.Include(m => m.Item).AsQueryable();
if (locationId.HasValue)
query = query.Where(m => m.FromLocationId == locationId.Value || m.ToLocationId == locationId.Value);
if (itemId.HasValue) query = query.Where(m => m.FK_ItemId == itemId.Value);
if (type.HasValue) query = query.Where(m => m.Type == type.Value);
if (from.HasValue) query = query.Where(m => m.CreatedAt >= from.Value);
if (to.HasValue) query = query.Where(m => m.CreatedAt <= to.Value);
return query.OrderByDescending(m => m.CreatedAt).Skip((page - 1) * pageSize).Take(pageSize).ToListAsync();
}
public Task<int> GetMovementsCountAsync(Guid? locationId, Guid? itemId, MovementType? type, DateTime? from, DateTime? to)
{
var query = db.StockMovements.AsQueryable();
if (locationId.HasValue)
query = query.Where(m => m.FromLocationId == locationId.Value || m.ToLocationId == locationId.Value);
if (itemId.HasValue) query = query.Where(m => m.FK_ItemId == itemId.Value);
if (type.HasValue) query = query.Where(m => m.Type == type.Value);
if (from.HasValue) query = query.Where(m => m.CreatedAt >= from.Value);
if (to.HasValue) query = query.Where(m => m.CreatedAt <= to.Value);
return query.CountAsync();
}
}
+228
View File
@@ -0,0 +1,228 @@
using Distribution.Features.Category;
using Distribution.Features.Item;
using Distribution.Infra.UoW;
using Distribution.Models;
namespace Distribution.Features.Stock;
public class StockService(IStockRepository repo, IItemRepository itemRepo, ICategoryRepository categoryRepo, IUnitOfWork uow)
{
private static StockBalanceResponse MapBalance(StockBalance b) =>
new(b.FK_ItemId, b.Item.Name, b.LocationId, b.LocationType, b.Quantity, b.UpdatedAt);
private static StockMovementResponse MapMovement(StockMovement m) =>
new(m.Id, m.FK_ItemId, m.Item.Name, m.FromLocationId, m.FromLocationType,
m.ToLocationId, m.ToLocationType, m.Quantity, m.Type, m.ReasonCode,
m.ReferenceId, m.CreatedAt, m.CreatedBy);
// ── Queries ────────────────────────────────────────────────────────────
public async Task<PagedResponse<StockBalanceResponse>> GetBalancesAsync(
Guid? locationId, LocationType? locationType, Guid? itemId, int page, int pageSize)
{
var items = await repo.GetBalancesAsync(locationId, locationType, itemId, page, pageSize);
var total = await repo.GetBalancesCountAsync(locationId, locationType, itemId);
return new PagedResponse<StockBalanceResponse>(page, pageSize, total, items.Select(MapBalance).ToList());
}
public async Task<StockTotalResponse?> GetItemTotalAsync(Guid itemId)
{
var item = await itemRepo.GetByIdAsync(itemId);
if (item is null || !item.IsActive) return null;
var balances = await repo.GetBalancesByItemAsync(itemId);
var total = balances.Sum(b => b.Quantity);
return new StockTotalResponse(itemId, item.Name, total, balances.Select(MapBalance).ToList());
}
public async Task<PagedResponse<StockMovementResponse>> GetMovementsAsync(
Guid? locationId, Guid? itemId, MovementType? type, DateTime? from, DateTime? to, int page, int pageSize)
{
var items = await repo.GetMovementsAsync(locationId, itemId, type, from, to, page, pageSize);
var total = await repo.GetMovementsCountAsync(locationId, itemId, type, from, to);
return new PagedResponse<StockMovementResponse>(page, pageSize, total, items.Select(MapMovement).ToList());
}
// ── Writes ─────────────────────────────────────────────────────────────
public async Task<StockMovementResponse> TransferAsync(TransferRequest request, Guid userId, CancellationToken ct = default)
{
await EnsureStockAsync(request.ItemId, request.FromLocationId, request.Quantity);
var movement = new StockMovement
{
FK_ItemId = request.ItemId,
FromLocationId = request.FromLocationId,
FromLocationType = request.FromLocationType,
ToLocationId = request.ToLocationId,
ToLocationType = request.ToLocationType,
Quantity = request.Quantity,
Type = MovementType.Transfer,
ReferenceId = request.ReferenceId,
CreatedBy = userId
};
await uow.BeginAsync(ct);
try
{
await ApplyTransferAsync(request.ItemId, request.FromLocationId, request.ToLocationId, request.ToLocationType, request.Quantity);
await repo.AddMovementAsync(movement);
await uow.CommitAsync(ct);
}
catch { await uow.RollbackAsync(ct); throw; }
return MapMovement(movement);
}
public async Task<StockMovementResponse> RecordDamageAsync(DamageRequest request, Guid userId, CancellationToken ct = default)
{
await EnsureStockAsync(request.ItemId, request.LocationId, request.Quantity);
var movement = new StockMovement
{
FK_ItemId = request.ItemId,
FromLocationId = request.LocationId,
FromLocationType = request.LocationType,
Quantity = request.Quantity,
Type = MovementType.Damage,
ReasonCode = request.ReasonCode,
CreatedBy = userId
};
await uow.BeginAsync(ct);
try
{
await DebitBalanceAsync(request.ItemId, request.LocationId, request.Quantity);
await repo.AddMovementAsync(movement);
await uow.CommitAsync(ct);
}
catch { await uow.RollbackAsync(ct); throw; }
return MapMovement(movement);
}
public async Task<StockMovementResponse> RecordCustomerReturnAsync(CustomerReturnRequest request, Guid userId, CancellationToken ct = default)
{
var item = await itemRepo.GetByIdAsync(request.ItemId)
?? throw new InvalidOperationException("Item not found.");
var category = await categoryRepo.GetByIdAsync(item.FK_CategoryId)
?? throw new InvalidOperationException("Category not found.");
if (!category.AllowCustomerReturn)
throw new InvalidOperationException($"Customer returns are not allowed for category '{category.Name}'.");
var movement = new StockMovement
{
FK_ItemId = request.ItemId,
ToLocationId = request.VehicleId,
ToLocationType = LocationType.Vehicle,
Quantity = request.Quantity,
Type = MovementType.CustomerReturn,
ReferenceId = request.ReferenceId,
CreatedBy = userId
};
await uow.BeginAsync(ct);
try
{
await CreditBalanceAsync(request.ItemId, request.VehicleId, LocationType.Vehicle, request.Quantity);
await repo.AddMovementAsync(movement);
await uow.CommitAsync(ct);
}
catch { await uow.RollbackAsync(ct); throw; }
return MapMovement(movement);
}
public async Task<StockMovementResponse> AdjustAsync(AdjustmentRequest request, Guid userId, CancellationToken ct = default)
{
var balance = await repo.GetBalanceAsync(request.ItemId, request.LocationId);
if (balance is not null && balance.Quantity + request.Quantity < 0)
throw new InvalidOperationException("Adjustment would result in negative stock.");
var movement = new StockMovement
{
FK_ItemId = request.ItemId,
FromLocationId = request.Quantity < 0 ? request.LocationId : null,
FromLocationType = request.Quantity < 0 ? request.LocationType : null,
ToLocationId = request.Quantity > 0 ? request.LocationId : null,
ToLocationType = request.Quantity > 0 ? request.LocationType : null,
Quantity = Math.Abs(request.Quantity),
Type = MovementType.Adjustment,
ReasonCode = request.ReasonCode,
CreatedBy = userId
};
await uow.BeginAsync(ct);
try
{
if (balance is null)
{
await repo.AddBalanceAsync(new StockBalance
{
FK_ItemId = request.ItemId,
LocationId = request.LocationId,
LocationType = request.LocationType,
Quantity = request.Quantity
});
}
else
{
balance.Quantity += request.Quantity;
balance.UpdatedAt = DateTime.Now;
repo.UpdateBalance(balance);
}
await repo.AddMovementAsync(movement);
await uow.CommitAsync(ct);
}
catch { await uow.RollbackAsync(ct); throw; }
return MapMovement(movement);
}
// ── Helpers ────────────────────────────────────────────────────────────
private async Task EnsureStockAsync(Guid itemId, Guid locationId, decimal quantity)
{
var balance = await repo.GetBalanceAsync(itemId, locationId);
if (balance is null || balance.Quantity < quantity)
throw new InvalidOperationException("Insufficient stock at the specified location.");
}
private async Task DebitBalanceAsync(Guid itemId, Guid locationId, decimal quantity)
{
var balance = await repo.GetBalanceAsync(itemId, locationId)
?? throw new InvalidOperationException("No stock balance found.");
balance.Quantity -= quantity;
balance.UpdatedAt = DateTime.Now;
repo.UpdateBalance(balance);
}
private async Task CreditBalanceAsync(Guid itemId, Guid locationId, LocationType locationType, decimal quantity)
{
var balance = await repo.GetBalanceAsync(itemId, locationId);
if (balance is null)
{
await repo.AddBalanceAsync(new StockBalance
{
FK_ItemId = itemId,
LocationId = locationId,
LocationType = locationType,
Quantity = quantity
});
}
else
{
balance.Quantity += quantity;
balance.UpdatedAt = DateTime.Now;
repo.UpdateBalance(balance);
}
}
private async Task ApplyTransferAsync(Guid itemId, Guid fromLocationId, Guid toLocationId, LocationType toLocationType, decimal quantity)
{
await DebitBalanceAsync(itemId, fromLocationId, quantity);
await CreditBalanceAsync(itemId, toLocationId, toLocationType, quantity);
}
}
@@ -7,4 +7,5 @@ public interface IStoreRepository
Task AddAsync(Models.Store entity);
void Update(Models.Store entity);
void Remove(Models.Store entity);
Task<List<Models.Order>> GetOrdersForDashboardAsync(Guid storeId, DateTime periodStart, DateTime periodEndExclusive);
}
@@ -0,0 +1,7 @@
namespace Distribution.Features.Store.Kyc;
public interface IStoreKycDocumentRepository
{
Task<List<Models.StoreKycDocument>> GetByStoreIdAsync(Guid storeId);
Task AddAsync(Models.StoreKycDocument entity);
}
@@ -0,0 +1,26 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace Distribution.Features.Store.Kyc;
[ApiController]
[Route("api/stores/{storeId:guid}/kyc-documents")]
[Authorize]
public class StoreKycDocumentController(StoreKycDocumentService service) : ControllerBase
{
[HttpGet]
[Authorize(Policy = "stores.view")]
public async Task<IActionResult> GetByStoreId(Guid storeId)
{
var result = await service.GetByStoreIdAsync(storeId);
return result is null ? NotFound() : Ok(result);
}
[HttpPost]
[Authorize(Policy = "stores.update")]
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);
}
}
@@ -0,0 +1,5 @@
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);
@@ -0,0 +1,13 @@
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);
}
@@ -0,0 +1,71 @@
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);
}
}
@@ -44,4 +44,68 @@ public class StoreController(StoreService service) : ControllerBase
var deleted = await service.DeleteAsync(id, ct);
return deleted ? NoContent() : NotFound();
}
[HttpPut("{id:guid}/credit-limit")]
[Authorize(Policy = "stores.credit.update")]
public async Task<IActionResult> UpdateCreditLimit(Guid id, UpdateCreditLimitRequest request, CancellationToken ct)
{
var result = await service.UpdateCreditLimitAsync(id, request, ct);
return result is null ? Forbid() : Ok(result);
}
[HttpPut("{id:guid}/outstanding-balance")]
[Authorize(Policy = "stores.credit.update")]
public async Task<IActionResult> UpdateOutstandingBalance(Guid id, UpdateOutstandingBalanceRequest request, CancellationToken ct)
{
var result = await service.UpdateOutstandingBalanceAsync(id, request, ct);
return result is null ? Forbid() : Ok(result);
}
[HttpGet("{id:guid}/outstanding-balance")]
[Authorize(Policy = "stores.view")]
public async Task<IActionResult> GetOutstandingBalance(Guid id)
{
var result = await service.GetOutstandingBalanceAsync(id);
return result is null ? NotFound() : Ok(result);
}
[HttpPut("{id:guid}/kyc/verify")]
[Authorize(Policy = "stores.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")]
[Authorize(Policy = "stores.update")]
public async Task<IActionResult> ClassifySegment(Guid id, ClassifySegmentRequest request, CancellationToken ct)
{
var result = await service.ClassifySegmentAsync(id, request, ct);
return result is null ? NotFound() : Ok(result);
}
[HttpGet("{id:guid}/visit-history")]
[Authorize(Policy = "stores.view")]
public async Task<IActionResult> GetVisitHistory(Guid id)
{
var result = await service.GetVisitHistoryAsync(id);
return result is null ? NotFound() : Ok(result);
}
[HttpGet("{id:guid}/schemes/{schemeId:guid}/eligibility")]
[Authorize(Policy = "stores.view")]
public async Task<IActionResult> CheckSchemeEligibility(Guid id, Guid schemeId)
{
var result = await service.CheckSchemeEligibilityAsync(id, schemeId);
return result is null ? NotFound() : Ok(result);
}
[HttpGet("{id:guid}/dashboard")]
[Authorize(Policy = "stores.view")]
public async Task<IActionResult> GetDashboard(Guid id, [FromQuery] string period)
{
var result = await service.GetDashboardAsync(id, period);
return result is null ? NotFound() : Ok(result);
}
}
+22
View File
@@ -1,3 +1,5 @@
using Distribution.Models;
namespace Distribution.Features.Store;
public record CreateStoreRequest(
@@ -30,5 +32,25 @@ public record StoreResponse(
decimal Longitude,
decimal OneTimeAmount,
decimal CreditAmount,
decimal CreditLimit,
decimal OutstandingBalance,
string? Segment,
KycStatus KycStatus,
bool IsActive,
DateTime CreatedAt);
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);
public record StoreVisitHistoryItem(Guid VisitId, DateTime VisitDate, Guid RepUserId, string Outcome);
public record SchemeEligibilityResponse(bool Eligible, string? Reason);
public record StoreDashboardResponse(Guid StoreId, string Period, int OrderCount, decimal TotalOrderValue, DateTime? LastOrderDate);
@@ -22,4 +22,9 @@ public class StoreRepository(AppDBContext context) : IStoreRepository
entity.IsActive = false;
context.Stores.Update(entity);
}
public async Task<List<Models.Order>> GetOrdersForDashboardAsync(Guid storeId, DateTime periodStart, DateTime periodEndExclusive) =>
await context.Orders
.Where(o => o.FK_StoreId == storeId && o.IsActive && o.OrderDate >= periodStart && o.OrderDate < periodEndExclusive)
.ToListAsync();
}
+137 -4
View File
@@ -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));
}
}
@@ -1,362 +0,0 @@
// <auto-generated />
using System;
using Distribution.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace Distribution.Migrations
{
[DbContext(typeof(AppDBContext))]
[Migration("20260818065939_initialDbCreate")]
partial class initialDbCreate
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "10.0.7")
.HasAnnotation("Relational:MaxIdentifierLength", 64);
modelBuilder.Entity("Distribution.Models.DeliveryRun", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("char(36)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
b.Property<Guid>("FK_RouteId")
.HasColumnType("char(36)");
b.Property<Guid>("FK_VehicleId")
.HasColumnType("char(36)");
b.Property<bool>("IsActive")
.HasColumnType("tinyint(1)");
b.Property<int>("Meterend")
.HasColumnType("int");
b.Property<int>("Meterstart")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("FK_RouteId");
b.HasIndex("FK_VehicleId");
b.ToTable("DeliveryRuns");
});
modelBuilder.Entity("Distribution.Models.Item", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("char(36)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
b.Property<bool>("IsActive")
.HasColumnType("tinyint(1)");
b.HasKey("Id");
b.ToTable("Items");
});
modelBuilder.Entity("Distribution.Models.Order", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("char(36)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
b.Property<Guid>("FK_StoreId")
.HasColumnType("char(36)");
b.Property<bool>("IsActive")
.HasColumnType("tinyint(1)");
b.Property<DateTime>("OrderDate")
.HasColumnType("datetime(6)");
b.Property<decimal>("TotalAmount")
.HasColumnType("decimal(18,2)");
b.HasKey("Id");
b.HasIndex("FK_StoreId");
b.ToTable("Orders");
});
modelBuilder.Entity("Distribution.Models.OrderItem", b =>
{
b.Property<Guid>("FK_OrderId")
.HasColumnType("char(36)");
b.Property<Guid>("FK_ItemId")
.HasColumnType("char(36)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
b.Property<Guid>("Id")
.HasColumnType("char(36)");
b.Property<bool>("IsActive")
.HasColumnType("tinyint(1)");
b.Property<int>("Quantity")
.HasColumnType("int");
b.HasKey("FK_OrderId", "FK_ItemId");
b.HasIndex("FK_ItemId");
b.ToTable("OrderItems");
});
modelBuilder.Entity("Distribution.Models.Region", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("char(36)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
b.Property<bool>("IsActive")
.HasColumnType("tinyint(1)");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("longtext");
b.HasKey("Id");
b.ToTable("Regions");
});
modelBuilder.Entity("Distribution.Models.Route", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("char(36)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
b.Property<string>("EndPoint")
.IsRequired()
.HasColumnType("longtext");
b.Property<bool>("IsActive")
.HasColumnType("tinyint(1)");
b.Property<decimal>("Length")
.HasColumnType("decimal(18,2)");
b.Property<string>("RouteName")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("StartPoint")
.IsRequired()
.HasColumnType("longtext");
b.HasKey("Id");
b.ToTable("Routes");
});
modelBuilder.Entity("Distribution.Models.SaleRoute", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("char(36)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
b.Property<bool>("IsActive")
.HasColumnType("tinyint(1)");
b.HasKey("Id");
b.ToTable("SaleRoutes");
});
modelBuilder.Entity("Distribution.Models.Store", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("char(36)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
b.Property<decimal>("CreditAmount")
.HasColumnType("decimal(18,2)");
b.Property<bool>("IsActive")
.HasColumnType("tinyint(1)");
b.Property<decimal>("Latitude")
.HasColumnType("decimal(18,2)");
b.Property<decimal>("Longitude")
.HasColumnType("decimal(18,2)");
b.Property<decimal>("OneTimeAmount")
.HasColumnType("decimal(18,2)");
b.Property<string>("OwnerName")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("StoreAddress")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("StoreName")
.IsRequired()
.HasColumnType("longtext");
b.HasKey("Id");
b.ToTable("Stores");
});
modelBuilder.Entity("Distribution.Models.Vehicle", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("char(36)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
b.Property<bool>("IsActive")
.HasColumnType("tinyint(1)");
b.Property<string>("NumberPlate")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("RegNumber")
.IsRequired()
.HasColumnType("longtext");
b.Property<int>("Type")
.HasColumnType("int");
b.HasKey("Id");
b.ToTable("Vehicles");
});
modelBuilder.Entity("Distribution.Models.Warehouse", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("char(36)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
b.Property<bool>("IsActive")
.HasColumnType("tinyint(1)");
b.Property<string>("Location")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("longtext");
b.Property<int>("Type")
.HasColumnType("int");
b.Property<Guid?>("VehicleId")
.HasColumnType("char(36)");
b.HasKey("Id");
b.HasIndex("VehicleId");
b.ToTable("Warehouses");
});
modelBuilder.Entity("Distribution.Models.DeliveryRun", b =>
{
b.HasOne("Distribution.Models.Route", "Route")
.WithMany()
.HasForeignKey("FK_RouteId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Distribution.Models.Vehicle", "Vehicle")
.WithMany()
.HasForeignKey("FK_VehicleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Route");
b.Navigation("Vehicle");
});
modelBuilder.Entity("Distribution.Models.Order", b =>
{
b.HasOne("Distribution.Models.Store", "Store")
.WithMany()
.HasForeignKey("FK_StoreId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Store");
});
modelBuilder.Entity("Distribution.Models.OrderItem", b =>
{
b.HasOne("Distribution.Models.Item", "Item")
.WithMany()
.HasForeignKey("FK_ItemId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Distribution.Models.Order", "Order")
.WithMany()
.HasForeignKey("FK_OrderId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Item");
b.Navigation("Order");
});
modelBuilder.Entity("Distribution.Models.Warehouse", b =>
{
b.HasOne("Distribution.Models.Vehicle", "Vehicle")
.WithMany()
.HasForeignKey("VehicleId");
b.Navigation("Vehicle");
});
#pragma warning restore 612, 618
}
}
}
@@ -1,281 +0,0 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Distribution.Migrations
{
/// <inheritdoc />
public partial class initialDbCreate : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterDatabase()
.Annotation("MySQL:Charset", "utf8mb4");
migrationBuilder.CreateTable(
name: "Items",
columns: table => new
{
Id = table.Column<Guid>(type: "char(36)", nullable: false),
CreatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false),
IsActive = table.Column<bool>(type: "tinyint(1)", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Items", x => x.Id);
})
.Annotation("MySQL:Charset", "utf8mb4");
migrationBuilder.CreateTable(
name: "Regions",
columns: table => new
{
Id = table.Column<Guid>(type: "char(36)", nullable: false),
Name = table.Column<string>(type: "longtext", nullable: false),
CreatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false),
IsActive = table.Column<bool>(type: "tinyint(1)", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Regions", x => x.Id);
})
.Annotation("MySQL:Charset", "utf8mb4");
migrationBuilder.CreateTable(
name: "Routes",
columns: table => new
{
Id = table.Column<Guid>(type: "char(36)", nullable: false),
RouteName = table.Column<string>(type: "longtext", nullable: false),
StartPoint = table.Column<string>(type: "longtext", nullable: false),
EndPoint = table.Column<string>(type: "longtext", nullable: false),
Length = table.Column<decimal>(type: "decimal(18,2)", nullable: false),
CreatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false),
IsActive = table.Column<bool>(type: "tinyint(1)", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Routes", x => x.Id);
})
.Annotation("MySQL:Charset", "utf8mb4");
migrationBuilder.CreateTable(
name: "SaleRoutes",
columns: table => new
{
Id = table.Column<Guid>(type: "char(36)", nullable: false),
CreatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false),
IsActive = table.Column<bool>(type: "tinyint(1)", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_SaleRoutes", x => x.Id);
})
.Annotation("MySQL:Charset", "utf8mb4");
migrationBuilder.CreateTable(
name: "Stores",
columns: table => new
{
Id = table.Column<Guid>(type: "char(36)", nullable: false),
OwnerName = table.Column<string>(type: "longtext", nullable: false),
StoreName = table.Column<string>(type: "longtext", nullable: false),
StoreAddress = table.Column<string>(type: "longtext", nullable: false),
Latitude = table.Column<decimal>(type: "decimal(18,2)", nullable: false),
Longitude = table.Column<decimal>(type: "decimal(18,2)", nullable: false),
OneTimeAmount = table.Column<decimal>(type: "decimal(18,2)", nullable: false),
CreditAmount = table.Column<decimal>(type: "decimal(18,2)", nullable: false),
CreatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false),
IsActive = table.Column<bool>(type: "tinyint(1)", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Stores", x => x.Id);
})
.Annotation("MySQL:Charset", "utf8mb4");
migrationBuilder.CreateTable(
name: "Vehicles",
columns: table => new
{
Id = table.Column<Guid>(type: "char(36)", nullable: false),
RegNumber = table.Column<string>(type: "longtext", nullable: false),
NumberPlate = table.Column<string>(type: "longtext", nullable: false),
Type = table.Column<int>(type: "int", nullable: false),
CreatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false),
IsActive = table.Column<bool>(type: "tinyint(1)", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Vehicles", x => x.Id);
})
.Annotation("MySQL:Charset", "utf8mb4");
migrationBuilder.CreateTable(
name: "Orders",
columns: table => new
{
Id = table.Column<Guid>(type: "char(36)", nullable: false),
OrderDate = table.Column<DateTime>(type: "datetime(6)", nullable: false),
FK_StoreId = table.Column<Guid>(type: "char(36)", nullable: false),
TotalAmount = table.Column<decimal>(type: "decimal(18,2)", nullable: false),
CreatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false),
IsActive = table.Column<bool>(type: "tinyint(1)", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Orders", x => x.Id);
table.ForeignKey(
name: "FK_Orders_Stores_FK_StoreId",
column: x => x.FK_StoreId,
principalTable: "Stores",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
})
.Annotation("MySQL:Charset", "utf8mb4");
migrationBuilder.CreateTable(
name: "DeliveryRuns",
columns: table => new
{
Id = table.Column<Guid>(type: "char(36)", nullable: false),
FK_VehicleId = table.Column<Guid>(type: "char(36)", nullable: false),
Meterstart = table.Column<int>(type: "int", nullable: false),
Meterend = table.Column<int>(type: "int", nullable: false),
FK_RouteId = table.Column<Guid>(type: "char(36)", nullable: false),
CreatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false),
IsActive = table.Column<bool>(type: "tinyint(1)", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_DeliveryRuns", x => x.Id);
table.ForeignKey(
name: "FK_DeliveryRuns_Routes_FK_RouteId",
column: x => x.FK_RouteId,
principalTable: "Routes",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_DeliveryRuns_Vehicles_FK_VehicleId",
column: x => x.FK_VehicleId,
principalTable: "Vehicles",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
})
.Annotation("MySQL:Charset", "utf8mb4");
migrationBuilder.CreateTable(
name: "Warehouses",
columns: table => new
{
Id = table.Column<Guid>(type: "char(36)", nullable: false),
Name = table.Column<string>(type: "longtext", nullable: false),
Location = table.Column<string>(type: "longtext", nullable: false),
Type = table.Column<int>(type: "int", nullable: false),
VehicleId = table.Column<Guid>(type: "char(36)", nullable: true),
CreatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false),
IsActive = table.Column<bool>(type: "tinyint(1)", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Warehouses", x => x.Id);
table.ForeignKey(
name: "FK_Warehouses_Vehicles_VehicleId",
column: x => x.VehicleId,
principalTable: "Vehicles",
principalColumn: "Id");
})
.Annotation("MySQL:Charset", "utf8mb4");
migrationBuilder.CreateTable(
name: "OrderItems",
columns: table => new
{
FK_OrderId = table.Column<Guid>(type: "char(36)", nullable: false),
FK_ItemId = table.Column<Guid>(type: "char(36)", nullable: false),
Quantity = table.Column<int>(type: "int", nullable: false),
Id = table.Column<Guid>(type: "char(36)", nullable: false),
CreatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false),
IsActive = table.Column<bool>(type: "tinyint(1)", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_OrderItems", x => new { x.FK_OrderId, x.FK_ItemId });
table.ForeignKey(
name: "FK_OrderItems_Items_FK_ItemId",
column: x => x.FK_ItemId,
principalTable: "Items",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_OrderItems_Orders_FK_OrderId",
column: x => x.FK_OrderId,
principalTable: "Orders",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
})
.Annotation("MySQL:Charset", "utf8mb4");
migrationBuilder.CreateIndex(
name: "IX_DeliveryRuns_FK_RouteId",
table: "DeliveryRuns",
column: "FK_RouteId");
migrationBuilder.CreateIndex(
name: "IX_DeliveryRuns_FK_VehicleId",
table: "DeliveryRuns",
column: "FK_VehicleId");
migrationBuilder.CreateIndex(
name: "IX_OrderItems_FK_ItemId",
table: "OrderItems",
column: "FK_ItemId");
migrationBuilder.CreateIndex(
name: "IX_Orders_FK_StoreId",
table: "Orders",
column: "FK_StoreId");
migrationBuilder.CreateIndex(
name: "IX_Warehouses_VehicleId",
table: "Warehouses",
column: "VehicleId");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "DeliveryRuns");
migrationBuilder.DropTable(
name: "OrderItems");
migrationBuilder.DropTable(
name: "Regions");
migrationBuilder.DropTable(
name: "SaleRoutes");
migrationBuilder.DropTable(
name: "Warehouses");
migrationBuilder.DropTable(
name: "Routes");
migrationBuilder.DropTable(
name: "Items");
migrationBuilder.DropTable(
name: "Orders");
migrationBuilder.DropTable(
name: "Vehicles");
migrationBuilder.DropTable(
name: "Stores");
}
}
}
File diff suppressed because it is too large Load Diff
@@ -1,456 +0,0 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
#pragma warning disable CA1814 // Prefer jagged arrays over multidimensional
namespace Distribution.Migrations
{
/// <inheritdoc />
public partial class AddUserManagement : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<Guid>(
name: "FK_RouteId",
table: "Stores",
type: "char(36)",
nullable: false,
defaultValue: new Guid("00000000-0000-0000-0000-000000000000"));
migrationBuilder.AddColumn<Guid>(
name: "FK_RegionId",
table: "Routes",
type: "char(36)",
nullable: false,
defaultValue: new Guid("00000000-0000-0000-0000-000000000000"));
migrationBuilder.CreateTable(
name: "Permissions",
columns: table => new
{
Id = table.Column<Guid>(type: "char(36)", nullable: false),
Code = table.Column<string>(type: "longtext", nullable: false),
Description = table.Column<string>(type: "longtext", nullable: false),
CreatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false),
IsActive = table.Column<bool>(type: "tinyint(1)", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Permissions", x => x.Id);
})
.Annotation("MySQL:Charset", "utf8mb4");
migrationBuilder.CreateTable(
name: "Roles",
columns: table => new
{
Id = table.Column<Guid>(type: "char(36)", nullable: false),
Name = table.Column<string>(type: "longtext", nullable: false),
Level = table.Column<int>(type: "int", nullable: false),
CreatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false),
IsActive = table.Column<bool>(type: "tinyint(1)", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Roles", x => x.Id);
})
.Annotation("MySQL:Charset", "utf8mb4");
migrationBuilder.CreateTable(
name: "RolePermissions",
columns: table => new
{
FK_RoleId = table.Column<Guid>(type: "char(36)", nullable: false),
FK_PermissionId = table.Column<Guid>(type: "char(36)", nullable: false),
Id = table.Column<Guid>(type: "char(36)", nullable: false),
CreatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false),
IsActive = table.Column<bool>(type: "tinyint(1)", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_RolePermissions", x => new { x.FK_RoleId, x.FK_PermissionId });
table.ForeignKey(
name: "FK_RolePermissions_Permissions_FK_PermissionId",
column: x => x.FK_PermissionId,
principalTable: "Permissions",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_RolePermissions_Roles_FK_RoleId",
column: x => x.FK_RoleId,
principalTable: "Roles",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
})
.Annotation("MySQL:Charset", "utf8mb4");
migrationBuilder.CreateTable(
name: "Users",
columns: table => new
{
Id = table.Column<Guid>(type: "char(36)", nullable: false),
Username = table.Column<string>(type: "longtext", nullable: false),
PasswordHash = table.Column<string>(type: "longtext", nullable: false),
FullName = table.Column<string>(type: "longtext", nullable: false),
FK_RoleId = table.Column<Guid>(type: "char(36)", nullable: false),
FK_ReportsToUserId = table.Column<Guid>(type: "char(36)", nullable: true),
UpdatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: true),
CreatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false),
IsActive = table.Column<bool>(type: "tinyint(1)", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Users", x => x.Id);
table.ForeignKey(
name: "FK_Users_Roles_FK_RoleId",
column: x => x.FK_RoleId,
principalTable: "Roles",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_Users_Users_FK_ReportsToUserId",
column: x => x.FK_ReportsToUserId,
principalTable: "Users",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
})
.Annotation("MySQL:Charset", "utf8mb4");
migrationBuilder.CreateTable(
name: "UserRegionScopes",
columns: table => new
{
FK_UserId = table.Column<Guid>(type: "char(36)", nullable: false),
FK_RegionId = table.Column<Guid>(type: "char(36)", nullable: false),
Id = table.Column<Guid>(type: "char(36)", nullable: false),
CreatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false),
IsActive = table.Column<bool>(type: "tinyint(1)", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_UserRegionScopes", x => new { x.FK_UserId, x.FK_RegionId });
table.ForeignKey(
name: "FK_UserRegionScopes_Regions_FK_RegionId",
column: x => x.FK_RegionId,
principalTable: "Regions",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_UserRegionScopes_Users_FK_UserId",
column: x => x.FK_UserId,
principalTable: "Users",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
})
.Annotation("MySQL:Charset", "utf8mb4");
migrationBuilder.CreateTable(
name: "UserRouteScopes",
columns: table => new
{
FK_UserId = table.Column<Guid>(type: "char(36)", nullable: false),
FK_RouteId = table.Column<Guid>(type: "char(36)", nullable: false),
Id = table.Column<Guid>(type: "char(36)", nullable: false),
CreatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false),
IsActive = table.Column<bool>(type: "tinyint(1)", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_UserRouteScopes", x => new { x.FK_UserId, x.FK_RouteId });
table.ForeignKey(
name: "FK_UserRouteScopes_Routes_FK_RouteId",
column: x => x.FK_RouteId,
principalTable: "Routes",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_UserRouteScopes_Users_FK_UserId",
column: x => x.FK_UserId,
principalTable: "Users",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
})
.Annotation("MySQL:Charset", "utf8mb4");
migrationBuilder.CreateTable(
name: "UserStoreScopes",
columns: table => new
{
FK_UserId = table.Column<Guid>(type: "char(36)", nullable: false),
FK_StoreId = table.Column<Guid>(type: "char(36)", nullable: false),
Id = table.Column<Guid>(type: "char(36)", nullable: false),
CreatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false),
IsActive = table.Column<bool>(type: "tinyint(1)", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_UserStoreScopes", x => new { x.FK_UserId, x.FK_StoreId });
table.ForeignKey(
name: "FK_UserStoreScopes_Stores_FK_StoreId",
column: x => x.FK_StoreId,
principalTable: "Stores",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_UserStoreScopes_Users_FK_UserId",
column: x => x.FK_UserId,
principalTable: "Users",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
})
.Annotation("MySQL:Charset", "utf8mb4");
migrationBuilder.InsertData(
table: "Permissions",
columns: new[] { "Id", "Code", "CreatedAt", "Description", "IsActive" },
values: new object[,]
{
{ new Guid("20000000-0000-0000-0000-000000000001"), "regions.view", new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), "View regions", true },
{ new Guid("20000000-0000-0000-0000-000000000002"), "regions.create", new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), "Create regions", true },
{ new Guid("20000000-0000-0000-0000-000000000003"), "regions.update", new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), "Update regions", true },
{ new Guid("20000000-0000-0000-0000-000000000004"), "regions.delete", new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), "Delete regions", true },
{ new Guid("20000000-0000-0000-0000-000000000005"), "routes.view", new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), "View routes", true },
{ new Guid("20000000-0000-0000-0000-000000000006"), "routes.create", new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), "Create routes", true },
{ new Guid("20000000-0000-0000-0000-000000000007"), "routes.update", new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), "Update routes", true },
{ new Guid("20000000-0000-0000-0000-000000000008"), "routes.delete", new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), "Delete routes", true },
{ new Guid("20000000-0000-0000-0000-000000000009"), "vehicles.view", new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), "View vehicles", true },
{ new Guid("20000000-0000-0000-0000-000000000010"), "vehicles.create", new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), "Create vehicles", true },
{ new Guid("20000000-0000-0000-0000-000000000011"), "vehicles.update", new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), "Update vehicles", true },
{ new Guid("20000000-0000-0000-0000-000000000012"), "vehicles.delete", new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), "Delete vehicles", true },
{ new Guid("20000000-0000-0000-0000-000000000013"), "stores.view", new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), "View stores", true },
{ new Guid("20000000-0000-0000-0000-000000000014"), "stores.create", new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), "Create stores", true },
{ new Guid("20000000-0000-0000-0000-000000000015"), "stores.update", new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), "Update stores", true },
{ new Guid("20000000-0000-0000-0000-000000000016"), "stores.delete", new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), "Delete stores", true },
{ new Guid("20000000-0000-0000-0000-000000000017"), "warehouses.view", new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), "View warehouses", true },
{ new Guid("20000000-0000-0000-0000-000000000018"), "warehouses.create", new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), "Create warehouses", true },
{ new Guid("20000000-0000-0000-0000-000000000019"), "warehouses.update", new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), "Update warehouses", true },
{ new Guid("20000000-0000-0000-0000-000000000020"), "warehouses.delete", new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), "Delete warehouses", true },
{ new Guid("20000000-0000-0000-0000-000000000021"), "orders.view", new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), "View orders", true },
{ new Guid("20000000-0000-0000-0000-000000000022"), "orders.create", new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), "Create orders", true },
{ new Guid("20000000-0000-0000-0000-000000000023"), "orders.update", new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), "Update orders", true },
{ new Guid("20000000-0000-0000-0000-000000000024"), "orders.delete", new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), "Delete orders", true },
{ new Guid("20000000-0000-0000-0000-000000000025"), "users.view", new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), "View users", true },
{ new Guid("20000000-0000-0000-0000-000000000026"), "users.create", new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), "Create users", true },
{ new Guid("20000000-0000-0000-0000-000000000027"), "users.update", new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), "Update users", true },
{ new Guid("20000000-0000-0000-0000-000000000028"), "users.deactivate", new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), "Deactivate users", true },
{ new Guid("20000000-0000-0000-0000-000000000029"), "users.assign_role", new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), "Assign a role to a user", true },
{ new Guid("20000000-0000-0000-0000-000000000030"), "users.assign_scope", new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), "Assign region/route/store scope to a user", true }
});
migrationBuilder.InsertData(
table: "Roles",
columns: new[] { "Id", "CreatedAt", "IsActive", "Level", "Name" },
values: new object[,]
{
{ new Guid("10000000-0000-0000-0000-000000000001"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), true, 10, "SalesRep" },
{ new Guid("10000000-0000-0000-0000-000000000002"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), true, 20, "Supervisor" },
{ new Guid("10000000-0000-0000-0000-000000000003"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), true, 30, "AreaManager" },
{ new Guid("10000000-0000-0000-0000-000000000004"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), true, 40, "AreaSupervisor" },
{ new Guid("10000000-0000-0000-0000-000000000005"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), true, 50, "RegionalManager" },
{ new Guid("10000000-0000-0000-0000-000000000006"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), true, 60, "RegionalAgency" }
});
migrationBuilder.InsertData(
table: "RolePermissions",
columns: new[] { "FK_PermissionId", "FK_RoleId", "CreatedAt", "Id", "IsActive" },
values: new object[,]
{
{ new Guid("20000000-0000-0000-0000-000000000005"), new Guid("10000000-0000-0000-0000-000000000001"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("75737fa8-fed0-d97a-8c24-5716758a7222"), true },
{ new Guid("20000000-0000-0000-0000-000000000013"), new Guid("10000000-0000-0000-0000-000000000001"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("0e0613fd-d62c-1e87-a5b6-8bd2c3697c2a"), true },
{ new Guid("20000000-0000-0000-0000-000000000021"), new Guid("10000000-0000-0000-0000-000000000001"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("85ef4e27-8643-9c43-71f6-da56f66a5c47"), true },
{ new Guid("20000000-0000-0000-0000-000000000022"), new Guid("10000000-0000-0000-0000-000000000001"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("1e353909-d682-58f6-df5c-8e57fa2512c9"), true },
{ new Guid("20000000-0000-0000-0000-000000000023"), new Guid("10000000-0000-0000-0000-000000000001"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("dfe4bb14-1047-219c-cabe-a2b49b105d86"), true },
{ new Guid("20000000-0000-0000-0000-000000000005"), new Guid("10000000-0000-0000-0000-000000000002"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("7ee233b9-3758-e005-7bb2-5ad1edf36e21"), true },
{ new Guid("20000000-0000-0000-0000-000000000013"), new Guid("10000000-0000-0000-0000-000000000002"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("eb4ffa50-b09a-f0a5-1994-3fc2ae3a35d0"), true },
{ new Guid("20000000-0000-0000-0000-000000000014"), new Guid("10000000-0000-0000-0000-000000000002"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("5adb4d1a-eba8-16cb-8603-c3443d4ee8c8"), true },
{ new Guid("20000000-0000-0000-0000-000000000015"), new Guid("10000000-0000-0000-0000-000000000002"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("23105bb1-ef1f-6e19-1a48-d62ccaec1db4"), true },
{ new Guid("20000000-0000-0000-0000-000000000021"), new Guid("10000000-0000-0000-0000-000000000002"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("e4648655-7267-141e-51fa-f7d5fe3d2356"), true },
{ new Guid("20000000-0000-0000-0000-000000000022"), new Guid("10000000-0000-0000-0000-000000000002"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("d427b9ea-2c9b-d93e-0843-02dfa1ae8887"), true },
{ new Guid("20000000-0000-0000-0000-000000000023"), new Guid("10000000-0000-0000-0000-000000000002"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("e1f39dc4-6c33-45f4-8c23-3ad8ae5784a8"), true },
{ new Guid("20000000-0000-0000-0000-000000000024"), new Guid("10000000-0000-0000-0000-000000000002"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("357c36de-9b6c-1c9a-4926-c18a9bccf0ca"), true },
{ new Guid("20000000-0000-0000-0000-000000000005"), new Guid("10000000-0000-0000-0000-000000000003"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("e9c8355a-26a9-4107-504e-d61722f9e7cd"), true },
{ new Guid("20000000-0000-0000-0000-000000000009"), new Guid("10000000-0000-0000-0000-000000000003"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("7fef12f3-6875-14bb-9554-0c9d82f02a37"), true },
{ new Guid("20000000-0000-0000-0000-000000000013"), new Guid("10000000-0000-0000-0000-000000000003"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("680b582c-dbc9-cfde-f54b-58c3ad5a57b6"), true },
{ new Guid("20000000-0000-0000-0000-000000000014"), new Guid("10000000-0000-0000-0000-000000000003"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("0b7d47cf-efd9-9589-682b-78df22062e5a"), true },
{ new Guid("20000000-0000-0000-0000-000000000015"), new Guid("10000000-0000-0000-0000-000000000003"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("4aaa0f3d-4360-5f11-41c1-5257662428af"), true },
{ new Guid("20000000-0000-0000-0000-000000000017"), new Guid("10000000-0000-0000-0000-000000000003"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("38ea5531-a29d-a6bb-eebe-a18f3afe066d"), true },
{ new Guid("20000000-0000-0000-0000-000000000021"), new Guid("10000000-0000-0000-0000-000000000003"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("0dd5c943-3ace-0560-6fbc-54d8e37ce014"), true },
{ new Guid("20000000-0000-0000-0000-000000000022"), new Guid("10000000-0000-0000-0000-000000000003"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("ca97fb3d-132c-d8a5-4ab8-f7b8fc60f5e5"), true },
{ new Guid("20000000-0000-0000-0000-000000000023"), new Guid("10000000-0000-0000-0000-000000000003"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("5cee999e-1b77-cb87-5148-6a793af7c44e"), true },
{ new Guid("20000000-0000-0000-0000-000000000024"), new Guid("10000000-0000-0000-0000-000000000003"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("73e00ce7-c124-4677-4369-1eb9876852c0"), true },
{ new Guid("20000000-0000-0000-0000-000000000005"), new Guid("10000000-0000-0000-0000-000000000004"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("c6d74049-3f46-88af-1067-c9ae133132f7"), true },
{ new Guid("20000000-0000-0000-0000-000000000006"), new Guid("10000000-0000-0000-0000-000000000004"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("29d71000-6715-de8a-a3af-814938d50dfb"), true },
{ new Guid("20000000-0000-0000-0000-000000000007"), new Guid("10000000-0000-0000-0000-000000000004"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("f58f4242-af91-0b8d-5be5-4b07b4367e8c"), true },
{ new Guid("20000000-0000-0000-0000-000000000009"), new Guid("10000000-0000-0000-0000-000000000004"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("dafe8f7e-e527-5863-5cde-92599c4ba9eb"), true },
{ new Guid("20000000-0000-0000-0000-000000000013"), new Guid("10000000-0000-0000-0000-000000000004"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("76f58da8-09f6-c4e2-01bd-ddd94809c1f5"), true },
{ new Guid("20000000-0000-0000-0000-000000000014"), new Guid("10000000-0000-0000-0000-000000000004"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("c99adad6-94e8-0d5b-3d35-086f393207fb"), true },
{ new Guid("20000000-0000-0000-0000-000000000015"), new Guid("10000000-0000-0000-0000-000000000004"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("b22d4f78-7a21-da50-5351-553bdf06c9c2"), true },
{ new Guid("20000000-0000-0000-0000-000000000017"), new Guid("10000000-0000-0000-0000-000000000004"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("3cb22998-b618-c13f-48cd-9e7416f324fc"), true },
{ new Guid("20000000-0000-0000-0000-000000000021"), new Guid("10000000-0000-0000-0000-000000000004"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("5c00e972-a697-d71a-6545-1fd8618826cf"), true },
{ new Guid("20000000-0000-0000-0000-000000000022"), new Guid("10000000-0000-0000-0000-000000000004"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("e9b6af8e-7947-7290-9c92-2d9b6239f395"), true },
{ new Guid("20000000-0000-0000-0000-000000000023"), new Guid("10000000-0000-0000-0000-000000000004"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("db15215a-0a16-ef1f-5c82-6bb902a2b319"), true },
{ new Guid("20000000-0000-0000-0000-000000000024"), new Guid("10000000-0000-0000-0000-000000000004"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("7b4f3482-b8bd-d348-e4c1-ae6dc93a3771"), true },
{ new Guid("20000000-0000-0000-0000-000000000001"), new Guid("10000000-0000-0000-0000-000000000005"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("3e3e6f06-72c8-42fa-f7e6-8feeaca36d60"), true },
{ new Guid("20000000-0000-0000-0000-000000000005"), new Guid("10000000-0000-0000-0000-000000000005"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("d95e7d5a-b12c-12b8-fee9-39576c905fed"), true },
{ new Guid("20000000-0000-0000-0000-000000000006"), new Guid("10000000-0000-0000-0000-000000000005"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("d97abb20-e53d-c970-d337-3764584874fa"), true },
{ new Guid("20000000-0000-0000-0000-000000000007"), new Guid("10000000-0000-0000-0000-000000000005"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("a63446ad-14a9-6239-8791-0a1f86813903"), true },
{ new Guid("20000000-0000-0000-0000-000000000008"), new Guid("10000000-0000-0000-0000-000000000005"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("7f9502a2-feb4-f0ef-cb71-a62d00570fd0"), true },
{ new Guid("20000000-0000-0000-0000-000000000009"), new Guid("10000000-0000-0000-0000-000000000005"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("d1f4316a-72a0-66af-2a8a-1fdbdb4f8c3a"), true },
{ new Guid("20000000-0000-0000-0000-000000000010"), new Guid("10000000-0000-0000-0000-000000000005"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("30e94e9b-c70c-2df3-0813-e99ce4511838"), true },
{ new Guid("20000000-0000-0000-0000-000000000011"), new Guid("10000000-0000-0000-0000-000000000005"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("94f2f2ec-94b7-b768-76a4-9ee4256d95c1"), true },
{ new Guid("20000000-0000-0000-0000-000000000012"), new Guid("10000000-0000-0000-0000-000000000005"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("7201efa2-f829-8035-6607-174214f8fdba"), true },
{ new Guid("20000000-0000-0000-0000-000000000013"), new Guid("10000000-0000-0000-0000-000000000005"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("fcc6af61-cc43-066c-5634-2ddc24a8460d"), true },
{ new Guid("20000000-0000-0000-0000-000000000014"), new Guid("10000000-0000-0000-0000-000000000005"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("2f994b6b-b055-28c2-2fb1-99a0076a0e3f"), true },
{ new Guid("20000000-0000-0000-0000-000000000015"), new Guid("10000000-0000-0000-0000-000000000005"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("55e84a29-428d-bf95-d589-793ecd8bac6e"), true },
{ new Guid("20000000-0000-0000-0000-000000000016"), new Guid("10000000-0000-0000-0000-000000000005"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("43ec940a-44d2-9c47-a37d-049f2cfb2615"), true },
{ new Guid("20000000-0000-0000-0000-000000000017"), new Guid("10000000-0000-0000-0000-000000000005"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("ee175c87-23bd-5529-ed18-6ce46985c6ed"), true },
{ new Guid("20000000-0000-0000-0000-000000000018"), new Guid("10000000-0000-0000-0000-000000000005"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("2ae2eba4-4427-ab1e-cf03-e59a3925ef89"), true },
{ new Guid("20000000-0000-0000-0000-000000000019"), new Guid("10000000-0000-0000-0000-000000000005"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("c88131d2-51b2-2bac-ad15-c7c130eb26f8"), true },
{ new Guid("20000000-0000-0000-0000-000000000020"), new Guid("10000000-0000-0000-0000-000000000005"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("3a2ccc28-7416-388d-17f2-424d0520049f"), true },
{ new Guid("20000000-0000-0000-0000-000000000021"), new Guid("10000000-0000-0000-0000-000000000005"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("08de9913-8cfc-4ab4-2cb9-2bc9217fbb6e"), true },
{ new Guid("20000000-0000-0000-0000-000000000022"), new Guid("10000000-0000-0000-0000-000000000005"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("f3c8f315-3b2f-f923-e3cd-f3e575010138"), true },
{ new Guid("20000000-0000-0000-0000-000000000023"), new Guid("10000000-0000-0000-0000-000000000005"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("4d682c93-d6df-4214-436d-85f76a3ade93"), true },
{ new Guid("20000000-0000-0000-0000-000000000024"), new Guid("10000000-0000-0000-0000-000000000005"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("4b2c3b3b-c27d-410c-c1fc-7e43e280242f"), true },
{ new Guid("20000000-0000-0000-0000-000000000025"), new Guid("10000000-0000-0000-0000-000000000005"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("1d227ae7-0eb9-c742-41df-3472718cb077"), true },
{ new Guid("20000000-0000-0000-0000-000000000001"), new Guid("10000000-0000-0000-0000-000000000006"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("f64e20e2-f9ca-7ea5-42f4-c1256b0c0798"), true },
{ new Guid("20000000-0000-0000-0000-000000000002"), new Guid("10000000-0000-0000-0000-000000000006"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("7b6765bc-1510-cfc6-55ae-ac689caf0dcc"), true },
{ new Guid("20000000-0000-0000-0000-000000000003"), new Guid("10000000-0000-0000-0000-000000000006"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("d9e06d2d-82d6-84e8-e9ba-5406e9b4122d"), true },
{ new Guid("20000000-0000-0000-0000-000000000004"), new Guid("10000000-0000-0000-0000-000000000006"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("b53e01bb-4e54-3270-87b1-0d54f947e09b"), true },
{ new Guid("20000000-0000-0000-0000-000000000005"), new Guid("10000000-0000-0000-0000-000000000006"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("111b7d3c-3640-66e6-0757-b9fb67572943"), true },
{ new Guid("20000000-0000-0000-0000-000000000006"), new Guid("10000000-0000-0000-0000-000000000006"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("7ee06ff0-8557-b5bd-02dc-c4ff99e76348"), true },
{ new Guid("20000000-0000-0000-0000-000000000007"), new Guid("10000000-0000-0000-0000-000000000006"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("d20bcba0-a5af-de47-919d-a8645a828d27"), true },
{ new Guid("20000000-0000-0000-0000-000000000008"), new Guid("10000000-0000-0000-0000-000000000006"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("b3ef35de-afc2-d80a-115f-77ba954f9586"), true },
{ new Guid("20000000-0000-0000-0000-000000000009"), new Guid("10000000-0000-0000-0000-000000000006"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("d1b7e0a2-149f-b56b-8c63-bb8d10bfcc1e"), true },
{ new Guid("20000000-0000-0000-0000-000000000010"), new Guid("10000000-0000-0000-0000-000000000006"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("51e06fd0-cd20-ceeb-04cc-7dc79388c4a1"), true },
{ new Guid("20000000-0000-0000-0000-000000000011"), new Guid("10000000-0000-0000-0000-000000000006"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("25639115-7083-1324-c3bd-3ad90fcfa1a0"), true },
{ new Guid("20000000-0000-0000-0000-000000000012"), new Guid("10000000-0000-0000-0000-000000000006"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("51011cb7-5753-14ad-280c-86a6bd33124f"), true },
{ new Guid("20000000-0000-0000-0000-000000000013"), new Guid("10000000-0000-0000-0000-000000000006"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("51ce3fad-cdbc-cfad-6cf0-52b47ec3f200"), true },
{ new Guid("20000000-0000-0000-0000-000000000014"), new Guid("10000000-0000-0000-0000-000000000006"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("4a080973-c23f-3d1b-9883-6b7e06025a77"), true },
{ new Guid("20000000-0000-0000-0000-000000000015"), new Guid("10000000-0000-0000-0000-000000000006"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("3f78910a-76ee-f34b-52d3-af79d7f3b724"), true },
{ new Guid("20000000-0000-0000-0000-000000000016"), new Guid("10000000-0000-0000-0000-000000000006"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("812cbb8d-e268-aad3-373e-42b8720f0f45"), true },
{ new Guid("20000000-0000-0000-0000-000000000017"), new Guid("10000000-0000-0000-0000-000000000006"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("046cd13e-77f1-78c7-fa52-d49ea7fb9855"), true },
{ new Guid("20000000-0000-0000-0000-000000000018"), new Guid("10000000-0000-0000-0000-000000000006"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("96f24adf-386d-14d0-46fe-a22ceff2f79b"), true },
{ new Guid("20000000-0000-0000-0000-000000000019"), new Guid("10000000-0000-0000-0000-000000000006"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("9b5f23fb-2396-3f43-1d3e-97551eb4248d"), true },
{ new Guid("20000000-0000-0000-0000-000000000020"), new Guid("10000000-0000-0000-0000-000000000006"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("30397d69-3b96-56e7-0ca1-1e612dfb64f1"), true },
{ new Guid("20000000-0000-0000-0000-000000000021"), new Guid("10000000-0000-0000-0000-000000000006"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("91260fa1-3754-ad7e-10d8-2a8fd9b208ff"), true },
{ new Guid("20000000-0000-0000-0000-000000000022"), new Guid("10000000-0000-0000-0000-000000000006"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("8b993e65-b066-198a-385d-602660ed85ac"), true },
{ new Guid("20000000-0000-0000-0000-000000000023"), new Guid("10000000-0000-0000-0000-000000000006"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("248d6092-c2e5-5f8f-5ef0-cd60dc94cbc8"), true },
{ new Guid("20000000-0000-0000-0000-000000000024"), new Guid("10000000-0000-0000-0000-000000000006"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("5f6e3c68-27d2-e00f-d5c9-4ce9e2b9587f"), true },
{ new Guid("20000000-0000-0000-0000-000000000025"), new Guid("10000000-0000-0000-0000-000000000006"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("96a4ac9e-cfea-6391-8020-75b2980bf2a3"), true },
{ new Guid("20000000-0000-0000-0000-000000000026"), new Guid("10000000-0000-0000-0000-000000000006"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("6eb59953-807d-aa61-29e3-1699c0aebda9"), true },
{ new Guid("20000000-0000-0000-0000-000000000027"), new Guid("10000000-0000-0000-0000-000000000006"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("1095440c-ce52-235b-b95f-6b28c5d1b3a8"), true },
{ new Guid("20000000-0000-0000-0000-000000000028"), new Guid("10000000-0000-0000-0000-000000000006"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("3a56ab5c-f0c5-a37b-0795-de061597628f"), true },
{ new Guid("20000000-0000-0000-0000-000000000029"), new Guid("10000000-0000-0000-0000-000000000006"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("104ebcfb-ca82-3f95-d75e-35a943618176"), true },
{ new Guid("20000000-0000-0000-0000-000000000030"), new Guid("10000000-0000-0000-0000-000000000006"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("ace990cf-6654-5e68-3dc8-1e62347e0946"), true }
});
migrationBuilder.CreateIndex(
name: "IX_Stores_FK_RouteId",
table: "Stores",
column: "FK_RouteId");
migrationBuilder.CreateIndex(
name: "IX_Routes_FK_RegionId",
table: "Routes",
column: "FK_RegionId");
migrationBuilder.CreateIndex(
name: "IX_RolePermissions_FK_PermissionId",
table: "RolePermissions",
column: "FK_PermissionId");
migrationBuilder.CreateIndex(
name: "IX_UserRegionScopes_FK_RegionId",
table: "UserRegionScopes",
column: "FK_RegionId");
migrationBuilder.CreateIndex(
name: "IX_UserRouteScopes_FK_RouteId",
table: "UserRouteScopes",
column: "FK_RouteId");
migrationBuilder.CreateIndex(
name: "IX_Users_FK_ReportsToUserId",
table: "Users",
column: "FK_ReportsToUserId");
migrationBuilder.CreateIndex(
name: "IX_Users_FK_RoleId",
table: "Users",
column: "FK_RoleId");
migrationBuilder.CreateIndex(
name: "IX_UserStoreScopes_FK_StoreId",
table: "UserStoreScopes",
column: "FK_StoreId");
migrationBuilder.AddForeignKey(
name: "FK_Routes_Regions_FK_RegionId",
table: "Routes",
column: "FK_RegionId",
principalTable: "Regions",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
migrationBuilder.AddForeignKey(
name: "FK_Stores_Routes_FK_RouteId",
table: "Stores",
column: "FK_RouteId",
principalTable: "Routes",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_Routes_Regions_FK_RegionId",
table: "Routes");
migrationBuilder.DropForeignKey(
name: "FK_Stores_Routes_FK_RouteId",
table: "Stores");
migrationBuilder.DropTable(
name: "RolePermissions");
migrationBuilder.DropTable(
name: "UserRegionScopes");
migrationBuilder.DropTable(
name: "UserRouteScopes");
migrationBuilder.DropTable(
name: "UserStoreScopes");
migrationBuilder.DropTable(
name: "Permissions");
migrationBuilder.DropTable(
name: "Users");
migrationBuilder.DropTable(
name: "Roles");
migrationBuilder.DropIndex(
name: "IX_Stores_FK_RouteId",
table: "Stores");
migrationBuilder.DropIndex(
name: "IX_Routes_FK_RegionId",
table: "Routes");
migrationBuilder.DropColumn(
name: "FK_RouteId",
table: "Stores");
migrationBuilder.DropColumn(
name: "FK_RegionId",
table: "Routes");
}
}
}
@@ -1,621 +0,0 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
#pragma warning disable CA1814 // Prefer jagged arrays over multidimensional
namespace Distribution.Migrations
{
/// <inheritdoc />
public partial class AddItemCatalog : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<decimal>(
name: "UnitPrice",
table: "OrderItems",
type: "decimal(18,2)",
nullable: false,
defaultValue: 0m);
migrationBuilder.AddColumn<string>(
name: "Description",
table: "Items",
type: "longtext",
nullable: true);
migrationBuilder.AddColumn<int>(
name: "ExternalErpItemId",
table: "Items",
type: "int",
nullable: true);
migrationBuilder.AddColumn<Guid>(
name: "FK_BaseUomId",
table: "Items",
type: "char(36)",
nullable: false,
defaultValue: new Guid("00000000-0000-0000-0000-000000000000"));
migrationBuilder.AddColumn<Guid>(
name: "FK_BrandId",
table: "Items",
type: "char(36)",
nullable: true);
migrationBuilder.AddColumn<Guid>(
name: "FK_CategoryId",
table: "Items",
type: "char(36)",
nullable: false,
defaultValue: new Guid("00000000-0000-0000-0000-000000000000"));
migrationBuilder.AddColumn<Guid>(
name: "FK_SubCategoryId",
table: "Items",
type: "char(36)",
nullable: true);
migrationBuilder.AddColumn<DateTime>(
name: "LastSyncedAt",
table: "Items",
type: "datetime(6)",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "Name",
table: "Items",
type: "longtext",
nullable: false);
migrationBuilder.AddColumn<decimal>(
name: "SalePrice",
table: "Items",
type: "decimal(18,2)",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "Sku",
table: "Items",
type: "longtext",
nullable: false);
migrationBuilder.AddColumn<int>(
name: "SyncSource",
table: "Items",
type: "int",
nullable: false,
defaultValue: 0);
migrationBuilder.CreateTable(
name: "Brands",
columns: table => new
{
Id = table.Column<Guid>(type: "char(36)", nullable: false),
Name = table.Column<string>(type: "longtext", nullable: false),
ExternalErpId = table.Column<int>(type: "int", nullable: true),
CreatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false),
IsActive = table.Column<bool>(type: "tinyint(1)", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Brands", x => x.Id);
})
.Annotation("MySQL:Charset", "utf8mb4");
migrationBuilder.CreateTable(
name: "Categories",
columns: table => new
{
Id = table.Column<Guid>(type: "char(36)", nullable: false),
Name = table.Column<string>(type: "longtext", nullable: false),
ExternalErpId = table.Column<int>(type: "int", nullable: true),
CreatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false),
IsActive = table.Column<bool>(type: "tinyint(1)", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Categories", x => x.Id);
})
.Annotation("MySQL:Charset", "utf8mb4");
migrationBuilder.CreateTable(
name: "Uoms",
columns: table => new
{
Id = table.Column<Guid>(type: "char(36)", nullable: false),
Name = table.Column<string>(type: "longtext", nullable: false),
ExternalErpId = table.Column<int>(type: "int", nullable: true),
CreatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false),
IsActive = table.Column<bool>(type: "tinyint(1)", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Uoms", x => x.Id);
})
.Annotation("MySQL:Charset", "utf8mb4");
migrationBuilder.CreateTable(
name: "SubCategories",
columns: table => new
{
Id = table.Column<Guid>(type: "char(36)", nullable: false),
Name = table.Column<string>(type: "longtext", nullable: false),
FK_CategoryId = table.Column<Guid>(type: "char(36)", nullable: false),
ExternalErpId = table.Column<int>(type: "int", nullable: true),
CreatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false),
IsActive = table.Column<bool>(type: "tinyint(1)", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_SubCategories", x => x.Id);
table.ForeignKey(
name: "FK_SubCategories_Categories_FK_CategoryId",
column: x => x.FK_CategoryId,
principalTable: "Categories",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
})
.Annotation("MySQL:Charset", "utf8mb4");
migrationBuilder.CreateTable(
name: "UomConversions",
columns: table => new
{
Id = table.Column<Guid>(type: "char(36)", nullable: false),
FK_ItemId = table.Column<Guid>(type: "char(36)", nullable: false),
FK_FromUomId = table.Column<Guid>(type: "char(36)", nullable: false),
FK_ToUomId = table.Column<Guid>(type: "char(36)", nullable: false),
Factor = table.Column<decimal>(type: "decimal(18,2)", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_UomConversions", x => x.Id);
table.ForeignKey(
name: "FK_UomConversions_Items_FK_ItemId",
column: x => x.FK_ItemId,
principalTable: "Items",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_UomConversions_Uoms_FK_FromUomId",
column: x => x.FK_FromUomId,
principalTable: "Uoms",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_UomConversions_Uoms_FK_ToUomId",
column: x => x.FK_ToUomId,
principalTable: "Uoms",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
})
.Annotation("MySQL:Charset", "utf8mb4");
migrationBuilder.InsertData(
table: "Permissions",
columns: new[] { "Id", "Code", "CreatedAt", "Description", "IsActive" },
values: new object[,]
{
{ new Guid("20000000-0000-0000-0000-000000000031"), "items.view", new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), "View items, brands and UOMs", true },
{ new Guid("20000000-0000-0000-0000-000000000032"), "items.create", new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), "Create items, brands and UOMs", true },
{ new Guid("20000000-0000-0000-0000-000000000033"), "items.update", new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), "Update items, brands and UOMs", true },
{ new Guid("20000000-0000-0000-0000-000000000034"), "items.delete", new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), "Delete items, brands and UOMs", true },
{ new Guid("20000000-0000-0000-0000-000000000035"), "categories.view", new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), "View categories", true },
{ new Guid("20000000-0000-0000-0000-000000000036"), "categories.create", new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), "Create categories and subcategories", true },
{ new Guid("20000000-0000-0000-0000-000000000037"), "categories.update", new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), "Update categories and subcategories", true },
{ new Guid("20000000-0000-0000-0000-000000000038"), "categories.delete", new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), "Delete categories and subcategories", true }
});
migrationBuilder.InsertData(
table: "RolePermissions",
columns: new[] { "FK_PermissionId", "FK_RoleId", "CreatedAt", "Id", "IsActive" },
values: new object[,]
{
{ new Guid("20000000-0000-0000-0000-000000000031"), new Guid("10000000-0000-0000-0000-000000000001"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("7596f703-c6ab-316c-7c80-3f4e3daa174e"), true },
{ new Guid("20000000-0000-0000-0000-000000000035"), new Guid("10000000-0000-0000-0000-000000000001"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("aa120c36-488e-34ba-e10a-2ac45e46e119"), true },
{ new Guid("20000000-0000-0000-0000-000000000031"), new Guid("10000000-0000-0000-0000-000000000002"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("b7dee5e3-f329-95a5-75c1-d02c3e40e229"), true },
{ new Guid("20000000-0000-0000-0000-000000000035"), new Guid("10000000-0000-0000-0000-000000000002"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("e5506b30-527d-08f9-6caa-8a45d3d02999"), true },
{ new Guid("20000000-0000-0000-0000-000000000031"), new Guid("10000000-0000-0000-0000-000000000003"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("7780d277-9597-84ff-1a1a-f63fc6b4d3cb"), true },
{ new Guid("20000000-0000-0000-0000-000000000032"), new Guid("10000000-0000-0000-0000-000000000003"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("8083c659-9506-0bd9-b793-3d6148ce9192"), true },
{ new Guid("20000000-0000-0000-0000-000000000033"), new Guid("10000000-0000-0000-0000-000000000003"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("d49f086e-30f0-6ce8-ae96-4395d5e04472"), true },
{ new Guid("20000000-0000-0000-0000-000000000035"), new Guid("10000000-0000-0000-0000-000000000003"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("1e7fe1b6-5150-4b99-38ed-088d9495cfd8"), true },
{ new Guid("20000000-0000-0000-0000-000000000031"), new Guid("10000000-0000-0000-0000-000000000004"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("a701da28-aa75-3c1d-abdb-4317404f8df6"), true },
{ new Guid("20000000-0000-0000-0000-000000000032"), new Guid("10000000-0000-0000-0000-000000000004"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("2c025310-765f-7d07-ab9a-50524c13d1d3"), true },
{ new Guid("20000000-0000-0000-0000-000000000033"), new Guid("10000000-0000-0000-0000-000000000004"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("7c392a13-1a57-0d85-dffa-2fb9e892abe9"), true },
{ new Guid("20000000-0000-0000-0000-000000000034"), new Guid("10000000-0000-0000-0000-000000000004"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("97708eed-6ce4-7710-abde-01d00c0c879b"), true },
{ new Guid("20000000-0000-0000-0000-000000000035"), new Guid("10000000-0000-0000-0000-000000000004"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("9afe507e-191e-bdee-0c5a-cfbcbf1efb3e"), true },
{ new Guid("20000000-0000-0000-0000-000000000036"), new Guid("10000000-0000-0000-0000-000000000004"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("9c278616-4289-96fb-944d-b8f1495ccb00"), true },
{ new Guid("20000000-0000-0000-0000-000000000037"), new Guid("10000000-0000-0000-0000-000000000004"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("d1dcef02-d4c3-caba-372f-dab82011e9c1"), true },
{ new Guid("20000000-0000-0000-0000-000000000038"), new Guid("10000000-0000-0000-0000-000000000004"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("9cc36229-dfba-49b9-1d6e-8929546e894d"), true },
{ new Guid("20000000-0000-0000-0000-000000000031"), new Guid("10000000-0000-0000-0000-000000000005"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("69f93a29-1df1-7b20-9d5c-94c264f8c4b8"), true },
{ new Guid("20000000-0000-0000-0000-000000000032"), new Guid("10000000-0000-0000-0000-000000000005"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("070813f7-4053-2889-0d14-d0d5834208c2"), true },
{ new Guid("20000000-0000-0000-0000-000000000033"), new Guid("10000000-0000-0000-0000-000000000005"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("0efa7259-aadb-0019-4be5-38d3f8f4cd54"), true },
{ new Guid("20000000-0000-0000-0000-000000000034"), new Guid("10000000-0000-0000-0000-000000000005"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("9e60e7d7-8bd6-c1c7-4bf1-4de2c9dcb91c"), true },
{ new Guid("20000000-0000-0000-0000-000000000035"), new Guid("10000000-0000-0000-0000-000000000005"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("5df82318-bc01-1947-09ef-60508e02b257"), true },
{ new Guid("20000000-0000-0000-0000-000000000036"), new Guid("10000000-0000-0000-0000-000000000005"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("34f478f4-6c96-4b31-b68a-28c7942c0789"), true },
{ new Guid("20000000-0000-0000-0000-000000000037"), new Guid("10000000-0000-0000-0000-000000000005"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("69598e0e-b7ad-477b-c587-7abf6189162d"), true },
{ new Guid("20000000-0000-0000-0000-000000000038"), new Guid("10000000-0000-0000-0000-000000000005"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("b36fb2d4-43df-44f8-ff51-de6d88644c94"), true },
{ new Guid("20000000-0000-0000-0000-000000000031"), new Guid("10000000-0000-0000-0000-000000000006"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("bd209f90-6671-8ea0-eb20-aa151faec6a0"), true },
{ new Guid("20000000-0000-0000-0000-000000000032"), new Guid("10000000-0000-0000-0000-000000000006"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("4cc32686-8391-0214-5747-db14f53c5b6f"), true },
{ new Guid("20000000-0000-0000-0000-000000000033"), new Guid("10000000-0000-0000-0000-000000000006"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("26c6d86a-835d-6f0d-3730-66a46ea26b11"), true },
{ new Guid("20000000-0000-0000-0000-000000000034"), new Guid("10000000-0000-0000-0000-000000000006"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("e8039d9c-a286-fb74-5abb-b5c6c67f243a"), true },
{ new Guid("20000000-0000-0000-0000-000000000035"), new Guid("10000000-0000-0000-0000-000000000006"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("d9e038cb-bcff-cce1-5d34-b4e63a9ce694"), true },
{ new Guid("20000000-0000-0000-0000-000000000036"), new Guid("10000000-0000-0000-0000-000000000006"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("343e71f0-da6b-2489-5571-f8d7c6378ee7"), true },
{ new Guid("20000000-0000-0000-0000-000000000037"), new Guid("10000000-0000-0000-0000-000000000006"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("ac894328-a935-8e6e-8427-6c0f36ac75da"), true },
{ new Guid("20000000-0000-0000-0000-000000000038"), new Guid("10000000-0000-0000-0000-000000000006"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("1d6273d4-db6a-d70e-4b5f-98cb634d668a"), true }
});
migrationBuilder.CreateIndex(
name: "IX_Items_FK_BaseUomId",
table: "Items",
column: "FK_BaseUomId");
migrationBuilder.CreateIndex(
name: "IX_Items_FK_BrandId",
table: "Items",
column: "FK_BrandId");
migrationBuilder.CreateIndex(
name: "IX_Items_FK_CategoryId",
table: "Items",
column: "FK_CategoryId");
migrationBuilder.CreateIndex(
name: "IX_Items_FK_SubCategoryId",
table: "Items",
column: "FK_SubCategoryId");
migrationBuilder.CreateIndex(
name: "IX_SubCategories_FK_CategoryId",
table: "SubCategories",
column: "FK_CategoryId");
migrationBuilder.CreateIndex(
name: "IX_UomConversions_FK_FromUomId",
table: "UomConversions",
column: "FK_FromUomId");
migrationBuilder.CreateIndex(
name: "IX_UomConversions_FK_ItemId",
table: "UomConversions",
column: "FK_ItemId");
migrationBuilder.CreateIndex(
name: "IX_UomConversions_FK_ToUomId",
table: "UomConversions",
column: "FK_ToUomId");
migrationBuilder.AddForeignKey(
name: "FK_Items_Brands_FK_BrandId",
table: "Items",
column: "FK_BrandId",
principalTable: "Brands",
principalColumn: "Id");
migrationBuilder.AddForeignKey(
name: "FK_Items_Categories_FK_CategoryId",
table: "Items",
column: "FK_CategoryId",
principalTable: "Categories",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
migrationBuilder.AddForeignKey(
name: "FK_Items_SubCategories_FK_SubCategoryId",
table: "Items",
column: "FK_SubCategoryId",
principalTable: "SubCategories",
principalColumn: "Id");
migrationBuilder.AddForeignKey(
name: "FK_Items_Uoms_FK_BaseUomId",
table: "Items",
column: "FK_BaseUomId",
principalTable: "Uoms",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_Items_Brands_FK_BrandId",
table: "Items");
migrationBuilder.DropForeignKey(
name: "FK_Items_Categories_FK_CategoryId",
table: "Items");
migrationBuilder.DropForeignKey(
name: "FK_Items_SubCategories_FK_SubCategoryId",
table: "Items");
migrationBuilder.DropForeignKey(
name: "FK_Items_Uoms_FK_BaseUomId",
table: "Items");
migrationBuilder.DropTable(
name: "Brands");
migrationBuilder.DropTable(
name: "SubCategories");
migrationBuilder.DropTable(
name: "UomConversions");
migrationBuilder.DropTable(
name: "Categories");
migrationBuilder.DropTable(
name: "Uoms");
migrationBuilder.DropIndex(
name: "IX_Items_FK_BaseUomId",
table: "Items");
migrationBuilder.DropIndex(
name: "IX_Items_FK_BrandId",
table: "Items");
migrationBuilder.DropIndex(
name: "IX_Items_FK_CategoryId",
table: "Items");
migrationBuilder.DropIndex(
name: "IX_Items_FK_SubCategoryId",
table: "Items");
migrationBuilder.DeleteData(
table: "RolePermissions",
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000031"), new Guid("10000000-0000-0000-0000-000000000001") });
migrationBuilder.DeleteData(
table: "RolePermissions",
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000035"), new Guid("10000000-0000-0000-0000-000000000001") });
migrationBuilder.DeleteData(
table: "RolePermissions",
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000031"), new Guid("10000000-0000-0000-0000-000000000002") });
migrationBuilder.DeleteData(
table: "RolePermissions",
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000035"), new Guid("10000000-0000-0000-0000-000000000002") });
migrationBuilder.DeleteData(
table: "RolePermissions",
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000031"), new Guid("10000000-0000-0000-0000-000000000003") });
migrationBuilder.DeleteData(
table: "RolePermissions",
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000032"), new Guid("10000000-0000-0000-0000-000000000003") });
migrationBuilder.DeleteData(
table: "RolePermissions",
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000033"), new Guid("10000000-0000-0000-0000-000000000003") });
migrationBuilder.DeleteData(
table: "RolePermissions",
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000035"), new Guid("10000000-0000-0000-0000-000000000003") });
migrationBuilder.DeleteData(
table: "RolePermissions",
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000031"), new Guid("10000000-0000-0000-0000-000000000004") });
migrationBuilder.DeleteData(
table: "RolePermissions",
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000032"), new Guid("10000000-0000-0000-0000-000000000004") });
migrationBuilder.DeleteData(
table: "RolePermissions",
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000033"), new Guid("10000000-0000-0000-0000-000000000004") });
migrationBuilder.DeleteData(
table: "RolePermissions",
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000034"), new Guid("10000000-0000-0000-0000-000000000004") });
migrationBuilder.DeleteData(
table: "RolePermissions",
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000035"), new Guid("10000000-0000-0000-0000-000000000004") });
migrationBuilder.DeleteData(
table: "RolePermissions",
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000036"), new Guid("10000000-0000-0000-0000-000000000004") });
migrationBuilder.DeleteData(
table: "RolePermissions",
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000037"), new Guid("10000000-0000-0000-0000-000000000004") });
migrationBuilder.DeleteData(
table: "RolePermissions",
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000038"), new Guid("10000000-0000-0000-0000-000000000004") });
migrationBuilder.DeleteData(
table: "RolePermissions",
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000031"), new Guid("10000000-0000-0000-0000-000000000005") });
migrationBuilder.DeleteData(
table: "RolePermissions",
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000032"), new Guid("10000000-0000-0000-0000-000000000005") });
migrationBuilder.DeleteData(
table: "RolePermissions",
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000033"), new Guid("10000000-0000-0000-0000-000000000005") });
migrationBuilder.DeleteData(
table: "RolePermissions",
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000034"), new Guid("10000000-0000-0000-0000-000000000005") });
migrationBuilder.DeleteData(
table: "RolePermissions",
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000035"), new Guid("10000000-0000-0000-0000-000000000005") });
migrationBuilder.DeleteData(
table: "RolePermissions",
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000036"), new Guid("10000000-0000-0000-0000-000000000005") });
migrationBuilder.DeleteData(
table: "RolePermissions",
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000037"), new Guid("10000000-0000-0000-0000-000000000005") });
migrationBuilder.DeleteData(
table: "RolePermissions",
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000038"), new Guid("10000000-0000-0000-0000-000000000005") });
migrationBuilder.DeleteData(
table: "RolePermissions",
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000031"), new Guid("10000000-0000-0000-0000-000000000006") });
migrationBuilder.DeleteData(
table: "RolePermissions",
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000032"), new Guid("10000000-0000-0000-0000-000000000006") });
migrationBuilder.DeleteData(
table: "RolePermissions",
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000033"), new Guid("10000000-0000-0000-0000-000000000006") });
migrationBuilder.DeleteData(
table: "RolePermissions",
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000034"), new Guid("10000000-0000-0000-0000-000000000006") });
migrationBuilder.DeleteData(
table: "RolePermissions",
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000035"), new Guid("10000000-0000-0000-0000-000000000006") });
migrationBuilder.DeleteData(
table: "RolePermissions",
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000036"), new Guid("10000000-0000-0000-0000-000000000006") });
migrationBuilder.DeleteData(
table: "RolePermissions",
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000037"), new Guid("10000000-0000-0000-0000-000000000006") });
migrationBuilder.DeleteData(
table: "RolePermissions",
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000038"), new Guid("10000000-0000-0000-0000-000000000006") });
migrationBuilder.DeleteData(
table: "Permissions",
keyColumn: "Id",
keyValue: new Guid("20000000-0000-0000-0000-000000000031"));
migrationBuilder.DeleteData(
table: "Permissions",
keyColumn: "Id",
keyValue: new Guid("20000000-0000-0000-0000-000000000032"));
migrationBuilder.DeleteData(
table: "Permissions",
keyColumn: "Id",
keyValue: new Guid("20000000-0000-0000-0000-000000000033"));
migrationBuilder.DeleteData(
table: "Permissions",
keyColumn: "Id",
keyValue: new Guid("20000000-0000-0000-0000-000000000034"));
migrationBuilder.DeleteData(
table: "Permissions",
keyColumn: "Id",
keyValue: new Guid("20000000-0000-0000-0000-000000000035"));
migrationBuilder.DeleteData(
table: "Permissions",
keyColumn: "Id",
keyValue: new Guid("20000000-0000-0000-0000-000000000036"));
migrationBuilder.DeleteData(
table: "Permissions",
keyColumn: "Id",
keyValue: new Guid("20000000-0000-0000-0000-000000000037"));
migrationBuilder.DeleteData(
table: "Permissions",
keyColumn: "Id",
keyValue: new Guid("20000000-0000-0000-0000-000000000038"));
migrationBuilder.DropColumn(
name: "UnitPrice",
table: "OrderItems");
migrationBuilder.DropColumn(
name: "Description",
table: "Items");
migrationBuilder.DropColumn(
name: "ExternalErpItemId",
table: "Items");
migrationBuilder.DropColumn(
name: "FK_BaseUomId",
table: "Items");
migrationBuilder.DropColumn(
name: "FK_BrandId",
table: "Items");
migrationBuilder.DropColumn(
name: "FK_CategoryId",
table: "Items");
migrationBuilder.DropColumn(
name: "FK_SubCategoryId",
table: "Items");
migrationBuilder.DropColumn(
name: "LastSyncedAt",
table: "Items");
migrationBuilder.DropColumn(
name: "Name",
table: "Items");
migrationBuilder.DropColumn(
name: "SalePrice",
table: "Items");
migrationBuilder.DropColumn(
name: "Sku",
table: "Items");
migrationBuilder.DropColumn(
name: "SyncSource",
table: "Items");
}
}
}
@@ -1,164 +0,0 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Distribution.Migrations
{
/// <inheritdoc />
public partial class item_catalog : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterColumn<decimal>(
name: "Factor",
table: "UomConversions",
type: "decimal(65,30)",
nullable: false,
oldClrType: typeof(decimal),
oldType: "decimal(18,2)");
migrationBuilder.AlterColumn<decimal>(
name: "OneTimeAmount",
table: "Stores",
type: "decimal(65,30)",
nullable: false,
oldClrType: typeof(decimal),
oldType: "decimal(18,2)");
migrationBuilder.AlterColumn<decimal>(
name: "Longitude",
table: "Stores",
type: "decimal(65,30)",
nullable: false,
oldClrType: typeof(decimal),
oldType: "decimal(18,2)");
migrationBuilder.AlterColumn<decimal>(
name: "Latitude",
table: "Stores",
type: "decimal(65,30)",
nullable: false,
oldClrType: typeof(decimal),
oldType: "decimal(18,2)");
migrationBuilder.AlterColumn<decimal>(
name: "CreditAmount",
table: "Stores",
type: "decimal(65,30)",
nullable: false,
oldClrType: typeof(decimal),
oldType: "decimal(18,2)");
migrationBuilder.AlterColumn<decimal>(
name: "Length",
table: "Routes",
type: "decimal(65,30)",
nullable: false,
oldClrType: typeof(decimal),
oldType: "decimal(18,2)");
migrationBuilder.AlterColumn<decimal>(
name: "TotalAmount",
table: "Orders",
type: "decimal(65,30)",
nullable: false,
oldClrType: typeof(decimal),
oldType: "decimal(18,2)");
migrationBuilder.AlterColumn<decimal>(
name: "UnitPrice",
table: "OrderItems",
type: "decimal(65,30)",
nullable: false,
oldClrType: typeof(decimal),
oldType: "decimal(18,2)");
migrationBuilder.AlterColumn<decimal>(
name: "SalePrice",
table: "Items",
type: "decimal(65,30)",
nullable: true,
oldClrType: typeof(decimal),
oldType: "decimal(18,2)",
oldNullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterColumn<decimal>(
name: "Factor",
table: "UomConversions",
type: "decimal(18,2)",
nullable: false,
oldClrType: typeof(decimal),
oldType: "decimal(65,30)");
migrationBuilder.AlterColumn<decimal>(
name: "OneTimeAmount",
table: "Stores",
type: "decimal(18,2)",
nullable: false,
oldClrType: typeof(decimal),
oldType: "decimal(65,30)");
migrationBuilder.AlterColumn<decimal>(
name: "Longitude",
table: "Stores",
type: "decimal(18,2)",
nullable: false,
oldClrType: typeof(decimal),
oldType: "decimal(65,30)");
migrationBuilder.AlterColumn<decimal>(
name: "Latitude",
table: "Stores",
type: "decimal(18,2)",
nullable: false,
oldClrType: typeof(decimal),
oldType: "decimal(65,30)");
migrationBuilder.AlterColumn<decimal>(
name: "CreditAmount",
table: "Stores",
type: "decimal(18,2)",
nullable: false,
oldClrType: typeof(decimal),
oldType: "decimal(65,30)");
migrationBuilder.AlterColumn<decimal>(
name: "Length",
table: "Routes",
type: "decimal(18,2)",
nullable: false,
oldClrType: typeof(decimal),
oldType: "decimal(65,30)");
migrationBuilder.AlterColumn<decimal>(
name: "TotalAmount",
table: "Orders",
type: "decimal(18,2)",
nullable: false,
oldClrType: typeof(decimal),
oldType: "decimal(65,30)");
migrationBuilder.AlterColumn<decimal>(
name: "UnitPrice",
table: "OrderItems",
type: "decimal(18,2)",
nullable: false,
oldClrType: typeof(decimal),
oldType: "decimal(65,30)");
migrationBuilder.AlterColumn<decimal>(
name: "SalePrice",
table: "Items",
type: "decimal(18,2)",
nullable: true,
oldClrType: typeof(decimal),
oldType: "decimal(65,30)",
oldNullable: true);
}
}
}
@@ -12,8 +12,8 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
namespace Distribution.Migrations
{
[DbContext(typeof(AppDBContext))]
[Migration("20260819074545_item_catalog")]
partial class item_catalog
[Migration("20260819163125_init")]
partial class init
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
@@ -55,6 +55,9 @@ namespace Distribution.Migrations
.ValueGeneratedOnAdd()
.HasColumnType("char(36)");
b.Property<bool>("AllowCustomerReturn")
.HasColumnType("tinyint(1)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
@@ -1682,6 +1685,81 @@ namespace Distribution.Migrations
b.ToTable("SaleRoutes");
});
modelBuilder.Entity("Distribution.Models.StockBalance", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("char(36)");
b.Property<Guid>("FK_ItemId")
.HasColumnType("char(36)");
b.Property<Guid>("LocationId")
.HasColumnType("char(36)");
b.Property<int>("LocationType")
.HasColumnType("int");
b.Property<decimal>("Quantity")
.HasColumnType("decimal(65,30)");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("datetime(6)");
b.HasKey("Id");
b.HasIndex("FK_ItemId", "LocationId")
.IsUnique();
b.ToTable("StockBalances");
});
modelBuilder.Entity("Distribution.Models.StockMovement", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("char(36)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
b.Property<Guid>("CreatedBy")
.HasColumnType("char(36)");
b.Property<Guid>("FK_ItemId")
.HasColumnType("char(36)");
b.Property<Guid?>("FromLocationId")
.HasColumnType("char(36)");
b.Property<int?>("FromLocationType")
.HasColumnType("int");
b.Property<decimal>("Quantity")
.HasColumnType("decimal(65,30)");
b.Property<string>("ReasonCode")
.HasColumnType("longtext");
b.Property<Guid?>("ReferenceId")
.HasColumnType("char(36)");
b.Property<Guid?>("ToLocationId")
.HasColumnType("char(36)");
b.Property<int?>("ToLocationType")
.HasColumnType("int");
b.Property<int>("Type")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("FK_ItemId");
b.ToTable("StockMovements");
});
modelBuilder.Entity("Distribution.Models.Store", b =>
{
b.Property<Guid>("Id")
@@ -2095,6 +2173,28 @@ namespace Distribution.Migrations
b.Navigation("Region");
});
modelBuilder.Entity("Distribution.Models.StockBalance", b =>
{
b.HasOne("Distribution.Models.Item", "Item")
.WithMany()
.HasForeignKey("FK_ItemId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Item");
});
modelBuilder.Entity("Distribution.Models.StockMovement", b =>
{
b.HasOne("Distribution.Models.Item", "Item")
.WithMany()
.HasForeignKey("FK_ItemId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Item");
});
modelBuilder.Entity("Distribution.Models.Store", b =>
{
b.HasOne("Distribution.Models.Route", "Route")
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,716 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
#pragma warning disable CA1814 // Prefer jagged arrays over multidimensional
namespace Distribution.Migrations
{
/// <inheritdoc />
public partial class AddStoreCreditAndKyc : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DeleteData(
table: "RolePermissions",
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000021"), new Guid("10000000-0000-0000-0000-000000000001") });
migrationBuilder.DeleteData(
table: "RolePermissions",
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000022"), new Guid("10000000-0000-0000-0000-000000000001") });
migrationBuilder.DeleteData(
table: "RolePermissions",
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000031"), new Guid("10000000-0000-0000-0000-000000000001") });
migrationBuilder.DeleteData(
table: "RolePermissions",
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000035"), new Guid("10000000-0000-0000-0000-000000000001") });
migrationBuilder.DeleteData(
table: "RolePermissions",
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000021"), new Guid("10000000-0000-0000-0000-000000000002") });
migrationBuilder.DeleteData(
table: "RolePermissions",
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000022"), new Guid("10000000-0000-0000-0000-000000000002") });
migrationBuilder.DeleteData(
table: "RolePermissions",
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000031"), new Guid("10000000-0000-0000-0000-000000000002") });
migrationBuilder.DeleteData(
table: "RolePermissions",
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000035"), new Guid("10000000-0000-0000-0000-000000000002") });
migrationBuilder.DeleteData(
table: "RolePermissions",
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000021"), new Guid("10000000-0000-0000-0000-000000000003") });
migrationBuilder.DeleteData(
table: "RolePermissions",
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000022"), new Guid("10000000-0000-0000-0000-000000000003") });
migrationBuilder.DeleteData(
table: "RolePermissions",
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000031"), new Guid("10000000-0000-0000-0000-000000000003") });
migrationBuilder.DeleteData(
table: "RolePermissions",
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000032"), new Guid("10000000-0000-0000-0000-000000000003") });
migrationBuilder.DeleteData(
table: "RolePermissions",
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000021"), new Guid("10000000-0000-0000-0000-000000000004") });
migrationBuilder.DeleteData(
table: "RolePermissions",
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000022"), new Guid("10000000-0000-0000-0000-000000000004") });
migrationBuilder.DeleteData(
table: "RolePermissions",
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000031"), new Guid("10000000-0000-0000-0000-000000000004") });
migrationBuilder.DeleteData(
table: "RolePermissions",
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000032"), new Guid("10000000-0000-0000-0000-000000000004") });
migrationBuilder.DeleteData(
table: "RolePermissions",
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000031"), new Guid("10000000-0000-0000-0000-000000000005") });
migrationBuilder.DeleteData(
table: "RolePermissions",
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000032"), new Guid("10000000-0000-0000-0000-000000000005") });
migrationBuilder.AddColumn<decimal>(
name: "CreditLimit",
table: "Stores",
type: "decimal(65,30)",
nullable: false,
defaultValue: 0m);
migrationBuilder.AddColumn<int>(
name: "KycStatus",
table: "Stores",
type: "int",
nullable: false,
defaultValue: 0);
migrationBuilder.AddColumn<decimal>(
name: "OutstandingBalance",
table: "Stores",
type: "decimal(65,30)",
nullable: false,
defaultValue: 0m);
migrationBuilder.AddColumn<string>(
name: "Segment",
table: "Stores",
type: "longtext",
nullable: true)
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.CreateTable(
name: "StoreKycDocuments",
columns: table => new
{
Id = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
FK_StoreId = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
DocumentType = table.Column<string>(type: "longtext", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
FileReference = table.Column<string>(type: "longtext", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
UploadedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false),
CreatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false),
IsActive = table.Column<bool>(type: "tinyint(1)", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_StoreKycDocuments", x => x.Id);
table.ForeignKey(
name: "FK_StoreKycDocuments_Stores_FK_StoreId",
column: x => x.FK_StoreId,
principalTable: "Stores",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
})
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.UpdateData(
table: "Permissions",
keyColumn: "Id",
keyValue: new Guid("20000000-0000-0000-0000-000000000017"),
columns: new[] { "Code", "Description" },
values: new object[] { "stores.kyc.verify", "Verify or reject store KYC documents" });
migrationBuilder.UpdateData(
table: "Permissions",
keyColumn: "Id",
keyValue: new Guid("20000000-0000-0000-0000-000000000018"),
columns: new[] { "Code", "Description" },
values: new object[] { "stores.credit.update", "Update store credit limit and outstanding balance" });
migrationBuilder.UpdateData(
table: "Permissions",
keyColumn: "Id",
keyValue: new Guid("20000000-0000-0000-0000-000000000019"),
columns: new[] { "Code", "Description" },
values: new object[] { "warehouses.view", "View warehouses" });
migrationBuilder.UpdateData(
table: "Permissions",
keyColumn: "Id",
keyValue: new Guid("20000000-0000-0000-0000-000000000020"),
columns: new[] { "Code", "Description" },
values: new object[] { "warehouses.create", "Create warehouses" });
migrationBuilder.UpdateData(
table: "Permissions",
keyColumn: "Id",
keyValue: new Guid("20000000-0000-0000-0000-000000000021"),
columns: new[] { "Code", "Description" },
values: new object[] { "warehouses.update", "Update warehouses" });
migrationBuilder.UpdateData(
table: "Permissions",
keyColumn: "Id",
keyValue: new Guid("20000000-0000-0000-0000-000000000022"),
columns: new[] { "Code", "Description" },
values: new object[] { "warehouses.delete", "Delete warehouses" });
migrationBuilder.UpdateData(
table: "Permissions",
keyColumn: "Id",
keyValue: new Guid("20000000-0000-0000-0000-000000000023"),
columns: new[] { "Code", "Description" },
values: new object[] { "orders.view", "View orders" });
migrationBuilder.UpdateData(
table: "Permissions",
keyColumn: "Id",
keyValue: new Guid("20000000-0000-0000-0000-000000000024"),
columns: new[] { "Code", "Description" },
values: new object[] { "orders.create", "Create orders" });
migrationBuilder.UpdateData(
table: "Permissions",
keyColumn: "Id",
keyValue: new Guid("20000000-0000-0000-0000-000000000025"),
columns: new[] { "Code", "Description" },
values: new object[] { "orders.update", "Update orders" });
migrationBuilder.UpdateData(
table: "Permissions",
keyColumn: "Id",
keyValue: new Guid("20000000-0000-0000-0000-000000000026"),
columns: new[] { "Code", "Description" },
values: new object[] { "orders.delete", "Delete orders" });
migrationBuilder.UpdateData(
table: "Permissions",
keyColumn: "Id",
keyValue: new Guid("20000000-0000-0000-0000-000000000027"),
columns: new[] { "Code", "Description" },
values: new object[] { "users.view", "View users" });
migrationBuilder.UpdateData(
table: "Permissions",
keyColumn: "Id",
keyValue: new Guid("20000000-0000-0000-0000-000000000028"),
columns: new[] { "Code", "Description" },
values: new object[] { "users.create", "Create users" });
migrationBuilder.UpdateData(
table: "Permissions",
keyColumn: "Id",
keyValue: new Guid("20000000-0000-0000-0000-000000000029"),
columns: new[] { "Code", "Description" },
values: new object[] { "users.update", "Update users" });
migrationBuilder.UpdateData(
table: "Permissions",
keyColumn: "Id",
keyValue: new Guid("20000000-0000-0000-0000-000000000030"),
columns: new[] { "Code", "Description" },
values: new object[] { "users.deactivate", "Deactivate users" });
migrationBuilder.UpdateData(
table: "Permissions",
keyColumn: "Id",
keyValue: new Guid("20000000-0000-0000-0000-000000000031"),
columns: new[] { "Code", "Description" },
values: new object[] { "users.assign_role", "Assign a role to a user" });
migrationBuilder.UpdateData(
table: "Permissions",
keyColumn: "Id",
keyValue: new Guid("20000000-0000-0000-0000-000000000032"),
columns: new[] { "Code", "Description" },
values: new object[] { "users.assign_scope", "Assign region/route/store scope to a user" });
migrationBuilder.UpdateData(
table: "Permissions",
keyColumn: "Id",
keyValue: new Guid("20000000-0000-0000-0000-000000000033"),
columns: new[] { "Code", "Description" },
values: new object[] { "items.view", "View items, brands and UOMs" });
migrationBuilder.UpdateData(
table: "Permissions",
keyColumn: "Id",
keyValue: new Guid("20000000-0000-0000-0000-000000000034"),
columns: new[] { "Code", "Description" },
values: new object[] { "items.create", "Create items, brands and UOMs" });
migrationBuilder.UpdateData(
table: "Permissions",
keyColumn: "Id",
keyValue: new Guid("20000000-0000-0000-0000-000000000035"),
columns: new[] { "Code", "Description" },
values: new object[] { "items.update", "Update items, brands and UOMs" });
migrationBuilder.UpdateData(
table: "Permissions",
keyColumn: "Id",
keyValue: new Guid("20000000-0000-0000-0000-000000000036"),
columns: new[] { "Code", "Description" },
values: new object[] { "items.delete", "Delete items, brands and UOMs" });
migrationBuilder.UpdateData(
table: "Permissions",
keyColumn: "Id",
keyValue: new Guid("20000000-0000-0000-0000-000000000037"),
columns: new[] { "Code", "Description" },
values: new object[] { "categories.view", "View categories" });
migrationBuilder.UpdateData(
table: "Permissions",
keyColumn: "Id",
keyValue: new Guid("20000000-0000-0000-0000-000000000038"),
columns: new[] { "Code", "Description" },
values: new object[] { "categories.create", "Create categories and subcategories" });
migrationBuilder.InsertData(
table: "Permissions",
columns: new[] { "Id", "Code", "CreatedAt", "Description", "IsActive" },
values: new object[,]
{
{ new Guid("20000000-0000-0000-0000-000000000039"), "categories.update", new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), "Update categories and subcategories", true },
{ new Guid("20000000-0000-0000-0000-000000000040"), "categories.delete", new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), "Delete categories and subcategories", true }
});
migrationBuilder.InsertData(
table: "RolePermissions",
columns: new[] { "FK_PermissionId", "FK_RoleId", "CreatedAt", "Id", "IsActive" },
values: new object[,]
{
{ new Guid("20000000-0000-0000-0000-000000000024"), new Guid("10000000-0000-0000-0000-000000000001"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("c0739b84-50a2-0971-429b-02bab8354c51"), true },
{ new Guid("20000000-0000-0000-0000-000000000025"), new Guid("10000000-0000-0000-0000-000000000001"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("c268a531-44eb-0f29-82ba-18394ac2dcb5"), true },
{ new Guid("20000000-0000-0000-0000-000000000033"), new Guid("10000000-0000-0000-0000-000000000001"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("ddb2d04d-19b0-20b1-2639-b157d12a9eec"), true },
{ new Guid("20000000-0000-0000-0000-000000000037"), new Guid("10000000-0000-0000-0000-000000000001"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("320ba3fb-666c-ce57-3e61-95c9824c4048"), true },
{ new Guid("20000000-0000-0000-0000-000000000018"), new Guid("10000000-0000-0000-0000-000000000002"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("f2c14d55-d557-5cd2-a70a-4a6f58933436"), true },
{ new Guid("20000000-0000-0000-0000-000000000025"), new Guid("10000000-0000-0000-0000-000000000002"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("1b126bb0-e582-3e84-4728-a7ab5628fe7b"), true },
{ new Guid("20000000-0000-0000-0000-000000000026"), new Guid("10000000-0000-0000-0000-000000000002"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("f50b201c-6c8d-0df8-85ad-08cb137bc213"), true },
{ new Guid("20000000-0000-0000-0000-000000000033"), new Guid("10000000-0000-0000-0000-000000000002"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("0f291826-e169-c06a-0735-bd06cbb2062d"), true },
{ new Guid("20000000-0000-0000-0000-000000000037"), new Guid("10000000-0000-0000-0000-000000000002"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("10dd0249-ce04-c5d9-8f94-ccb348c1d979"), true },
{ new Guid("20000000-0000-0000-0000-000000000018"), new Guid("10000000-0000-0000-0000-000000000003"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("d92d572f-f1bf-9e7d-1d4d-7c7b0d575c8e"), true },
{ new Guid("20000000-0000-0000-0000-000000000019"), new Guid("10000000-0000-0000-0000-000000000003"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("f367f6f6-3d26-7a6c-888b-da1a240b59af"), true },
{ new Guid("20000000-0000-0000-0000-000000000025"), new Guid("10000000-0000-0000-0000-000000000003"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("929950da-d51b-ad5d-c4e5-55175908fb43"), true },
{ new Guid("20000000-0000-0000-0000-000000000026"), new Guid("10000000-0000-0000-0000-000000000003"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("a9a49749-f99d-3166-5d92-b7e380fcabb2"), true },
{ new Guid("20000000-0000-0000-0000-000000000034"), new Guid("10000000-0000-0000-0000-000000000003"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("b17df3df-022c-c903-6ce0-e6423e648dc8"), true },
{ new Guid("20000000-0000-0000-0000-000000000037"), new Guid("10000000-0000-0000-0000-000000000003"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("a41f94ba-c24d-93dc-9f25-c462df19785a"), true },
{ new Guid("20000000-0000-0000-0000-000000000016"), new Guid("10000000-0000-0000-0000-000000000004"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("e0b27435-f505-43f5-7ed9-4711f2248495"), true },
{ new Guid("20000000-0000-0000-0000-000000000018"), new Guid("10000000-0000-0000-0000-000000000004"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("5e10d61c-2d7a-3388-722f-45232d2cfb71"), true },
{ new Guid("20000000-0000-0000-0000-000000000019"), new Guid("10000000-0000-0000-0000-000000000004"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("d8257363-d838-fe7e-57a5-51b626dbfc83"), true },
{ new Guid("20000000-0000-0000-0000-000000000025"), new Guid("10000000-0000-0000-0000-000000000004"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("04ece77e-b36f-8ed0-4ef9-2f6962b6f9c6"), true },
{ new Guid("20000000-0000-0000-0000-000000000026"), new Guid("10000000-0000-0000-0000-000000000004"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("bf2a2f96-04ed-905d-5a8a-9047d814aa3e"), true },
{ new Guid("20000000-0000-0000-0000-000000000026"), new Guid("10000000-0000-0000-0000-000000000005"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("4d287b19-ba40-7d4d-3d39-291be64197f8"), true },
{ new Guid("20000000-0000-0000-0000-000000000027"), new Guid("10000000-0000-0000-0000-000000000005"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("43a4c7b4-a50f-aa37-b147-a30636d9d65f"), true },
{ new Guid("20000000-0000-0000-0000-000000000039"), new Guid("10000000-0000-0000-0000-000000000004"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("a66f3854-5a8f-95c2-cb1c-b2b2e67ee226"), true },
{ new Guid("20000000-0000-0000-0000-000000000040"), new Guid("10000000-0000-0000-0000-000000000004"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("14bcecd1-b1a2-6da3-155a-233611484467"), true },
{ new Guid("20000000-0000-0000-0000-000000000039"), new Guid("10000000-0000-0000-0000-000000000005"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("aeb29e0e-e819-1446-6cc6-1fb64b5ecc6b"), true },
{ new Guid("20000000-0000-0000-0000-000000000040"), new Guid("10000000-0000-0000-0000-000000000005"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("e18a6eca-1009-bcc6-35e9-ead943c1649f"), true },
{ new Guid("20000000-0000-0000-0000-000000000039"), new Guid("10000000-0000-0000-0000-000000000006"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("3f32cdac-6925-23f2-064e-8226d781d4a3"), true },
{ new Guid("20000000-0000-0000-0000-000000000040"), new Guid("10000000-0000-0000-0000-000000000006"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("dc3dd9ff-b123-a8d4-dcea-b4f5f8ff171e"), true }
});
migrationBuilder.CreateIndex(
name: "IX_StoreKycDocuments_FK_StoreId",
table: "StoreKycDocuments",
column: "FK_StoreId");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "StoreKycDocuments");
migrationBuilder.DeleteData(
table: "RolePermissions",
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000024"), new Guid("10000000-0000-0000-0000-000000000001") });
migrationBuilder.DeleteData(
table: "RolePermissions",
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000025"), new Guid("10000000-0000-0000-0000-000000000001") });
migrationBuilder.DeleteData(
table: "RolePermissions",
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000033"), new Guid("10000000-0000-0000-0000-000000000001") });
migrationBuilder.DeleteData(
table: "RolePermissions",
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000037"), new Guid("10000000-0000-0000-0000-000000000001") });
migrationBuilder.DeleteData(
table: "RolePermissions",
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000018"), new Guid("10000000-0000-0000-0000-000000000002") });
migrationBuilder.DeleteData(
table: "RolePermissions",
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000025"), new Guid("10000000-0000-0000-0000-000000000002") });
migrationBuilder.DeleteData(
table: "RolePermissions",
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000026"), new Guid("10000000-0000-0000-0000-000000000002") });
migrationBuilder.DeleteData(
table: "RolePermissions",
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000033"), new Guid("10000000-0000-0000-0000-000000000002") });
migrationBuilder.DeleteData(
table: "RolePermissions",
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000037"), new Guid("10000000-0000-0000-0000-000000000002") });
migrationBuilder.DeleteData(
table: "RolePermissions",
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000018"), new Guid("10000000-0000-0000-0000-000000000003") });
migrationBuilder.DeleteData(
table: "RolePermissions",
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000019"), new Guid("10000000-0000-0000-0000-000000000003") });
migrationBuilder.DeleteData(
table: "RolePermissions",
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000025"), new Guid("10000000-0000-0000-0000-000000000003") });
migrationBuilder.DeleteData(
table: "RolePermissions",
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000026"), new Guid("10000000-0000-0000-0000-000000000003") });
migrationBuilder.DeleteData(
table: "RolePermissions",
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000034"), new Guid("10000000-0000-0000-0000-000000000003") });
migrationBuilder.DeleteData(
table: "RolePermissions",
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000037"), new Guid("10000000-0000-0000-0000-000000000003") });
migrationBuilder.DeleteData(
table: "RolePermissions",
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000016"), new Guid("10000000-0000-0000-0000-000000000004") });
migrationBuilder.DeleteData(
table: "RolePermissions",
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000018"), new Guid("10000000-0000-0000-0000-000000000004") });
migrationBuilder.DeleteData(
table: "RolePermissions",
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000019"), new Guid("10000000-0000-0000-0000-000000000004") });
migrationBuilder.DeleteData(
table: "RolePermissions",
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000025"), new Guid("10000000-0000-0000-0000-000000000004") });
migrationBuilder.DeleteData(
table: "RolePermissions",
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000026"), new Guid("10000000-0000-0000-0000-000000000004") });
migrationBuilder.DeleteData(
table: "RolePermissions",
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000039"), new Guid("10000000-0000-0000-0000-000000000004") });
migrationBuilder.DeleteData(
table: "RolePermissions",
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000040"), new Guid("10000000-0000-0000-0000-000000000004") });
migrationBuilder.DeleteData(
table: "RolePermissions",
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000026"), new Guid("10000000-0000-0000-0000-000000000005") });
migrationBuilder.DeleteData(
table: "RolePermissions",
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000027"), new Guid("10000000-0000-0000-0000-000000000005") });
migrationBuilder.DeleteData(
table: "RolePermissions",
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000039"), new Guid("10000000-0000-0000-0000-000000000005") });
migrationBuilder.DeleteData(
table: "RolePermissions",
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000040"), new Guid("10000000-0000-0000-0000-000000000005") });
migrationBuilder.DeleteData(
table: "RolePermissions",
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000039"), new Guid("10000000-0000-0000-0000-000000000006") });
migrationBuilder.DeleteData(
table: "RolePermissions",
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000040"), new Guid("10000000-0000-0000-0000-000000000006") });
migrationBuilder.DeleteData(
table: "Permissions",
keyColumn: "Id",
keyValue: new Guid("20000000-0000-0000-0000-000000000039"));
migrationBuilder.DeleteData(
table: "Permissions",
keyColumn: "Id",
keyValue: new Guid("20000000-0000-0000-0000-000000000040"));
migrationBuilder.DropColumn(
name: "CreditLimit",
table: "Stores");
migrationBuilder.DropColumn(
name: "KycStatus",
table: "Stores");
migrationBuilder.DropColumn(
name: "OutstandingBalance",
table: "Stores");
migrationBuilder.DropColumn(
name: "Segment",
table: "Stores");
migrationBuilder.UpdateData(
table: "Permissions",
keyColumn: "Id",
keyValue: new Guid("20000000-0000-0000-0000-000000000017"),
columns: new[] { "Code", "Description" },
values: new object[] { "warehouses.view", "View warehouses" });
migrationBuilder.UpdateData(
table: "Permissions",
keyColumn: "Id",
keyValue: new Guid("20000000-0000-0000-0000-000000000018"),
columns: new[] { "Code", "Description" },
values: new object[] { "warehouses.create", "Create warehouses" });
migrationBuilder.UpdateData(
table: "Permissions",
keyColumn: "Id",
keyValue: new Guid("20000000-0000-0000-0000-000000000019"),
columns: new[] { "Code", "Description" },
values: new object[] { "warehouses.update", "Update warehouses" });
migrationBuilder.UpdateData(
table: "Permissions",
keyColumn: "Id",
keyValue: new Guid("20000000-0000-0000-0000-000000000020"),
columns: new[] { "Code", "Description" },
values: new object[] { "warehouses.delete", "Delete warehouses" });
migrationBuilder.UpdateData(
table: "Permissions",
keyColumn: "Id",
keyValue: new Guid("20000000-0000-0000-0000-000000000021"),
columns: new[] { "Code", "Description" },
values: new object[] { "orders.view", "View orders" });
migrationBuilder.UpdateData(
table: "Permissions",
keyColumn: "Id",
keyValue: new Guid("20000000-0000-0000-0000-000000000022"),
columns: new[] { "Code", "Description" },
values: new object[] { "orders.create", "Create orders" });
migrationBuilder.UpdateData(
table: "Permissions",
keyColumn: "Id",
keyValue: new Guid("20000000-0000-0000-0000-000000000023"),
columns: new[] { "Code", "Description" },
values: new object[] { "orders.update", "Update orders" });
migrationBuilder.UpdateData(
table: "Permissions",
keyColumn: "Id",
keyValue: new Guid("20000000-0000-0000-0000-000000000024"),
columns: new[] { "Code", "Description" },
values: new object[] { "orders.delete", "Delete orders" });
migrationBuilder.UpdateData(
table: "Permissions",
keyColumn: "Id",
keyValue: new Guid("20000000-0000-0000-0000-000000000025"),
columns: new[] { "Code", "Description" },
values: new object[] { "users.view", "View users" });
migrationBuilder.UpdateData(
table: "Permissions",
keyColumn: "Id",
keyValue: new Guid("20000000-0000-0000-0000-000000000026"),
columns: new[] { "Code", "Description" },
values: new object[] { "users.create", "Create users" });
migrationBuilder.UpdateData(
table: "Permissions",
keyColumn: "Id",
keyValue: new Guid("20000000-0000-0000-0000-000000000027"),
columns: new[] { "Code", "Description" },
values: new object[] { "users.update", "Update users" });
migrationBuilder.UpdateData(
table: "Permissions",
keyColumn: "Id",
keyValue: new Guid("20000000-0000-0000-0000-000000000028"),
columns: new[] { "Code", "Description" },
values: new object[] { "users.deactivate", "Deactivate users" });
migrationBuilder.UpdateData(
table: "Permissions",
keyColumn: "Id",
keyValue: new Guid("20000000-0000-0000-0000-000000000029"),
columns: new[] { "Code", "Description" },
values: new object[] { "users.assign_role", "Assign a role to a user" });
migrationBuilder.UpdateData(
table: "Permissions",
keyColumn: "Id",
keyValue: new Guid("20000000-0000-0000-0000-000000000030"),
columns: new[] { "Code", "Description" },
values: new object[] { "users.assign_scope", "Assign region/route/store scope to a user" });
migrationBuilder.UpdateData(
table: "Permissions",
keyColumn: "Id",
keyValue: new Guid("20000000-0000-0000-0000-000000000031"),
columns: new[] { "Code", "Description" },
values: new object[] { "items.view", "View items, brands and UOMs" });
migrationBuilder.UpdateData(
table: "Permissions",
keyColumn: "Id",
keyValue: new Guid("20000000-0000-0000-0000-000000000032"),
columns: new[] { "Code", "Description" },
values: new object[] { "items.create", "Create items, brands and UOMs" });
migrationBuilder.UpdateData(
table: "Permissions",
keyColumn: "Id",
keyValue: new Guid("20000000-0000-0000-0000-000000000033"),
columns: new[] { "Code", "Description" },
values: new object[] { "items.update", "Update items, brands and UOMs" });
migrationBuilder.UpdateData(
table: "Permissions",
keyColumn: "Id",
keyValue: new Guid("20000000-0000-0000-0000-000000000034"),
columns: new[] { "Code", "Description" },
values: new object[] { "items.delete", "Delete items, brands and UOMs" });
migrationBuilder.UpdateData(
table: "Permissions",
keyColumn: "Id",
keyValue: new Guid("20000000-0000-0000-0000-000000000035"),
columns: new[] { "Code", "Description" },
values: new object[] { "categories.view", "View categories" });
migrationBuilder.UpdateData(
table: "Permissions",
keyColumn: "Id",
keyValue: new Guid("20000000-0000-0000-0000-000000000036"),
columns: new[] { "Code", "Description" },
values: new object[] { "categories.create", "Create categories and subcategories" });
migrationBuilder.UpdateData(
table: "Permissions",
keyColumn: "Id",
keyValue: new Guid("20000000-0000-0000-0000-000000000037"),
columns: new[] { "Code", "Description" },
values: new object[] { "categories.update", "Update categories and subcategories" });
migrationBuilder.UpdateData(
table: "Permissions",
keyColumn: "Id",
keyValue: new Guid("20000000-0000-0000-0000-000000000038"),
columns: new[] { "Code", "Description" },
values: new object[] { "categories.delete", "Delete categories and subcategories" });
migrationBuilder.InsertData(
table: "RolePermissions",
columns: new[] { "FK_PermissionId", "FK_RoleId", "CreatedAt", "Id", "IsActive" },
values: new object[,]
{
{ new Guid("20000000-0000-0000-0000-000000000021"), new Guid("10000000-0000-0000-0000-000000000001"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("85ef4e27-8643-9c43-71f6-da56f66a5c47"), true },
{ new Guid("20000000-0000-0000-0000-000000000022"), new Guid("10000000-0000-0000-0000-000000000001"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("1e353909-d682-58f6-df5c-8e57fa2512c9"), true },
{ new Guid("20000000-0000-0000-0000-000000000031"), new Guid("10000000-0000-0000-0000-000000000001"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("7596f703-c6ab-316c-7c80-3f4e3daa174e"), true },
{ new Guid("20000000-0000-0000-0000-000000000035"), new Guid("10000000-0000-0000-0000-000000000001"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("aa120c36-488e-34ba-e10a-2ac45e46e119"), true },
{ new Guid("20000000-0000-0000-0000-000000000021"), new Guid("10000000-0000-0000-0000-000000000002"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("e4648655-7267-141e-51fa-f7d5fe3d2356"), true },
{ new Guid("20000000-0000-0000-0000-000000000022"), new Guid("10000000-0000-0000-0000-000000000002"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("d427b9ea-2c9b-d93e-0843-02dfa1ae8887"), true },
{ new Guid("20000000-0000-0000-0000-000000000031"), new Guid("10000000-0000-0000-0000-000000000002"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("b7dee5e3-f329-95a5-75c1-d02c3e40e229"), true },
{ new Guid("20000000-0000-0000-0000-000000000035"), new Guid("10000000-0000-0000-0000-000000000002"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("e5506b30-527d-08f9-6caa-8a45d3d02999"), true },
{ new Guid("20000000-0000-0000-0000-000000000021"), new Guid("10000000-0000-0000-0000-000000000003"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("0dd5c943-3ace-0560-6fbc-54d8e37ce014"), true },
{ new Guid("20000000-0000-0000-0000-000000000022"), new Guid("10000000-0000-0000-0000-000000000003"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("ca97fb3d-132c-d8a5-4ab8-f7b8fc60f5e5"), true },
{ new Guid("20000000-0000-0000-0000-000000000031"), new Guid("10000000-0000-0000-0000-000000000003"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("7780d277-9597-84ff-1a1a-f63fc6b4d3cb"), true },
{ new Guid("20000000-0000-0000-0000-000000000032"), new Guid("10000000-0000-0000-0000-000000000003"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("8083c659-9506-0bd9-b793-3d6148ce9192"), true },
{ new Guid("20000000-0000-0000-0000-000000000021"), new Guid("10000000-0000-0000-0000-000000000004"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("5c00e972-a697-d71a-6545-1fd8618826cf"), true },
{ new Guid("20000000-0000-0000-0000-000000000022"), new Guid("10000000-0000-0000-0000-000000000004"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("e9b6af8e-7947-7290-9c92-2d9b6239f395"), true },
{ new Guid("20000000-0000-0000-0000-000000000031"), new Guid("10000000-0000-0000-0000-000000000004"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("a701da28-aa75-3c1d-abdb-4317404f8df6"), true },
{ new Guid("20000000-0000-0000-0000-000000000032"), new Guid("10000000-0000-0000-0000-000000000004"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("2c025310-765f-7d07-ab9a-50524c13d1d3"), true },
{ new Guid("20000000-0000-0000-0000-000000000031"), new Guid("10000000-0000-0000-0000-000000000005"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("69f93a29-1df1-7b20-9d5c-94c264f8c4b8"), true },
{ new Guid("20000000-0000-0000-0000-000000000032"), new Guid("10000000-0000-0000-0000-000000000005"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("070813f7-4053-2889-0d14-d0d5834208c2"), true }
});
}
}
}
@@ -52,6 +52,9 @@ namespace Distribution.Migrations
.ValueGeneratedOnAdd()
.HasColumnType("char(36)");
b.Property<bool>("AllowCustomerReturn")
.HasColumnType("tinyint(1)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
@@ -377,6 +380,22 @@ namespace Distribution.Migrations
new
{
Id = new Guid("20000000-0000-0000-0000-000000000017"),
Code = "stores.kyc.verify",
CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified),
Description = "Verify or reject store KYC documents",
IsActive = true
},
new
{
Id = new Guid("20000000-0000-0000-0000-000000000018"),
Code = "stores.credit.update",
CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified),
Description = "Update store credit limit and outstanding balance",
IsActive = true
},
new
{
Id = new Guid("20000000-0000-0000-0000-000000000019"),
Code = "warehouses.view",
CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified),
Description = "View warehouses",
@@ -384,7 +403,7 @@ namespace Distribution.Migrations
},
new
{
Id = new Guid("20000000-0000-0000-0000-000000000018"),
Id = new Guid("20000000-0000-0000-0000-000000000020"),
Code = "warehouses.create",
CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified),
Description = "Create warehouses",
@@ -392,7 +411,7 @@ namespace Distribution.Migrations
},
new
{
Id = new Guid("20000000-0000-0000-0000-000000000019"),
Id = new Guid("20000000-0000-0000-0000-000000000021"),
Code = "warehouses.update",
CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified),
Description = "Update warehouses",
@@ -400,7 +419,7 @@ namespace Distribution.Migrations
},
new
{
Id = new Guid("20000000-0000-0000-0000-000000000020"),
Id = new Guid("20000000-0000-0000-0000-000000000022"),
Code = "warehouses.delete",
CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified),
Description = "Delete warehouses",
@@ -408,7 +427,7 @@ namespace Distribution.Migrations
},
new
{
Id = new Guid("20000000-0000-0000-0000-000000000021"),
Id = new Guid("20000000-0000-0000-0000-000000000023"),
Code = "orders.view",
CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified),
Description = "View orders",
@@ -416,7 +435,7 @@ namespace Distribution.Migrations
},
new
{
Id = new Guid("20000000-0000-0000-0000-000000000022"),
Id = new Guid("20000000-0000-0000-0000-000000000024"),
Code = "orders.create",
CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified),
Description = "Create orders",
@@ -424,7 +443,7 @@ namespace Distribution.Migrations
},
new
{
Id = new Guid("20000000-0000-0000-0000-000000000023"),
Id = new Guid("20000000-0000-0000-0000-000000000025"),
Code = "orders.update",
CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified),
Description = "Update orders",
@@ -432,7 +451,7 @@ namespace Distribution.Migrations
},
new
{
Id = new Guid("20000000-0000-0000-0000-000000000024"),
Id = new Guid("20000000-0000-0000-0000-000000000026"),
Code = "orders.delete",
CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified),
Description = "Delete orders",
@@ -440,7 +459,7 @@ namespace Distribution.Migrations
},
new
{
Id = new Guid("20000000-0000-0000-0000-000000000025"),
Id = new Guid("20000000-0000-0000-0000-000000000027"),
Code = "users.view",
CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified),
Description = "View users",
@@ -448,7 +467,7 @@ namespace Distribution.Migrations
},
new
{
Id = new Guid("20000000-0000-0000-0000-000000000026"),
Id = new Guid("20000000-0000-0000-0000-000000000028"),
Code = "users.create",
CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified),
Description = "Create users",
@@ -456,7 +475,7 @@ namespace Distribution.Migrations
},
new
{
Id = new Guid("20000000-0000-0000-0000-000000000027"),
Id = new Guid("20000000-0000-0000-0000-000000000029"),
Code = "users.update",
CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified),
Description = "Update users",
@@ -464,7 +483,7 @@ namespace Distribution.Migrations
},
new
{
Id = new Guid("20000000-0000-0000-0000-000000000028"),
Id = new Guid("20000000-0000-0000-0000-000000000030"),
Code = "users.deactivate",
CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified),
Description = "Deactivate users",
@@ -472,7 +491,7 @@ namespace Distribution.Migrations
},
new
{
Id = new Guid("20000000-0000-0000-0000-000000000029"),
Id = new Guid("20000000-0000-0000-0000-000000000031"),
Code = "users.assign_role",
CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified),
Description = "Assign a role to a user",
@@ -480,7 +499,7 @@ namespace Distribution.Migrations
},
new
{
Id = new Guid("20000000-0000-0000-0000-000000000030"),
Id = new Guid("20000000-0000-0000-0000-000000000032"),
Code = "users.assign_scope",
CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified),
Description = "Assign region/route/store scope to a user",
@@ -488,7 +507,7 @@ namespace Distribution.Migrations
},
new
{
Id = new Guid("20000000-0000-0000-0000-000000000031"),
Id = new Guid("20000000-0000-0000-0000-000000000033"),
Code = "items.view",
CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified),
Description = "View items, brands and UOMs",
@@ -496,7 +515,7 @@ namespace Distribution.Migrations
},
new
{
Id = new Guid("20000000-0000-0000-0000-000000000032"),
Id = new Guid("20000000-0000-0000-0000-000000000034"),
Code = "items.create",
CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified),
Description = "Create items, brands and UOMs",
@@ -504,7 +523,7 @@ namespace Distribution.Migrations
},
new
{
Id = new Guid("20000000-0000-0000-0000-000000000033"),
Id = new Guid("20000000-0000-0000-0000-000000000035"),
Code = "items.update",
CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified),
Description = "Update items, brands and UOMs",
@@ -512,7 +531,7 @@ namespace Distribution.Migrations
},
new
{
Id = new Guid("20000000-0000-0000-0000-000000000034"),
Id = new Guid("20000000-0000-0000-0000-000000000036"),
Code = "items.delete",
CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified),
Description = "Delete items, brands and UOMs",
@@ -520,7 +539,7 @@ namespace Distribution.Migrations
},
new
{
Id = new Guid("20000000-0000-0000-0000-000000000035"),
Id = new Guid("20000000-0000-0000-0000-000000000037"),
Code = "categories.view",
CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified),
Description = "View categories",
@@ -528,7 +547,7 @@ namespace Distribution.Migrations
},
new
{
Id = new Guid("20000000-0000-0000-0000-000000000036"),
Id = new Guid("20000000-0000-0000-0000-000000000038"),
Code = "categories.create",
CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified),
Description = "Create categories and subcategories",
@@ -536,7 +555,7 @@ namespace Distribution.Migrations
},
new
{
Id = new Guid("20000000-0000-0000-0000-000000000037"),
Id = new Guid("20000000-0000-0000-0000-000000000039"),
Code = "categories.update",
CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified),
Description = "Update categories and subcategories",
@@ -544,7 +563,7 @@ namespace Distribution.Migrations
},
new
{
Id = new Guid("20000000-0000-0000-0000-000000000038"),
Id = new Guid("20000000-0000-0000-0000-000000000040"),
Code = "categories.delete",
CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified),
Description = "Delete categories and subcategories",
@@ -671,22 +690,6 @@ namespace Distribution.Migrations
b.ToTable("RolePermissions");
b.HasData(
new
{
FK_RoleId = new Guid("10000000-0000-0000-0000-000000000001"),
FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000021"),
CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified),
Id = new Guid("85ef4e27-8643-9c43-71f6-da56f66a5c47"),
IsActive = true
},
new
{
FK_RoleId = new Guid("10000000-0000-0000-0000-000000000001"),
FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000022"),
CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified),
Id = new Guid("1e353909-d682-58f6-df5c-8e57fa2512c9"),
IsActive = true
},
new
{
FK_RoleId = new Guid("10000000-0000-0000-0000-000000000001"),
@@ -696,6 +699,22 @@ namespace Distribution.Migrations
IsActive = true
},
new
{
FK_RoleId = new Guid("10000000-0000-0000-0000-000000000001"),
FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000024"),
CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified),
Id = new Guid("c0739b84-50a2-0971-429b-02bab8354c51"),
IsActive = true
},
new
{
FK_RoleId = new Guid("10000000-0000-0000-0000-000000000001"),
FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000025"),
CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified),
Id = new Guid("c268a531-44eb-0f29-82ba-18394ac2dcb5"),
IsActive = true
},
new
{
FK_RoleId = new Guid("10000000-0000-0000-0000-000000000001"),
FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000013"),
@@ -714,33 +733,17 @@ namespace Distribution.Migrations
new
{
FK_RoleId = new Guid("10000000-0000-0000-0000-000000000001"),
FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000031"),
FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000033"),
CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified),
Id = new Guid("7596f703-c6ab-316c-7c80-3f4e3daa174e"),
Id = new Guid("ddb2d04d-19b0-20b1-2639-b157d12a9eec"),
IsActive = true
},
new
{
FK_RoleId = new Guid("10000000-0000-0000-0000-000000000001"),
FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000035"),
FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000037"),
CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified),
Id = new Guid("aa120c36-488e-34ba-e10a-2ac45e46e119"),
IsActive = true
},
new
{
FK_RoleId = new Guid("10000000-0000-0000-0000-000000000002"),
FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000021"),
CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified),
Id = new Guid("e4648655-7267-141e-51fa-f7d5fe3d2356"),
IsActive = true
},
new
{
FK_RoleId = new Guid("10000000-0000-0000-0000-000000000002"),
FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000022"),
CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified),
Id = new Guid("d427b9ea-2c9b-d93e-0843-02dfa1ae8887"),
Id = new Guid("320ba3fb-666c-ce57-3e61-95c9824c4048"),
IsActive = true
},
new
@@ -752,6 +755,22 @@ namespace Distribution.Migrations
IsActive = true
},
new
{
FK_RoleId = new Guid("10000000-0000-0000-0000-000000000002"),
FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000024"),
CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified),
Id = new Guid("357c36de-9b6c-1c9a-4926-c18a9bccf0ca"),
IsActive = true
},
new
{
FK_RoleId = new Guid("10000000-0000-0000-0000-000000000002"),
FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000025"),
CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified),
Id = new Guid("1b126bb0-e582-3e84-4728-a7ab5628fe7b"),
IsActive = true
},
new
{
FK_RoleId = new Guid("10000000-0000-0000-0000-000000000002"),
FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000013"),
@@ -770,25 +789,25 @@ namespace Distribution.Migrations
new
{
FK_RoleId = new Guid("10000000-0000-0000-0000-000000000002"),
FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000031"),
FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000033"),
CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified),
Id = new Guid("b7dee5e3-f329-95a5-75c1-d02c3e40e229"),
Id = new Guid("0f291826-e169-c06a-0735-bd06cbb2062d"),
IsActive = true
},
new
{
FK_RoleId = new Guid("10000000-0000-0000-0000-000000000002"),
FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000035"),
FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000037"),
CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified),
Id = new Guid("e5506b30-527d-08f9-6caa-8a45d3d02999"),
Id = new Guid("10dd0249-ce04-c5d9-8f94-ccb348c1d979"),
IsActive = true
},
new
{
FK_RoleId = new Guid("10000000-0000-0000-0000-000000000002"),
FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000024"),
FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000026"),
CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified),
Id = new Guid("357c36de-9b6c-1c9a-4926-c18a9bccf0ca"),
Id = new Guid("f50b201c-6c8d-0df8-85ad-08cb137bc213"),
IsActive = true
},
new
@@ -809,18 +828,10 @@ namespace Distribution.Migrations
},
new
{
FK_RoleId = new Guid("10000000-0000-0000-0000-000000000003"),
FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000021"),
FK_RoleId = new Guid("10000000-0000-0000-0000-000000000002"),
FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000018"),
CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified),
Id = new Guid("0dd5c943-3ace-0560-6fbc-54d8e37ce014"),
IsActive = true
},
new
{
FK_RoleId = new Guid("10000000-0000-0000-0000-000000000003"),
FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000022"),
CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified),
Id = new Guid("ca97fb3d-132c-d8a5-4ab8-f7b8fc60f5e5"),
Id = new Guid("f2c14d55-d557-5cd2-a70a-4a6f58933436"),
IsActive = true
},
new
@@ -832,6 +843,22 @@ namespace Distribution.Migrations
IsActive = true
},
new
{
FK_RoleId = new Guid("10000000-0000-0000-0000-000000000003"),
FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000024"),
CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified),
Id = new Guid("73e00ce7-c124-4677-4369-1eb9876852c0"),
IsActive = true
},
new
{
FK_RoleId = new Guid("10000000-0000-0000-0000-000000000003"),
FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000025"),
CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified),
Id = new Guid("929950da-d51b-ad5d-c4e5-55175908fb43"),
IsActive = true
},
new
{
FK_RoleId = new Guid("10000000-0000-0000-0000-000000000003"),
FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000013"),
@@ -850,25 +877,25 @@ namespace Distribution.Migrations
new
{
FK_RoleId = new Guid("10000000-0000-0000-0000-000000000003"),
FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000031"),
FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000033"),
CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified),
Id = new Guid("7780d277-9597-84ff-1a1a-f63fc6b4d3cb"),
Id = new Guid("d49f086e-30f0-6ce8-ae96-4395d5e04472"),
IsActive = true
},
new
{
FK_RoleId = new Guid("10000000-0000-0000-0000-000000000003"),
FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000035"),
FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000037"),
CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified),
Id = new Guid("1e7fe1b6-5150-4b99-38ed-088d9495cfd8"),
Id = new Guid("a41f94ba-c24d-93dc-9f25-c462df19785a"),
IsActive = true
},
new
{
FK_RoleId = new Guid("10000000-0000-0000-0000-000000000003"),
FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000024"),
FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000026"),
CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified),
Id = new Guid("73e00ce7-c124-4677-4369-1eb9876852c0"),
Id = new Guid("a9a49749-f99d-3166-5d92-b7e380fcabb2"),
IsActive = true
},
new
@@ -888,6 +915,14 @@ namespace Distribution.Migrations
IsActive = true
},
new
{
FK_RoleId = new Guid("10000000-0000-0000-0000-000000000003"),
FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000018"),
CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified),
Id = new Guid("d92d572f-f1bf-9e7d-1d4d-7c7b0d575c8e"),
IsActive = true
},
new
{
FK_RoleId = new Guid("10000000-0000-0000-0000-000000000003"),
FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000009"),
@@ -896,6 +931,14 @@ namespace Distribution.Migrations
IsActive = true
},
new
{
FK_RoleId = new Guid("10000000-0000-0000-0000-000000000003"),
FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000019"),
CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified),
Id = new Guid("f367f6f6-3d26-7a6c-888b-da1a240b59af"),
IsActive = true
},
new
{
FK_RoleId = new Guid("10000000-0000-0000-0000-000000000003"),
FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000017"),
@@ -906,33 +949,17 @@ namespace Distribution.Migrations
new
{
FK_RoleId = new Guid("10000000-0000-0000-0000-000000000003"),
FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000032"),
FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000034"),
CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified),
Id = new Guid("8083c659-9506-0bd9-b793-3d6148ce9192"),
Id = new Guid("b17df3df-022c-c903-6ce0-e6423e648dc8"),
IsActive = true
},
new
{
FK_RoleId = new Guid("10000000-0000-0000-0000-000000000003"),
FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000033"),
FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000035"),
CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified),
Id = new Guid("d49f086e-30f0-6ce8-ae96-4395d5e04472"),
IsActive = true
},
new
{
FK_RoleId = new Guid("10000000-0000-0000-0000-000000000004"),
FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000021"),
CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified),
Id = new Guid("5c00e972-a697-d71a-6545-1fd8618826cf"),
IsActive = true
},
new
{
FK_RoleId = new Guid("10000000-0000-0000-0000-000000000004"),
FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000022"),
CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified),
Id = new Guid("e9b6af8e-7947-7290-9c92-2d9b6239f395"),
Id = new Guid("1e7fe1b6-5150-4b99-38ed-088d9495cfd8"),
IsActive = true
},
new
@@ -944,6 +971,22 @@ namespace Distribution.Migrations
IsActive = true
},
new
{
FK_RoleId = new Guid("10000000-0000-0000-0000-000000000004"),
FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000024"),
CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified),
Id = new Guid("7b4f3482-b8bd-d348-e4c1-ae6dc93a3771"),
IsActive = true
},
new
{
FK_RoleId = new Guid("10000000-0000-0000-0000-000000000004"),
FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000025"),
CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified),
Id = new Guid("04ece77e-b36f-8ed0-4ef9-2f6962b6f9c6"),
IsActive = true
},
new
{
FK_RoleId = new Guid("10000000-0000-0000-0000-000000000004"),
FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000013"),
@@ -962,25 +1005,25 @@ namespace Distribution.Migrations
new
{
FK_RoleId = new Guid("10000000-0000-0000-0000-000000000004"),
FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000031"),
FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000033"),
CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified),
Id = new Guid("a701da28-aa75-3c1d-abdb-4317404f8df6"),
Id = new Guid("7c392a13-1a57-0d85-dffa-2fb9e892abe9"),
IsActive = true
},
new
{
FK_RoleId = new Guid("10000000-0000-0000-0000-000000000004"),
FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000035"),
FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000037"),
CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified),
Id = new Guid("9afe507e-191e-bdee-0c5a-cfbcbf1efb3e"),
Id = new Guid("d1dcef02-d4c3-caba-372f-dab82011e9c1"),
IsActive = true
},
new
{
FK_RoleId = new Guid("10000000-0000-0000-0000-000000000004"),
FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000024"),
FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000026"),
CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified),
Id = new Guid("7b4f3482-b8bd-d348-e4c1-ae6dc93a3771"),
Id = new Guid("bf2a2f96-04ed-905d-5a8a-9047d814aa3e"),
IsActive = true
},
new
@@ -1000,6 +1043,14 @@ namespace Distribution.Migrations
IsActive = true
},
new
{
FK_RoleId = new Guid("10000000-0000-0000-0000-000000000004"),
FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000018"),
CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified),
Id = new Guid("5e10d61c-2d7a-3388-722f-45232d2cfb71"),
IsActive = true
},
new
{
FK_RoleId = new Guid("10000000-0000-0000-0000-000000000004"),
FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000009"),
@@ -1008,6 +1059,14 @@ namespace Distribution.Migrations
IsActive = true
},
new
{
FK_RoleId = new Guid("10000000-0000-0000-0000-000000000004"),
FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000019"),
CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified),
Id = new Guid("d8257363-d838-fe7e-57a5-51b626dbfc83"),
IsActive = true
},
new
{
FK_RoleId = new Guid("10000000-0000-0000-0000-000000000004"),
FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000017"),
@@ -1018,17 +1077,17 @@ namespace Distribution.Migrations
new
{
FK_RoleId = new Guid("10000000-0000-0000-0000-000000000004"),
FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000032"),
FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000034"),
CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified),
Id = new Guid("2c025310-765f-7d07-ab9a-50524c13d1d3"),
Id = new Guid("97708eed-6ce4-7710-abde-01d00c0c879b"),
IsActive = true
},
new
{
FK_RoleId = new Guid("10000000-0000-0000-0000-000000000004"),
FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000033"),
FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000035"),
CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified),
Id = new Guid("7c392a13-1a57-0d85-dffa-2fb9e892abe9"),
Id = new Guid("9afe507e-191e-bdee-0c5a-cfbcbf1efb3e"),
IsActive = true
},
new
@@ -1050,9 +1109,9 @@ namespace Distribution.Migrations
new
{
FK_RoleId = new Guid("10000000-0000-0000-0000-000000000004"),
FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000034"),
FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000016"),
CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified),
Id = new Guid("97708eed-6ce4-7710-abde-01d00c0c879b"),
Id = new Guid("e0b27435-f505-43f5-7ed9-4711f2248495"),
IsActive = true
},
new
@@ -1064,14 +1123,6 @@ namespace Distribution.Migrations
IsActive = true
},
new
{
FK_RoleId = new Guid("10000000-0000-0000-0000-000000000004"),
FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000037"),
CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified),
Id = new Guid("d1dcef02-d4c3-caba-372f-dab82011e9c1"),
IsActive = true
},
new
{
FK_RoleId = new Guid("10000000-0000-0000-0000-000000000004"),
FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000038"),
@@ -1081,18 +1132,18 @@ namespace Distribution.Migrations
},
new
{
FK_RoleId = new Guid("10000000-0000-0000-0000-000000000005"),
FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000021"),
FK_RoleId = new Guid("10000000-0000-0000-0000-000000000004"),
FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000039"),
CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified),
Id = new Guid("08de9913-8cfc-4ab4-2cb9-2bc9217fbb6e"),
Id = new Guid("a66f3854-5a8f-95c2-cb1c-b2b2e67ee226"),
IsActive = true
},
new
{
FK_RoleId = new Guid("10000000-0000-0000-0000-000000000005"),
FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000022"),
FK_RoleId = new Guid("10000000-0000-0000-0000-000000000004"),
FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000040"),
CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified),
Id = new Guid("f3c8f315-3b2f-f923-e3cd-f3e575010138"),
Id = new Guid("14bcecd1-b1a2-6da3-155a-233611484467"),
IsActive = true
},
new
@@ -1112,6 +1163,22 @@ namespace Distribution.Migrations
IsActive = true
},
new
{
FK_RoleId = new Guid("10000000-0000-0000-0000-000000000005"),
FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000025"),
CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified),
Id = new Guid("1d227ae7-0eb9-c742-41df-3472718cb077"),
IsActive = true
},
new
{
FK_RoleId = new Guid("10000000-0000-0000-0000-000000000005"),
FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000026"),
CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified),
Id = new Guid("4d287b19-ba40-7d4d-3d39-291be64197f8"),
IsActive = true
},
new
{
FK_RoleId = new Guid("10000000-0000-0000-0000-000000000005"),
FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000013"),
@@ -1144,6 +1211,22 @@ namespace Distribution.Migrations
IsActive = true
},
new
{
FK_RoleId = new Guid("10000000-0000-0000-0000-000000000005"),
FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000017"),
CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified),
Id = new Guid("ee175c87-23bd-5529-ed18-6ce46985c6ed"),
IsActive = true
},
new
{
FK_RoleId = new Guid("10000000-0000-0000-0000-000000000005"),
FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000018"),
CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified),
Id = new Guid("2ae2eba4-4427-ab1e-cf03-e59a3925ef89"),
IsActive = true
},
new
{
FK_RoleId = new Guid("10000000-0000-0000-0000-000000000005"),
FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000005"),
@@ -1208,22 +1291,6 @@ namespace Distribution.Migrations
IsActive = true
},
new
{
FK_RoleId = new Guid("10000000-0000-0000-0000-000000000005"),
FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000017"),
CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified),
Id = new Guid("ee175c87-23bd-5529-ed18-6ce46985c6ed"),
IsActive = true
},
new
{
FK_RoleId = new Guid("10000000-0000-0000-0000-000000000005"),
FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000018"),
CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified),
Id = new Guid("2ae2eba4-4427-ab1e-cf03-e59a3925ef89"),
IsActive = true
},
new
{
FK_RoleId = new Guid("10000000-0000-0000-0000-000000000005"),
FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000019"),
@@ -1240,6 +1307,22 @@ namespace Distribution.Migrations
IsActive = true
},
new
{
FK_RoleId = new Guid("10000000-0000-0000-0000-000000000005"),
FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000021"),
CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified),
Id = new Guid("08de9913-8cfc-4ab4-2cb9-2bc9217fbb6e"),
IsActive = true
},
new
{
FK_RoleId = new Guid("10000000-0000-0000-0000-000000000005"),
FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000022"),
CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified),
Id = new Guid("f3c8f315-3b2f-f923-e3cd-f3e575010138"),
IsActive = true
},
new
{
FK_RoleId = new Guid("10000000-0000-0000-0000-000000000005"),
FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000001"),
@@ -1250,25 +1333,9 @@ namespace Distribution.Migrations
new
{
FK_RoleId = new Guid("10000000-0000-0000-0000-000000000005"),
FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000025"),
FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000027"),
CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified),
Id = new Guid("1d227ae7-0eb9-c742-41df-3472718cb077"),
IsActive = true
},
new
{
FK_RoleId = new Guid("10000000-0000-0000-0000-000000000005"),
FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000031"),
CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified),
Id = new Guid("69f93a29-1df1-7b20-9d5c-94c264f8c4b8"),
IsActive = true
},
new
{
FK_RoleId = new Guid("10000000-0000-0000-0000-000000000005"),
FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000032"),
CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified),
Id = new Guid("070813f7-4053-2889-0d14-d0d5834208c2"),
Id = new Guid("43a4c7b4-a50f-aa37-b147-a30636d9d65f"),
IsActive = true
},
new
@@ -1320,6 +1387,22 @@ namespace Distribution.Migrations
IsActive = true
},
new
{
FK_RoleId = new Guid("10000000-0000-0000-0000-000000000005"),
FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000039"),
CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified),
Id = new Guid("aeb29e0e-e819-1446-6cc6-1fb64b5ecc6b"),
IsActive = true
},
new
{
FK_RoleId = new Guid("10000000-0000-0000-0000-000000000005"),
FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000040"),
CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified),
Id = new Guid("e18a6eca-1009-bcc6-35e9-ead943c1649f"),
IsActive = true
},
new
{
FK_RoleId = new Guid("10000000-0000-0000-0000-000000000006"),
FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000001"),
@@ -1622,6 +1705,22 @@ namespace Distribution.Migrations
CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified),
Id = new Guid("1d6273d4-db6a-d70e-4b5f-98cb634d668a"),
IsActive = true
},
new
{
FK_RoleId = new Guid("10000000-0000-0000-0000-000000000006"),
FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000039"),
CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified),
Id = new Guid("3f32cdac-6925-23f2-064e-8226d781d4a3"),
IsActive = true
},
new
{
FK_RoleId = new Guid("10000000-0000-0000-0000-000000000006"),
FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000040"),
CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified),
Id = new Guid("dc3dd9ff-b123-a8d4-dcea-b4f5f8ff171e"),
IsActive = true
});
});
@@ -1679,6 +1778,81 @@ namespace Distribution.Migrations
b.ToTable("SaleRoutes");
});
modelBuilder.Entity("Distribution.Models.StockBalance", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("char(36)");
b.Property<Guid>("FK_ItemId")
.HasColumnType("char(36)");
b.Property<Guid>("LocationId")
.HasColumnType("char(36)");
b.Property<int>("LocationType")
.HasColumnType("int");
b.Property<decimal>("Quantity")
.HasColumnType("decimal(65,30)");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("datetime(6)");
b.HasKey("Id");
b.HasIndex("FK_ItemId", "LocationId")
.IsUnique();
b.ToTable("StockBalances");
});
modelBuilder.Entity("Distribution.Models.StockMovement", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("char(36)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
b.Property<Guid>("CreatedBy")
.HasColumnType("char(36)");
b.Property<Guid>("FK_ItemId")
.HasColumnType("char(36)");
b.Property<Guid?>("FromLocationId")
.HasColumnType("char(36)");
b.Property<int?>("FromLocationType")
.HasColumnType("int");
b.Property<decimal>("Quantity")
.HasColumnType("decimal(65,30)");
b.Property<string>("ReasonCode")
.HasColumnType("longtext");
b.Property<Guid?>("ReferenceId")
.HasColumnType("char(36)");
b.Property<Guid?>("ToLocationId")
.HasColumnType("char(36)");
b.Property<int?>("ToLocationType")
.HasColumnType("int");
b.Property<int>("Type")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("FK_ItemId");
b.ToTable("StockMovements");
});
modelBuilder.Entity("Distribution.Models.Store", b =>
{
b.Property<Guid>("Id")
@@ -1691,12 +1865,18 @@ namespace Distribution.Migrations
b.Property<decimal>("CreditAmount")
.HasColumnType("decimal(65,30)");
b.Property<decimal>("CreditLimit")
.HasColumnType("decimal(65,30)");
b.Property<Guid>("FK_RouteId")
.HasColumnType("char(36)");
b.Property<bool>("IsActive")
.HasColumnType("tinyint(1)");
b.Property<int>("KycStatus")
.HasColumnType("int");
b.Property<decimal>("Latitude")
.HasColumnType("decimal(65,30)");
@@ -1706,10 +1886,16 @@ namespace Distribution.Migrations
b.Property<decimal>("OneTimeAmount")
.HasColumnType("decimal(65,30)");
b.Property<decimal>("OutstandingBalance")
.HasColumnType("decimal(65,30)");
b.Property<string>("OwnerName")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("Segment")
.HasColumnType("longtext");
b.Property<string>("StoreAddress")
.IsRequired()
.HasColumnType("longtext");
@@ -1725,6 +1911,39 @@ namespace Distribution.Migrations
b.ToTable("Stores");
});
modelBuilder.Entity("Distribution.Models.StoreKycDocument", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("char(36)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
b.Property<string>("DocumentType")
.IsRequired()
.HasColumnType("longtext");
b.Property<Guid>("FK_StoreId")
.HasColumnType("char(36)");
b.Property<string>("FileReference")
.IsRequired()
.HasColumnType("longtext");
b.Property<bool>("IsActive")
.HasColumnType("tinyint(1)");
b.Property<DateTime>("UploadedAt")
.HasColumnType("datetime(6)");
b.HasKey("Id");
b.HasIndex("FK_StoreId");
b.ToTable("StoreKycDocuments");
});
modelBuilder.Entity("Distribution.Models.SubCategory", b =>
{
b.Property<Guid>("Id")
@@ -2092,6 +2311,28 @@ namespace Distribution.Migrations
b.Navigation("Region");
});
modelBuilder.Entity("Distribution.Models.StockBalance", b =>
{
b.HasOne("Distribution.Models.Item", "Item")
.WithMany()
.HasForeignKey("FK_ItemId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Item");
});
modelBuilder.Entity("Distribution.Models.StockMovement", b =>
{
b.HasOne("Distribution.Models.Item", "Item")
.WithMany()
.HasForeignKey("FK_ItemId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Item");
});
modelBuilder.Entity("Distribution.Models.Store", b =>
{
b.HasOne("Distribution.Models.Route", "Route")
@@ -2103,6 +2344,17 @@ namespace Distribution.Migrations
b.Navigation("Route");
});
modelBuilder.Entity("Distribution.Models.StoreKycDocument", b =>
{
b.HasOne("Distribution.Models.Store", "Store")
.WithMany()
.HasForeignKey("FK_StoreId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Store");
});
modelBuilder.Entity("Distribution.Models.SubCategory", b =>
{
b.HasOne("Distribution.Models.Category", "Category")
+1
View File
@@ -4,5 +4,6 @@ namespace Distribution.Models
{
public string Name { get; set; }
public int? ExternalErpId { get; set; }
public bool AllowCustomerReturn { get; set; } = false;
}
}
+23
View File
@@ -0,0 +1,23 @@
namespace Distribution.Models
{
public enum LocationType
{
Warehouse,
Vehicle
}
public enum KycStatus
{
Pending,
Verified,
Rejected }
public enum MovementType
{
Sale,
CustomerReturn,
Transfer,
Damage,
Adjustment
}
}
+19
View File
@@ -0,0 +1,19 @@
using System.ComponentModel.DataAnnotations.Schema;
namespace Distribution.Models
{
public class StockBalance
{
public Guid Id { get; set; } = Guid.NewGuid();
public Guid FK_ItemId { get; set; }
[ForeignKey("FK_ItemId")]
public Item Item { get; set; }
public Guid LocationId { get; set; }
public LocationType LocationType { get; set; }
public decimal Quantity { get; set; }
public DateTime UpdatedAt { get; set; } = DateTime.Now;
}
}
+28
View File
@@ -0,0 +1,28 @@
using System.ComponentModel.DataAnnotations.Schema;
namespace Distribution.Models
{
public class StockMovement
{
public Guid Id { get; set; } = Guid.NewGuid();
public Guid FK_ItemId { get; set; }
[ForeignKey("FK_ItemId")]
public Item Item { get; set; }
public Guid? FromLocationId { get; set; }
public LocationType? FromLocationType { get; set; }
public Guid? ToLocationId { get; set; }
public LocationType? ToLocationType { get; set; }
public decimal Quantity { get; set; }
public MovementType Type { get; set; }
public string? ReasonCode { get; set; }
public Guid? ReferenceId { get; set; }
public DateTime CreatedAt { get; set; } = DateTime.Now;
public Guid CreatedBy { get; set; }
}
}
+4
View File
@@ -11,6 +11,10 @@ namespace Distribution.Models
public decimal Longitude { get; set; } = 0;
public decimal OneTimeAmount { get; set; } = 0;
public decimal CreditAmount { get; set; } = 0;
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
@@ -0,0 +1,14 @@
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;
}
}
+6
View File
@@ -9,10 +9,12 @@ using Distribution.Features.Region;
using Distribution.Features.Role;
using Distribution.Features.Route;
using Distribution.Features.Store;
using Distribution.Features.Store.Kyc;
using Distribution.Features.Sync;
using Distribution.Features.Uom;
using Distribution.Features.User;
using Distribution.Features.Vehicle;
using Distribution.Features.Stock;
using Distribution.Features.Warehouse;
using Distribution.Infra.Authorization;
using Distribution.Infra.UoW;
@@ -44,6 +46,8 @@ builder.Services.AddScoped<ISubCategoryRepository, SubCategoryRepository>();
builder.Services.AddScoped<IBrandRepository, BrandRepository>();
builder.Services.AddScoped<IUomRepository, UomRepository>();
builder.Services.AddScoped<IItemRepository, ItemRepository>();
builder.Services.AddScoped<IStockRepository, StockRepository>();
builder.Services.AddScoped<IStoreKycDocumentRepository, StoreKycDocumentRepository>();
builder.Services.AddScoped<RegionService>();
builder.Services.AddScoped<RouteService>();
@@ -59,6 +63,8 @@ builder.Services.AddScoped<BrandService>();
builder.Services.AddScoped<UomService>();
builder.Services.AddScoped<ItemService>();
builder.Services.AddScoped<SyncService>();
builder.Services.AddScoped<StockService>();
builder.Services.AddScoped<StoreKycDocumentService>();
builder.Services.AddHttpContextAccessor();
builder.Services.AddScoped<ICurrentUserService, CurrentUserService>();
+4 -1
View File
@@ -1,6 +1,6 @@
{
"ConnectionStrings": {
"DefaultConnection": "Server=187.127.102.190;Port=3306;Database=distribution_db;User=kalana;Password=kalana@hexdive"
"DefaultConnection": "Server=187.127.102.190;Port=3306;Database=distribution;User=kalana;Password=kalana@hexdive"
},
"Logging": {
"LogLevel": {
@@ -14,5 +14,8 @@
"Issuer": "Distribution.Api",
"Audience": "Distribution.Api",
"ExpiryMinutes": 120
},
"StoreManagement": {
"UnverifiedKycOrderThreshold": 0
}
}
+62 -9
View File
@@ -7,7 +7,11 @@
## Goal
Close every hole identified in the currently-implemented modules (Region, Route, Vehicle, Store,
Warehouse, Order, User/Role/Permission) before any feature from [mainPlan.md](./mainPlan.md)
is built on top of them. This file is the running list of "Partially implemented" and foundational
"Not implemented" items pulled from that reference doc that block later steps, not just cosmetic
cleanup.
---
@@ -15,45 +19,94 @@
### 1. Route DTO — Missing RegionId
`RouteDTOs.cs` does not expose `RegionId` on create/update, so a Route cannot be linked to a
Region via the API even though `Route.FK_RegionId` exists on the model. Blocks §2/§3 of the
function reference (`assignAreaToRegion`, `getRegionHierarchy`) and any region-scoped authorization.
### 2. Store DTO — Missing RouteId
`StoreDTOs.cs` does not expose `RouteId` on create/update, so a Store cannot be linked to a Route
via the API even though `Store.FK_RouteId` exists on the model. Blocks `assignOutletsToRoute` (§3)
and route/region-scoped authorization for Stores and Orders.
### 3. Role Controller — Missing Authorization
`RoleController`'s GET endpoints carry only `[Authorize]`, with no `[Authorize(Policy = "...")]`
per action like every other controller. Any authenticated user — including a SalesRep — can read
every role and its full permission list today.
### 4. Secrets & Configuration Management
JWT signing key and DB connection string currently live in `appsettings.json`. Move both to
environment variables or user secrets before any non-local environment is stood up.
### 5. Input Validation
No DTO in any feature carries validation attributes (`[Required]`, `[MaxLength]`, range checks,
etc.). Bad data (empty names, negative levels, malformed emails) can enter the system silently
through any existing POST/PUT endpoint.
### 6. Item Model — Missing Properties
`Item : Template` has no properties (no SKU, name, unit of measure, category). `OrderItem`
references `Item`, but there's nothing there to reference. Fully scoped as its own step in
[01_item_catalog.md](./01_item_catalog.md) — tracked here only as the blocking dependency.
### 7. Order Has No Status / State Machine
`Order` has no status field, so `updateOrderStatus`, `approveOrder`/`rejectOrder`, and
`convertOrderToInvoice` (§1 of the function reference) have nowhere to write to. Every order-status
consumer downstream (delivery ride execution, sale ride execution, reconciliation, agent controls)
depends on this existing first.
### 8. No Credit Limit / Segment Fields on Store
`validateCreditLimit`, `updateStoreCreditLimit`, `getStoreOutstandingBalance`, and `classifyStore`
(§1, §4) all need fields that don't exist on `Store` today (credit limit, outstanding balance,
segment). Not scoped to a single feature file yet — flagged here so it isn't silently assumed by
a later step.
### 9. No Area Entity Between Region and Route
`assignAreaToRegion`, `createRoute(name, areaId)` (§2, §3) assume an `Area` level between Region
and Route that doesn't exist in the schema — only `Region → Route → Store` exists. Confirm with
the user whether `Area` is a real requirement or whether Region/Route is the intended two-level
hierarchy before building anything that assumes a third level.
### 10. No Driver Entity
`DeliveryRun.FK_DriverId`/`Driver` navigation is commented out in the model. `assignDriverToVehicle`,
`checkDriverCompliance` (§5) and any ride that needs a named driver depend on this existing.
Blocks [03_ride_lifecycle.md](./03_ride_lifecycle.md).
---
## Scope (What This Step Does NOT Include)
- No new feature modules (Procurement, Finance, Promotions, etc.) — those are separate steps,
see [mainPlan.md](./mainPlan.md) for the full list and their own
numbered files below.
- No UI/client changes.
- No changes to authentication (JWT) — only authorization policy coverage on `RoleController`.
---
## Business Rules
- A gap fix must not change the shape of an already-shipped response DTO in a breaking way without
flagging it — additive fields only unless explicitly agreed.
- Soft-delete (`IsActive`) pattern must be preserved on every entity touched.
---
## Open Questions
- Is `Area` a real business concept (a level between Region and Route), or should
`createRoute`/`assignAreaToRegion` in the function reference be reinterpreted as Region-only?
- Should `Store.CreditLimit` and `Store.OutstandingBalance` be added now as part of gap-fixing, or
deferred to a dedicated Finance step (§13)?
- Does `Order.Status` need a fixed enum now (`pending/approved/picked/dispatched/delivered/invoiced`
per §1), or a smaller interim set until the fuller order lifecycle is built?
---
+210 -14
View File
@@ -1,62 +1,258 @@
# 02 — Stock Ledger
**Status**: Not Started
**Status**: Done
**Depends On**: 01 — Item Catalog
---
## Goal
Introduce stock tracking per location. Every warehouse and vehicle holds a quantity of each item.
Stock is never edited directly — all changes happen through movement transactions.
The current balance is always the materialized sum kept in sync with the transaction log.
---
## Background / Context
Two tables work together:
- **StockBalance** — one row per (ItemId, LocationId). Updated atomically on every movement. Used for fast reads and enforcing stock limits (e.g. driver can't sell more than loaded).
- **StockMovement** — append-only transaction log. Source of truth. Used for audit, reconciliation at ride close (Step 07), and re-deriving balance if drift is ever suspected.
Both are updated in the same UoW commit. They must always agree.
**Location abstraction:** A single `LocationId` (+ `LocationType` enum) covers both warehouses and vehicles. Same query pattern regardless of location type.
---
## Data Model Changes
### Stock Balance (per location)
### New Enum — LocationType
```csharp
public enum LocationType { Warehouse, Vehicle }
```
### New Enum — MovementType
### Stock Movement Transaction
```csharp
public enum MovementType
{
Sale,
CustomerReturn,
Transfer, // covers: warehouse→vehicle (load), vehicle→warehouse (driver return), warehouse→warehouse
Damage,
Adjustment
}
```
### New Entity — StockBalance
One row per (ItemId, LocationId). Updated on every movement.
### Location Abstraction (Warehouse vs Vehicle)
| Column | Type | Notes |
|---|---|---|
| Id | Guid | PK |
| FK_ItemId | Guid | FK → Item |
| LocationId | Guid | FK → Warehouse or Vehicle |
| LocationType | LocationType | Warehouse or Vehicle |
| Quantity | decimal | Current balance. Never goes below 0 |
| UpdatedAt | DateTime | Last movement time |
No `IsActive` / no soft delete — balance rows are permanent once created.
Composite unique index on `(FK_ItemId, LocationId)`.
### New Entity — StockMovement
Append-only. Never updated or deleted.
| Column | Type | Notes |
|---|---|---|
| Id | Guid | PK |
| FK_ItemId | Guid | FK → Item |
| FromLocationId | Guid? | Null for Sale, Damage, Adjustment (single-location) |
| FromLocationType | LocationType? | Null when FromLocationId is null |
| ToLocationId | Guid? | Null for Sale, Damage (stock leaves system) |
| ToLocationType | LocationType? | Null when ToLocationId is null |
| Quantity | decimal | Always positive — direction implied by movement type |
| Type | MovementType | |
| ReasonCode | string? | Required for Adjustment and Damage |
| ReferenceId | Guid? | Links to the originating business event (OrderId, RideId, etc.) |
| CreatedAt | DateTime | |
| CreatedBy | Guid | UserId of who triggered this |
---
## Category Configuration
`Category` gets one new flag to support configurable customer returns:
| Column | Type | Notes |
|---|---|---|
| AllowCustomerReturn | bool | Default false. If false, CustomerReturn movements are rejected for items in this category |
---
## API Endpoints
Base route: `/api/stock`
### Query
| Method | Route | Description |
|---|---|---|
| GET | `/balances` | Get all stock balances (paginated). Filter by `locationId`, `locationType`, `itemId` |
| GET | `/balances/{itemId}/total` | Total stock of one item across all locations |
| GET | `/movements` | Get movement history (paginated). Filter by `locationId`, `itemId`, `type`, `from`, `to` |
### Write
| Method | Route | Description |
|---|---|---|
| POST | `/transfer` | Move stock between two locations |
| POST | `/damage` | Record damaged stock at a location |
| POST | `/customer-return` | Record a customer return to a vehicle |
| POST | `/adjustment` | Manual correction at a location (requires reason code) |
---
## Request / Response Shapes
### GET `/balances` response (paginated)
```json
{
"page": 1,
"pageSize": 20,
"total": 85,
"items": [
{
"itemId": "...",
"itemName": "Sprite 330ml",
"locationId": "...",
"locationType": "Warehouse",
"quantity": 120,
"updatedAt": "2026-08-19T10:00:00"
}
]
}
```
### GET `/balances/{itemId}/total` response
```json
{
"itemId": "...",
"itemName": "Sprite 330ml",
"totalQuantity": 340,
"breakdown": [
{ "locationId": "...", "locationType": "Warehouse", "quantity": 200 },
{ "locationId": "...", "locationType": "Vehicle", "quantity": 140 }
]
}
```
### GET `/movements` response (paginated)
```json
{
"page": 1,
"pageSize": 20,
"total": 412,
"items": [
{
"id": "...",
"itemId": "...",
"itemName": "Sprite 330ml",
"fromLocationId": "...",
"fromLocationType": "Warehouse",
"toLocationId": "...",
"toLocationType": "Vehicle",
"quantity": 50,
"type": "Transfer",
"reasonCode": null,
"referenceId": "...",
"createdAt": "2026-08-19T08:00:00",
"createdBy": "..."
}
]
}
```
### POST `/transfer` request
```json
{
"itemId": "...",
"fromLocationId": "...",
"fromLocationType": "Warehouse",
"toLocationId": "...",
"toLocationType": "Vehicle",
"quantity": 50,
"referenceId": "..."
}
```
### POST `/damage` request
```json
{
"itemId": "...",
"locationId": "...",
"locationType": "Warehouse",
"quantity": 3,
"reasonCode": "Broken during handling"
}
```
### POST `/customer-return` request
```json
{
"itemId": "...",
"vehicleId": "...",
"quantity": 2,
"referenceId": "..."
}
```
### POST `/adjustment` request
```json
{
"itemId": "...",
"locationId": "...",
"locationType": "Warehouse",
"quantity": -2,
"reasonCode": "Counting error corrected after audit"
}
```
---
## Business Rules
---
## Fraud / Control Considerations
1. `StockBalance.Quantity` must never go below 0. Any movement that would result in negative stock is rejected with a 400.
2. `Adjustment` and `Damage` require a non-empty `ReasonCode`.
3. `CustomerReturn` is rejected if `Category.AllowCustomerReturn = false` for the item's category.
4. `Transfer` debits `FromLocation` and credits `ToLocation` atomically.
5. Both `StockBalance` update and `StockMovement` insert happen in the same transaction — never one without the other.
6. `StockMovement` rows are never updated or deleted. Wrong entries are corrected by a counter-movement.
7. All list endpoints return paginated responses.
---
## Open Questions
None — all design decisions resolved in planning discussion.
---
## Implementation Notes
> Fill this section in during planning discussion before writing any code.
**Feature folder:** `Features/Stock/`
**Entities:** `StockBalance.cs`, `StockMovement.cs` in `Models/`
**Migration:** Add after entities and DbContext are updated
**Service layer:** One `StockService` handles all movement types. Each write method: begins transaction → validates stock → inserts movement → updates balance → commits.
**Category change:** Add `AllowCustomerReturn` column to existing `Category` entity and migration.
+29 -9
View File
@@ -7,13 +7,20 @@
## Goal
Turn the currently-unused `DeliveryRun` model into a working ride: a session that turns a vehicle
into a temporary warehouse. Covers `assignVehicleToRoute`, `assignDriverToVehicle`,
`checkDriverCompliance` from [mainPlan.md §5](./mainPlan.md#5-vehicle--fleet-management)
and `planDeliveryRoute` from §3.
---
## Background / Context
`Models/DeliveryRun.cs` exists (`FK_VehicleId`, `FK_RouteId`, `Meterstart`, `Meterend`) but has no
repository, service, or controller — it is completely unwired today, per [mainPlan.md](./mainPlan.md)'s
"Known Gaps" table. `FK_DriverId`/`Driver` navigation is commented out because no `Driver` entity
exists yet (gap #10 in [00_existing_gaps.md](./00_existing_gaps.md)) — this step is blocked on that
being resolved first if driver assignment is required at ride-open time.
---
@@ -21,39 +28,52 @@
### Ride (extending DeliveryRun)
Add a `Status` field to `DeliveryRun` for the state machine below, and a `RideType` field
(`Delivery` | `Sale`).
### Ride State Machine
`Draft → Open → Active → Closed`. A vehicle can only be on one `Open`/`Active` ride at a time —
enforced in the service layer, not the database.
### Ride Type (Delivery vs Sale)
`Delivery` rides fulfill pre-confirmed orders (see [05_delivery_ride.md](./05_delivery_ride.md));
`Sale` rides create invoices on the spot with no pre-orders (see [06_sale_ride.md](./06_sale_ride.md)).
Both share the same ride lifecycle and loading event.
---
## API Endpoints
| Function reference | Endpoint intent |
|---|---|
| `assignVehicleToRoute(vehicleId, routeId)` (§5) | Implicit in `POST /api/rides` — a ride binds a vehicle to a route for its duration |
| `assignDriverToVehicle(vehicleId, driverId)` (§5) | `PUT /api/rides/{id}/driver` — blocked on Driver entity, see gap #10 |
| `checkDriverCompliance(driverId)` (§5) | Called internally before a ride can move `Draft → Open` |
| `planDeliveryRoute(warehouseId, orderIds[])` (§3) | `POST /api/rides` (for `RideType = Delivery`) |
| — | `POST /api/rides/{id}/open`, `POST /api/rides/{id}/close` — state transitions |
---
## Business Rules
- A ride cannot open without a compliant driver (once Driver entity exists) and a vehicle not
already on an open ride.
- Opening a ride creates the vehicle's stock ledger location (see [02_stock_ledger.md](./02_stock_ledger.md)).
---
## Fraud / Control Considerations
- `checkDriverCompliance` failures must block ride open, not just warn.
---
## Open Questions
- Is a `Driver` a new entity distinct from `User`, or is "driver" just a `User` with a specific
`Role`? Affects whether gap #10 needs a new table or just a role addition.
---
+30 -9
View File
@@ -7,13 +7,18 @@
## Goal
Move stock from a source warehouse onto an opened ride's vehicle, with two-party confirmation.
Covers the dispatch half of [mainPlan.md §6](./mainPlan.md#6-warehouse-management):
`generatePickList`, `confirmPicking`, `packOrder`, `buildDispatchLoad`, and `calculateLoadPlan`
from §5.
---
## Background / Context
This is the first place the [02_stock_ledger.md](./02_stock_ledger.md) movement model gets
exercised end-to-end: a `Load` movement debits the warehouse and credits the vehicle location
created when the ride opened ([03_ride_lifecycle.md](./03_ride_lifecycle.md)).
---
@@ -21,39 +26,55 @@
### Loading Sheet
Header record: `RideId`, `SourceWarehouseId`, `Status` (`Pending`, `DispatchConfirmed`,
`ReceiptConfirmed`, `DiscrepancyFlagged`, `Resolved`).
### Loading Line Item
`LoadingSheetId`, `ItemId`, `QuantityDispatched` (set by warehouse manager), `QuantityReceived`
(set by driver) — a mismatch between the two is a discrepancy.
### Two-Party Confirmation
Warehouse manager confirms what was dispatched (`generatePickList` → `confirmPicking` →
`packOrder`); driver separately confirms what was received onto the vehicle. Both confirmations are
required before the ride can move `Open → Active`.
---
## API Endpoints
| Function reference | Endpoint intent |
|---|---|
| `generatePickList(orderIds[])` (§6) | `POST /api/loading-sheets` |
| `confirmPicking(pickListId, pickedItems[])` (§6) | `POST /api/loading-sheets/{id}/confirm-dispatch` |
| `packOrder(orderId, packageDetails)` (§6) | `POST /api/loading-sheets/{id}/pack` |
| `buildDispatchLoad(vehicleId, orderIds[])` (§6) | Groups multiple orders into one loading sheet |
| `calculateLoadPlan(vehicleId, orderIds[])` (§5) | Validates total volume/weight against vehicle capacity before dispatch confirmation |
| — | `POST /api/loading-sheets/{id}/confirm-receipt` — driver-side confirmation |
---
## Business Rules
- `QuantityDispatched != QuantityReceived` on any line flags the sheet `DiscrepancyFlagged` and
blocks ride activation until resolved.
- `calculateLoadPlan` must reject a load exceeding vehicle capacity before dispatch is confirmed,
not after.
---
## Fraud / Control Considerations
- Discrepancies must be immediately visible, not silently averaged into stock adjustments — see
[09_fraud_controls.md](./09_fraud_controls.md).
---
## Open Questions
- Does `Vehicle` need a `Capacity` field added now (volume/weight), or is `calculateLoadPlan`
deferred until that's available?
---
+26 -9
View File
@@ -7,13 +7,18 @@
## Goal
Execute pre-confirmed orders against a loaded, active delivery ride. Covers the order-status half
of [mainPlan.md §1](./mainPlan.md#1-order-management):
`updateOrderStatus`, `convertOrderToInvoice`, `createBackorder`. Requires `Order.Status` to exist
first — see gap #7 in [00_existing_gaps.md](./00_existing_gaps.md).
---
## Background / Context
Today `Order` has no status and no link to a ride. This step is where the order lifecycle
(`pending → approved → picked → dispatched → delivered → invoiced`) actually gets driven, once the
order has been picked/loaded ([04_loading_event.md](./04_loading_event.md)).
---
@@ -21,39 +26,51 @@
### Delivery Task (per order on ride)
`RideId`, `OrderId`, `Sequence` (visit order), `Status` (`Pending`, `Delivered`, `PartiallyDelivered`,
`Failed`).
### Delivery Outcome
Per delivery task: quantities actually delivered per line, vs. ordered — feeds `createBackorder`
for the shortfall.
### Customer Confirmation
Signature or OTP captured against the delivery task before it can be marked complete.
---
## API Endpoints
| Function reference | Endpoint intent |
|---|---|
| `updateOrderStatus(orderId, status)` (§1) | `PUT /api/rides/{rideId}/tasks/{orderId}/status` |
| `convertOrderToInvoice(orderId)` (§1) | Triggered automatically on `Delivered`/`PartiallyDelivered` — see [19_finance_accounting.md](./19_finance_accounting.md) |
| `createBackorder(orderId, unfulfilledItems[])` (§1) | Triggered automatically on `PartiallyDelivered` |
| — | `POST /api/rides/{rideId}/tasks/{orderId}/confirm` — captures customer signature/OTP |
---
## Business Rules
- A delivery task cannot be marked `Delivered` without a customer confirmation.
- `PartiallyDelivered` always creates a `createBackorder` entry and a credit note for the
undelivered quantity (see [18_returns_claims.md](./18_returns_claims.md) for credit note shape).
- Each confirmed delivery task creates a stock movement (`Sale`/consumption) against the vehicle
location per [02_stock_ledger.md](./02_stock_ledger.md).
---
## Fraud / Control Considerations
- OTP/signature capture must be tamper-evident (timestamped, tied to the delivery task, not
editable after submission).
---
## Open Questions
- Signature capture (image) or OTP (SMS code) — one, or both, in v1?
---
+27 -9
View File
@@ -7,13 +7,18 @@
## Goal
Support on-the-spot van sales with no pre-order: driver picks items/quantities, an invoice is
generated immediately, and payment is captured at point of sale. Covers `createInvoice`,
`recordPayment` from [mainPlan.md §13](./mainPlan.md#13-finance--accounting),
and reuses `applyPricingAndDiscounts` (§1) plus `evaluateSchemeEligibility`/`applySchemeDiscount`
(§11) if promotions are live by the time this ships.
---
## Background / Context
Distinct from [05_delivery_ride.md](./05_delivery_ride.md): there is no pre-existing `Order` row —
the field invoice *is* the order, created and settled in one step.
---
@@ -21,39 +26,52 @@
### Field Invoice
`RideId`, `StoreId`, `CreatedByUserId`, `TotalAmount`, `PaymentStatus` (`Paid`, `PartiallyPaid`,
`OnCredit`).
### Field Invoice Line Item
`FieldInvoiceId`, `ItemId`, `Quantity`, `UnitPrice` (subject to the discount-limit rules in
[08_sales_agent_controls.md](./08_sales_agent_controls.md)).
### Payment Record
`FieldInvoiceId`, `Amount`, `Method` (`Cash`, `Credit`), `CollectedAt`.
---
## API Endpoints
| Function reference | Endpoint intent |
|---|---|
| `createInvoice(orderId)` (§13) | `POST /api/rides/{rideId}/field-invoices` — orderId is the field invoice itself here |
| `recordPayment(invoiceId, amount, method)` (§13) | `POST /api/field-invoices/{id}/payments` |
| `applyPricingAndDiscounts(orderId)` (§1) | Applied at line-item entry time, within agent discount limits |
---
## Business Rules
- Driver cannot sell more than what is loaded on the vehicle — every line item is checked against
the vehicle's current stock balance ([02_stock_ledger.md](./02_stock_ledger.md)) before the
invoice is finalized.
- Each confirmed sale line creates a `Sale` stock movement against the vehicle location,
immediately (not batched at ride close).
- Discount applied per line must not exceed the agent's role-level limit (§8).
---
## Fraud / Control Considerations
- Cash collected must reconcile against invoiced totals at ride close — see
[07_ride_reconciliation.md](./07_ride_reconciliation.md).
---
## Open Questions
- Are field invoices numbered/sequenced separately from warehouse-issued invoices, or share one
sequence?
---
+31 -10
View File
@@ -7,13 +7,18 @@
## Goal
Physically count what's left on the vehicle at ride close and reconcile it against the stock
ledger and cash collected. Covers `runCycleCount` and `reconcileStock` from
[mainPlan.md §6](./mainPlan.md#6-warehouse-management), applied to a
vehicle location instead of a warehouse zone.
---
## Background / Context
This is the control point that catches loading discrepancies ([04_loading_event.md](./04_loading_event.md))
that went unresolved, and any shortfall between what was sold/delivered and what's physically on
the vehicle.
---
@@ -21,45 +26,61 @@
### Ride Close Summary
`RideId`, `ClosedAt`, `ClosedByUserId`, computed totals for the reconciliation equation below.
### Variance Record
`RideId`, `ItemId` (or `Cash` for the monetary variance), `ExpectedQuantity`/`ExpectedAmount`,
`ActualQuantity`/`ActualAmount`, `Variance`, `ReasonCode`, `Status` (`Pending`, `Approved`,
`Rejected`).
### Supervisor Approval
Every non-zero variance requires a supervisor-level approval (`Role.Level >= 20`) before the ride
can transition to `Closed`.
---
## The Reconciliation Equation
```
Opening Stock − Sales − Damages + Returns = Closing Stock (physical count) [runCycleCount / reconcileStock]
Cash Collected + Credit Extended = Total Sales − Returns
```
---
## API Endpoints
| Function reference | Endpoint intent |
|---|---|
| `runCycleCount(warehouseId, zoneId)` (§6) | `POST /api/rides/{id}/cycle-count` — `zoneId` maps to the ride's vehicle location |
| `reconcileStock(warehouseId, countResults)` (§6) | `POST /api/rides/{id}/reconcile` |
| — | `POST /api/rides/{id}/variances/{id}/approve` — supervisor sign-off |
| — | `POST /api/rides/{id}/close` — only succeeds once every variance is `Approved` |
---
## Business Rules
- A ride cannot close with an unexplained variance — every `Variance` row must be `Approved` or
`Rejected` (with the discrepancy converted to a stock `Adjustment` movement) before close.
- `reconcileStock` writes an `Adjustment` movement per variance line, never edits the running
balance directly (see [02_stock_ledger.md](./02_stock_ledger.md)).
---
## Fraud / Control Considerations
- Unresolved/high-frequency discrepancies are logged permanently against the agent's record — feeds
the anomaly detection in [09_fraud_controls.md](./09_fraud_controls.md).
---
## Open Questions
- Is there a variance threshold below which supervisor approval is auto-granted (e.g. rounding-
level cash differences), or does every non-zero variance require explicit sign-off?
---
+34 -9
View File
@@ -7,13 +7,21 @@
## Goal
Wire role levels to concrete business limits and approval chains. Covers the approval-routing half
of [mainPlan.md §1](./mainPlan.md#1-order-management)
(`submitOrderForApproval`, `approveOrder`, `rejectOrder`) and the performance-tracking functions
from [§10 SFA/CRM](./mainPlan.md#10-sales-force-automation-sfa--crm)
(`trackTargetVsAchievement`, `calculateIncentive`). Lead/interaction functions from §10
(`logCustomerInteraction`, `createLead`, `convertLeadToStore`) are out of scope here — see
[16_sfa_crm.md](./16_sfa_crm.md).
---
## Background / Context
`Role.Level` already exists and drives the permission seed
(`Data/Seed/RoleAndPermissionSeed.cs`), but nothing today turns a level into a discount limit,
credit limit, or approval threshold — it only gates which API endpoints a role can call.
---
@@ -21,39 +29,56 @@
### Agent Limit Configuration (per role level)
`RoleId`, `MaxDiscountPercent`, `MaxSingleTransactionValue` — read by both
[06_sale_ride.md](./06_sale_ride.md) (line-item discount check) and order approval routing here.
### Override Log
`OverrideById` (the supervisor), `TargetType`/`TargetId` (e.g. `Order`, `FieldInvoice`), `OldValue`,
`NewValue`, `Reason`, `CreatedAt` — a one-time exception, logged against the supervisor's account.
### Customer Credit Profile
Reuses `Store.CreditLimit`/`Store.OutstandingBalance` once added (gap #8 in
[00_existing_gaps.md](./00_existing_gaps.md)) rather than a separate entity.
---
## API Endpoints
| Function reference | Endpoint intent |
|---|---|
| `submitOrderForApproval(orderId)` (§1) | Triggered automatically when an order exceeds its agent's limit |
| `approveOrder(orderId, approverId)` / `rejectOrder(orderId, reason)` (§1) | `POST /api/orders/{id}/approve`, `POST /api/orders/{id}/reject` |
| `trackTargetVsAchievement(userId, period)` (§10) | `GET /api/agents/{userId}/performance?period=...` |
| `calculateIncentive(userId, period)` (§10) | `GET /api/agents/{userId}/incentive?period=...` |
| — | `POST /api/orders/{id}/override` — supervisor one-time exception, writes an Override Log entry |
---
## Business Rules
- An order/invoice whose value or discount exceeds the agent's `Agent Limit Configuration` cannot
proceed without either supervisor approval or a logged override — no silent pass-through.
- `calculateIncentive` reads only settled/closed rides (post-[07_ride_reconciliation.md](./07_ride_reconciliation.md)),
never in-flight ones.
---
## Fraud / Control Considerations
- Every `Override Log` entry is permanent and immutable once written — feeds
[09_fraud_controls.md](./09_fraud_controls.md)'s anomaly detection (agents with unusually
frequent overrides).
---
## Open Questions
- Are discount/transaction limits configured per `Role`, or can an individual `User` have a
tighter/looser override of their role's default?
- What sales target source feeds `trackTargetVsAchievement` — is there a target-setting endpoint
needed here, or does it come from §2's `setRegionalTarget` rolled down to the individual?
---
+39 -10
View File
@@ -7,13 +7,18 @@
## Goal
A dedicated, immutable audit log plus automated anomaly detection over everything built in steps
01–08. Covers [mainPlan.md §18](./mainPlan.md#18-compliance--audit)
(`logAuditEvent`, `checkLicenseCompliance`, `generateComplianceReport`) and the fraud-relevant piece
of §15 (`sendRouteDeviationAlert`) and §12 (return-without-prior-sale detection).
---
## Background / Context
Every prior step in this plan references "logged" or "flagged" behavior (override logs, loading
discrepancies, reconciliation variances) — this step is where that logging actually becomes a
queryable, immutable audit trail instead of scattered per-feature notes.
---
@@ -21,45 +26,69 @@
### Audit Log Entry
`UserId`, `Action`, `EntityType`, `EntityId`, `OldValue`/`NewValue` (JSON snapshot), `CreatedAt`,
`SourceIp`/`RequestId` if available. Append-only — no update or delete path exposed anywhere.
### Anomaly Flag
`Type` (`ReturnWithoutSale`, `RouteDeviation`, `HighVarianceRate`, `CashShortfall`), `EntityType`/
`EntityId`, `RaisedAt`, `Status` (`Open`, `Reviewed`, `Dismissed`).
### Return Approval Record
Reuses the `approveReturn` flow from [18_returns_claims.md](./18_returns_claims.md) — this step
adds the automated pre-check (`ReturnWithoutSale`) that runs before a human approver sees it.
---
## API Endpoints
| Function reference | Endpoint intent |
|---|---|
| `logAuditEvent(userId, action, entityType, entityId)` (§18) | Internal — called by every mutating service, not directly exposed |
| `checkLicenseCompliance(entityId, licenseType)` (§18) | `GET /api/compliance/{entityType}/{entityId}/license-check` |
| `generateComplianceReport(period)` (§18) | `GET /api/compliance/report?period=...` |
| `sendRouteDeviationAlert(routeId, salesRepId)` (§15) | Triggered from route check-in/out data (§3 of the function reference; not yet in any numbered step — see [11_route_sales_management.md](./11_route_sales_management.md)) |
| — | `GET /api/audit-log?entityType=...&entityId=...` |
| — | `GET /api/anomaly-flags?status=Open` |
---
## Automated Checks & Flags
- Returns without a matching prior sale (`ReturnWithoutSale`).
- Sales/deliveries outside the geofenced route (`RouteDeviation`) — depends on GPS check-in/out
data from [11_route_sales_management.md](./11_route_sales_management.md).
- Agents with consistently high variance rates across closed rides (`HighVarianceRate`) — reads
`Variance Record` history from [07_ride_reconciliation.md](./07_ride_reconciliation.md).
- Cash collected below invoiced amount (`CashShortfall`) — reads `Payment Record` from
[06_sale_ride.md](./06_sale_ride.md).
---
## Business Rules
- Every state-changing action across every feature must call `logAuditEvent` — this is the one
cross-cutting requirement every other numbered step inherits once this lands.
- Return approval requires supervisor sign-off before stock is credited back
([18_returns_claims.md](./18_returns_claims.md)), regardless of whether an anomaly flag fired.
---
## Fraud / Control Considerations
- Anomaly reports are surfaced to area managers on a scheduled basis — batch job or on-demand
endpoint, not real-time push, in v1.
---
## Open Questions
- Should `logAuditEvent` be wired in as a cross-cutting concern (e.g. an EF `SaveChanges`
interceptor) rather than an explicit call in every service, to guarantee nothing is missed?
- What licenses does `checkLicenseCompliance` actually need to validate for this business — is
regulated goods handling in scope at all, or is this function speculative until a real
requirement shows up?
---
+71
View File
@@ -0,0 +1,71 @@
# 10 — Region Management (Beyond CRUD)
**Status**: Not Started
**Depends On**: 00 — Existing Gap Fixes
---
## Goal
Region CRUD already exists (`Features/Region`). This step covers everything in
[mainPlan.md §2](./mainPlan.md#2-region-management) that isn't plain
CRUD: targets, budgets, agency mapping, tax rules, hierarchy, and dashboarding.
---
## Background / Context
`Region` today has only a `Name`. Nothing points to it except `Route.FK_RegionId`. There is no
`Area` level (see Open Question in [00_existing_gaps.md](./00_existing_gaps.md#9-no-area-entity-between-region-and-route)).
---
## Data Model Changes
### Regional Target / Budget
`RegionId`, `Period` (e.g. `2026-08`), `TargetValue`, `BudgetAmount`.
### Agency-Region Map
`RegionId`, `AgencyId` — depends on whether "Regional Agency" becomes a distinct entity or stays a
`Role` (see Open Questions).
### Regional Tax Rule
`RegionId`, `RuleConfig` (JSON) — deferred to whenever tax/pricing rule overrides are prioritized.
---
## API Endpoints
| Function reference | Endpoint intent |
|---|---|
| `assignAreaToRegion(areaId, regionId)` | Blocked — no `Area` entity exists (see [00_existing_gaps.md](./00_existing_gaps.md#9-no-area-entity-between-region-and-route)) |
| `setRegionalTarget(regionId, period, targetValue)` | `PUT /api/regions/{id}/targets/{period}` |
| `setRegionalBudget(regionId, period, budgetAmount)` | `PUT /api/regions/{id}/budgets/{period}` |
| `mapAgencyToRegion(agencyId, regionId)` | `POST /api/regions/{id}/agencies` |
| `setRegionalTaxRule(regionId, ruleConfig)` | `PUT /api/regions/{id}/tax-rule` |
| `getRegionHierarchy(regionId)` | `GET /api/regions/{id}/hierarchy` |
| `getRegionDashboard(regionId, period)` | `GET /api/regions/{id}/dashboard?period=...` |
---
## Business Rules
- Only `RegionalManager` and above (per role level in `Data/Seed/RoleAndPermissionSeed.cs`) can
set targets/budgets for their own region; only `RegionalAgency` can set them across regions.
---
## Open Questions
- Is "Regional Agency" a business entity distinct from the `RegionalAgency` role (e.g. a contracted
third party with its own record), or is the role sufficient?
- Does `getRegionDashboard` need real-time aggregation, or a scheduled/cached rollup?
---
## Implementation Notes
> Fill this section in during planning discussion before writing any code.
@@ -0,0 +1,71 @@
# 11 — Route & Sales Visit Management
**Status**: Not Started
**Depends On**: 00 — Existing Gap Fixes
---
## Goal
Route CRUD already exists (`Features/Route`). This step covers the sales-visit half of
[mainPlan.md §3](./mainPlan.md#3-route-management) — the part that is
about a SalesRep's daily visit schedule, not the ride/dispatch route from
[03_ride_lifecycle.md](./03_ride_lifecycle.md)'s `planDeliveryRoute`.
---
## Background / Context
Today a `Store` has a single `FK_RouteId` (bulk-assign not yet supported via API — see gap #2 in
[00_existing_gaps.md](./00_existing_gaps.md)), and route access is only granted via `UserRouteScope`
(a many-to-many grant), not single ownership. There is no visit check-in/out data anywhere.
---
## Data Model Changes
### Route Ownership
`RouteId`, `OwnerUserId` — single-owner assignment, distinct from the broader `UserRouteScope`
access grant which may cover multiple users (e.g. a Supervisor overseeing several reps' routes).
### Route Visit Log
`RouteId`, `StoreId`, `UserId`, `CheckInAt`, `CheckInGps`, `CheckOutAt` — one row per planned visit.
---
## API Endpoints
| Function reference | Endpoint intent |
|---|---|
| `assignOutletsToRoute(routeId, outletIds[])` | `POST /api/routes/{id}/stores` — bulk version of the existing single `Store.RouteId` field |
| `assignRouteToSalesRep(routeId, salesRepId)` | `PUT /api/routes/{id}/owner` |
| `optimizeRouteSequence(routeId)` | `POST /api/routes/{id}/optimize` |
| `recordRouteCheckIn(routeId, outletId, gpsCoords, timestamp)` | `POST /api/routes/{id}/visits/check-in` |
| `recordRouteCheckOut(routeId, outletId, timestamp)` | `POST /api/routes/{id}/visits/check-out` |
| `getRouteAdherence(routeId, date)` | `GET /api/routes/{id}/adherence?date=...` |
| `reassignRoute(routeId, newSalesRepId)` | `PUT /api/routes/{id}/owner` (same as assign, different actor) |
| `getRoutePerformance(routeId, period)` | `GET /api/routes/{id}/performance?period=...` |
---
## Business Rules
- Check-in `gpsCoords` feeds `sendRouteDeviationAlert` in
[09_fraud_controls.md](./09_fraud_controls.md) — a check-in materially outside the store's
registered location should flag, not silently accept.
- `getStoreVisitHistory` (§4) reads directly from `Route Visit Log` — no separate storage.
---
## Open Questions
- Is route sequence optimization (`optimizeRouteSequence`) a manual reorder endpoint, or does it
need actual distance/priority-based computation (external mapping API)?
---
## Implementation Notes
> Fill this section in during planning discussion before writing any code.
+257
View File
@@ -0,0 +1,257 @@
# 12 — Store Management (Beyond CRUD)
**Status**: Done — migration applied to the database
**Depends On**: 00 — Existing Gap Fixes, 10 — Region Management, 11 — Route & Sales Management
---
## Goal
Store CRUD already exists (`Features/Store`). This step adds everything in
[mainPlan.md §4](./mainPlan.md#4-store-outlet-management) that isn't plain CRUD: credit control,
KYC verification, segmentation, outstanding balance, visit history, scheme eligibility, and a
sales dashboard.
---
## Background / Context
`Store` currently has no credit limit, outstanding balance, segment, or KYC fields — flagged as
gap #8 in [00_existing_gaps.md](./00_existing_gaps.md). Credit fields exist informally today as
`OneTimeAmount` / `CreditAmount`, which stay as-is; this step adds the real credit-limit +
outstanding-balance + KYC-gated-ordering flow on top.
`getStoreVisitHistory` and `checkStoreSchemeEligibility` are read-only integration points into
modules that don't exist yet ([11_route_sales_management.md](./11_route_sales_management.md) for
visits, [17_promotions_schemes.md](./17_promotions_schemes.md) for schemes). Implement them now
with the query shape defined below; wire the actual join once those modules land — return an empty
list / `eligible: true` (no active schemes to check) until then, guarded by a `NotImplementedYet`
style short-circuit, not a broken query.
---
## Data Model Changes
### Store — New Fields
```
Store (existing, add columns)
├── CreditLimit (decimal) — default 0. Max outstanding allowed before new orders are blocked.
├── OutstandingBalance (decimal) — default 0. Cached/denormalized. Updated by Finance module
│ (step 19) on invoice/payment events via UpdateOutstandingBalanceAsync.
│ Until Finance exists, only moved by manual adjustment endpoint.
├── Segment (string?) — free text, e.g. "Gold", "Silver", "Key Account", "High Risk".
├── KycStatus (enum) — Pending | Verified | Rejected. Default Pending.
```
### New Enum — KycStatus
```csharp
public enum KycStatus { Pending, Verified, Rejected }
```
### New Entity — StoreKycDocument
```
StoreKycDocument
├── Id (Guid) — PK, from Template
├── FK_StoreId (Guid) — required
├── DocumentType (string) — free text, e.g. "GST Certificate", "PAN Card", "Shop License,NIC"
├── FileReference (string) — relative path under wwwroot/uploads/kyc/{storeId}/
├── UploadedAt (DateTime)
├── IsActive, CreatedAt — from Template
```
### AppSettings — New Config Key
```
StoreManagement:UnverifiedKycOrderThreshold (decimal) — default 0.
Max single-order value a store with KycStatus != Verified may place.
0 disables ordering entirely for unverified stores; set higher to allow small orders through.
```
---
## Migrations
One migration needed:
1. **AddStoreCreditAndKyc** — adds `CreditLimit`, `OutstandingBalance`, `Segment`, `KycStatus`
columns to `Stores`; creates `StoreKycDocuments` table.
---
## API Endpoints
### Existing (unchanged)
| Method | Route | Permission | Notes |
|---|---|---|---|
| GET | /api/stores | stores.view | |
| GET | /api/stores/{id} | stores.view | |
| POST | /api/stores | stores.create | |
| PUT | /api/stores/{id} | stores.update | |
| DELETE | /api/stores/{id} | stores.delete | Soft delete |
### New
| Method | Route | Permission | Notes |
|---|---|---|---|
| POST | /api/stores/{id}/kyc-documents | stores.update | Multipart upload, appends a KYC document |
| GET | /api/stores/{id}/kyc-documents | stores.view | Lists documents for a store |
| PUT | /api/stores/{id}/kyc/verify | stores.kyc.verify | Sets KycStatus, body carries `status` (Verified/Rejected) + optional `notes` |
| PUT | /api/stores/{id}/credit-limit | stores.credit.update | Requires Supervisor (roleLevel ≥ 20) or above |
| PUT | /api/stores/{id}/outstanding-balance | stores.credit.update | Manual adjustment until Finance owns this; same permission as credit-limit |
| GET | /api/stores/{id}/outstanding-balance | stores.view | Returns cached value + CreditLimit + available credit |
| PUT | /api/stores/{id}/segment | stores.update | Body: `{ "segment": "Gold" }` |
| GET | /api/stores/{id}/visit-history | stores.view | Paginated. Empty list until Step 11 wiring lands |
| GET | /api/stores/{id}/schemes/{schemeId}/eligibility | stores.view | Returns `{ eligible: true, reason: null }` until Step 17 wiring lands |
| GET | /api/stores/{id}/dashboard?period=... | stores.view | Aggregates order count/value for the period from existing Order tables |
---
## Request / Response Shapes
### POST `/api/stores/{id}/kyc-documents` (multipart/form-data)
Fields: `documentType` (string), `file` (binary).
Response:
```json
{ "id": "...", "storeId": "...", "documentType": "GST Certificate", "fileReference": "uploads/kyc/{storeId}/{guid}.pdf", "uploadedAt": "2026-08-20T10:00:00" }
```
### PUT `/api/stores/{id}/kyc/verify` request
```json
{ "status": "Verified", "notes": "GST + PAN checked against ERP records" }
```
### PUT `/api/stores/{id}/credit-limit` request
```json
{ "newLimit": 50000, "approverId": "..." }
```
`approverId` must resolve to a user with `RoleLevel >= 20` (Supervisor or above); enforced
server-side from the authenticated caller, not trusted from the body — the field is echoed back
for audit display only.
### GET `/api/stores/{id}/outstanding-balance` response
```json
{ "storeId": "...", "creditLimit": 50000, "outstandingBalance": 32000, "availableCredit": 18000 }
```
### GET `/api/stores/{id}/dashboard` response
```json
{
"storeId": "...",
"period": "2026-08",
"orderCount": 12,
"totalOrderValue": 145000,
"lastOrderDate": "2026-08-18T09:00:00"
}
```
---
## Business Rules
1. `updateStoreCreditLimit` requires the authenticated caller to have `RoleLevel >= 20`
(Supervisor or above) — enforced by the `stores.credit.update` policy plus a role-level check
in the service, same pattern as `StoreService.HasGlobalScope`.
2. A store with `KycStatus != Verified` may only place orders up to
`StoreManagement:UnverifiedKycOrderThreshold`. `OrderService` must check this at order creation
time; orders exceeding the threshold are rejected with a 400. This mirrors the stock-ledger
pattern of rejecting at the point of the write, not after.
3. `deactivateStore` (existing `DELETE /api/stores/{id}`) blocks new orders against the store,
matching the existing `IsActive` soft-delete filter already used in `OrderService`. No separate
reason field is added — soft delete already covers it via the existing `Template.IsActive` flag.
4. `OutstandingBalance` is never edited directly by client-supplied arbitrary values outside the
dedicated endpoint — always through `UpdateOutstandingBalanceAsync`, so Finance (step 19) has a
single integration point to call into later.
5. KYC documents are append-only from the API's perspective — no update endpoint. To replace a
document, upload a new one; old ones stay for audit trail (soft-deletable via `IsActive` if
ever needed).
6. Scope rules for all new endpoints follow the existing `StoreService` pattern: global-scope roles
(`RoleLevel >= 50`) see everything, others are checked via `IScopeAccessService.HasStoreScopeAsync`.
---
## Permissions to Add to Seed
New permission codes to add to the existing seed:
```
stores.kyc.verify, stores.credit.update
```
(`stores.view`, `stores.create`, `stores.update`, `stores.delete` already exist.)
Role assignments:
- **SalesRep (10)**: stores.view (unchanged)
- **Supervisor (20)**: stores.view, stores.update, stores.credit.update
- **AreaManager (30)**: stores.view, stores.update, stores.credit.update, stores.kyc.verify
- **AreaSupervisor (40)**: all store permissions
- **RegionalManager (50)**: all store permissions
- **RegionalAgency (60)**: all store permissions
---
## Open Questions
- None. Resolved in planning discussion (2026-08-20): OutstandingBalance is cached/denormalized;
KYC document type is free text with local disk storage; unverified stores are gated by a
configurable order-value threshold rather than a hard block.
---
## Implementation Notes
### File structure created / touched
```
Features/
Store/
StoreController.cs (extended — credit-limit, balance, kyc/verify, segment,
visit-history, scheme-eligibility, dashboard endpoints)
StoreDTOs.cs (extended — new request/response records)
StoreService.cs (extended — new methods, RoleLevel >= 20 credit-approver check)
IStoreRepository.cs (extended — GetOrdersForDashboardAsync)
StoreRepository.cs (extended)
Kyc/
StoreKycDocumentController.cs
StoreKycDocumentDTOs.cs
StoreKycDocumentService.cs
IStoreKycDocumentRepository.cs
StoreKycDocumentRepository.cs
Order/
OrderService.cs (extended — KycStatus + UnverifiedKycOrderThreshold gate in CreateAsync)
Models/
Store.cs (add CreditLimit, OutstandingBalance, Segment, KycStatus columns)
StoreKycDocument.cs (new)
Enums.cs (add KycStatus enum)
Migrations/
AddStoreCreditAndKyc
appsettings.json
add "StoreManagement": { "UnverifiedKycOrderThreshold": 0 }
Program.cs
register IStoreKycDocumentRepository, StoreKycDocumentRepository, StoreKycDocumentService
```
KYC documents live as a sub-namespace `Features/Store/Kyc/` rather than a top-level feature,
matching the one-feature-per-folder convention (`Features/Store`, `Features/Stock`) — they only
ever belong to a Store and have no independent lifecycle.
### KYC upload logic (pseudo)
```
POST /api/stores/{id}/kyc-documents — multipart (documentType, file):
1. Load Store by id, reject if missing/inactive or caller lacks store scope
2. Ensure wwwroot/uploads/kyc/{storeId}/ exists
3. Save file as {guid}.{ext}, record FileReference = "uploads/kyc/{storeId}/{guid}.{ext}"
4. Insert StoreKycDocument row, commit in the same UoW transaction
```
### Order-creation KYC gate (pseudo)
```
OrderService.CreateAsync — before inserting the order:
1. Load Store by request.StoreId
2. If Store.KycStatus != Verified:
threshold = config["StoreManagement:UnverifiedKycOrderThreshold"]
if request.TotalAmount > threshold: reject (return null → 403 at controller)
```
</content>
+78
View File
@@ -0,0 +1,78 @@
# 13 — Fleet Management (Beyond CRUD)
**Status**: Not Started
**Depends On**: 00 — Existing Gap Fixes
---
## Goal
Vehicle CRUD already exists (`Features/Vehicle`). This step covers everything in
[mainPlan.md §5](./mainPlan.md#5-vehicle--fleet-management) that isn't
plain CRUD or already scoped into [03_ride_lifecycle.md](./03_ride_lifecycle.md)/
[04_loading_event.md](./04_loading_event.md): maintenance, fuel, telemetry, and utilization.
---
## Background / Context
`Vehicle` today only has `RegNumber`/`NumberPlate`. No capacity, ownership, maintenance, or fuel
data exists. There is no `Driver` entity — see gap #10 in
[00_existing_gaps.md](./00_existing_gaps.md).
---
## Data Model Changes
### Vehicle — New Fields
`CapacityVolume`, `CapacityWeight` (needed by `calculateLoadPlan` in
[04_loading_event.md](./04_loading_event.md)), `OwnershipType` (`Owned`, `Leased`, `Contracted`).
### Maintenance Task
`VehicleId`, `MaintenanceType`, `DueAt` (date or mileage-based), `CompletedAt`.
### Fuel Log
`VehicleId`, `Amount`, `Liters`, `Date`.
### Telemetry Record
`VehicleId`, `GpsCoords`, `Speed`, `Timestamp` — high-volume; likely a separate store/retention
policy from the rest of the schema (see Open Questions).
---
## API Endpoints
| Function reference | Endpoint intent |
|---|---|
| `scheduleMaintenance(vehicleId, maintenanceType, dueAt)` | `POST /api/vehicles/{id}/maintenance` |
| `logFuelExpense(vehicleId, amount, liters, date)` | `POST /api/vehicles/{id}/fuel-logs` |
| `recordVehicleTelemetry(vehicleId, gpsCoords, speed, timestamp)` | `POST /api/vehicles/{id}/telemetry` — likely ingested via [22_integration_api.md](./22_integration_api.md) rather than direct client calls |
| `getVehicleUtilization(vehicleId, period)` | `GET /api/vehicles/{id}/utilization?period=...` |
| `retireVehicle(vehicleId, reason)` | Extends existing `DELETE /api/vehicles/{id}` with a reason field |
---
## Business Rules
- `retireVehicle` must be blocked while the vehicle is on an open/active ride
([03_ride_lifecycle.md](./03_ride_lifecycle.md)).
- Overdue `Maintenance Task`s should block a vehicle from being assigned to a new ride — exact
enforcement level (hard block vs. warning) to confirm.
---
## Open Questions
- Is real-time telemetry (`recordVehicleTelemetry`) actually in scope, or is odometer
start/end (`DeliveryRun.Meterstart`/`Meterend`, already modeled) sufficient for this business?
- What third-party telematics provider, if any, is being integrated (affects [22_integration_api.md](./22_integration_api.md))?
---
## Implementation Notes
> Fill this section in during planning discussion before writing any code.
@@ -0,0 +1,70 @@
# 14 — Pricing & Supplier Master Data
**Status**: Not Started
**Depends On**: 01 — Item Catalog
---
## Goal
Covers the remainder of [mainPlan.md §7](./mainPlan.md#7-master-data-management)
not already handled by [01_item_catalog.md](./01_item_catalog.md): price lists and supplier master
records. `createProduct`/`deactivateSku` live in 01; this step is everything else in §7.
---
## Background / Context
`Item.BasePrice` (from [01_item_catalog.md](./01_item_catalog.md)) is a single flat price. There is
no concept of a region- or date-scoped price list, and no `Supplier` entity anywhere in the schema.
---
## Data Model Changes
### Price List
`Name`, `RegionId` (nullable — null means global), `EffectiveFrom`, `EffectiveTo`.
### Price List Item
`PriceListId`, `ItemId`, `Price` — overrides `Item.BasePrice` when an applicable, currently-
effective price list exists for the order's region.
### Supplier
`Name`, `ContactInfo`, `PerformanceScore` (nullable, populated by
[15_procurement.md](./15_procurement.md)'s `evaluateSupplier`).
---
## API Endpoints
| Function reference | Endpoint intent |
|---|---|
| `createPriceList(name, region, effectiveDates)` | `POST /api/price-lists` |
| `updateProductPricing(sku, priceListId, price)` | `PUT /api/price-lists/{id}/items/{sku}` |
| `createSupplierMaster(supplierData)` | `POST /api/suppliers` |
---
## Business Rules
- `applyPricingAndDiscounts` (§1, used in [01_item_catalog.md](./01_item_catalog.md) and
[06_sale_ride.md](./06_sale_ride.md)) resolves price in this order: region-scoped active price
list → global active price list → `Item.BasePrice`.
- Two price lists cannot have overlapping `EffectiveFrom`/`EffectiveTo` ranges for the same
`(ItemId, RegionId)` pair.
---
## Open Questions
- Are price lists customer/segment-specific as well as region-specific (ties into `Store.Segment`
from [12_store_management.md](./12_store_management.md)), or region-only in v1?
---
## Implementation Notes
> Fill this section in during planning discussion before writing any code.
+74
View File
@@ -0,0 +1,74 @@
# 15 — Procurement / Purchasing
**Status**: Not Started
**Depends On**: 01 — Item Catalog, 14 — Pricing & Supplier Master Data
---
## Goal
Covers [mainPlan.md §9](./mainPlan.md#9-procurement--purchasing) in full
— an entirely new subsystem, nothing in this area exists in the current schema.
---
## Background / Context
Inbound stock (`createGoodsReceipt` in [04_loading_event.md](./04_loading_event.md)/
[mainPlan.md §6](./mainPlan.md#6-warehouse-management)) currently has no
purchase order to receive against — this step is what creates that PO in the first place.
---
## Data Model Changes
### Purchase Requisition
`RequestedByUserId`, `Status` (`Draft`, `Submitted`, `Approved`, `Rejected`), line items
(`ItemId`, `Quantity`).
### Purchase Order
`SupplierId`, `RequisitionId` (nullable — a PO can be raised without a prior requisition), `Status`
(`Issued`, `PartiallyReceived`, `Received`, `Closed`), line items (`ItemId`, `Quantity`, `UnitCost`).
### Purchase Return
`PurchaseOrderId`, reason, line items — goods sent back to the supplier post-receipt.
---
## API Endpoints
| Function reference | Endpoint intent |
|---|---|
| `createPurchaseRequisition(items[], requestedBy)` | `POST /api/purchase-requisitions` |
| `createPurchaseOrder(supplierId, items[])` | `POST /api/purchase-orders` |
| `matchInvoiceToPO(invoiceId, poId, grnId)` | `POST /api/purchase-orders/{id}/match-invoice` |
| `evaluateSupplier(supplierId, criteria)` | `POST /api/suppliers/{id}/evaluate` |
| `createPurchaseReturn(poId, items[], reason)` | `POST /api/purchase-orders/{id}/returns` |
---
## Business Rules
- `createGoodsReceipt` (§6, [04_loading_event.md](./04_loading_event.md)'s warehouse-inbound
counterpart) must reference an `Issued`/`PartiallyReceived` Purchase Order — no receipts against
a closed or nonexistent PO.
- `matchInvoiceToPO` (3-way match) must block payment posting in
[19_finance_accounting.md](./19_finance_accounting.md) until PO, GRN, and invoice quantities/
amounts reconcile within a configurable tolerance.
---
## Open Questions
- Is procurement in scope for this project at all right now, or is inbound stock assumed to be
seeded/manual for the foreseeable future? Confirm before investing in this step — it's the
largest net-new subsystem in the reference doc.
---
## Implementation Notes
> Fill this section in during planning discussion before writing any code.
+68
View File
@@ -0,0 +1,68 @@
# 16 — Sales Force Automation (SFA) / CRM
**Status**: Not Started
**Depends On**: 12 — Store Management
---
## Goal
Covers the lead/interaction half of
[mainPlan.md §10](./mainPlan.md#10-sales-force-automation-sfa--crm):
`logCustomerInteraction`, `createLead`, `convertLeadToStore`. Performance/incentive functions
(`trackTargetVsAchievement`, `calculateIncentive`) are already scoped in
[08_sales_agent_controls.md](./08_sales_agent_controls.md) — not repeated here.
---
## Background / Context
There is no `Lead` entity or interaction log anywhere in the schema today — new outlet onboarding
currently goes straight to `POST /api/stores` with no pre-qualification step.
---
## Data Model Changes
### Lead
`ProspectName`, `ContactInfo`, `Notes`, `Status` (`New`, `Qualified`, `Converted`, `Rejected`),
`AssignedToUserId`.
### Customer Interaction
`StoreId` (nullable — can be logged against a `Lead` instead), `LeadId` (nullable), `UserId`,
`Notes`, `CreatedAt`.
---
## API Endpoints
| Function reference | Endpoint intent |
|---|---|
| `logCustomerInteraction(storeId, notes)` | `POST /api/stores/{id}/interactions` |
| `createLead(prospectData)` | `POST /api/leads` |
| `convertLeadToStore(leadId)` | `POST /api/leads/{id}/convert` — creates a `Store` via the existing `POST /api/stores` path internally |
---
## Business Rules
- `convertLeadToStore` reuses `createStore` (§4, [12_store_management.md](./12_store_management.md))
rather than duplicating store-creation logic — a `Lead` conversion is not a separate creation
path.
- Exactly one of `StoreId`/`LeadId` must be set on a `Customer Interaction` row, never both, never
neither.
---
## Open Questions
- Does a `Lead` need its own scope/authorization model (region/route), or does it inherit the
creating user's scope with no further restriction until converted?
---
## Implementation Notes
> Fill this section in during planning discussion before writing any code.
@@ -0,0 +1,70 @@
# 17 — Promotions & Trade Scheme Management
**Status**: Not Started
**Depends On**: 01 — Item Catalog, 14 — Pricing & Supplier Master Data
---
## Goal
Covers [mainPlan.md §11](./mainPlan.md#11-promotions--trade-scheme-management)
in full — an entirely new subsystem. Once built, `applyPricingAndDiscounts` (§1) and
`checkStoreSchemeEligibility` (§4) both read from it.
---
## Background / Context
No scheme/promotion entity exists today. Every reference to "eligible discount" elsewhere in this
doc set (order pricing, field invoices, agent discount limits) currently has no scheme data source
— it only enforces the flat per-role discount ceiling from
[08_sales_agent_controls.md](./08_sales_agent_controls.md).
---
## Data Model Changes
### Scheme
`Name`, `RuleType` (`BuyXGetY`, `SlabDiscount`, `FlatPercent`), `RuleConfig` (JSON), `Eligibility`
(store segment / region scope), `DateRangeFrom`, `DateRangeTo`.
### Scheme Redemption
`SchemeId`, `OrderId` (or `FieldInvoiceId`), `DiscountAmount`, `RedeemedAt` — feeds
`trackSchemeSpend` and `getSchemeROI` without re-scanning every order.
---
## API Endpoints
| Function reference | Endpoint intent |
|---|---|
| `createScheme(schemeRules, eligibility, dateRange)` | `POST /api/schemes` |
| `evaluateSchemeEligibility(storeId, orderId)` | `GET /api/schemes/eligible?storeId=...&orderId=...` |
| `applySchemeDiscount(orderId, schemeId)` | `POST /api/orders/{id}/apply-scheme` |
| `trackSchemeSpend(schemeId)` | `GET /api/schemes/{id}/spend` |
| `getSchemeROI(schemeId)` | `GET /api/schemes/{id}/roi` |
---
## Business Rules
- A scheme discount stacks with, but does not bypass, the per-role discount ceiling in
[08_sales_agent_controls.md](./08_sales_agent_controls.md) unless explicitly flagged as an
agent-limit-exempt scheme.
- `getSchemeROI` requires a baseline (pre-scheme expected sales) to compare against — confirm data
source before implementing (see Open Questions).
---
## Open Questions
- What counts as "incremental sales" for `getSchemeROI` — a manual baseline entered when the scheme
is created, or a trailing-period average computed automatically?
---
## Implementation Notes
> Fill this section in during planning discussion before writing any code.
+74
View File
@@ -0,0 +1,74 @@
# 18 — Returns & Claims Management
**Status**: Not Started
**Depends On**: 05 — Delivery Ride Execution, 06 — Sale Ride Execution
---
## Goal
Covers [mainPlan.md §12](./mainPlan.md#12-returns--claims-management) in
full. [05_delivery_ride.md](./05_delivery_ride.md) and [09_fraud_controls.md](./09_fraud_controls.md)
both reference this step's credit note and approval shapes — this is where those are actually
defined.
---
## Background / Context
No return, credit note, or claim entity exists today. `createBackorder` (§1,
[05_delivery_ride.md](./05_delivery_ride.md)) needs a credit note target once this step lands.
---
## Data Model Changes
### Return Request
`OrderId`, `Items[]` (`ItemId`, `Quantity`, `ReasonCode`), `Status` (`Pending`, `Approved`,
`Rejected`), `RequestedByUserId`.
### Credit Note
`ReturnRequestId`, `InvoiceId`, `Amount`, `IssuedAt`.
### Claim
`AgencyId`, `Details`, `Status` (`Open`, `Settled`), `SettlementAmount` (nullable until settled).
---
## API Endpoints
| Function reference | Endpoint intent |
|---|---|
| `createReturnRequest(orderId, items[], reasonCode)` | `POST /api/returns` |
| `approveReturn(returnId, approverId)` | `POST /api/returns/{id}/approve` |
| `generateCreditNote(returnId)` | `POST /api/returns/{id}/credit-note` — auto-triggered on approval |
| `createClaim(agencyId, claimDetails)` | `POST /api/claims` |
| `settleClaim(claimId, settlementAmount)` | `POST /api/claims/{id}/settle` |
---
## Business Rules
- `approveReturn` requires supervisor sign-off before stock is credited back — matches the rule
already stated in [09_fraud_controls.md](./09_fraud_controls.md).
- A `Return Request` must reference a real prior sale/delivery line — the absence of one is exactly
the `ReturnWithoutSale` anomaly flagged automatically in
[09_fraud_controls.md](./09_fraud_controls.md).
- Approving a return creates a `Return` stock movement crediting the relevant location, per
[02_stock_ledger.md](./02_stock_ledger.md).
---
## Open Questions
- Are `Claim`s only against Regional Agencies (as the reference doc frames them), or can a claim
also be raised against an internal warehouse/supplier discrepancy?
---
## Implementation Notes
> Fill this section in during planning discussion before writing any code.
@@ -0,0 +1,75 @@
# 19 — Finance & Accounting
**Status**: Not Started
**Depends On**: 05 — Delivery Ride Execution, 06 — Sale Ride Execution, 18 — Returns & Claims Management
---
## Goal
Covers [mainPlan.md §13](./mainPlan.md#13-finance--accounting) beyond
what [06_sale_ride.md](./06_sale_ride.md) already scoped for field invoices
(`createInvoice`/`recordPayment` there are the van-sale-specific case; this step is the general
warehouse-fulfilled-order invoicing path plus GL posting and reporting).
---
## Background / Context
`convertOrderToInvoice` is referenced from [05_delivery_ride.md](./05_delivery_ride.md) but has no
home until this step. There is no invoice, payment, or GL entity for warehouse-fulfilled
(non-field-sale) orders today.
---
## Data Model Changes
### Invoice
`OrderId` (nullable if it originates from a field invoice instead), `StoreId`, `TotalAmount`,
`TaxAmount`, `Status` (`Issued`, `PartiallyPaid`, `Paid`), `IssuedAt`.
### Payment
`InvoiceId`, `Amount`, `Method`, `CollectedAt` — same shape as the `Payment Record` in
[06_sale_ride.md](./06_sale_ride.md); consider whether to unify into one table keyed by a generic
`InvoiceId` covering both field invoices and warehouse invoices.
### GL Posting
`TransactionId`, `Account`, `DebitAmount`, `CreditAmount`, `PostedAt`.
---
## API Endpoints
| Function reference | Endpoint intent |
|---|---|
| `createInvoice(orderId)` | `POST /api/orders/{id}/invoice` — the warehouse-fulfilled counterpart to [06_sale_ride.md](./06_sale_ride.md)'s field invoice creation |
| `recordPayment(invoiceId, amount, method)` | `POST /api/invoices/{id}/payments` |
| `postToGeneralLedger(transactionId)` | Internal — triggered on invoice/payment/credit-note events |
| `generateTaxInvoice(invoiceId, jurisdiction)` | `GET /api/invoices/{id}/tax-invoice?jurisdiction=...` |
| `getFinancialReport(entityId, period, reportType)` | `GET /api/finance/reports?entityId=...&period=...&reportType=...` |
---
## Business Rules
- `Store.OutstandingBalance` (from [12_store_management.md](./12_store_management.md)) is
maintained by this step's invoice/payment events, not edited directly.
- `postToGeneralLedger` must be idempotent per `TransactionId` — replays must not double-post.
---
## Open Questions
- Should field invoices ([06_sale_ride.md](./06_sale_ride.md)) and warehouse invoices share one
`Invoice` table, or stay as separate entities that both feed the same GL posting logic?
- Is e-invoicing (`generateTaxInvoice` for a specific jurisdiction like GST/VAT) an actual near-term
requirement, or speculative until a specific market is targeted?
---
## Implementation Notes
> Fill this section in during planning discussion before writing any code.
@@ -0,0 +1,65 @@
# 20 — Reporting, Analytics & BI
**Status**: Not Started
**Depends On**: Every prior numbered step whose data it reports on (this step is a read-only
aggregation layer, not a new source of truth)
---
## Goal
Covers [mainPlan.md §14](./mainPlan.md#14-reporting-analytics--bi) in
full. This step deliberately reads data other steps already own — it does not introduce new
mutable entities.
---
## Background / Context
Individual dashboards are already referenced piecemeal elsewhere (`getRegionDashboard` in
[10_region_management.md](./10_region_management.md), `getStoreSalesDashboard` in
[12_store_management.md](./12_store_management.md), `getWarehouseKpis` in
[04_loading_event.md](./04_loading_event.md)/[07_ride_reconciliation.md](./07_ride_reconciliation.md)
context). This step is the role-aware, cross-cutting dashboard/report layer that sits above all of
them.
---
## Data Model Changes
None expected — this step reads existing tables. If a specific report proves too expensive to
compute live, a cached/materialized summary table may be added per-report, decided when that report
is actually built.
---
## API Endpoints
| Function reference | Endpoint intent |
|---|---|
| `getDashboardData(role, scope)` | `GET /api/dashboard?scope=...` — role taken from the authenticated user, not a request param |
| `generateKpiReport(kpiType, filters)` | `GET /api/reports/{kpiType}?filters=...` |
| `getPredictiveAlert(type, scope)` | `GET /api/alerts/predictive?type=...&scope=...` — explicitly flagged below as the highest-risk item in this step |
---
## Business Rules
- `getDashboardData` must respect the same scope rules (`IScopeAccessService`) as every other
endpoint — a dashboard is not an exception to region/route/store scoping.
---
## Open Questions
- `getPredictiveAlert` implies a forecasting/ML component that doesn't exist anywhere in this
codebase today — is this in scope at all for the near term, or should it be dropped from this
step until a concrete predictive use case and data volume exist to justify it?
- Which specific KPIs matter first (fill rate, OOS%, credit risk are mentioned in the reference —
confirm the actual priority list with the user before building `generateKpiReport`)?
---
## Implementation Notes
> Fill this section in during planning discussion before writing any code.
@@ -0,0 +1,67 @@
# 21 — Notifications & Alerts
**Status**: Not Started
**Depends On**: 02 — Stock Ledger, 12 — Store Management, 08 — Sales Agent Controls
---
## Goal
Covers [mainPlan.md §15](./mainPlan.md#15-notifications--alerts) in full
— a cross-cutting delivery mechanism (email/SMS/push) triggered by events other steps already
produce.
---
## Background / Context
No notification channel exists in this codebase today. This step is purely the delivery mechanism
and trigger wiring; the underlying conditions it watches for are already defined elsewhere
(reorder point in [02_stock_ledger.md](./02_stock_ledger.md), credit limit in
[12_store_management.md](./12_store_management.md), route deviation logic in
[09_fraud_controls.md](./09_fraud_controls.md)).
---
## Data Model Changes
### Notification
`RecipientUserId`, `Type`, `Payload` (JSON), `Channel` (`Email`, `SMS`, `Push`, `InApp`), `SentAt`,
`ReadAt` (nullable).
---
## API Endpoints
| Function reference | Endpoint intent |
|---|---|
| `sendLowStockAlert(warehouseId, sku)` | Internal — triggered when a stock movement crosses the reorder point set in [02_stock_ledger.md](./02_stock_ledger.md) |
| `sendCreditLimitBreachAlert(storeId)` | Internal — triggered on order/invoice creation when `Store.OutstandingBalance` exceeds `Store.CreditLimit` |
| `sendApprovalPendingNotification(approverId, requestId)` | Internal — triggered by `submitOrderForApproval` ([08_sales_agent_controls.md](./08_sales_agent_controls.md)) and return approval ([18_returns_claims.md](./18_returns_claims.md)) |
| `sendRouteDeviationAlert(routeId, salesRepId)` | Internal — triggered from [09_fraud_controls.md](./09_fraud_controls.md)'s anomaly flag |
| — | `GET /api/notifications` — recipient's own notification inbox |
| — | `PUT /api/notifications/{id}/read` |
---
## Business Rules
- Every `send*` function in §15 is a trigger, not a user-facing endpoint — the only client-facing
endpoints are inbox read/list.
- Notification delivery failures (e.g. SMS provider down) must not roll back the business
transaction that triggered them — fire-and-forget, logged separately.
---
## Open Questions
- Which channels are actually needed at launch — in-app only, or SMS/email/push from day one? This
significantly changes the infrastructure needed (third-party provider integration, see
[22_integration_api.md](./22_integration_api.md)).
---
## Implementation Notes
> Fill this section in during planning discussion before writing any code.
+64
View File
@@ -0,0 +1,64 @@
# 22 — Integration & API Layer
**Status**: Not Started
**Depends On**: 19 — Finance & Accounting, 13 — Fleet Management
---
## Goal
Covers [mainPlan.md §16](./mainPlan.md#16-integration--api-layer) in full
— outbound/inbound integration with third-party systems.
---
## Background / Context
No external system integration exists today. Each function here depends on a specific named
third-party provider (accounting platform, telematics provider, B2B portal) that hasn't been
chosen yet — this step cannot be scoped precisely until that choice is made per Open Questions.
---
## Data Model Changes
### Integration Sync Log
`TargetSystem`, `EntityType`, `EntityId`, `Status` (`Success`, `Failed`, `Retrying`), `AttemptedAt`,
`ErrorDetail` (nullable).
---
## API Endpoints
| Function reference | Endpoint intent |
|---|---|
| `syncToAccountingSystem(transactionId, targetSystem)` | Internal/background job — triggered on GL posting ([19_finance_accounting.md](./19_finance_accounting.md)) |
| `syncTelematicsData(vehicleId, provider)` | Internal/background job — feeds `recordVehicleTelemetry` ([13_fleet_management.md](./13_fleet_management.md)) |
| `pushOrderToDistributorPortal(orderId)` | Internal/background job — triggered on order status change ([05_delivery_ride.md](./05_delivery_ride.md)) |
| — | `GET /api/integrations/sync-log?targetSystem=...` — for support/debugging |
---
## Business Rules
- Every outbound sync must be retryable and idempotent — a failed sync logs to `Integration Sync
Log` and retries rather than silently dropping.
- No integration should be able to block the originating business transaction (order creation, GL
posting) — sync happens after commit, asynchronously.
---
## Open Questions
- Which accounting platform (QuickBooks/SAP/Xero — reference doc names all three as examples), and
which telematics provider — is either actually contracted yet, or is this step purely
speculative until a vendor is chosen?
- Does a "distributor-facing B2B portal" already exist to push orders to, or is this describing a
future/external system not yet built?
---
## Implementation Notes
> Fill this section in during planning discussion before writing any code.
+66
View File
@@ -0,0 +1,66 @@
# 23 — Mobile & Offline Capability
**Status**: Not Started
**Depends On**: 06 — Sale Ride Execution, 05 — Delivery Ride Execution
---
## Goal
Covers [mainPlan.md §17](./mainPlan.md#17-mobile--offline-capability) in
full — offline-first support for field agents (drivers/sales reps) whose connectivity can't be
assumed during a ride.
---
## Background / Context
There is no mobile client in this repository — this step is the server-side contract a future
mobile client would sync against (queueing, conflict resolution, barcode resolution), not a client
implementation itself.
---
## Data Model Changes
### Offline Action Queue Entry
`DeviceId`, `UserId`, `ActionType` (e.g. `CreateFieldInvoice`, `RecordDelivery`), `Payload` (JSON),
`ClientCreatedAt`, `SyncedAt` (nullable), `ConflictStatus` (`None`, `Resolved`, `Unresolved`).
---
## API Endpoints
| Function reference | Endpoint intent |
|---|---|
| `queueOfflineOrder(orderData)` | Client-side only — server sees it as a normal write once synced |
| `syncOfflineQueue(deviceId)` | `POST /api/sync?deviceId=...` — batch endpoint accepting queued actions, applying them in order, returning per-action success/conflict |
| `scanBarcode(barcodeValue)` | `GET /api/items/by-barcode/{barcodeValue}` — resolves to an `Item` (needs a `Barcode` field added to `Item`, see [01_item_catalog.md](./01_item_catalog.md)) |
---
## Business Rules
- Every action type accepted by `syncOfflineQueue` must be safely replayable (idempotent by a
client-generated id) — a duplicate sync of the same queued action must not double-apply (e.g.
double-decrement stock).
- A sync conflict (e.g. stock sold twice from two devices while offline) must not silently pick a
winner — flagged `Unresolved` and surfaced to a supervisor, consistent with the reconciliation
approach in [07_ride_reconciliation.md](./07_ride_reconciliation.md).
---
## Open Questions
- Is a mobile client actually planned/being built by this team, or is offline support scoped for a
hypothetical future client? This changes whether the sync contract needs to be finalized now or
can wait.
- Does `Item` need a `Barcode` field added now (extends [01_item_catalog.md](./01_item_catalog.md)),
or is barcode scanning deferred with the rest of this step?
---
## Implementation Notes
> Fill this section in during planning discussion before writing any code.
+234 -1
View File
@@ -36,7 +36,7 @@ They must be resolved before new feature steps begin because later steps depend
|---|---|---|---|---|
| 00 | Existing Gap Fixes | Done | — | [00_existing_gaps.md](./00_existing_gaps.md) |
| 01 | Item Catalog | Done | 00 | [01_item_catalog.md](./01_item_catalog.md) |
| 02 | Stock Ledger | Not Started | 01 | [02_stock_ledger.md](./02_stock_ledger.md) |
| 02 | Stock Ledger | Done | 01 | [02_stock_ledger.md](./02_stock_ledger.md) |
| 03 | Ride Lifecycle | Not Started | 02 | [03_ride_lifecycle.md](./03_ride_lifecycle.md) |
| 04 | Loading Event | Not Started | 03 | [04_loading_event.md](./04_loading_event.md) |
| 05 | Delivery Ride Execution | Not Started | 04 | [05_delivery_ride.md](./05_delivery_ride.md) |
@@ -44,6 +44,239 @@ They must be resolved before new feature steps begin because later steps depend
| 07 | Ride Close & Reconciliation | Not Started | 05, 06 | [07_ride_reconciliation.md](./07_ride_reconciliation.md) |
| 08 | Sales Agent Controls | Not Started | 07 | [08_sales_agent_controls.md](./08_sales_agent_controls.md) |
| 09 | Fraud Controls & Audit Trail | Not Started | 08 | [09_fraud_controls.md](./09_fraud_controls.md) |
| 10 | Region Management (Beyond CRUD) | Not Started | 00 | [10_region_management.md](./10_region_management.md) |
| 11 | Route & Sales Visit Management | Not Started | 00 | [11_route_sales_management.md](./11_route_sales_management.md) |
| 12 | Store Management (Beyond CRUD) | Not Started | 00 | [12_store_management.md](./12_store_management.md) |
| 13 | Fleet Management (Beyond CRUD) | Not Started | 00 | [13_fleet_management.md](./13_fleet_management.md) |
| 14 | Pricing & Supplier Master Data | Not Started | 01 | [14_pricing_and_suppliers.md](./14_pricing_and_suppliers.md) |
| 15 | Procurement / Purchasing | Not Started | 01, 14 | [15_procurement.md](./15_procurement.md) |
| 16 | Sales Force Automation / CRM | Not Started | 12 | [16_sfa_crm.md](./16_sfa_crm.md) |
| 17 | Promotions & Trade Scheme Management | Not Started | 01, 14 | [17_promotions_schemes.md](./17_promotions_schemes.md) |
| 18 | Returns & Claims Management | Not Started | 05, 06 | [18_returns_claims.md](./18_returns_claims.md) |
| 19 | Finance & Accounting | Not Started | 05, 06, 18 | [19_finance_accounting.md](./19_finance_accounting.md) |
| 20 | Reporting, Analytics & BI | Not Started | all prior | [20_reporting_analytics.md](./20_reporting_analytics.md) |
| 21 | Notifications & Alerts | Not Started | 02, 12, 08 | [21_notifications_alerts.md](./21_notifications_alerts.md) |
| 22 | Integration & API Layer | Not Started | 19, 13 | [22_integration_api.md](./22_integration_api.md) |
| 23 | Mobile & Offline Capability | Not Started | 05, 06 | [23_mobile_offline.md](./23_mobile_offline.md) |
Steps 00–09 are the original ride/stock-ledger roadmap and keep their strict dependency order.
Steps 10–23 fill in every remaining area from the [Function Reference by Feature](#function-reference-by-feature)
section above that wasn't already covered by 00–09; they depend only on the specific earlier steps
noted above, not on the full 00–09 chain, and can be resequenced relative to each other as
priorities become clearer.
---
## Function Reference by Feature
Reference only, not a build plan — background material to consult while planning/implementing the
steps above and below. Service-layer function names and what each should do when implemented.
Names are illustrative (camelCase) — adapt to your language/framework conventions. None of the
functions below are implemented yet unless noted. User Management/RBAC functions aren't repeated
here — see `SRS_User_Management_Module.pdf`.
### 1. Order Management
| Function | What it does | Status |
|---|---|---|
| `createOrder(storeId, items[], salesRepId)` | Validates stock/credit, creates a new order in `pending` state. | Partially implemented — `OrderService.CreateAsync` exists but has no credit/stock validation or state machine yet. |
| `validateCreditLimit(storeId, orderAmount)` | Checks store's outstanding balance + new order value against its credit limit. | Not implemented — no credit limit field on `Store` yet. |
| `checkStockAvailability(items[])` | Confirms requested SKUs/quantities are available at the fulfilling warehouse. | Not implemented — depends on [02_stock_ledger.md](./02_stock_ledger.md). |
| `applyPricingAndDiscounts(orderId)` | Runs the active price list and eligible scheme rules against order lines. | Not implemented — depends on §7 Master Data and §11 Promotions below. |
| `submitOrderForApproval(orderId)` | Routes the order to the correct approver based on its value/discount threshold. | Not implemented — depends on [08_sales_agent_controls.md](./08_sales_agent_controls.md). |
| `approveOrder(orderId, approverId)` / `rejectOrder(orderId, reason)` | Moves order to `approved` or `rejected`, logging the actor. | Not implemented. |
| `updateOrderStatus(orderId, status)` | Transitions order through `pending → approved → picked → dispatched → delivered → invoiced`. | Not implemented — `Order` has no status field or state machine today. |
| `convertOrderToInvoice(orderId)` | Generates the invoice once the order is dispatched/delivered. | Not implemented — depends on §13 Finance & Accounting. |
| `createBackorder(orderId, unfulfilledItems[])` | Splits unfulfillable quantity into a linked backorder. | Not implemented. |
| `cancelOrder(orderId, reason)` | Cancels an order pre-dispatch and releases any reserved stock. | Not implemented — `OrderController` supports soft delete only, no reason code or stock release. |
| `getOrdersByStore(storeId)` / `getOrdersBySalesRep(salesRepId)` | Scoped order listings. | `getOrdersByStore` implemented (`GET /api/orders/store/{storeId}`). `getOrdersBySalesRep` not implemented. |
| `getOrderAnalytics(filters)` | Returns fill rate, average order value, rejection rate over a period. | Not implemented. |
### 2. Region Management
| Function | What it does | Status |
|---|---|---|
| `createRegion(name, boundaries)` | Creates a new region record. | Partially implemented — `RegionController` supports create with `Name` only, no `boundaries` field. |
| `assignAreaToRegion(areaId, regionId)` | Links an existing area under a region. | Not implemented — no `Area` entity exists in the schema. |
| `setRegionalTarget(regionId, period, targetValue)` | Sets the sales target for a region for a given period. | Not implemented. |
| `setRegionalBudget(regionId, period, budgetAmount)` | Allocates budget at the regional level. | Not implemented. |
| `mapAgencyToRegion(agencyId, regionId)` | Assigns a Regional Agency's contract scope to a region. | Not implemented — `RegionalAgency` is currently a `Role`, not a distinct agency entity; scope is via `UserRegionScope`. |
| `setRegionalTaxRule(regionId, ruleConfig)` | Overrides tax/pricing rules for a specific region. | Not implemented. |
| `getRegionHierarchy(regionId)` | Returns all areas/territories nested under a region. | Not implemented — see [user_management_PLAN.md](../user_management_PLAN.md) territory hierarchy prerequisite. |
| `getRegionDashboard(regionId, period)` | Aggregated KPIs: sales, fill rate, agency performance for the region. | Not implemented. |
### 3. Route Management
| Function | What it does | Status |
|---|---|---|
| `createRoute(name, areaId)` | Creates a new sales visit route within an area. | Partially implemented — `RouteController` supports create, but by `RegionId` not `areaId` (no `Area` entity exists). |
| `assignOutletsToRoute(routeId, outletIds[])` | Maps a set of stores to a route. | Partially implemented — a `Store` has a single `FK_RouteId`, set via `StoreController` create/update; no bulk-assign endpoint. |
| `assignRouteToSalesRep(routeId, salesRepId)` | Assigns ownership of a route to an SR. | Not implemented — only `UserRouteScope` exists (scope grant), not a single-owner assignment. |
| `optimizeRouteSequence(routeId)` | Reorders the visit sequence for efficiency (distance/priority-based). | Not implemented. |
| `recordRouteCheckIn(routeId, outletId, gpsCoords, timestamp)` | Logs an SR's arrival at an outlet. | Not implemented. |
| `recordRouteCheckOut(routeId, outletId, timestamp)` | Logs departure and closes out the visit. | Not implemented. |
| `getRouteAdherence(routeId, date)` | Compares planned vs. actual visits for a route/day. | Not implemented — depends on check-in/check-out above. |
| `reassignRoute(routeId, newSalesRepId)` | Moves a route to a different SR. | Not implemented. |
| `getRoutePerformance(routeId, period)` | Sales per route, strike rate, missed-visit count. | Not implemented. |
| `planDeliveryRoute(warehouseId, orderIds[])` | Builds the warehouse-to-outlet dispatch route (separate from the sales visit route). | Not implemented — this is the `DeliveryRun` concept; see [03_ride_lifecycle.md](./03_ride_lifecycle.md). |
### 4. Store (Outlet) Management
| Function | What it does | Status |
|---|---|---|
| `createStore(storeData, kycDocs)` | Onboards a new outlet with required KYC. | Partially implemented — `StoreController` create exists, no KYC document handling. |
| `verifyStoreKyc(storeId)` | Marks KYC as verified/rejected by an approver. | Not implemented. |
| `updateStoreCreditLimit(storeId, newLimit, approverId)` | Changes a store's credit limit, subject to approval authority. | Not implemented — no credit limit field on `Store`. |
| `getStoreOutstandingBalance(storeId)` | Returns current receivables against the store. | Not implemented — depends on §13 Finance & Accounting. |
| `classifyStore(storeId, segment)` | Sets/updates the store's segment (key account, general trade, etc.). | Not implemented — no segment field on `Store`. |
| `deactivateStore(storeId, reason)` | Soft-deletes a store, blocking new orders against it. | Partially implemented — `DELETE /api/stores/{id}` soft-deletes via `IsActive`, no reason code captured. |
| `getStoreVisitHistory(storeId)` | Returns visit compliance history from Route Management. | Not implemented — depends on route check-in/check-out (§3). |
| `checkStoreSchemeEligibility(storeId, schemeId)` | Evaluates whether a store qualifies for a given promotion. | Not implemented — depends on §11 Promotions. |
| `getStoreSalesDashboard(storeId, period)` | Store-level sales and outstanding summary. | Not implemented. |
### 5. Vehicle / Fleet Management
| Function | What it does | Status |
|---|---|---|
| `registerVehicle(vehicleData)` | Creates the vehicle master record (registration, capacity, ownership). | Partially implemented — `VehicleController` create exists with `RegNumber`/`NumberPlate` only, no capacity/ownership fields. |
| `assignVehicleToRoute(vehicleId, routeId)` | Assigns a vehicle to a delivery/sales route. | Not implemented — `Vehicle` has no route link; only `DeliveryRun.FK_RouteId`/`FK_VehicleId` model this, and `DeliveryRun` is unwired (see [03_ride_lifecycle.md](./03_ride_lifecycle.md)). |
| `assignDriverToVehicle(vehicleId, driverId)` | Links a driver to a vehicle, checking license validity. | Not implemented — no `Driver` entity exists yet (`DeliveryRun.FK_DriverId` is commented out in the model). |
| `scheduleMaintenance(vehicleId, maintenanceType, dueAt)` | Sets up a preventive maintenance task (mileage/calendar-based). | Not implemented. |
| `logFuelExpense(vehicleId, amount, liters, date)` | Records a fuel transaction against the vehicle. | Not implemented. |
| `recordVehicleTelemetry(vehicleId, gpsCoords, speed, timestamp)` | Ingests telematics data (location, driver behavior). | Not implemented — see §16 Integration. |
| `checkDriverCompliance(driverId)` | Validates license/permit status before dispatch. | Not implemented — depends on a `Driver` entity. |
| `calculateLoadPlan(vehicleId, orderIds[])` | Matches order volume/weight to available vehicle capacity. | Not implemented — depends on [04_loading_event.md](./04_loading_event.md). |
| `getVehicleUtilization(vehicleId, period)` | Utilization and cost-per-delivery metrics. | Not implemented. |
| `retireVehicle(vehicleId, reason)` | Removes a vehicle from active fleet, retaining history. | Partially implemented — `DELETE /api/vehicles/{id}` soft-deletes via `IsActive`, no reason code captured. |
### 6. Warehouse Management
| Function | What it does | Status |
|---|---|---|
| `createGoodsReceipt(poId, receivedItems[])` | Records inbound stock against a purchase order (GRN). | Not implemented — depends on §9 Procurement. |
| `inspectGoodsReceipt(grnId, inspectionResult)` | Logs quality-check outcome before putaway. | Not implemented. |
| `putawayStock(grnId, locationAssignments[])` | Assigns received stock to warehouse locations (zone/ABC-based). | Not implemented — depends on [02_stock_ledger.md](./02_stock_ledger.md). |
| `getStockByLocation(warehouseId, sku)` | Returns real-time quantity and location for a SKU. | Not implemented — depends on [02_stock_ledger.md](./02_stock_ledger.md). |
| `generatePickList(orderIds[])` | Creates a pick list/task for one or more orders. | Not implemented — overlaps with [04_loading_event.md](./04_loading_event.md). |
| `confirmPicking(pickListId, pickedItems[])` | Marks items as picked, decrementing available stock. | Not implemented. |
| `packOrder(orderId, packageDetails)` | Records packing completion and package details. | Not implemented. |
| `createCrossDockTransfer(inboundId, outboundOrderId)` | Routes inbound stock directly to an outbound order, skipping storage. | Not implemented. |
| `runCycleCount(warehouseId, zoneId)` | Initiates a scheduled partial stock count. | Not implemented — overlaps with [07_ride_reconciliation.md](./07_ride_reconciliation.md) physical count concept. |
| `reconcileStock(warehouseId, countResults)` | Adjusts system stock to match physical count results. | Not implemented — see [07_ride_reconciliation.md](./07_ride_reconciliation.md). |
| `buildDispatchLoad(vehicleId, orderIds[])` | Groups orders into a dispatch load for a specific vehicle/route. | Not implemented — see [04_loading_event.md](./04_loading_event.md). |
| `receiveReturn(returnId, items[], condition)` | Processes returned goods back into the warehouse. | Not implemented — see §12 Returns & Claims. |
| `getWarehouseKpis(warehouseId, period)` | Dock-to-stock time, pick accuracy, turnover rate. | Not implemented. |
### 7. Master Data Management
| Function | What it does | Status |
|---|---|---|
| `createProduct(skuData)` | Creates a SKU with category, UOM, barcode, pack hierarchy. | Not implemented — `Item` model exists but has no properties; see [01_item_catalog.md](./01_item_catalog.md). |
| `deactivateSku(sku, reason)` | Retires a SKU from active ordering. | Not implemented. |
| `createPriceList(name, region, effectiveDates)` | Defines a new price list scoped to a region/date range. | Not implemented — no price list entity exists. |
| `updateProductPricing(sku, priceListId, price)` | Sets/updates the price of a SKU within a price list. | Not implemented. |
| `createSupplierMaster(supplierData)` | Registers a new supplier/vendor. | Not implemented — no `Supplier` entity exists. |
### 8. Enterprise-wide Inventory & Stock
| Function | What it does | Status |
|---|---|---|
| `getMultiLocationStock(sku)` | Returns stock levels for a SKU across all warehouses/agencies. | Not implemented — see [02_stock_ledger.md](./02_stock_ledger.md). |
| `setReorderPoint(sku, warehouseId, threshold)` | Configures the auto-replenishment trigger level. | Not implemented. |
| `createStockTransfer(fromWarehouseId, toWarehouseId, items[])` | Initiates an inter-warehouse stock movement. | Not implemented. |
| `trackBatchExpiry(sku, batchNo, expiryDate)` | Registers batch/expiry data for FEFO enforcement. | Not implemented. |
| `calculateStockValuation(warehouseId, method)` | Computes stock value using FIFO/weighted-average. | Not implemented. |
### 9. Procurement / Purchasing
| Function | What it does | Status |
|---|---|---|
| `createPurchaseRequisition(items[], requestedBy)` | Raises an internal request to purchase stock. | Not implemented — no procurement entities exist. |
| `createPurchaseOrder(supplierId, items[])` | Issues a PO to a supplier. | Not implemented. |
| `matchInvoiceToPO(invoiceId, poId, grnId)` | Performs the 3-way match (PO–GRN–Invoice) before payment. | Not implemented. |
| `evaluateSupplier(supplierId, criteria)` | Scores a supplier on delivery/quality/price performance. | Not implemented. |
| `createPurchaseReturn(poId, items[], reason)` | Initiates a return of received goods to the supplier. | Not implemented. |
### 10. Sales Force Automation (SFA) / CRM
| Function | What it does | Status |
|---|---|---|
| `logCustomerInteraction(storeId, notes)` | Records a visit note or customer interaction. | Not implemented. |
| `trackTargetVsAchievement(userId, period)` | Compares a user's sales target to actuals. | Not implemented. |
| `calculateIncentive(userId, period)` | Computes commission/incentive payout based on performance. | Not implemented. |
| `createLead(prospectData)` | Captures a prospective new outlet. | Not implemented — no `Lead` entity exists. |
| `convertLeadToStore(leadId)` | Converts a qualified lead into an active store record. | Not implemented. |
### 11. Promotions & Trade Scheme Management
| Function | What it does | Status |
|---|---|---|
| `createScheme(schemeRules, eligibility, dateRange)` | Defines a promotion (buy-X-get-Y, slab discount, etc.). | Not implemented — no scheme entity exists. |
| `evaluateSchemeEligibility(storeId, orderId)` | Checks whether an order qualifies for a scheme. | Not implemented. |
| `applySchemeDiscount(orderId, schemeId)` | Applies the scheme's discount to order pricing. | Not implemented. |
| `trackSchemeSpend(schemeId)` | Aggregates total discount value given under a scheme. | Not implemented. |
| `getSchemeROI(schemeId)` | Compares scheme spend against incremental sales generated. | Not implemented. |
### 12. Returns & Claims Management
| Function | What it does | Status |
|---|---|---|
| `createReturnRequest(orderId, items[], reasonCode)` | Initiates a return against a delivered order. | Not implemented — see [09_fraud_controls.md](./09_fraud_controls.md) for the fraud-control angle on returns. |
| `approveReturn(returnId, approverId)` | Approves/rejects the return request. | Not implemented. |
| `generateCreditNote(returnId)` | Issues a credit note against the original invoice. | Not implemented. |
| `createClaim(agencyId, claimDetails)` | Raises a claim against a Regional Agency (e.g., damaged stock). | Not implemented. |
| `settleClaim(claimId, settlementAmount)` | Closes out a claim with a final settlement amount. | Not implemented. |
### 13. Finance & Accounting
| Function | What it does | Status |
|---|---|---|
| `createInvoice(orderId)` | Generates a formal invoice from a fulfilled order. | Not implemented — no invoice entity exists. |
| `recordPayment(invoiceId, amount, method)` | Logs a payment/collection against an invoice. | Not implemented. |
| `postToGeneralLedger(transactionId)` | Posts a financial transaction to the GL. | Not implemented. |
| `generateTaxInvoice(invoiceId, jurisdiction)` | Produces a tax-compliant invoice (GST/VAT e-invoicing). | Not implemented. |
| `getFinancialReport(entityId, period, reportType)` | Returns P&L/cash-flow reports scoped to a region/area. | Not implemented. |
### 14. Reporting, Analytics & BI
| Function | What it does | Status |
|---|---|---|
| `getDashboardData(role, scope)` | Returns the role-appropriate dashboard payload. | Not implemented. |
| `generateKpiReport(kpiType, filters)` | Produces a specific KPI report (fill rate, OOS%, credit risk, etc.). | Not implemented. |
| `getPredictiveAlert(type, scope)` | Returns AI-flagged issues, e.g. a region's declining fill rate. | Not implemented. |
### 15. Notifications & Alerts
| Function | What it does | Status |
|---|---|---|
| `sendLowStockAlert(warehouseId, sku)` | Notifies relevant roles when stock drops below the reorder point. | Not implemented. |
| `sendCreditLimitBreachAlert(storeId)` | Notifies approvers when a store exceeds its credit limit. | Not implemented. |
| `sendApprovalPendingNotification(approverId, requestId)` | Notifies an approver of a pending action. | Not implemented. |
| `sendRouteDeviationAlert(routeId, salesRepId)` | Flags when an SR deviates materially from the planned route. | Not implemented — see [09_fraud_controls.md](./09_fraud_controls.md) geofencing check. |
### 16. Integration & API Layer
| Function | What it does | Status |
|---|---|---|
| `syncToAccountingSystem(transactionId, targetSystem)` | Pushes a transaction to an external accounting platform (QuickBooks/SAP/Xero). | Not implemented. |
| `syncTelematicsData(vehicleId, provider)` | Pulls GPS/telematics data from a third-party provider. | Not implemented. |
| `pushOrderToDistributorPortal(orderId)` | Syncs order status to a distributor-facing B2B portal. | Not implemented. |
### 17. Mobile & Offline Capability
| Function | What it does | Status |
|---|---|---|
| `queueOfflineOrder(orderData)` | Stores an order locally on the device when offline. | Not implemented — no mobile client in this repository. |
| `syncOfflineQueue(deviceId)` | Pushes queued offline actions to the server once connectivity returns. | Not implemented. |
| `scanBarcode(barcodeValue)` | Resolves a scanned barcode to a SKU or warehouse location. | Not implemented. |
### 18. Compliance & Audit
| Function | What it does | Status |
|---|---|---|
| `logAuditEvent(userId, action, entityType, entityId)` | Writes an immutable audit trail entry for a state-changing action. | Not implemented — see [09_fraud_controls.md](./09_fraud_controls.md), which scopes an audit log as part of Fraud Controls. |
| `checkLicenseCompliance(entityId, licenseType)` | Validates a required license/permit is current (e.g., regulated goods). | Not implemented. |
| `generateComplianceReport(period)` | Produces a report of compliance status across the org for a period. | Not implemented. |
---