Add returns and claims
This commit is contained in:
@@ -50,6 +50,12 @@ public class AppDBContext(DbContextOptions<AppDBContext> options) : DbContext(op
|
|||||||
public DbSet<GoodsReceiptItem> GoodsReceiptItems { get; set; }
|
public DbSet<GoodsReceiptItem> GoodsReceiptItems { get; set; }
|
||||||
public DbSet<PurchaseReturn> PurchaseReturns { get; set; }
|
public DbSet<PurchaseReturn> PurchaseReturns { get; set; }
|
||||||
public DbSet<PurchaseReturnItem> PurchaseReturnItems { get; set; }
|
public DbSet<PurchaseReturnItem> PurchaseReturnItems { get; set; }
|
||||||
|
public DbSet<SaleReturn> SaleReturns { get; set; }
|
||||||
|
public DbSet<SaleReturnItem> SaleReturnItems { get; set; }
|
||||||
|
public DbSet<CreditNote> CreditNotes { get; set; }
|
||||||
|
public DbSet<Claim> Claims { get; set; }
|
||||||
|
public DbSet<SupplierClaim> SupplierClaims { get; set; }
|
||||||
|
public DbSet<SupplierClaimItem> SupplierClaimItems { get; set; }
|
||||||
|
|
||||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||||
{
|
{
|
||||||
@@ -270,6 +276,91 @@ public class AppDBContext(DbContextOptions<AppDBContext> options) : DbContext(op
|
|||||||
modelBuilder.Entity<PurchaseReturnItem>()
|
modelBuilder.Entity<PurchaseReturnItem>()
|
||||||
.HasIndex(i => i.FK_PurchaseReturnId);
|
.HasIndex(i => i.FK_PurchaseReturnId);
|
||||||
|
|
||||||
|
modelBuilder.Entity<SaleReturn>()
|
||||||
|
.HasOne(r => r.Order)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey(r => r.FK_OrderId)
|
||||||
|
.OnDelete(DeleteBehavior.Restrict);
|
||||||
|
|
||||||
|
modelBuilder.Entity<SaleReturn>()
|
||||||
|
.HasOne(r => r.CreatedByUser)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey(r => r.CreatedByUserId)
|
||||||
|
.OnDelete(DeleteBehavior.Restrict);
|
||||||
|
|
||||||
|
modelBuilder.Entity<SaleReturnItem>()
|
||||||
|
.HasOne(i => i.SaleReturn)
|
||||||
|
.WithMany(r => r.Items)
|
||||||
|
.HasForeignKey(i => i.FK_SaleReturnId)
|
||||||
|
.OnDelete(DeleteBehavior.Restrict);
|
||||||
|
|
||||||
|
modelBuilder.Entity<SaleReturnItem>()
|
||||||
|
.HasOne(i => i.Item)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey(i => i.FK_ItemId)
|
||||||
|
.OnDelete(DeleteBehavior.Restrict);
|
||||||
|
|
||||||
|
modelBuilder.Entity<SaleReturnItem>()
|
||||||
|
.HasIndex(i => i.FK_SaleReturnId);
|
||||||
|
|
||||||
|
modelBuilder.Entity<CreditNote>()
|
||||||
|
.HasOne(n => n.SaleReturn)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey(n => n.FK_SaleReturnId)
|
||||||
|
.OnDelete(DeleteBehavior.Restrict);
|
||||||
|
|
||||||
|
modelBuilder.Entity<Claim>()
|
||||||
|
.HasOne(c => c.AgencyUser)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey(c => c.FK_AgencyUserId)
|
||||||
|
.OnDelete(DeleteBehavior.Restrict);
|
||||||
|
|
||||||
|
modelBuilder.Entity<Claim>()
|
||||||
|
.HasOne(c => c.SettledByUser)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey(c => c.SettledByUserId)
|
||||||
|
.OnDelete(DeleteBehavior.Restrict);
|
||||||
|
|
||||||
|
modelBuilder.Entity<SupplierClaim>()
|
||||||
|
.HasOne(c => c.PurchaseOrder)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey(c => c.FK_PurchaseOrderId)
|
||||||
|
.OnDelete(DeleteBehavior.Restrict);
|
||||||
|
|
||||||
|
modelBuilder.Entity<SupplierClaim>()
|
||||||
|
.HasOne(c => c.GoodsReceipt)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey(c => c.FK_GoodsReceiptId)
|
||||||
|
.OnDelete(DeleteBehavior.Restrict)
|
||||||
|
.IsRequired(false);
|
||||||
|
|
||||||
|
modelBuilder.Entity<SupplierClaim>()
|
||||||
|
.HasOne(c => c.RaisedByUser)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey(c => c.RaisedByUserId)
|
||||||
|
.OnDelete(DeleteBehavior.Restrict);
|
||||||
|
|
||||||
|
modelBuilder.Entity<SupplierClaim>()
|
||||||
|
.HasOne(c => c.ResolvedByUser)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey(c => c.ResolvedByUserId)
|
||||||
|
.OnDelete(DeleteBehavior.Restrict);
|
||||||
|
|
||||||
|
modelBuilder.Entity<SupplierClaimItem>()
|
||||||
|
.HasOne(i => i.SupplierClaim)
|
||||||
|
.WithMany(c => c.Items)
|
||||||
|
.HasForeignKey(i => i.FK_SupplierClaimId)
|
||||||
|
.OnDelete(DeleteBehavior.Restrict);
|
||||||
|
|
||||||
|
modelBuilder.Entity<SupplierClaimItem>()
|
||||||
|
.HasOne(i => i.Item)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey(i => i.FK_ItemId)
|
||||||
|
.OnDelete(DeleteBehavior.Restrict);
|
||||||
|
|
||||||
|
modelBuilder.Entity<SupplierClaimItem>()
|
||||||
|
.HasIndex(i => i.FK_SupplierClaimId);
|
||||||
|
|
||||||
modelBuilder.Entity<DeliveryTask>()
|
modelBuilder.Entity<DeliveryTask>()
|
||||||
.HasOne(t => t.Ride)
|
.HasOne(t => t.Ride)
|
||||||
.WithMany()
|
.WithMany()
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
using Distribution.Infra.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace Distribution.Features.Claims;
|
||||||
|
|
||||||
|
[ApiController]
|
||||||
|
[Authorize]
|
||||||
|
public class ClaimController(ClaimService service, ICurrentUserService currentUser) : ControllerBase
|
||||||
|
{
|
||||||
|
[HttpGet("api/claims/{id:guid}")]
|
||||||
|
public async Task<IActionResult> GetById(Guid id)
|
||||||
|
{
|
||||||
|
var result = await service.GetByIdAsync(id);
|
||||||
|
return result is null ? NotFound() : Ok(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("api/claims")]
|
||||||
|
public async Task<IActionResult> Create(CreateClaimRequest request, CancellationToken ct)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var result = await service.CreateAsync(request, ct);
|
||||||
|
return CreatedAtAction(nameof(GetById), new { id = result.Id }, result);
|
||||||
|
}
|
||||||
|
catch (InvalidOperationException ex) { return Conflict(new { error = ex.Message }); }
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("api/claims/{id:guid}/settle")]
|
||||||
|
[Authorize(Policy = "claims.settle")]
|
||||||
|
public async Task<IActionResult> Settle(Guid id, SettleClaimRequest request, CancellationToken ct)
|
||||||
|
{
|
||||||
|
var userId = currentUser.UserId ?? throw new UnauthorizedAccessException();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var result = await service.SettleAsync(id, request, userId, ct);
|
||||||
|
return Ok(result);
|
||||||
|
}
|
||||||
|
catch (KeyNotFoundException) { return NotFound(); }
|
||||||
|
catch (InvalidOperationException ex) { return Conflict(new { error = ex.Message }); }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
using Distribution.Models;
|
||||||
|
|
||||||
|
namespace Distribution.Features.Claims;
|
||||||
|
|
||||||
|
public record CreateClaimRequest(Guid AgencyUserId, string Details);
|
||||||
|
|
||||||
|
public record SettleClaimRequest(decimal SettlementAmount);
|
||||||
|
|
||||||
|
public record ClaimResponse(
|
||||||
|
Guid Id,
|
||||||
|
Guid AgencyUserId,
|
||||||
|
string Details,
|
||||||
|
ClaimStatus Status,
|
||||||
|
decimal? SettlementAmount,
|
||||||
|
DateTime? SettledAt,
|
||||||
|
bool IsActive,
|
||||||
|
DateTime CreatedAt);
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
using Distribution.Data;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace Distribution.Features.Claims;
|
||||||
|
|
||||||
|
public class ClaimRepository(AppDBContext context) : IClaimRepository
|
||||||
|
{
|
||||||
|
public async Task<Models.Claim?> GetByIdAsync(Guid id) =>
|
||||||
|
await context.Claims.FirstOrDefaultAsync(c => c.Id == id);
|
||||||
|
|
||||||
|
public async Task<List<Models.Claim>> GetByAgencyUserIdAsync(Guid agencyUserId) =>
|
||||||
|
await context.Claims
|
||||||
|
.Where(c => c.FK_AgencyUserId == agencyUserId && c.IsActive)
|
||||||
|
.OrderByDescending(c => c.CreatedAt)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
public async Task AddAsync(Models.Claim entity) =>
|
||||||
|
await context.Claims.AddAsync(entity);
|
||||||
|
|
||||||
|
public void Update(Models.Claim entity) =>
|
||||||
|
context.Claims.Update(entity);
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
using Distribution.Infra.UoW;
|
||||||
|
using Distribution.Models;
|
||||||
|
|
||||||
|
namespace Distribution.Features.Claims;
|
||||||
|
|
||||||
|
public class ClaimService(IClaimRepository repo, IUnitOfWork uow)
|
||||||
|
{
|
||||||
|
private static ClaimResponse Map(Claim c) =>
|
||||||
|
new(c.Id, c.FK_AgencyUserId, c.Details, c.Status, c.SettlementAmount, c.SettledAt, c.IsActive, c.CreatedAt);
|
||||||
|
|
||||||
|
public async Task<ClaimResponse?> GetByIdAsync(Guid id)
|
||||||
|
{
|
||||||
|
var c = await repo.GetByIdAsync(id);
|
||||||
|
if (c is null || !c.IsActive) return null;
|
||||||
|
return Map(c);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<ClaimResponse>> GetByAgencyUserIdAsync(Guid agencyUserId) =>
|
||||||
|
(await repo.GetByAgencyUserIdAsync(agencyUserId)).Select(Map).ToList();
|
||||||
|
|
||||||
|
public async Task<ClaimResponse> CreateAsync(CreateClaimRequest request, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(request.Details))
|
||||||
|
throw new InvalidOperationException("Claim details are required.");
|
||||||
|
|
||||||
|
var claim = new Claim
|
||||||
|
{
|
||||||
|
FK_AgencyUserId = request.AgencyUserId,
|
||||||
|
Details = request.Details
|
||||||
|
};
|
||||||
|
|
||||||
|
await uow.BeginAsync(ct);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await repo.AddAsync(claim);
|
||||||
|
await uow.CommitAsync(ct);
|
||||||
|
}
|
||||||
|
catch { await uow.RollbackAsync(ct); throw; }
|
||||||
|
|
||||||
|
return Map(claim);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ClaimResponse> SettleAsync(Guid id, SettleClaimRequest request, Guid approverId, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
var claim = await repo.GetByIdAsync(id);
|
||||||
|
if (claim is null || !claim.IsActive)
|
||||||
|
throw new KeyNotFoundException("Claim not found.");
|
||||||
|
if (claim.Status == ClaimStatus.Settled)
|
||||||
|
throw new InvalidOperationException("Claim is already settled.");
|
||||||
|
if (request.SettlementAmount < 0)
|
||||||
|
throw new InvalidOperationException("Settlement amount cannot be negative.");
|
||||||
|
|
||||||
|
claim.Status = ClaimStatus.Settled;
|
||||||
|
claim.SettlementAmount = request.SettlementAmount;
|
||||||
|
claim.SettledByUserId = approverId;
|
||||||
|
claim.SettledAt = DateTime.Now;
|
||||||
|
|
||||||
|
await uow.BeginAsync(ct);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
repo.Update(claim);
|
||||||
|
await uow.CommitAsync(ct);
|
||||||
|
}
|
||||||
|
catch { await uow.RollbackAsync(ct); throw; }
|
||||||
|
|
||||||
|
return Map(claim);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
namespace Distribution.Features.Claims;
|
||||||
|
|
||||||
|
public interface IClaimRepository
|
||||||
|
{
|
||||||
|
Task<Models.Claim?> GetByIdAsync(Guid id);
|
||||||
|
Task<List<Models.Claim>> GetByAgencyUserIdAsync(Guid agencyUserId);
|
||||||
|
Task AddAsync(Models.Claim entity);
|
||||||
|
void Update(Models.Claim entity);
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
namespace Distribution.Features.Returns;
|
||||||
|
|
||||||
|
public interface IReturnRepository
|
||||||
|
{
|
||||||
|
Task<Models.SaleReturn?> GetByIdAsync(Guid id);
|
||||||
|
Task<List<Models.SaleReturn>> GetByOrderIdAsync(Guid orderId);
|
||||||
|
Task AddAsync(Models.SaleReturn entity);
|
||||||
|
Task AddCreditNoteAsync(Models.CreditNote entity);
|
||||||
|
|
||||||
|
/// <summary>Whether this store has ever had this item on any of its (active) orders.</summary>
|
||||||
|
Task<bool> StoreHasPriorSaleAsync(Guid storeId, Guid itemId);
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
using Distribution.Infra.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace Distribution.Features.Returns;
|
||||||
|
|
||||||
|
[ApiController]
|
||||||
|
[Authorize]
|
||||||
|
public class ReturnController(ReturnService service, ICurrentUserService currentUser) : ControllerBase
|
||||||
|
{
|
||||||
|
[HttpGet("api/returns/{id:guid}")]
|
||||||
|
public async Task<IActionResult> GetById(Guid id)
|
||||||
|
{
|
||||||
|
var result = await service.GetByIdAsync(id);
|
||||||
|
return result is null ? NotFound() : Ok(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("api/orders/{id:guid}/returns")]
|
||||||
|
public async Task<IActionResult> GetByOrder(Guid id) =>
|
||||||
|
Ok(await service.GetByOrderIdAsync(id));
|
||||||
|
|
||||||
|
[HttpPost("api/orders/{id:guid}/returns")]
|
||||||
|
public async Task<IActionResult> Create(Guid id, CreateSaleReturnRequest request, CancellationToken ct)
|
||||||
|
{
|
||||||
|
var userId = currentUser.UserId ?? throw new UnauthorizedAccessException();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var result = await service.CreateAsync(id, request, userId, ct);
|
||||||
|
return CreatedAtAction(nameof(GetById), new { id = result.Id }, result);
|
||||||
|
}
|
||||||
|
catch (InvalidOperationException ex) { return Conflict(new { error = ex.Message }); }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
using Distribution.Models;
|
||||||
|
|
||||||
|
namespace Distribution.Features.Returns;
|
||||||
|
|
||||||
|
public record ReturnItemRequest(Guid ItemId, decimal Quantity, ReturnReasonCode ReasonCode);
|
||||||
|
|
||||||
|
public record CreateSaleReturnRequest(ReturnMode Mode, List<ReturnItemRequest> Items);
|
||||||
|
|
||||||
|
public record SaleReturnItemResponse(
|
||||||
|
Guid Id,
|
||||||
|
Guid ItemId,
|
||||||
|
string Sku,
|
||||||
|
string ItemName,
|
||||||
|
decimal Quantity,
|
||||||
|
ReturnReasonCode ReasonCode,
|
||||||
|
decimal UnitPrice);
|
||||||
|
|
||||||
|
public record SaleReturnResponse(
|
||||||
|
Guid Id,
|
||||||
|
Guid OrderId,
|
||||||
|
ReturnMode Mode,
|
||||||
|
decimal TotalCreditAmount,
|
||||||
|
decimal OrderTotalAmountAfter,
|
||||||
|
bool IsActive,
|
||||||
|
DateTime CreatedAt,
|
||||||
|
List<SaleReturnItemResponse> Items);
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
using Distribution.Data;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace Distribution.Features.Returns;
|
||||||
|
|
||||||
|
public class ReturnRepository(AppDBContext context) : IReturnRepository
|
||||||
|
{
|
||||||
|
public async Task<Models.SaleReturn?> GetByIdAsync(Guid id) =>
|
||||||
|
await context.SaleReturns
|
||||||
|
.Include(r => r.Order)
|
||||||
|
.Include(r => r.Items).ThenInclude(i => i.Item)
|
||||||
|
.FirstOrDefaultAsync(r => r.Id == id);
|
||||||
|
|
||||||
|
public async Task<List<Models.SaleReturn>> GetByOrderIdAsync(Guid orderId) =>
|
||||||
|
await context.SaleReturns
|
||||||
|
.Include(r => r.Order)
|
||||||
|
.Include(r => r.Items).ThenInclude(i => i.Item)
|
||||||
|
.Where(r => r.FK_OrderId == orderId && r.IsActive)
|
||||||
|
.OrderByDescending(r => r.CreatedAt)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
public async Task AddAsync(Models.SaleReturn entity) =>
|
||||||
|
await context.SaleReturns.AddAsync(entity);
|
||||||
|
|
||||||
|
public async Task AddCreditNoteAsync(Models.CreditNote entity) =>
|
||||||
|
await context.CreditNotes.AddAsync(entity);
|
||||||
|
|
||||||
|
public async Task<bool> StoreHasPriorSaleAsync(Guid storeId, Guid itemId) =>
|
||||||
|
await context.OrderItems
|
||||||
|
.Include(oi => oi.Order)
|
||||||
|
.AnyAsync(oi => oi.Order.FK_StoreId == storeId && oi.Order.IsActive && oi.FK_ItemId == itemId);
|
||||||
|
}
|
||||||
@@ -0,0 +1,169 @@
|
|||||||
|
using Distribution.Features.Item;
|
||||||
|
using Distribution.Features.Order;
|
||||||
|
using Distribution.Features.Ride;
|
||||||
|
using Distribution.Features.Sale;
|
||||||
|
using Distribution.Features.Stock;
|
||||||
|
using Distribution.Infra.UoW;
|
||||||
|
using Distribution.Models;
|
||||||
|
|
||||||
|
namespace Distribution.Features.Returns;
|
||||||
|
|
||||||
|
public class ReturnService(
|
||||||
|
IReturnRepository repo,
|
||||||
|
IOrderRepository orderRepo,
|
||||||
|
ISaleRepository saleRepo,
|
||||||
|
IRideRepository rideRepo,
|
||||||
|
IItemRepository itemRepo,
|
||||||
|
IStockRepository stockRepo,
|
||||||
|
IUnitOfWork uow)
|
||||||
|
{
|
||||||
|
private static SaleReturnItemResponse MapItem(SaleReturnItem i) =>
|
||||||
|
new(i.Id, i.FK_ItemId, i.Item.Sku, i.Item.Name, i.Quantity, i.ReasonCode, i.UnitPrice);
|
||||||
|
|
||||||
|
private static SaleReturnResponse Map(SaleReturn r) =>
|
||||||
|
new(r.Id, r.FK_OrderId, r.Mode, r.TotalCreditAmount, r.Order.TotalAmount, r.IsActive, r.CreatedAt,
|
||||||
|
r.Items.Where(i => i.IsActive).Select(MapItem).ToList());
|
||||||
|
|
||||||
|
public async Task<SaleReturnResponse?> GetByIdAsync(Guid id)
|
||||||
|
{
|
||||||
|
var r = await repo.GetByIdAsync(id);
|
||||||
|
if (r is null || !r.IsActive) return null;
|
||||||
|
return Map(r);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<SaleReturnResponse>> GetByOrderIdAsync(Guid orderId) =>
|
||||||
|
(await repo.GetByOrderIdAsync(orderId)).Select(Map).ToList();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Records a return recognized during the same store visit as an open order and settles it
|
||||||
|
/// immediately: stock is credited back to the rep's vehicle, and either the order total is
|
||||||
|
/// reduced (CreditDeduction) or the matching order line quantities are reduced (ItemExchange).
|
||||||
|
/// A return is only allowed for an item the store has previously ordered.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<SaleReturnResponse> CreateAsync(Guid orderId, CreateSaleReturnRequest request, Guid createdByUserId, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
var order = await orderRepo.GetByIdAsync(orderId);
|
||||||
|
if (order is null || !order.IsActive)
|
||||||
|
throw new InvalidOperationException("Order not found.");
|
||||||
|
if (order.Status is OrderStatus.Cancelled or OrderStatus.Fulfilled)
|
||||||
|
throw new InvalidOperationException("Cannot record a return against a closed order.");
|
||||||
|
|
||||||
|
if (request.Items is null || request.Items.Count == 0)
|
||||||
|
throw new InvalidOperationException("A return must have at least one line item.");
|
||||||
|
|
||||||
|
var sale = await saleRepo.GetByIdAsync(order.FK_SaleId)
|
||||||
|
?? throw new InvalidOperationException("Sale not found for this order.");
|
||||||
|
if (sale.FK_RideId is null)
|
||||||
|
throw new InvalidOperationException("A return can only be recorded for an order created during an active ride.");
|
||||||
|
|
||||||
|
var ride = await rideRepo.GetByIdAsync(sale.FK_RideId.Value)
|
||||||
|
?? throw new InvalidOperationException("Ride not found.");
|
||||||
|
var vehicleId = ride.FK_VehicleId;
|
||||||
|
|
||||||
|
var orderItems = await orderRepo.GetItemsByOrderIdAsync(orderId);
|
||||||
|
var orderItemsByItemId = orderItems.ToDictionary(oi => oi.FK_ItemId);
|
||||||
|
|
||||||
|
var saleReturn = new SaleReturn
|
||||||
|
{
|
||||||
|
FK_OrderId = orderId,
|
||||||
|
Mode = request.Mode,
|
||||||
|
CreatedByUserId = createdByUserId
|
||||||
|
};
|
||||||
|
|
||||||
|
foreach (var line in request.Items)
|
||||||
|
{
|
||||||
|
if (line.Quantity <= 0)
|
||||||
|
throw new InvalidOperationException("Return quantity must be greater than zero.");
|
||||||
|
|
||||||
|
var item = await itemRepo.GetByIdAsync(line.ItemId)
|
||||||
|
?? throw new InvalidOperationException($"Item {line.ItemId} not found.");
|
||||||
|
|
||||||
|
if (!await repo.StoreHasPriorSaleAsync(order.FK_StoreId, line.ItemId))
|
||||||
|
throw new InvalidOperationException($"No prior sale of item '{item.Name}' found for this store.");
|
||||||
|
|
||||||
|
var unitPrice = orderItemsByItemId.TryGetValue(line.ItemId, out var matchingOrderItem)
|
||||||
|
? matchingOrderItem.UnitPrice
|
||||||
|
: item.SalePrice ?? 0;
|
||||||
|
|
||||||
|
saleReturn.Items.Add(new SaleReturnItem
|
||||||
|
{
|
||||||
|
FK_ItemId = line.ItemId,
|
||||||
|
Quantity = line.Quantity,
|
||||||
|
ReasonCode = line.ReasonCode,
|
||||||
|
UnitPrice = unitPrice
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await uow.BeginAsync(ct);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
foreach (var line in saleReturn.Items)
|
||||||
|
{
|
||||||
|
var balance = await stockRepo.GetBalanceAsync(line.FK_ItemId, vehicleId);
|
||||||
|
if (balance is null)
|
||||||
|
{
|
||||||
|
await stockRepo.AddBalanceAsync(new StockBalance
|
||||||
|
{
|
||||||
|
FK_ItemId = line.FK_ItemId,
|
||||||
|
LocationId = vehicleId,
|
||||||
|
LocationType = LocationType.Vehicle,
|
||||||
|
Quantity = line.Quantity
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
balance.Quantity += line.Quantity;
|
||||||
|
balance.UpdatedAt = DateTime.Now;
|
||||||
|
stockRepo.UpdateBalance(balance);
|
||||||
|
}
|
||||||
|
|
||||||
|
await stockRepo.AddMovementAsync(new StockMovement
|
||||||
|
{
|
||||||
|
FK_ItemId = line.FK_ItemId,
|
||||||
|
ToLocationId = vehicleId,
|
||||||
|
ToLocationType = LocationType.Vehicle,
|
||||||
|
Quantity = line.Quantity,
|
||||||
|
Type = MovementType.CustomerReturn,
|
||||||
|
ReasonCode = line.ReasonCode.ToString(),
|
||||||
|
ReferenceId = saleReturn.Id,
|
||||||
|
CreatedBy = createdByUserId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await repo.AddAsync(saleReturn);
|
||||||
|
|
||||||
|
if (request.Mode == ReturnMode.CreditDeduction)
|
||||||
|
{
|
||||||
|
var total = saleReturn.Items.Sum(i => i.Quantity * i.UnitPrice);
|
||||||
|
saleReturn.TotalCreditAmount = total;
|
||||||
|
order.TotalAmount = Math.Max(0, order.TotalAmount - total);
|
||||||
|
orderRepo.Update(order);
|
||||||
|
|
||||||
|
await repo.AddCreditNoteAsync(new CreditNote
|
||||||
|
{
|
||||||
|
FK_SaleReturnId = saleReturn.Id,
|
||||||
|
Amount = total
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else // ItemExchange
|
||||||
|
{
|
||||||
|
foreach (var line in saleReturn.Items)
|
||||||
|
{
|
||||||
|
if (!orderItemsByItemId.TryGetValue(line.FK_ItemId, out var orderItem))
|
||||||
|
continue; // no matching line on this order — nothing to offset
|
||||||
|
|
||||||
|
var offsetQty = Math.Min(line.Quantity, orderItem.Quantity);
|
||||||
|
orderItem.Quantity -= (int)offsetQty;
|
||||||
|
order.TotalAmount = Math.Max(0, order.TotalAmount - offsetQty * orderItem.UnitPrice);
|
||||||
|
}
|
||||||
|
orderRepo.Update(order);
|
||||||
|
}
|
||||||
|
|
||||||
|
await uow.CommitAsync(ct);
|
||||||
|
}
|
||||||
|
catch { await uow.RollbackAsync(ct); throw; }
|
||||||
|
|
||||||
|
var saved = await repo.GetByIdAsync(saleReturn.Id);
|
||||||
|
return Map(saved!);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
namespace Distribution.Features.SupplierClaims;
|
||||||
|
|
||||||
|
public interface ISupplierClaimRepository
|
||||||
|
{
|
||||||
|
Task<Models.SupplierClaim?> GetByIdAsync(Guid id);
|
||||||
|
Task<List<Models.SupplierClaim>> GetByPurchaseOrderIdAsync(Guid purchaseOrderId);
|
||||||
|
Task AddAsync(Models.SupplierClaim entity);
|
||||||
|
void Update(Models.SupplierClaim entity);
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
using Distribution.Infra.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace Distribution.Features.SupplierClaims;
|
||||||
|
|
||||||
|
[ApiController]
|
||||||
|
[Authorize]
|
||||||
|
public class SupplierClaimController(SupplierClaimService service, ICurrentUserService currentUser) : ControllerBase
|
||||||
|
{
|
||||||
|
[HttpGet("api/supplier-claims/{id:guid}")]
|
||||||
|
public async Task<IActionResult> GetById(Guid id)
|
||||||
|
{
|
||||||
|
var result = await service.GetByIdAsync(id);
|
||||||
|
return result is null ? NotFound() : Ok(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("api/purchase-orders/{id:guid}/claims")]
|
||||||
|
public async Task<IActionResult> GetByPurchaseOrder(Guid id) =>
|
||||||
|
Ok(await service.GetByPurchaseOrderIdAsync(id));
|
||||||
|
|
||||||
|
[HttpPost("api/purchase-orders/{id:guid}/claims")]
|
||||||
|
public async Task<IActionResult> Create(Guid id, CreateSupplierClaimRequest request, CancellationToken ct)
|
||||||
|
{
|
||||||
|
var userId = currentUser.UserId ?? throw new UnauthorizedAccessException();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var result = await service.CreateAsync(id, request, userId, ct);
|
||||||
|
return CreatedAtAction(nameof(GetById), new { id = result.Id }, result);
|
||||||
|
}
|
||||||
|
catch (InvalidOperationException ex) { return Conflict(new { error = ex.Message }); }
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("api/supplier-claims/{id:guid}/approve")]
|
||||||
|
[Authorize(Policy = "suppliers.claims.approve")]
|
||||||
|
public async Task<IActionResult> Approve(Guid id, ApproveSupplierClaimRequest request, CancellationToken ct)
|
||||||
|
{
|
||||||
|
var userId = currentUser.UserId ?? throw new UnauthorizedAccessException();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var result = await service.ApproveAsync(id, request, userId, ct);
|
||||||
|
return Ok(result);
|
||||||
|
}
|
||||||
|
catch (KeyNotFoundException) { return NotFound(); }
|
||||||
|
catch (InvalidOperationException ex) { return Conflict(new { error = ex.Message }); }
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("api/supplier-claims/{id:guid}/reject")]
|
||||||
|
[Authorize(Policy = "suppliers.claims.approve")]
|
||||||
|
public async Task<IActionResult> Reject(Guid id, RejectSupplierClaimRequest request, CancellationToken ct)
|
||||||
|
{
|
||||||
|
var userId = currentUser.UserId ?? throw new UnauthorizedAccessException();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var result = await service.RejectAsync(id, request, userId, ct);
|
||||||
|
return Ok(result);
|
||||||
|
}
|
||||||
|
catch (KeyNotFoundException) { return NotFound(); }
|
||||||
|
catch (InvalidOperationException ex) { return Conflict(new { error = ex.Message }); }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
using Distribution.Models;
|
||||||
|
|
||||||
|
namespace Distribution.Features.SupplierClaims;
|
||||||
|
|
||||||
|
public record SupplierClaimItemRequest(Guid ItemId, decimal Quantity, SupplierClaimIssueType IssueType);
|
||||||
|
|
||||||
|
public record CreateSupplierClaimRequest(
|
||||||
|
Guid? GoodsReceiptId,
|
||||||
|
string Details,
|
||||||
|
string? EvidenceUrl,
|
||||||
|
List<SupplierClaimItemRequest> Items);
|
||||||
|
|
||||||
|
public record ApproveSupplierClaimRequest(
|
||||||
|
SupplierClaimResolutionType ResolutionType,
|
||||||
|
decimal? ResolutionAmount,
|
||||||
|
string? ResolutionNotes);
|
||||||
|
|
||||||
|
public record RejectSupplierClaimRequest(string? ResolutionNotes);
|
||||||
|
|
||||||
|
public record SupplierClaimItemResponse(
|
||||||
|
Guid Id,
|
||||||
|
Guid ItemId,
|
||||||
|
string Sku,
|
||||||
|
string ItemName,
|
||||||
|
decimal Quantity,
|
||||||
|
SupplierClaimIssueType IssueType);
|
||||||
|
|
||||||
|
public record SupplierClaimResponse(
|
||||||
|
Guid Id,
|
||||||
|
Guid PurchaseOrderId,
|
||||||
|
Guid? GoodsReceiptId,
|
||||||
|
string Details,
|
||||||
|
string? EvidenceUrl,
|
||||||
|
SupplierClaimStatus Status,
|
||||||
|
SupplierClaimResolutionType? ResolutionType,
|
||||||
|
decimal? ResolutionAmount,
|
||||||
|
string? ResolutionNotes,
|
||||||
|
DateTime? ResolvedAt,
|
||||||
|
bool IsActive,
|
||||||
|
DateTime CreatedAt,
|
||||||
|
List<SupplierClaimItemResponse> Items);
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
using Distribution.Data;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace Distribution.Features.SupplierClaims;
|
||||||
|
|
||||||
|
public class SupplierClaimRepository(AppDBContext context) : ISupplierClaimRepository
|
||||||
|
{
|
||||||
|
public async Task<Models.SupplierClaim?> GetByIdAsync(Guid id) =>
|
||||||
|
await context.SupplierClaims
|
||||||
|
.Include(c => c.Items).ThenInclude(i => i.Item)
|
||||||
|
.FirstOrDefaultAsync(c => c.Id == id);
|
||||||
|
|
||||||
|
public async Task<List<Models.SupplierClaim>> GetByPurchaseOrderIdAsync(Guid purchaseOrderId) =>
|
||||||
|
await context.SupplierClaims
|
||||||
|
.Include(c => c.Items).ThenInclude(i => i.Item)
|
||||||
|
.Where(c => c.FK_PurchaseOrderId == purchaseOrderId && c.IsActive)
|
||||||
|
.OrderByDescending(c => c.CreatedAt)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
public async Task AddAsync(Models.SupplierClaim entity) =>
|
||||||
|
await context.SupplierClaims.AddAsync(entity);
|
||||||
|
|
||||||
|
public void Update(Models.SupplierClaim entity) =>
|
||||||
|
context.SupplierClaims.Update(entity);
|
||||||
|
}
|
||||||
@@ -0,0 +1,136 @@
|
|||||||
|
using Distribution.Features.Grn;
|
||||||
|
using Distribution.Features.Po;
|
||||||
|
using Distribution.Infra.UoW;
|
||||||
|
using Distribution.Models;
|
||||||
|
|
||||||
|
namespace Distribution.Features.SupplierClaims;
|
||||||
|
|
||||||
|
public class SupplierClaimService(
|
||||||
|
ISupplierClaimRepository repo,
|
||||||
|
IPoRepository poRepo,
|
||||||
|
IGrnRepository grnRepo,
|
||||||
|
IUnitOfWork uow)
|
||||||
|
{
|
||||||
|
private static SupplierClaimItemResponse MapItem(SupplierClaimItem i) =>
|
||||||
|
new(i.Id, i.FK_ItemId, i.Item.Sku, i.Item.Name, i.Quantity, i.IssueType);
|
||||||
|
|
||||||
|
private static SupplierClaimResponse Map(SupplierClaim c) =>
|
||||||
|
new(c.Id, c.FK_PurchaseOrderId, c.FK_GoodsReceiptId, c.Details, c.EvidenceUrl, c.Status,
|
||||||
|
c.ResolutionType, c.ResolutionAmount, c.ResolutionNotes, c.ResolvedAt, c.IsActive, c.CreatedAt,
|
||||||
|
c.Items.Where(i => i.IsActive).Select(MapItem).ToList());
|
||||||
|
|
||||||
|
public async Task<SupplierClaimResponse?> GetByIdAsync(Guid id)
|
||||||
|
{
|
||||||
|
var c = await repo.GetByIdAsync(id);
|
||||||
|
if (c is null || !c.IsActive) return null;
|
||||||
|
return Map(c);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<SupplierClaimResponse>> GetByPurchaseOrderIdAsync(Guid purchaseOrderId) =>
|
||||||
|
(await repo.GetByPurchaseOrderIdAsync(purchaseOrderId)).Select(Map).ToList();
|
||||||
|
|
||||||
|
public async Task<SupplierClaimResponse> CreateAsync(Guid purchaseOrderId, CreateSupplierClaimRequest request, Guid raisedByUserId, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
var po = await poRepo.GetByIdAsync(purchaseOrderId);
|
||||||
|
if (po is null || !po.IsActive)
|
||||||
|
throw new InvalidOperationException("Purchase order not found.");
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(request.Details))
|
||||||
|
throw new InvalidOperationException("Claim details are required.");
|
||||||
|
|
||||||
|
if (request.Items is null || request.Items.Count == 0)
|
||||||
|
throw new InvalidOperationException("A supplier claim must have at least one line item.");
|
||||||
|
|
||||||
|
if (request.GoodsReceiptId is not null)
|
||||||
|
{
|
||||||
|
var grn = await grnRepo.GetByIdAsync(request.GoodsReceiptId.Value)
|
||||||
|
?? throw new InvalidOperationException("Goods receipt not found.");
|
||||||
|
if (grn.FK_PurchaseOrderId != purchaseOrderId)
|
||||||
|
throw new InvalidOperationException("Goods receipt does not belong to this purchase order.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var claim = new SupplierClaim
|
||||||
|
{
|
||||||
|
FK_PurchaseOrderId = purchaseOrderId,
|
||||||
|
FK_GoodsReceiptId = request.GoodsReceiptId,
|
||||||
|
Details = request.Details,
|
||||||
|
EvidenceUrl = request.EvidenceUrl,
|
||||||
|
RaisedByUserId = raisedByUserId
|
||||||
|
};
|
||||||
|
|
||||||
|
foreach (var line in request.Items)
|
||||||
|
{
|
||||||
|
if (line.Quantity <= 0)
|
||||||
|
throw new InvalidOperationException("Claim quantity must be greater than zero.");
|
||||||
|
|
||||||
|
claim.Items.Add(new SupplierClaimItem
|
||||||
|
{
|
||||||
|
FK_ItemId = line.ItemId,
|
||||||
|
Quantity = line.Quantity,
|
||||||
|
IssueType = line.IssueType
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await uow.BeginAsync(ct);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await repo.AddAsync(claim);
|
||||||
|
await uow.CommitAsync(ct);
|
||||||
|
}
|
||||||
|
catch { await uow.RollbackAsync(ct); throw; }
|
||||||
|
|
||||||
|
var saved = await repo.GetByIdAsync(claim.Id);
|
||||||
|
return Map(saved!);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<SupplierClaimResponse> ApproveAsync(Guid id, ApproveSupplierClaimRequest request, Guid approverId, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
var claim = await repo.GetByIdAsync(id)
|
||||||
|
?? throw new KeyNotFoundException("Supplier claim not found.");
|
||||||
|
if (claim.Status != SupplierClaimStatus.Open)
|
||||||
|
throw new InvalidOperationException("Only an open claim can be approved.");
|
||||||
|
|
||||||
|
if (request.ResolutionType != SupplierClaimResolutionType.Replacement && request.ResolutionAmount is null)
|
||||||
|
throw new InvalidOperationException("ResolutionAmount is required for Credit, Refund, and PriceAdjustment.");
|
||||||
|
|
||||||
|
claim.Status = SupplierClaimStatus.Approved;
|
||||||
|
claim.ResolutionType = request.ResolutionType;
|
||||||
|
claim.ResolutionAmount = request.ResolutionType == SupplierClaimResolutionType.Replacement ? null : request.ResolutionAmount;
|
||||||
|
claim.ResolutionNotes = request.ResolutionNotes;
|
||||||
|
claim.ResolvedByUserId = approverId;
|
||||||
|
claim.ResolvedAt = DateTime.Now;
|
||||||
|
|
||||||
|
await uow.BeginAsync(ct);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
repo.Update(claim);
|
||||||
|
await uow.CommitAsync(ct);
|
||||||
|
}
|
||||||
|
catch { await uow.RollbackAsync(ct); throw; }
|
||||||
|
|
||||||
|
return Map(claim);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<SupplierClaimResponse> RejectAsync(Guid id, RejectSupplierClaimRequest request, Guid approverId, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
var claim = await repo.GetByIdAsync(id)
|
||||||
|
?? throw new KeyNotFoundException("Supplier claim not found.");
|
||||||
|
if (claim.Status != SupplierClaimStatus.Open)
|
||||||
|
throw new InvalidOperationException("Only an open claim can be rejected.");
|
||||||
|
|
||||||
|
claim.Status = SupplierClaimStatus.Rejected;
|
||||||
|
claim.ResolutionNotes = request.ResolutionNotes;
|
||||||
|
claim.ResolvedByUserId = approverId;
|
||||||
|
claim.ResolvedAt = DateTime.Now;
|
||||||
|
|
||||||
|
await uow.BeginAsync(ct);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
repo.Update(claim);
|
||||||
|
await uow.CommitAsync(ct);
|
||||||
|
}
|
||||||
|
catch { await uow.RollbackAsync(ct); throw; }
|
||||||
|
|
||||||
|
return Map(claim);
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,183 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Distribution.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddReturnsAndClaims : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "Claims",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
|
||||||
|
FK_AgencyUserId = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
|
||||||
|
Details = table.Column<string>(type: "longtext", nullable: false)
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||||
|
Status = table.Column<int>(type: "int", nullable: false),
|
||||||
|
SettlementAmount = table.Column<decimal>(type: "decimal(65,30)", nullable: true),
|
||||||
|
SettledByUserId = table.Column<Guid>(type: "char(36)", nullable: true, collation: "ascii_general_ci"),
|
||||||
|
SettledAt = 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_Claims", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_Claims_Users_FK_AgencyUserId",
|
||||||
|
column: x => x.FK_AgencyUserId,
|
||||||
|
principalTable: "Users",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Restrict);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_Claims_Users_SettledByUserId",
|
||||||
|
column: x => x.SettledByUserId,
|
||||||
|
principalTable: "Users",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Restrict);
|
||||||
|
})
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4");
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "SaleReturns",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
|
||||||
|
FK_OrderId = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
|
||||||
|
Mode = table.Column<int>(type: "int", nullable: false),
|
||||||
|
TotalCreditAmount = table.Column<decimal>(type: "decimal(65,30)", nullable: false),
|
||||||
|
CreatedByUserId = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
|
||||||
|
CreatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false),
|
||||||
|
IsActive = table.Column<bool>(type: "tinyint(1)", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_SaleReturns", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_SaleReturns_Orders_FK_OrderId",
|
||||||
|
column: x => x.FK_OrderId,
|
||||||
|
principalTable: "Orders",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Restrict);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_SaleReturns_Users_CreatedByUserId",
|
||||||
|
column: x => x.CreatedByUserId,
|
||||||
|
principalTable: "Users",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Restrict);
|
||||||
|
})
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4");
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "CreditNotes",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
|
||||||
|
FK_SaleReturnId = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
|
||||||
|
Amount = table.Column<decimal>(type: "decimal(65,30)", nullable: false),
|
||||||
|
IssuedAt = 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_CreditNotes", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_CreditNotes_SaleReturns_FK_SaleReturnId",
|
||||||
|
column: x => x.FK_SaleReturnId,
|
||||||
|
principalTable: "SaleReturns",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Restrict);
|
||||||
|
})
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4");
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "SaleReturnItems",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
|
||||||
|
FK_SaleReturnId = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
|
||||||
|
FK_ItemId = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
|
||||||
|
Quantity = table.Column<decimal>(type: "decimal(65,30)", nullable: false),
|
||||||
|
ReasonCode = table.Column<int>(type: "int", nullable: false),
|
||||||
|
UnitPrice = table.Column<decimal>(type: "decimal(65,30)", 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_SaleReturnItems", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_SaleReturnItems_Items_FK_ItemId",
|
||||||
|
column: x => x.FK_ItemId,
|
||||||
|
principalTable: "Items",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Restrict);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_SaleReturnItems_SaleReturns_FK_SaleReturnId",
|
||||||
|
column: x => x.FK_SaleReturnId,
|
||||||
|
principalTable: "SaleReturns",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Restrict);
|
||||||
|
})
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_Claims_FK_AgencyUserId",
|
||||||
|
table: "Claims",
|
||||||
|
column: "FK_AgencyUserId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_Claims_SettledByUserId",
|
||||||
|
table: "Claims",
|
||||||
|
column: "SettledByUserId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_CreditNotes_FK_SaleReturnId",
|
||||||
|
table: "CreditNotes",
|
||||||
|
column: "FK_SaleReturnId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_SaleReturnItems_FK_ItemId",
|
||||||
|
table: "SaleReturnItems",
|
||||||
|
column: "FK_ItemId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_SaleReturnItems_FK_SaleReturnId",
|
||||||
|
table: "SaleReturnItems",
|
||||||
|
column: "FK_SaleReturnId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_SaleReturns_CreatedByUserId",
|
||||||
|
table: "SaleReturns",
|
||||||
|
column: "CreatedByUserId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_SaleReturns_FK_OrderId",
|
||||||
|
table: "SaleReturns",
|
||||||
|
column: "FK_OrderId");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "Claims");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "CreditNotes");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "SaleReturnItems");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "SaleReturns");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,137 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Distribution.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddSupplierClaims : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "SupplierClaims",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
|
||||||
|
FK_PurchaseOrderId = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
|
||||||
|
FK_GoodsReceiptId = table.Column<Guid>(type: "char(36)", nullable: true, collation: "ascii_general_ci"),
|
||||||
|
Details = table.Column<string>(type: "longtext", nullable: false)
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||||
|
EvidenceUrl = table.Column<string>(type: "longtext", nullable: true)
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||||
|
Status = table.Column<int>(type: "int", nullable: false),
|
||||||
|
ResolutionType = table.Column<int>(type: "int", nullable: true),
|
||||||
|
ResolutionAmount = table.Column<decimal>(type: "decimal(65,30)", nullable: true),
|
||||||
|
ResolutionNotes = table.Column<string>(type: "longtext", nullable: true)
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||||
|
RaisedByUserId = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
|
||||||
|
ResolvedByUserId = table.Column<Guid>(type: "char(36)", nullable: true, collation: "ascii_general_ci"),
|
||||||
|
ResolvedAt = 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_SupplierClaims", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_SupplierClaims_GoodsReceipts_FK_GoodsReceiptId",
|
||||||
|
column: x => x.FK_GoodsReceiptId,
|
||||||
|
principalTable: "GoodsReceipts",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Restrict);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_SupplierClaims_PurchaseOrders_FK_PurchaseOrderId",
|
||||||
|
column: x => x.FK_PurchaseOrderId,
|
||||||
|
principalTable: "PurchaseOrders",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Restrict);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_SupplierClaims_Users_RaisedByUserId",
|
||||||
|
column: x => x.RaisedByUserId,
|
||||||
|
principalTable: "Users",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Restrict);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_SupplierClaims_Users_ResolvedByUserId",
|
||||||
|
column: x => x.ResolvedByUserId,
|
||||||
|
principalTable: "Users",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Restrict);
|
||||||
|
})
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4");
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "SupplierClaimItems",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
|
||||||
|
FK_SupplierClaimId = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
|
||||||
|
FK_ItemId = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
|
||||||
|
Quantity = table.Column<decimal>(type: "decimal(65,30)", nullable: false),
|
||||||
|
IssueType = 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_SupplierClaimItems", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_SupplierClaimItems_Items_FK_ItemId",
|
||||||
|
column: x => x.FK_ItemId,
|
||||||
|
principalTable: "Items",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Restrict);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_SupplierClaimItems_SupplierClaims_FK_SupplierClaimId",
|
||||||
|
column: x => x.FK_SupplierClaimId,
|
||||||
|
principalTable: "SupplierClaims",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Restrict);
|
||||||
|
})
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_SupplierClaimItems_FK_ItemId",
|
||||||
|
table: "SupplierClaimItems",
|
||||||
|
column: "FK_ItemId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_SupplierClaimItems_FK_SupplierClaimId",
|
||||||
|
table: "SupplierClaimItems",
|
||||||
|
column: "FK_SupplierClaimId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_SupplierClaims_FK_GoodsReceiptId",
|
||||||
|
table: "SupplierClaims",
|
||||||
|
column: "FK_GoodsReceiptId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_SupplierClaims_FK_PurchaseOrderId",
|
||||||
|
table: "SupplierClaims",
|
||||||
|
column: "FK_PurchaseOrderId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_SupplierClaims_RaisedByUserId",
|
||||||
|
table: "SupplierClaims",
|
||||||
|
column: "RaisedByUserId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_SupplierClaims_ResolvedByUserId",
|
||||||
|
table: "SupplierClaims",
|
||||||
|
column: "ResolvedByUserId");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "SupplierClaimItems");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "SupplierClaims");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -73,6 +73,74 @@ namespace Distribution.Migrations
|
|||||||
b.ToTable("Categories");
|
b.ToTable("Categories");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Distribution.Models.Claim", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("char(36)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.Property<string>("Details")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("longtext");
|
||||||
|
|
||||||
|
b.Property<Guid>("FK_AgencyUserId")
|
||||||
|
.HasColumnType("char(36)");
|
||||||
|
|
||||||
|
b.Property<bool>("IsActive")
|
||||||
|
.HasColumnType("tinyint(1)");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("SettledAt")
|
||||||
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.Property<Guid?>("SettledByUserId")
|
||||||
|
.HasColumnType("char(36)");
|
||||||
|
|
||||||
|
b.Property<decimal?>("SettlementAmount")
|
||||||
|
.HasColumnType("decimal(65,30)");
|
||||||
|
|
||||||
|
b.Property<int>("Status")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("FK_AgencyUserId");
|
||||||
|
|
||||||
|
b.HasIndex("SettledByUserId");
|
||||||
|
|
||||||
|
b.ToTable("Claims");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Distribution.Models.CreditNote", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("char(36)");
|
||||||
|
|
||||||
|
b.Property<decimal>("Amount")
|
||||||
|
.HasColumnType("decimal(65,30)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.Property<Guid>("FK_SaleReturnId")
|
||||||
|
.HasColumnType("char(36)");
|
||||||
|
|
||||||
|
b.Property<bool>("IsActive")
|
||||||
|
.HasColumnType("tinyint(1)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("IssuedAt")
|
||||||
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("FK_SaleReturnId");
|
||||||
|
|
||||||
|
b.ToTable("CreditNotes");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Distribution.Models.DeliveryRun", b =>
|
modelBuilder.Entity("Distribution.Models.DeliveryRun", b =>
|
||||||
{
|
{
|
||||||
b.Property<Guid>("Id")
|
b.Property<Guid>("Id")
|
||||||
@@ -1006,6 +1074,75 @@ namespace Distribution.Migrations
|
|||||||
b.ToTable("Sales");
|
b.ToTable("Sales");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Distribution.Models.SaleReturn", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("char(36)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.Property<Guid>("CreatedByUserId")
|
||||||
|
.HasColumnType("char(36)");
|
||||||
|
|
||||||
|
b.Property<Guid>("FK_OrderId")
|
||||||
|
.HasColumnType("char(36)");
|
||||||
|
|
||||||
|
b.Property<bool>("IsActive")
|
||||||
|
.HasColumnType("tinyint(1)");
|
||||||
|
|
||||||
|
b.Property<int>("Mode")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<decimal>("TotalCreditAmount")
|
||||||
|
.HasColumnType("decimal(65,30)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("CreatedByUserId");
|
||||||
|
|
||||||
|
b.HasIndex("FK_OrderId");
|
||||||
|
|
||||||
|
b.ToTable("SaleReturns");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Distribution.Models.SaleReturnItem", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("char(36)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.Property<Guid>("FK_ItemId")
|
||||||
|
.HasColumnType("char(36)");
|
||||||
|
|
||||||
|
b.Property<Guid>("FK_SaleReturnId")
|
||||||
|
.HasColumnType("char(36)");
|
||||||
|
|
||||||
|
b.Property<bool>("IsActive")
|
||||||
|
.HasColumnType("tinyint(1)");
|
||||||
|
|
||||||
|
b.Property<decimal>("Quantity")
|
||||||
|
.HasColumnType("decimal(65,30)");
|
||||||
|
|
||||||
|
b.Property<int>("ReasonCode")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<decimal>("UnitPrice")
|
||||||
|
.HasColumnType("decimal(65,30)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("FK_ItemId");
|
||||||
|
|
||||||
|
b.HasIndex("FK_SaleReturnId");
|
||||||
|
|
||||||
|
b.ToTable("SaleReturnItems");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Distribution.Models.SaleRoute", b =>
|
modelBuilder.Entity("Distribution.Models.SaleRoute", b =>
|
||||||
{
|
{
|
||||||
b.Property<Guid>("Id")
|
b.Property<Guid>("Id")
|
||||||
@@ -1377,6 +1514,98 @@ namespace Distribution.Migrations
|
|||||||
b.ToTable("Suppliers");
|
b.ToTable("Suppliers");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Distribution.Models.SupplierClaim", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("char(36)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.Property<string>("Details")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("longtext");
|
||||||
|
|
||||||
|
b.Property<string>("EvidenceUrl")
|
||||||
|
.HasColumnType("longtext");
|
||||||
|
|
||||||
|
b.Property<Guid?>("FK_GoodsReceiptId")
|
||||||
|
.HasColumnType("char(36)");
|
||||||
|
|
||||||
|
b.Property<Guid>("FK_PurchaseOrderId")
|
||||||
|
.HasColumnType("char(36)");
|
||||||
|
|
||||||
|
b.Property<bool>("IsActive")
|
||||||
|
.HasColumnType("tinyint(1)");
|
||||||
|
|
||||||
|
b.Property<Guid>("RaisedByUserId")
|
||||||
|
.HasColumnType("char(36)");
|
||||||
|
|
||||||
|
b.Property<decimal?>("ResolutionAmount")
|
||||||
|
.HasColumnType("decimal(65,30)");
|
||||||
|
|
||||||
|
b.Property<string>("ResolutionNotes")
|
||||||
|
.HasColumnType("longtext");
|
||||||
|
|
||||||
|
b.Property<int?>("ResolutionType")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("ResolvedAt")
|
||||||
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.Property<Guid?>("ResolvedByUserId")
|
||||||
|
.HasColumnType("char(36)");
|
||||||
|
|
||||||
|
b.Property<int>("Status")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("FK_GoodsReceiptId");
|
||||||
|
|
||||||
|
b.HasIndex("FK_PurchaseOrderId");
|
||||||
|
|
||||||
|
b.HasIndex("RaisedByUserId");
|
||||||
|
|
||||||
|
b.HasIndex("ResolvedByUserId");
|
||||||
|
|
||||||
|
b.ToTable("SupplierClaims");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Distribution.Models.SupplierClaimItem", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("char(36)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.Property<Guid>("FK_ItemId")
|
||||||
|
.HasColumnType("char(36)");
|
||||||
|
|
||||||
|
b.Property<Guid>("FK_SupplierClaimId")
|
||||||
|
.HasColumnType("char(36)");
|
||||||
|
|
||||||
|
b.Property<bool>("IsActive")
|
||||||
|
.HasColumnType("tinyint(1)");
|
||||||
|
|
||||||
|
b.Property<int>("IssueType")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<decimal>("Quantity")
|
||||||
|
.HasColumnType("decimal(65,30)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("FK_ItemId");
|
||||||
|
|
||||||
|
b.HasIndex("FK_SupplierClaimId");
|
||||||
|
|
||||||
|
b.ToTable("SupplierClaimItems");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Distribution.Models.Uom", b =>
|
modelBuilder.Entity("Distribution.Models.Uom", b =>
|
||||||
{
|
{
|
||||||
b.Property<Guid>("Id")
|
b.Property<Guid>("Id")
|
||||||
@@ -1610,6 +1839,35 @@ namespace Distribution.Migrations
|
|||||||
b.ToTable("Warehouses");
|
b.ToTable("Warehouses");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Distribution.Models.Claim", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Distribution.Models.User", "AgencyUser")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("FK_AgencyUserId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("Distribution.Models.User", "SettledByUser")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("SettledByUserId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict);
|
||||||
|
|
||||||
|
b.Navigation("AgencyUser");
|
||||||
|
|
||||||
|
b.Navigation("SettledByUser");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Distribution.Models.CreditNote", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Distribution.Models.SaleReturn", "SaleReturn")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("FK_SaleReturnId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("SaleReturn");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Distribution.Models.DeliveryRun", b =>
|
modelBuilder.Entity("Distribution.Models.DeliveryRun", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("Distribution.Models.DriverProfile", "DriverProfile")
|
b.HasOne("Distribution.Models.DriverProfile", "DriverProfile")
|
||||||
@@ -1994,6 +2252,44 @@ namespace Distribution.Migrations
|
|||||||
b.Navigation("Store");
|
b.Navigation("Store");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Distribution.Models.SaleReturn", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Distribution.Models.User", "CreatedByUser")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("CreatedByUserId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("Distribution.Models.Order", "Order")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("FK_OrderId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("CreatedByUser");
|
||||||
|
|
||||||
|
b.Navigation("Order");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Distribution.Models.SaleReturnItem", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Distribution.Models.Item", "Item")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("FK_ItemId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("Distribution.Models.SaleReturn", "SaleReturn")
|
||||||
|
.WithMany("Items")
|
||||||
|
.HasForeignKey("FK_SaleReturnId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Item");
|
||||||
|
|
||||||
|
b.Navigation("SaleReturn");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Distribution.Models.Scheme", b =>
|
modelBuilder.Entity("Distribution.Models.Scheme", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("Distribution.Models.Item", "FreeItem")
|
b.HasOne("Distribution.Models.Item", "FreeItem")
|
||||||
@@ -2116,6 +2412,58 @@ namespace Distribution.Migrations
|
|||||||
b.Navigation("Category");
|
b.Navigation("Category");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Distribution.Models.SupplierClaim", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Distribution.Models.GoodsReceipt", "GoodsReceipt")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("FK_GoodsReceiptId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict);
|
||||||
|
|
||||||
|
b.HasOne("Distribution.Models.PurchaseOrder", "PurchaseOrder")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("FK_PurchaseOrderId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("Distribution.Models.User", "RaisedByUser")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("RaisedByUserId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("Distribution.Models.User", "ResolvedByUser")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("ResolvedByUserId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict);
|
||||||
|
|
||||||
|
b.Navigation("GoodsReceipt");
|
||||||
|
|
||||||
|
b.Navigation("PurchaseOrder");
|
||||||
|
|
||||||
|
b.Navigation("RaisedByUser");
|
||||||
|
|
||||||
|
b.Navigation("ResolvedByUser");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Distribution.Models.SupplierClaimItem", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Distribution.Models.Item", "Item")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("FK_ItemId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("Distribution.Models.SupplierClaim", "SupplierClaim")
|
||||||
|
.WithMany("Items")
|
||||||
|
.HasForeignKey("FK_SupplierClaimId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Item");
|
||||||
|
|
||||||
|
b.Navigation("SupplierClaim");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Distribution.Models.UomConversion", b =>
|
modelBuilder.Entity("Distribution.Models.UomConversion", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("Distribution.Models.Uom", "FromUom")
|
b.HasOne("Distribution.Models.Uom", "FromUom")
|
||||||
@@ -2270,12 +2618,22 @@ namespace Distribution.Migrations
|
|||||||
b.Navigation("Items");
|
b.Navigation("Items");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Distribution.Models.SaleReturn", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Items");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Distribution.Models.Scheme", b =>
|
modelBuilder.Entity("Distribution.Models.Scheme", b =>
|
||||||
{
|
{
|
||||||
b.Navigation("BundleComponents");
|
b.Navigation("BundleComponents");
|
||||||
|
|
||||||
b.Navigation("Redemptions");
|
b.Navigation("Redemptions");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Distribution.Models.SupplierClaim", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Items");
|
||||||
|
});
|
||||||
#pragma warning restore 612, 618
|
#pragma warning restore 612, 618
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
|
|
||||||
|
namespace Distribution.Models
|
||||||
|
{
|
||||||
|
public class Claim : Template
|
||||||
|
{
|
||||||
|
/// <summary>User holding the RegionalAgency role that this claim is raised against.</summary>
|
||||||
|
public Guid FK_AgencyUserId { get; set; }
|
||||||
|
[ForeignKey("FK_AgencyUserId")]
|
||||||
|
public User AgencyUser { get; set; }
|
||||||
|
|
||||||
|
public string Details { get; set; }
|
||||||
|
public ClaimStatus Status { get; set; } = ClaimStatus.Open;
|
||||||
|
public decimal? SettlementAmount { get; set; }
|
||||||
|
|
||||||
|
public Guid? SettledByUserId { get; set; }
|
||||||
|
[ForeignKey("SettledByUserId")]
|
||||||
|
public User? SettledByUser { get; set; }
|
||||||
|
public DateTime? SettledAt { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
|
|
||||||
|
namespace Distribution.Models
|
||||||
|
{
|
||||||
|
/// <summary>Audit record of the credit amount netted off an order by a CreditDeduction-mode SaleReturn.</summary>
|
||||||
|
public class CreditNote : Template
|
||||||
|
{
|
||||||
|
public Guid FK_SaleReturnId { get; set; }
|
||||||
|
[ForeignKey("FK_SaleReturnId")]
|
||||||
|
public SaleReturn SaleReturn { get; set; }
|
||||||
|
|
||||||
|
public decimal Amount { get; set; }
|
||||||
|
public DateTime IssuedAt { get; set; } = DateTime.Now;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,37 +1,13 @@
|
|||||||
namespace Distribution.Models
|
namespace Distribution.Models
|
||||||
{
|
{
|
||||||
public enum LocationType
|
public enum LocationType
|
||||||
{
|
{ Warehouse, Vehicle }
|
||||||
Warehouse,
|
|
||||||
Vehicle
|
|
||||||
}
|
|
||||||
|
|
||||||
public enum KycStatus
|
public enum KycStatus { Pending,Verified, Rejected }
|
||||||
{
|
|
||||||
Pending,
|
|
||||||
Verified,
|
|
||||||
Rejected }
|
|
||||||
|
|
||||||
public enum MovementType
|
public enum MovementType { Sale, CustomerReturn, Transfer, Damage, Adjustment, Load, PurchaseReceipt, PurchaseReturn }
|
||||||
{
|
|
||||||
Sale,
|
|
||||||
CustomerReturn,
|
|
||||||
Transfer,
|
|
||||||
Damage,
|
|
||||||
Adjustment,
|
|
||||||
Load,
|
|
||||||
PurchaseReceipt,
|
|
||||||
PurchaseReturn
|
|
||||||
}
|
|
||||||
|
|
||||||
public enum LoadingSheetStatus
|
public enum LoadingSheetStatus { Pending, DispatchConfirmed, ReceiptConfirmed, DiscrepancyFlagged, Resolved }
|
||||||
{
|
|
||||||
Pending,
|
|
||||||
DispatchConfirmed,
|
|
||||||
ReceiptConfirmed,
|
|
||||||
DiscrepancyFlagged,
|
|
||||||
Resolved
|
|
||||||
}
|
|
||||||
|
|
||||||
public enum RideType { Delivery, Sale }
|
public enum RideType { Delivery, Sale }
|
||||||
|
|
||||||
@@ -52,10 +28,17 @@ namespace Distribution.Models
|
|||||||
public enum PaymentMethod { Cash, Credit }
|
public enum PaymentMethod { Cash, Credit }
|
||||||
|
|
||||||
public enum DeliveryTaskStatus
|
public enum DeliveryTaskStatus
|
||||||
{
|
{ Pending, Delivered, PartiallyDelivered, Failed}
|
||||||
Pending,
|
|
||||||
Delivered,
|
public enum ReturnMode { CreditDeduction, ItemExchange }
|
||||||
PartiallyDelivered,
|
|
||||||
Failed
|
public enum ReturnReasonCode { Expired, Damaged, OrderError, Other }
|
||||||
}
|
|
||||||
|
public enum ClaimStatus { Open, Settled }
|
||||||
|
|
||||||
|
public enum SupplierClaimStatus { Open, Approved, Rejected }
|
||||||
|
|
||||||
|
public enum SupplierClaimIssueType { Damaged, Short, Expired, WrongItem, Other }
|
||||||
|
|
||||||
|
public enum SupplierClaimResolutionType { Replacement, Credit, Refund, PriceAdjustment }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
|
|
||||||
|
namespace Distribution.Models
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A return recorded by a sales rep against an open Order during the same store visit.
|
||||||
|
/// Settled immediately on creation — no supervisor approval step.
|
||||||
|
/// </summary>
|
||||||
|
public class SaleReturn : Template
|
||||||
|
{
|
||||||
|
public Guid FK_OrderId { get; set; }
|
||||||
|
[ForeignKey("FK_OrderId")]
|
||||||
|
public Order Order { get; set; }
|
||||||
|
|
||||||
|
public ReturnMode Mode { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Amount netted off the order's TotalAmount. Zero for ItemExchange mode.</summary>
|
||||||
|
public decimal TotalCreditAmount { get; set; }
|
||||||
|
|
||||||
|
public Guid CreatedByUserId { get; set; }
|
||||||
|
[ForeignKey("CreatedByUserId")]
|
||||||
|
public User CreatedByUser { get; set; }
|
||||||
|
|
||||||
|
public ICollection<SaleReturnItem> Items { get; set; } = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
|
|
||||||
|
namespace Distribution.Models
|
||||||
|
{
|
||||||
|
public class SaleReturnItem : Template
|
||||||
|
{
|
||||||
|
public Guid FK_SaleReturnId { get; set; }
|
||||||
|
[ForeignKey("FK_SaleReturnId")]
|
||||||
|
public SaleReturn SaleReturn { get; set; }
|
||||||
|
|
||||||
|
public Guid FK_ItemId { get; set; }
|
||||||
|
[ForeignKey("FK_ItemId")]
|
||||||
|
public Item Item { get; set; }
|
||||||
|
|
||||||
|
public decimal Quantity { get; set; }
|
||||||
|
public ReturnReasonCode ReasonCode { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Snapshot of the item's unit price at return time, used to compute credit amount.</summary>
|
||||||
|
public decimal UnitPrice { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
|
|
||||||
|
namespace Distribution.Models
|
||||||
|
{
|
||||||
|
public class SupplierClaim : Template
|
||||||
|
{
|
||||||
|
public Guid FK_PurchaseOrderId { get; set; }
|
||||||
|
[ForeignKey("FK_PurchaseOrderId")]
|
||||||
|
public PurchaseOrder PurchaseOrder { get; set; }
|
||||||
|
|
||||||
|
public Guid? FK_GoodsReceiptId { get; set; }
|
||||||
|
[ForeignKey("FK_GoodsReceiptId")]
|
||||||
|
public GoodsReceipt? GoodsReceipt { get; set; }
|
||||||
|
|
||||||
|
public string Details { get; set; }
|
||||||
|
public string? EvidenceUrl { get; set; }
|
||||||
|
|
||||||
|
public SupplierClaimStatus Status { get; set; } = SupplierClaimStatus.Open;
|
||||||
|
|
||||||
|
public SupplierClaimResolutionType? ResolutionType { get; set; }
|
||||||
|
public decimal? ResolutionAmount { get; set; }
|
||||||
|
public string? ResolutionNotes { get; set; }
|
||||||
|
|
||||||
|
public Guid RaisedByUserId { get; set; }
|
||||||
|
[ForeignKey("RaisedByUserId")]
|
||||||
|
public User RaisedByUser { get; set; }
|
||||||
|
|
||||||
|
public Guid? ResolvedByUserId { get; set; }
|
||||||
|
[ForeignKey("ResolvedByUserId")]
|
||||||
|
public User? ResolvedByUser { get; set; }
|
||||||
|
public DateTime? ResolvedAt { get; set; }
|
||||||
|
|
||||||
|
public ICollection<SupplierClaimItem> Items { get; set; } = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
|
|
||||||
|
namespace Distribution.Models
|
||||||
|
{
|
||||||
|
public class SupplierClaimItem : Template
|
||||||
|
{
|
||||||
|
public Guid FK_SupplierClaimId { get; set; }
|
||||||
|
[ForeignKey("FK_SupplierClaimId")]
|
||||||
|
public SupplierClaim SupplierClaim { get; set; }
|
||||||
|
|
||||||
|
public Guid FK_ItemId { get; set; }
|
||||||
|
[ForeignKey("FK_ItemId")]
|
||||||
|
public Item Item { get; set; }
|
||||||
|
|
||||||
|
public decimal Quantity { get; set; }
|
||||||
|
public SupplierClaimIssueType IssueType { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,6 +13,9 @@ using Distribution.Features.Order;
|
|||||||
using Distribution.Features.PriceList;
|
using Distribution.Features.PriceList;
|
||||||
using Distribution.Features.Po;
|
using Distribution.Features.Po;
|
||||||
using Distribution.Features.PurchaseReturn;
|
using Distribution.Features.PurchaseReturn;
|
||||||
|
using Distribution.Features.Returns;
|
||||||
|
using Distribution.Features.Claims;
|
||||||
|
using Distribution.Features.SupplierClaims;
|
||||||
using Distribution.Features.Region;
|
using Distribution.Features.Region;
|
||||||
using Distribution.Features.LoadingEvent;
|
using Distribution.Features.LoadingEvent;
|
||||||
using Distribution.Features.Ride;
|
using Distribution.Features.Ride;
|
||||||
@@ -73,6 +76,9 @@ builder.Services.AddScoped<ISchemeRepository, SchemeRepository>();
|
|||||||
builder.Services.AddScoped<IPoRepository, PoRepository>();
|
builder.Services.AddScoped<IPoRepository, PoRepository>();
|
||||||
builder.Services.AddScoped<IGrnRepository, GrnRepository>();
|
builder.Services.AddScoped<IGrnRepository, GrnRepository>();
|
||||||
builder.Services.AddScoped<IPurchaseReturnRepository, PurchaseReturnRepository>();
|
builder.Services.AddScoped<IPurchaseReturnRepository, PurchaseReturnRepository>();
|
||||||
|
builder.Services.AddScoped<IReturnRepository, ReturnRepository>();
|
||||||
|
builder.Services.AddScoped<IClaimRepository, ClaimRepository>();
|
||||||
|
builder.Services.AddScoped<ISupplierClaimRepository, SupplierClaimRepository>();
|
||||||
builder.Services.AddScoped<IDeliveryTaskRepository, DeliveryTaskRepository>();
|
builder.Services.AddScoped<IDeliveryTaskRepository, DeliveryTaskRepository>();
|
||||||
builder.Services.AddScoped<IPaymentRepository, PaymentRepository>();
|
builder.Services.AddScoped<IPaymentRepository, PaymentRepository>();
|
||||||
|
|
||||||
@@ -103,6 +109,9 @@ builder.Services.AddScoped<SchemeService>();
|
|||||||
builder.Services.AddScoped<PoService>();
|
builder.Services.AddScoped<PoService>();
|
||||||
builder.Services.AddScoped<GrnService>();
|
builder.Services.AddScoped<GrnService>();
|
||||||
builder.Services.AddScoped<PurchaseReturnService>();
|
builder.Services.AddScoped<PurchaseReturnService>();
|
||||||
|
builder.Services.AddScoped<ReturnService>();
|
||||||
|
builder.Services.AddScoped<ClaimService>();
|
||||||
|
builder.Services.AddScoped<SupplierClaimService>();
|
||||||
builder.Services.AddScoped<DeliveryTaskService>();
|
builder.Services.AddScoped<DeliveryTaskService>();
|
||||||
builder.Services.AddScoped<PaymentService>();
|
builder.Services.AddScoped<PaymentService>();
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user