Compare commits
68 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f69d87fdd7 | |||
| c775146b64 | |||
| 005f0414a7 | |||
| 6e4bebb89b | |||
| 932d09c135 | |||
| 455ce699ad | |||
| 7d7e9f1318 | |||
| 87e3112a9a | |||
| 801276559d | |||
| b1bda1448e | |||
| 58247a1a0b | |||
| 3a503abe6a | |||
| 855eb44118 | |||
| 33dc4adce2 | |||
| 5b2b39b7bf | |||
| b7f58e6b73 | |||
| e13ec0f109 | |||
| 2c39b34c5f | |||
| 99832df134 | |||
| 2be9d9b396 | |||
| a12e170205 | |||
| fdfca7cf5e | |||
| e84ce29064 | |||
| 2be2928ad1 | |||
| 872fd42c4a | |||
| 13fdb28ec7 | |||
| 39ad424d21 | |||
| d79c5e55d0 | |||
| 50d3113c28 | |||
| 34d33571de | |||
| 0f575370d4 | |||
| e4061fa94c | |||
| 5f8bc70174 | |||
| f2d5c9f50a | |||
| 9e39981c9a | |||
| bd8b2d8b44 | |||
| f26557a66e | |||
| 91959913a8 | |||
| 48f607d623 | |||
| ee9a5385bf | |||
| 82be251878 | |||
| 209ee7da2d | |||
| 6b9f27573b | |||
| a0df2a1bbf | |||
| d1c3d4e22f | |||
| 138bf2f925 | |||
| a80512994b | |||
| 1b4461dc12 | |||
| 2de9e5e4ad | |||
| cc33ecdd54 | |||
| 5707bd4c31 | |||
| f77db46a02 | |||
| 28c465f59b | |||
| 21b38b5276 | |||
| 267144f7a1 | |||
| 992cd292fa | |||
| efb5657b6b | |||
| 7b6c9ac92a | |||
| 622b75e187 | |||
| eb0b042f0c | |||
| 293df41d4e | |||
| a980e636f5 | |||
| b992d07689 | |||
| 5e81fd77b1 | |||
| a4d58f3377 | |||
| 94db2a311c | |||
| a1fd5f1a08 | |||
| 87c807c16c |
@@ -0,0 +1,209 @@
|
||||
using Distribution.Features.Notifications;
|
||||
using Distribution.Infra.UoW;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Moq;
|
||||
using Xunit;
|
||||
|
||||
namespace Distribution.Tests.Services;
|
||||
|
||||
public class NotificationServiceTests
|
||||
{
|
||||
private readonly Mock<INotificationRepository> _repo = new();
|
||||
private readonly Mock<IUnitOfWork> _uow = new();
|
||||
private readonly Mock<IHubContext<NotificationHub>> _hub = new();
|
||||
private readonly Mock<IHubClients> _clients = new();
|
||||
private readonly Mock<IClientProxy> _proxy = new();
|
||||
private readonly NotificationService _sut;
|
||||
|
||||
public NotificationServiceTests()
|
||||
{
|
||||
_repo.Setup(r => r.FilterActiveUserIdsAsync(It.IsAny<IEnumerable<Guid>>()))
|
||||
.ReturnsAsync((IEnumerable<Guid> ids) => ids.ToList());
|
||||
_hub.SetupGet(h => h.Clients).Returns(_clients.Object);
|
||||
_clients.Setup(c => c.User(It.IsAny<string>())).Returns(_proxy.Object);
|
||||
|
||||
_sut = new NotificationService(
|
||||
_repo.Object,
|
||||
new NotificationRecipientResolver(_repo.Object),
|
||||
_hub.Object,
|
||||
_uow.Object,
|
||||
NullLogger<NotificationService>.Instance);
|
||||
}
|
||||
|
||||
private static NotificationArgs Args(Guid? actor = null) =>
|
||||
new(new { StoreName = "Fort Mini Mart" }, "DeliveryTask", Guid.NewGuid(), "/operations/delivery-tasks/1", actor);
|
||||
|
||||
// Registry
|
||||
|
||||
[Fact]
|
||||
public void EveryNotificationTypeConstant_HasADefinition()
|
||||
{
|
||||
var constants = typeof(NotificationTypes)
|
||||
.GetFields()
|
||||
.Where(f => f.IsLiteral && f.FieldType == typeof(string))
|
||||
.Select(f => (string)f.GetRawConstantValue()!)
|
||||
.ToList();
|
||||
|
||||
Assert.NotEmpty(constants);
|
||||
Assert.Equal(constants.Count, constants.Distinct().Count());
|
||||
Assert.All(constants, c => Assert.True(NotificationTypes.All.ContainsKey(c), $"{c} has no definition"));
|
||||
}
|
||||
|
||||
// NotifyAsync
|
||||
|
||||
[Fact]
|
||||
public async Task NotifyAsync_DoesNothing_WhenTypeIsNotRegistered()
|
||||
{
|
||||
await _sut.NotifyAsync("NotARealType", [RecipientRule.Users(Guid.NewGuid())], Args());
|
||||
|
||||
_repo.Verify(r => r.AddAsync(It.IsAny<Models.Notification>()), Times.Never);
|
||||
_uow.Verify(u => u.CommitAsync(It.IsAny<CancellationToken>()), Times.Never);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task NotifyAsync_DoesNothing_WhenThereAreNoRecipients()
|
||||
{
|
||||
await _sut.NotifyAsync(NotificationTypes.DeliveryTaskFailed, [RecipientRule.Users()], Args());
|
||||
|
||||
_repo.Verify(r => r.AddAsync(It.IsAny<Models.Notification>()), Times.Never);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task NotifyAsync_ExcludesTheActingUser()
|
||||
{
|
||||
var actor = Guid.NewGuid();
|
||||
var other = Guid.NewGuid();
|
||||
Models.Notification? saved = null;
|
||||
_repo.Setup(r => r.AddAsync(It.IsAny<Models.Notification>()))
|
||||
.Callback<Models.Notification>(n => saved = n)
|
||||
.Returns(Task.CompletedTask);
|
||||
|
||||
await _sut.NotifyAsync(NotificationTypes.DeliveryTaskFailed,
|
||||
[RecipientRule.Users(actor, other)], Args(actor));
|
||||
|
||||
Assert.NotNull(saved);
|
||||
var recipient = Assert.Single(saved!.Recipients);
|
||||
Assert.Equal(other, recipient.FK_UserId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task NotifyAsync_RendersTemplate_CommitsAndPushesToEachRecipient()
|
||||
{
|
||||
var a = Guid.NewGuid();
|
||||
var b = Guid.NewGuid();
|
||||
Models.Notification? saved = null;
|
||||
_repo.Setup(r => r.AddAsync(It.IsAny<Models.Notification>()))
|
||||
.Callback<Models.Notification>(n => saved = n)
|
||||
.Returns(Task.CompletedTask);
|
||||
|
||||
await _sut.NotifyAsync(NotificationTypes.DeliveryTaskFailed, [RecipientRule.Users(a, b)], Args());
|
||||
|
||||
Assert.Equal("Delivery to Fort Mini Mart failed.", saved!.Message);
|
||||
Assert.Equal(Models.NotificationSeverity.Warning, saved.Severity);
|
||||
_uow.Verify(u => u.CommitAsync(It.IsAny<CancellationToken>()), Times.Once);
|
||||
_clients.Verify(c => c.User(a.ToString()), Times.Once);
|
||||
_clients.Verify(c => c.User(b.ToString()), Times.Once);
|
||||
_proxy.Verify(p => p.SendCoreAsync(NotificationHub.ClientEvent,
|
||||
It.IsAny<object?[]>(), It.IsAny<CancellationToken>()), Times.Exactly(2));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task NotifyAsync_DoesNotThrow_WhenSavingFails_AndDiscardsTheEntity()
|
||||
{
|
||||
_uow.Setup(u => u.CommitAsync(It.IsAny<CancellationToken>())).ThrowsAsync(new InvalidOperationException("db down"));
|
||||
|
||||
var ex = await Record.ExceptionAsync(() =>
|
||||
_sut.NotifyAsync(NotificationTypes.DeliveryTaskFailed, [RecipientRule.Users(Guid.NewGuid())], Args()));
|
||||
|
||||
Assert.Null(ex);
|
||||
_repo.Verify(r => r.Discard(It.IsAny<Models.Notification>()), Times.Once);
|
||||
_proxy.Verify(p => p.SendCoreAsync(It.IsAny<string>(), It.IsAny<object?[]>(), It.IsAny<CancellationToken>()), Times.Never);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task NotifyAsync_DoesNotThrow_WhenPushFails_AndKeepsTheSavedNotification()
|
||||
{
|
||||
_proxy.Setup(p => p.SendCoreAsync(It.IsAny<string>(), It.IsAny<object?[]>(), It.IsAny<CancellationToken>()))
|
||||
.ThrowsAsync(new InvalidOperationException("socket gone"));
|
||||
|
||||
var ex = await Record.ExceptionAsync(() =>
|
||||
_sut.NotifyAsync(NotificationTypes.DeliveryTaskFailed, [RecipientRule.Users(Guid.NewGuid())], Args()));
|
||||
|
||||
Assert.Null(ex);
|
||||
_uow.Verify(u => u.CommitAsync(It.IsAny<CancellationToken>()), Times.Once);
|
||||
_repo.Verify(r => r.Discard(It.IsAny<Models.Notification>()), Times.Never);
|
||||
}
|
||||
|
||||
// Recipient rules
|
||||
|
||||
[Fact]
|
||||
public async Task PermissionInScope_ResolvesToTheIntersectionOfPermissionAndScope()
|
||||
{
|
||||
var both = Guid.NewGuid();
|
||||
var permissionOnly = Guid.NewGuid();
|
||||
var scopeOnly = Guid.NewGuid();
|
||||
var storeId = Guid.NewGuid();
|
||||
_repo.Setup(r => r.GetUserIdsByPermissionAsync("store.manage")).ReturnsAsync([both, permissionOnly]);
|
||||
_repo.Setup(r => r.GetUserIdsByScopeAsync(ScopeKind.Store, storeId)).ReturnsAsync([both, scopeOnly]);
|
||||
|
||||
var ids = await new NotificationRecipientResolver(_repo.Object)
|
||||
.ResolveAsync([RecipientRule.PermissionInScope("store.manage", ScopeKind.Store, storeId)], actingUserId: null);
|
||||
|
||||
Assert.Equal([both], ids);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ReportsTo_ResolvesToTheSupervisor_AndIsEmptyWhenThereIsNone()
|
||||
{
|
||||
var user = Guid.NewGuid();
|
||||
var supervisor = Guid.NewGuid();
|
||||
var resolver = new NotificationRecipientResolver(_repo.Object);
|
||||
|
||||
_repo.Setup(r => r.GetReportsToUserIdAsync(user)).ReturnsAsync(supervisor);
|
||||
Assert.Equal([supervisor], await resolver.ResolveAsync([RecipientRule.ReportsTo(user)], null));
|
||||
|
||||
_repo.Setup(r => r.GetReportsToUserIdAsync(user)).ReturnsAsync((Guid?)null);
|
||||
Assert.Empty(await resolver.ResolveAsync([RecipientRule.ReportsTo(user)], null));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RouteOwner_ResolvesToTheOwner_AndIsEmptyWhenTheRouteHasNone()
|
||||
{
|
||||
var storeId = Guid.NewGuid();
|
||||
var owner = Guid.NewGuid();
|
||||
var resolver = new NotificationRecipientResolver(_repo.Object);
|
||||
|
||||
_repo.Setup(r => r.GetRouteOwnerUserIdAsync(ScopeKind.Store, storeId)).ReturnsAsync(owner);
|
||||
Assert.Equal([owner], await resolver.ResolveAsync([RecipientRule.RouteOwner(ScopeKind.Store, storeId)], null));
|
||||
|
||||
_repo.Setup(r => r.GetRouteOwnerUserIdAsync(ScopeKind.Store, storeId)).ReturnsAsync((Guid?)null);
|
||||
Assert.Empty(await resolver.ResolveAsync([RecipientRule.RouteOwner(ScopeKind.Store, storeId)], null));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task OptionalTokenAtTheEndOfATemplate_LeavesNoTrailingSpace()
|
||||
{
|
||||
Models.Notification? saved = null;
|
||||
_repo.Setup(r => r.AddAsync(It.IsAny<Models.Notification>()))
|
||||
.Callback<Models.Notification>(n => saved = n)
|
||||
.Returns(Task.CompletedTask);
|
||||
|
||||
await _sut.NotifyAsync(NotificationTypes.DeliveryTaskFailed, [RecipientRule.Users(Guid.NewGuid())],
|
||||
new NotificationArgs(new { StoreName = "Fort Mini Mart", Note = (string?)null }, "DeliveryTask", Guid.NewGuid(), "/x"));
|
||||
|
||||
Assert.Equal("Delivery to Fort Mini Mart failed.", saved!.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Resolve_DeduplicatesUsersAcrossRules()
|
||||
{
|
||||
var user = Guid.NewGuid();
|
||||
_repo.Setup(r => r.GetUserIdsByPermissionAsync("stock.manage")).ReturnsAsync([user]);
|
||||
|
||||
var ids = await new NotificationRecipientResolver(_repo.Object)
|
||||
.ResolveAsync([RecipientRule.Users(user), RecipientRule.Permission("stock.manage")], null);
|
||||
|
||||
Assert.Equal([user], ids);
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,9 @@ public class AppDBContext(DbContextOptions<AppDBContext> options) : DbContext(op
|
||||
public DbSet<ShopPO> ShopPOs { get; set; }
|
||||
public DbSet<ShopPOItem> ShopPOItems { get; set; }
|
||||
public DbSet<Invoice> Invoices { get; set; }
|
||||
public DbSet<InvoiceItem> InvoiceItems { get; set; }
|
||||
public DbSet<InvoiceVersion> InvoiceVersions { get; set; }
|
||||
public DbSet<InvoiceVersionItem> InvoiceVersionItems { get; set; }
|
||||
public DbSet<LoadingSheetOrder> LoadingSheetOrders { get; set; }
|
||||
public DbSet<DeliveryRun> DeliveryRuns { get; set; }
|
||||
public DbSet<User> Users { get; set; }
|
||||
@@ -65,6 +68,20 @@ public class AppDBContext(DbContextOptions<AppDBContext> options) : DbContext(op
|
||||
public DbSet<RegionalTaxRule> RegionalTaxRules { get; set; }
|
||||
public DbSet<RouteVisitLog> RouteVisitLogs { get; set; }
|
||||
public DbSet<SysConfig> SysConfigs { get; set; }
|
||||
public DbSet<BankAccount> BankAccounts { get; set; }
|
||||
public DbSet<ChequeBook> ChequeBooks { get; set; }
|
||||
public DbSet<ChequePage> ChequePages { get; set; }
|
||||
public DbSet<ReceivedCheque> ReceivedCheques { get; set; }
|
||||
public DbSet<SupplierPayment> SupplierPayments { get; set; }
|
||||
public DbSet<SupplierPaymentGRN> SupplierPaymentGRNs { get; set; }
|
||||
public DbSet<Notification> Notifications { get; set; }
|
||||
public DbSet<NotificationRecipient> NotificationRecipients { get; set; }
|
||||
public DbSet<ExpenseCategory> ExpenseCategories { get; set; }
|
||||
public DbSet<ExpenseVendor> ExpenseVendors { get; set; }
|
||||
public DbSet<Expense> Expenses { get; set; }
|
||||
public DbSet<ExpenseAttachment> ExpenseAttachments { get; set; }
|
||||
public DbSet<RecurringExpenseTemplate> RecurringExpenseTemplates { get; set; }
|
||||
public DbSet<StoreCreditMovement> StoreCreditMovements { get; set; }
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
@@ -166,6 +183,54 @@ public class AppDBContext(DbContextOptions<AppDBContext> options) : DbContext(op
|
||||
.HasForeignKey(i => i.FK_ShopPOId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
modelBuilder.Entity<InvoiceItem>()
|
||||
.HasOne(i => i.Invoice)
|
||||
.WithMany(inv => inv.Items)
|
||||
.HasForeignKey(i => i.FK_InvoiceId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
modelBuilder.Entity<InvoiceItem>()
|
||||
.HasOne(i => i.ShopPOItem)
|
||||
.WithMany()
|
||||
.HasForeignKey(i => i.FK_ShopPOItemId)
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired(false);
|
||||
|
||||
modelBuilder.Entity<InvoiceItem>()
|
||||
.HasOne(i => i.Item)
|
||||
.WithMany()
|
||||
.HasForeignKey(i => i.FK_ItemId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
modelBuilder.Entity<InvoiceVersion>()
|
||||
.HasOne(v => v.Invoice)
|
||||
.WithMany()
|
||||
.HasForeignKey(v => v.FK_InvoiceId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
modelBuilder.Entity<InvoiceVersion>()
|
||||
.HasIndex(v => new { v.FK_InvoiceId, v.VersionNumber })
|
||||
.IsUnique();
|
||||
|
||||
modelBuilder.Entity<InvoiceVersionItem>()
|
||||
.HasOne(i => i.InvoiceVersion)
|
||||
.WithMany(v => v.Items)
|
||||
.HasForeignKey(i => i.FK_InvoiceVersionId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
modelBuilder.Entity<InvoiceVersionItem>()
|
||||
.HasOne(i => i.ShopPOItem)
|
||||
.WithMany()
|
||||
.HasForeignKey(i => i.FK_ShopPOItemId)
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired(false);
|
||||
|
||||
modelBuilder.Entity<InvoiceVersionItem>()
|
||||
.HasOne(i => i.Item)
|
||||
.WithMany()
|
||||
.HasForeignKey(i => i.FK_ItemId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
modelBuilder.Entity<Invoice>()
|
||||
.HasMany(i => i.Payments)
|
||||
.WithOne(p => p.Invoice)
|
||||
@@ -594,7 +659,247 @@ public class AppDBContext(DbContextOptions<AppDBContext> options) : DbContext(op
|
||||
new SysConfig { Key = Distribution.Features.SysConfig.SysConfigService.KeyDefaultWarrantyPeriodDays, Value = "0", UpdatedAt = new DateTime(2026, 10, 1), UpdatedBy = null }
|
||||
);
|
||||
|
||||
// --- Payment service: Bank Accounts & Cheques ---
|
||||
|
||||
modelBuilder.Entity<BankAccount>()
|
||||
.Property(a => a.AccountName).HasMaxLength(200);
|
||||
modelBuilder.Entity<BankAccount>()
|
||||
.Property(a => a.BankName).HasMaxLength(200);
|
||||
modelBuilder.Entity<BankAccount>()
|
||||
.Property(a => a.CashAccountTypeName).HasMaxLength(100);
|
||||
modelBuilder.Entity<BankAccount>()
|
||||
.Property(a => a.AccountNumber).HasMaxLength(100);
|
||||
modelBuilder.Entity<BankAccount>()
|
||||
.Property(a => a.CurrencyCode).HasMaxLength(10);
|
||||
modelBuilder.Entity<BankAccount>()
|
||||
.HasIndex(a => new { a.AccountType, a.AccountName })
|
||||
.IsUnique();
|
||||
|
||||
modelBuilder.Entity<BankAccount>().HasData(new BankAccount
|
||||
{
|
||||
Id = new Guid("11111111-1111-1111-1111-000000000001"),
|
||||
CreatedAt = new DateTime(2026, 10, 19),
|
||||
IsActive = true,
|
||||
AccountType = BankAccountType.Cash,
|
||||
AccountName = "Petty Cash",
|
||||
CashAccountTypeName = "Petty Cash",
|
||||
CurrencyCode = "LKR",
|
||||
IsDefault = true
|
||||
});
|
||||
|
||||
modelBuilder.Entity<ChequeBook>()
|
||||
.Property(b => b.ChequeBookNo).HasMaxLength(100);
|
||||
modelBuilder.Entity<ChequeBook>()
|
||||
.Property(b => b.StartChequeNo).HasMaxLength(50);
|
||||
modelBuilder.Entity<ChequeBook>()
|
||||
.Property(b => b.EndChequeNo).HasMaxLength(50);
|
||||
modelBuilder.Entity<ChequeBook>()
|
||||
.HasIndex(b => b.ChequeBookNo).IsUnique();
|
||||
modelBuilder.Entity<ChequeBook>()
|
||||
.HasOne(b => b.BankAccount)
|
||||
.WithMany()
|
||||
.HasForeignKey(b => b.FK_BankAccountId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
modelBuilder.Entity<ChequePage>()
|
||||
.Property(p => p.ChequeNo).HasMaxLength(50);
|
||||
modelBuilder.Entity<ChequePage>()
|
||||
.Property(p => p.CurrencyCode).HasMaxLength(10);
|
||||
modelBuilder.Entity<ChequePage>()
|
||||
.HasIndex(p => p.ChequeNo).IsUnique();
|
||||
modelBuilder.Entity<ChequePage>()
|
||||
.HasOne(p => p.ChequeBook)
|
||||
.WithMany(b => b.Pages)
|
||||
.HasForeignKey(p => p.FK_ChequeBookId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
modelBuilder.Entity<ReceivedCheque>()
|
||||
.Property(c => c.ChequeNo).HasMaxLength(50);
|
||||
modelBuilder.Entity<ReceivedCheque>()
|
||||
.Property(c => c.ReceivedFromName).HasMaxLength(200);
|
||||
modelBuilder.Entity<ReceivedCheque>()
|
||||
.HasIndex(c => c.ChequeNo);
|
||||
modelBuilder.Entity<ReceivedCheque>()
|
||||
.HasOne(c => c.DepositBankAccount)
|
||||
.WithMany()
|
||||
.HasForeignKey(c => c.FK_DepositBankAccountId)
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired(false);
|
||||
|
||||
// --- SupplierPayment ---
|
||||
|
||||
modelBuilder.Entity<SupplierPayment>()
|
||||
.HasOne(p => p.Supplier)
|
||||
.WithMany()
|
||||
.HasForeignKey(p => p.FK_SupplierId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
modelBuilder.Entity<SupplierPayment>()
|
||||
.HasOne(p => p.BankAccount)
|
||||
.WithMany()
|
||||
.HasForeignKey(p => p.FK_BankAccountId)
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired(false);
|
||||
|
||||
modelBuilder.Entity<SupplierPayment>()
|
||||
.HasOne(p => p.ChequePage)
|
||||
.WithMany()
|
||||
.HasForeignKey(p => p.FK_ChequePageId)
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired(false);
|
||||
|
||||
modelBuilder.Entity<SupplierPayment>()
|
||||
.Property(p => p.Amount).HasPrecision(18, 2);
|
||||
|
||||
modelBuilder.Entity<SupplierPaymentGRN>()
|
||||
.HasOne(a => a.SupplierPayment)
|
||||
.WithMany(p => p.Allocations)
|
||||
.HasForeignKey(a => a.FK_SupplierPaymentId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
modelBuilder.Entity<SupplierPaymentGRN>()
|
||||
.HasOne(a => a.GoodsReceipt)
|
||||
.WithMany()
|
||||
.HasForeignKey(a => a.FK_GoodsReceiptId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
modelBuilder.Entity<SupplierPaymentGRN>()
|
||||
.HasIndex(a => a.FK_GoodsReceiptId);
|
||||
|
||||
// --- Notifications ---
|
||||
|
||||
modelBuilder.Entity<Notification>()
|
||||
.HasIndex(n => new { n.EntityType, n.EntityId });
|
||||
|
||||
modelBuilder.Entity<NotificationRecipient>()
|
||||
.HasOne(r => r.Notification)
|
||||
.WithMany(n => n.Recipients)
|
||||
.HasForeignKey(r => r.FK_NotificationId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
modelBuilder.Entity<NotificationRecipient>()
|
||||
.HasOne(r => r.User)
|
||||
.WithMany()
|
||||
.HasForeignKey(r => r.FK_UserId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
modelBuilder.Entity<NotificationRecipient>()
|
||||
.HasIndex(r => new { r.FK_UserId, r.IsRead });
|
||||
|
||||
modelBuilder.Entity<NotificationRecipient>()
|
||||
.HasIndex(r => new { r.FK_NotificationId, r.FK_UserId })
|
||||
.IsUnique();
|
||||
|
||||
// --- StoreCreditMovement ---
|
||||
|
||||
modelBuilder.Entity<StoreCreditMovement>()
|
||||
.HasOne(m => m.Store)
|
||||
.WithMany()
|
||||
.HasForeignKey(m => m.FK_StoreId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
modelBuilder.Entity<StoreCreditMovement>()
|
||||
.HasOne(m => m.Invoice)
|
||||
.WithMany()
|
||||
.HasForeignKey(m => m.FK_InvoiceId)
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired(false);
|
||||
|
||||
modelBuilder.Entity<StoreCreditMovement>()
|
||||
.HasOne(m => m.Payment)
|
||||
.WithMany()
|
||||
.HasForeignKey(m => m.FK_PaymentId)
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired(false);
|
||||
|
||||
modelBuilder.Entity<StoreCreditMovement>()
|
||||
.Property(m => m.Amount).HasPrecision(18, 2);
|
||||
|
||||
modelBuilder.Entity<StoreCreditMovement>()
|
||||
.HasIndex(m => new { m.FK_StoreId, m.OccurredAt });
|
||||
|
||||
// --- Expense ---
|
||||
|
||||
modelBuilder.Entity<ExpenseCategory>()
|
||||
.HasOne(c => c.Parent)
|
||||
.WithMany()
|
||||
.HasForeignKey(c => c.FK_ParentCategoryId)
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired(false);
|
||||
|
||||
modelBuilder.Entity<ExpenseCategory>()
|
||||
.Property(c => c.Name).HasMaxLength(100);
|
||||
modelBuilder.Entity<ExpenseCategory>()
|
||||
.Property(c => c.Code).HasMaxLength(50);
|
||||
modelBuilder.Entity<ExpenseCategory>()
|
||||
.HasIndex(c => new { c.FK_ParentCategoryId, c.Name }).IsUnique();
|
||||
|
||||
modelBuilder.Entity<ExpenseVendor>()
|
||||
.Property(v => v.Name).HasMaxLength(200);
|
||||
modelBuilder.Entity<ExpenseVendor>()
|
||||
.HasIndex(v => v.Name).IsUnique();
|
||||
|
||||
modelBuilder.Entity<Expense>()
|
||||
.HasOne(e => e.Category).WithMany()
|
||||
.HasForeignKey(e => e.FK_CategoryId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
modelBuilder.Entity<Expense>()
|
||||
.HasOne(e => e.Warehouse).WithMany()
|
||||
.HasForeignKey(e => e.FK_WarehouseId)
|
||||
.OnDelete(DeleteBehavior.Restrict).IsRequired(false);
|
||||
modelBuilder.Entity<Expense>()
|
||||
.HasOne(e => e.Vendor).WithMany()
|
||||
.HasForeignKey(e => e.FK_VendorId)
|
||||
.OnDelete(DeleteBehavior.Restrict).IsRequired(false);
|
||||
modelBuilder.Entity<Expense>()
|
||||
.HasOne(e => e.BankAccount).WithMany()
|
||||
.HasForeignKey(e => e.FK_BankAccountId)
|
||||
.OnDelete(DeleteBehavior.Restrict).IsRequired(false);
|
||||
modelBuilder.Entity<Expense>()
|
||||
.HasOne(e => e.ChequePage).WithMany()
|
||||
.HasForeignKey(e => e.FK_ChequePageId)
|
||||
.OnDelete(DeleteBehavior.Restrict).IsRequired(false);
|
||||
modelBuilder.Entity<Expense>()
|
||||
.HasOne(e => e.RecurringTemplate).WithMany()
|
||||
.HasForeignKey(e => e.FK_RecurringTemplateId)
|
||||
.OnDelete(DeleteBehavior.SetNull).IsRequired(false);
|
||||
modelBuilder.Entity<Expense>()
|
||||
.Property(e => e.Payee).HasMaxLength(200);
|
||||
modelBuilder.Entity<Expense>()
|
||||
.Property(e => e.BillRef).HasMaxLength(100);
|
||||
modelBuilder.Entity<Expense>()
|
||||
.HasIndex(e => new { e.Status, e.BillDate });
|
||||
|
||||
modelBuilder.Entity<ExpenseAttachment>()
|
||||
.HasOne(a => a.Expense).WithMany(e => e.Attachments)
|
||||
.HasForeignKey(a => a.FK_ExpenseId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
modelBuilder.Entity<ExpenseAttachment>()
|
||||
.Property(a => a.OriginalFileName).HasMaxLength(260);
|
||||
modelBuilder.Entity<ExpenseAttachment>()
|
||||
.Property(a => a.StoredPath).HasMaxLength(500);
|
||||
modelBuilder.Entity<ExpenseAttachment>()
|
||||
.Property(a => a.ContentType).HasMaxLength(100);
|
||||
|
||||
modelBuilder.Entity<RecurringExpenseTemplate>()
|
||||
.HasOne(t => t.Category).WithMany()
|
||||
.HasForeignKey(t => t.FK_CategoryId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
modelBuilder.Entity<RecurringExpenseTemplate>()
|
||||
.HasOne(t => t.Warehouse).WithMany()
|
||||
.HasForeignKey(t => t.FK_WarehouseId)
|
||||
.OnDelete(DeleteBehavior.Restrict).IsRequired(false);
|
||||
modelBuilder.Entity<RecurringExpenseTemplate>()
|
||||
.HasOne(t => t.Vendor).WithMany()
|
||||
.HasForeignKey(t => t.FK_VendorId)
|
||||
.OnDelete(DeleteBehavior.Restrict).IsRequired(false);
|
||||
modelBuilder.Entity<RecurringExpenseTemplate>()
|
||||
.Property(t => t.Name).HasMaxLength(200);
|
||||
modelBuilder.Entity<RecurringExpenseTemplate>()
|
||||
.HasIndex(t => new { t.IsPaused, t.NextRunDate });
|
||||
|
||||
Seed.RoleAndPermissionSeed.Apply(modelBuilder);
|
||||
Seed.ExpenseSeed.Apply(modelBuilder);
|
||||
Seed.MasterDataSeed.Apply(modelBuilder);
|
||||
Seed.OperationsDataSeed.Apply(modelBuilder);
|
||||
Seed.ProcurementSeed.Apply(modelBuilder);
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
using Distribution.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Distribution.Data.Seed;
|
||||
|
||||
public static class ExpenseSeed
|
||||
{
|
||||
private static readonly DateTime SeedDate = new(2026, 10, 21);
|
||||
|
||||
// Top-level categories
|
||||
private static readonly Guid CatSalaries = new("30000000-0000-0000-0000-000000000001");
|
||||
private static readonly Guid CatUtilities = new("30000000-0000-0000-0000-000000000002");
|
||||
private static readonly Guid CatRent = new("30000000-0000-0000-0000-000000000003");
|
||||
private static readonly Guid CatVehicle = new("30000000-0000-0000-0000-000000000004");
|
||||
private static readonly Guid CatOffice = new("30000000-0000-0000-0000-000000000005");
|
||||
private static readonly Guid CatBankFees = new("30000000-0000-0000-0000-000000000006");
|
||||
private static readonly Guid CatMisc = new("30000000-0000-0000-0000-000000000007");
|
||||
|
||||
public static void Apply(ModelBuilder modelBuilder)
|
||||
{
|
||||
modelBuilder.Entity<ExpenseCategory>().HasData(
|
||||
// Top-level
|
||||
new ExpenseCategory { Id = CatSalaries, Name = "Salaries", Code = "SAL", CreatedAt = SeedDate, IsActive = true },
|
||||
new ExpenseCategory { Id = CatUtilities, Name = "Utilities", Code = "UTL", CreatedAt = SeedDate, IsActive = true },
|
||||
new ExpenseCategory { Id = CatRent, Name = "Rent", Code = "RNT", CreatedAt = SeedDate, IsActive = true },
|
||||
new ExpenseCategory { Id = CatVehicle, Name = "Vehicle", Code = "VEH", CreatedAt = SeedDate, IsActive = true },
|
||||
new ExpenseCategory { Id = CatOffice, Name = "Office", Code = "OFF", CreatedAt = SeedDate, IsActive = true },
|
||||
new ExpenseCategory { Id = CatBankFees, Name = "Bank Fees", Code = "BNK", CreatedAt = SeedDate, IsActive = true },
|
||||
new ExpenseCategory { Id = CatMisc, Name = "Miscellaneous", Code = "MSC", CreatedAt = SeedDate, IsActive = true },
|
||||
|
||||
// Titles under Salaries
|
||||
new ExpenseCategory { Id = new("30000000-0000-0000-0000-000000000101"), Name = "Basic Salary", FK_ParentCategoryId = CatSalaries, CreatedAt = SeedDate, IsActive = true },
|
||||
new ExpenseCategory { Id = new("30000000-0000-0000-0000-000000000102"), Name = "Bonus", FK_ParentCategoryId = CatSalaries, CreatedAt = SeedDate, IsActive = true },
|
||||
new ExpenseCategory { Id = new("30000000-0000-0000-0000-000000000103"), Name = "Overtime", FK_ParentCategoryId = CatSalaries, CreatedAt = SeedDate, IsActive = true },
|
||||
|
||||
// Titles under Utilities
|
||||
new ExpenseCategory { Id = new("30000000-0000-0000-0000-000000000201"), Name = "Electricity", FK_ParentCategoryId = CatUtilities, CreatedAt = SeedDate, IsActive = true },
|
||||
new ExpenseCategory { Id = new("30000000-0000-0000-0000-000000000202"), Name = "Water", FK_ParentCategoryId = CatUtilities, CreatedAt = SeedDate, IsActive = true },
|
||||
new ExpenseCategory { Id = new("30000000-0000-0000-0000-000000000203"), Name = "Internet", FK_ParentCategoryId = CatUtilities, CreatedAt = SeedDate, IsActive = true },
|
||||
|
||||
// Titles under Rent
|
||||
new ExpenseCategory { Id = new("30000000-0000-0000-0000-000000000301"), Name = "Warehouse Rent", FK_ParentCategoryId = CatRent, CreatedAt = SeedDate, IsActive = true },
|
||||
new ExpenseCategory { Id = new("30000000-0000-0000-0000-000000000302"), Name = "Office Rent", FK_ParentCategoryId = CatRent, CreatedAt = SeedDate, IsActive = true },
|
||||
|
||||
// Titles under Vehicle
|
||||
new ExpenseCategory { Id = new("30000000-0000-0000-0000-000000000401"), Name = "Fuel", FK_ParentCategoryId = CatVehicle, CreatedAt = SeedDate, IsActive = true },
|
||||
new ExpenseCategory { Id = new("30000000-0000-0000-0000-000000000402"), Name = "Maintenance", FK_ParentCategoryId = CatVehicle, CreatedAt = SeedDate, IsActive = true },
|
||||
new ExpenseCategory { Id = new("30000000-0000-0000-0000-000000000403"), Name = "Insurance", FK_ParentCategoryId = CatVehicle, CreatedAt = SeedDate, IsActive = true },
|
||||
|
||||
// Titles under Office
|
||||
new ExpenseCategory { Id = new("30000000-0000-0000-0000-000000000501"), Name = "Stationery", FK_ParentCategoryId = CatOffice, CreatedAt = SeedDate, IsActive = true },
|
||||
new ExpenseCategory { Id = new("30000000-0000-0000-0000-000000000502"), Name = "Cleaning", FK_ParentCategoryId = CatOffice, CreatedAt = SeedDate, IsActive = true },
|
||||
|
||||
// Titles under Bank Fees
|
||||
new ExpenseCategory { Id = new("30000000-0000-0000-0000-000000000601"), Name = "Service Charges", FK_ParentCategoryId = CatBankFees, CreatedAt = SeedDate, IsActive = true },
|
||||
|
||||
// Titles under Misc
|
||||
new ExpenseCategory { Id = new("30000000-0000-0000-0000-000000000701"), Name = "Other", FK_ParentCategoryId = CatMisc, CreatedAt = SeedDate, IsActive = true }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -68,6 +68,13 @@ public static class RoleAndPermissionSeed
|
||||
private static readonly Guid PermSupplierClaimManage= new("20000000-0000-0000-0000-000000000048");
|
||||
private static readonly Guid PermSysConfigView = new("20000000-0000-0000-0000-000000000049");
|
||||
private static readonly Guid PermSysConfigManage = new("20000000-0000-0000-0000-000000000050");
|
||||
private static readonly Guid PermExpenseView = new("20000000-0000-0000-0000-000000000051");
|
||||
private static readonly Guid PermExpenseCreate = new("20000000-0000-0000-0000-000000000052");
|
||||
private static readonly Guid PermExpensePost = new("20000000-0000-0000-0000-000000000053");
|
||||
private static readonly Guid PermExpensePay = new("20000000-0000-0000-0000-000000000054");
|
||||
private static readonly Guid PermExpenseVoid = new("20000000-0000-0000-0000-000000000055");
|
||||
private static readonly Guid PermExpenseManageCats = new("20000000-0000-0000-0000-000000000056");
|
||||
private static readonly Guid PermExpenseManageRec = new("20000000-0000-0000-0000-000000000057");
|
||||
|
||||
public static void Apply(ModelBuilder modelBuilder)
|
||||
{
|
||||
@@ -134,7 +141,14 @@ public static class RoleAndPermissionSeed
|
||||
new Permission { Id = PermSupplierClaimView, Code = "supplier-claim.view", Description = "View supplier claims", CreatedAt = SeedDate, IsActive = true },
|
||||
new Permission { Id = PermSupplierClaimManage,Code = "supplier-claim.manage", Description = "Create and resolve supplier claims", CreatedAt = SeedDate, IsActive = true },
|
||||
new Permission { Id = PermSysConfigView, Code = "sys-config.view", Description = "View system configuration settings", CreatedAt = SeedDate, IsActive = true },
|
||||
new Permission { Id = PermSysConfigManage, Code = "sys-config.manage", Description = "Update system configuration settings", CreatedAt = SeedDate, IsActive = true }
|
||||
new Permission { Id = PermSysConfigManage, Code = "sys-config.manage", Description = "Update system configuration settings", CreatedAt = SeedDate, IsActive = true },
|
||||
new Permission { Id = PermExpenseView, Code = "expense.view", Description = "View expenses, categories, vendors and recurring templates", CreatedAt = SeedDate, IsActive = true },
|
||||
new Permission { Id = PermExpenseCreate, Code = "expense.create", Description = "Create and edit draft expenses", CreatedAt = SeedDate, IsActive = true },
|
||||
new Permission { Id = PermExpensePost, Code = "expense.post", Description = "Post a draft expense (approve it)", CreatedAt = SeedDate, IsActive = true },
|
||||
new Permission { Id = PermExpensePay, Code = "expense.pay", Description = "Pay a posted expense (debit a bank account or issue a cheque)", CreatedAt = SeedDate, IsActive = true },
|
||||
new Permission { Id = PermExpenseVoid, Code = "expense.void", Description = "Void an expense", CreatedAt = SeedDate, IsActive = true },
|
||||
new Permission { Id = PermExpenseManageCats, Code = "expense.manage_categories", Description = "Manage expense categories, titles and vendors", CreatedAt = SeedDate, IsActive = true },
|
||||
new Permission { Id = PermExpenseManageRec, Code = "expense.manage_recurring", Description = "Manage recurring expense templates", CreatedAt = SeedDate, IsActive = true }
|
||||
);
|
||||
|
||||
// SuperAdmin gets every permission
|
||||
@@ -191,7 +205,14 @@ public static class RoleAndPermissionSeed
|
||||
new RolePermission { FK_RoleId = SuperAdminRoleId, FK_PermissionId = PermSupplierClaimView, CreatedAt = SeedDate, IsActive = true },
|
||||
new RolePermission { FK_RoleId = SuperAdminRoleId, FK_PermissionId = PermSupplierClaimManage,CreatedAt = SeedDate, IsActive = true },
|
||||
new RolePermission { FK_RoleId = SuperAdminRoleId, FK_PermissionId = PermSysConfigView, CreatedAt = SeedDate, IsActive = true },
|
||||
new RolePermission { FK_RoleId = SuperAdminRoleId, FK_PermissionId = PermSysConfigManage, CreatedAt = SeedDate, IsActive = true }
|
||||
new RolePermission { FK_RoleId = SuperAdminRoleId, FK_PermissionId = PermSysConfigManage, CreatedAt = SeedDate, IsActive = true },
|
||||
new RolePermission { FK_RoleId = SuperAdminRoleId, FK_PermissionId = PermExpenseView, CreatedAt = SeedDate, IsActive = true },
|
||||
new RolePermission { FK_RoleId = SuperAdminRoleId, FK_PermissionId = PermExpenseCreate, CreatedAt = SeedDate, IsActive = true },
|
||||
new RolePermission { FK_RoleId = SuperAdminRoleId, FK_PermissionId = PermExpensePost, CreatedAt = SeedDate, IsActive = true },
|
||||
new RolePermission { FK_RoleId = SuperAdminRoleId, FK_PermissionId = PermExpensePay, CreatedAt = SeedDate, IsActive = true },
|
||||
new RolePermission { FK_RoleId = SuperAdminRoleId, FK_PermissionId = PermExpenseVoid, CreatedAt = SeedDate, IsActive = true },
|
||||
new RolePermission { FK_RoleId = SuperAdminRoleId, FK_PermissionId = PermExpenseManageCats, CreatedAt = SeedDate, IsActive = true },
|
||||
new RolePermission { FK_RoleId = SuperAdminRoleId, FK_PermissionId = PermExpenseManageRec, CreatedAt = SeedDate, IsActive = true }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
using Distribution.Models;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Distribution.Features.BankAccount;
|
||||
|
||||
[ApiController]
|
||||
[Route("api")]
|
||||
public class BankAccountController(BankAccountService service) : ControllerBase
|
||||
{
|
||||
[HttpGet("bank-accounts")]
|
||||
public async Task<IActionResult> List([FromQuery] BankAccountType? accountType)
|
||||
{
|
||||
var result = await service.ListAsync(accountType);
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
[HttpPost("bank-accounts")]
|
||||
public async Task<IActionResult> CreateBank(CreateBankAccountRequest request, CancellationToken ct)
|
||||
{
|
||||
try { return Ok(await service.CreateBankAsync(request, ct)); }
|
||||
catch (InvalidOperationException ex) { return BadRequest(new { error = ex.Message }); }
|
||||
}
|
||||
|
||||
[HttpPost("cash-accounts")]
|
||||
public async Task<IActionResult> CreateCash(CreateCashAccountRequest request, CancellationToken ct)
|
||||
{
|
||||
try { return Ok(await service.CreateCashAsync(request, ct)); }
|
||||
catch (InvalidOperationException ex) { return BadRequest(new { error = ex.Message }); }
|
||||
}
|
||||
|
||||
[HttpPut("cash-accounts/{id:guid}")]
|
||||
public async Task<IActionResult> UpdateCash(Guid id, UpdateCashAccountRequest request, CancellationToken ct)
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = await service.UpdateCashAsync(id, request, ct);
|
||||
return result is null ? NotFound() : Ok(result);
|
||||
}
|
||||
catch (InvalidOperationException ex) { return BadRequest(new { error = ex.Message }); }
|
||||
}
|
||||
|
||||
[HttpPatch("cash-accounts/{id:guid}/status")]
|
||||
public async Task<IActionResult> SetCashStatus(Guid id, SetCashAccountStatusRequest request, CancellationToken ct)
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = await service.SetCashStatusAsync(id, request, ct);
|
||||
return result is null ? NotFound() : Ok(result);
|
||||
}
|
||||
catch (InvalidOperationException ex) { return BadRequest(new { error = ex.Message }); }
|
||||
}
|
||||
|
||||
[HttpDelete("cash-accounts/{id:guid}")]
|
||||
public async Task<IActionResult> DeleteCash(Guid id, CancellationToken ct)
|
||||
{
|
||||
try
|
||||
{
|
||||
var ok = await service.DeleteCashAsync(id, ct);
|
||||
return ok ? NoContent() : NotFound();
|
||||
}
|
||||
catch (InvalidOperationException ex) { return BadRequest(new { error = ex.Message }); }
|
||||
}
|
||||
|
||||
[HttpGet("cash-account-types")]
|
||||
public async Task<IActionResult> ListCashAccountTypes() => Ok(await service.ListCashAccountTypesAsync());
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
using Distribution.Models;
|
||||
|
||||
namespace Distribution.Features.BankAccount;
|
||||
|
||||
public record BankAccountResponse(
|
||||
BankAccountType AccountType,
|
||||
Guid AccountId,
|
||||
string AccountName,
|
||||
string? BankName,
|
||||
string? CashAccountTypeName,
|
||||
string? AccountNumber,
|
||||
string CurrencyCode,
|
||||
DateTime CreatedAt,
|
||||
bool? IsActive,
|
||||
bool? IsDefault);
|
||||
|
||||
public record CreateBankAccountRequest(
|
||||
string AccountName,
|
||||
string? BankName,
|
||||
string? AccountNumber,
|
||||
string? CurrencyCode);
|
||||
|
||||
public record CreateCashAccountRequest(
|
||||
string AccountName,
|
||||
string CashAccountTypeName,
|
||||
string? AccountNumber,
|
||||
string? CurrencyCode);
|
||||
|
||||
public record UpdateCashAccountRequest(string AccountName);
|
||||
|
||||
public record SetCashAccountStatusRequest(bool IsActive);
|
||||
|
||||
public record CashAccountTypeDto(string Name);
|
||||
@@ -0,0 +1,40 @@
|
||||
using Distribution.Data;
|
||||
using Distribution.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Distribution.Features.BankAccount;
|
||||
|
||||
public class BankAccountRepository(AppDBContext context) : IBankAccountRepository
|
||||
{
|
||||
public async Task<Models.BankAccount?> GetByIdAsync(Guid id) =>
|
||||
await context.BankAccounts.FirstOrDefaultAsync(a => a.Id == id);
|
||||
|
||||
public async Task<List<Models.BankAccount>> ListAsync(BankAccountType? accountType)
|
||||
{
|
||||
var query = context.BankAccounts.AsQueryable();
|
||||
if (accountType is not null)
|
||||
query = query.Where(a => a.AccountType == accountType);
|
||||
return await query.OrderByDescending(a => a.CreatedAt).ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<List<string>> ListCashAccountTypeNamesAsync() =>
|
||||
await context.BankAccounts
|
||||
.Where(a => a.AccountType == BankAccountType.Cash && a.CashAccountTypeName != null)
|
||||
.Select(a => a.CashAccountTypeName!)
|
||||
.Distinct()
|
||||
.OrderBy(n => n)
|
||||
.ToListAsync();
|
||||
|
||||
public async Task<bool> ExistsByNameAsync(BankAccountType type, string accountName, Guid? excludeId = null) =>
|
||||
await context.BankAccounts.AnyAsync(a =>
|
||||
a.AccountType == type &&
|
||||
a.AccountName == accountName &&
|
||||
(excludeId == null || a.Id != excludeId));
|
||||
|
||||
public async Task AddAsync(Models.BankAccount account) =>
|
||||
await context.BankAccounts.AddAsync(account);
|
||||
|
||||
public void Update(Models.BankAccount account) => context.BankAccounts.Update(account);
|
||||
|
||||
public void Remove(Models.BankAccount account) => context.BankAccounts.Remove(account);
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
using Distribution.Infra.UoW;
|
||||
using Distribution.Models;
|
||||
|
||||
namespace Distribution.Features.BankAccount;
|
||||
|
||||
public class BankAccountService(IBankAccountRepository repo, IUnitOfWork uow)
|
||||
{
|
||||
private const string DefaultCurrency = "LKR";
|
||||
|
||||
private static BankAccountResponse ToResponse(Models.BankAccount a) =>
|
||||
new(
|
||||
a.AccountType,
|
||||
a.Id,
|
||||
a.AccountName,
|
||||
a.AccountType == BankAccountType.Bank ? a.BankName : null,
|
||||
a.AccountType == BankAccountType.Cash ? a.CashAccountTypeName : null,
|
||||
a.AccountNumber,
|
||||
a.CurrencyCode,
|
||||
a.CreatedAt,
|
||||
a.AccountType == BankAccountType.Cash ? a.IsActive : null,
|
||||
a.AccountType == BankAccountType.Cash ? a.IsDefault : null);
|
||||
|
||||
public async Task<List<BankAccountResponse>> ListAsync(BankAccountType? accountType) =>
|
||||
(await repo.ListAsync(accountType)).Select(ToResponse).ToList();
|
||||
|
||||
public async Task<List<CashAccountTypeDto>> ListCashAccountTypesAsync() =>
|
||||
(await repo.ListCashAccountTypeNamesAsync()).Select(n => new CashAccountTypeDto(n)).ToList();
|
||||
|
||||
public async Task<BankAccountResponse> CreateBankAsync(CreateBankAccountRequest request, CancellationToken ct = default)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(request.AccountName))
|
||||
throw new InvalidOperationException("AccountName is required");
|
||||
if (await repo.ExistsByNameAsync(BankAccountType.Bank, request.AccountName))
|
||||
throw new InvalidOperationException("A bank account with this name already exists");
|
||||
|
||||
var account = new Models.BankAccount
|
||||
{
|
||||
AccountType = BankAccountType.Bank,
|
||||
AccountName = request.AccountName,
|
||||
BankName = request.BankName,
|
||||
AccountNumber = request.AccountNumber,
|
||||
CurrencyCode = string.IsNullOrWhiteSpace(request.CurrencyCode) ? DefaultCurrency : request.CurrencyCode!,
|
||||
IsActive = true,
|
||||
IsDefault = false
|
||||
};
|
||||
|
||||
await repo.AddAsync(account);
|
||||
await uow.CommitAsync(ct);
|
||||
return ToResponse(account);
|
||||
}
|
||||
|
||||
public async Task<BankAccountResponse> CreateCashAsync(CreateCashAccountRequest request, CancellationToken ct = default)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(request.AccountName))
|
||||
throw new InvalidOperationException("AccountName is required");
|
||||
if (string.IsNullOrWhiteSpace(request.CashAccountTypeName))
|
||||
throw new InvalidOperationException("CashAccountTypeName is required");
|
||||
if (await repo.ExistsByNameAsync(BankAccountType.Cash, request.AccountName))
|
||||
throw new InvalidOperationException("A cash account with this name already exists");
|
||||
|
||||
var account = new Models.BankAccount
|
||||
{
|
||||
AccountType = BankAccountType.Cash,
|
||||
AccountName = request.AccountName,
|
||||
CashAccountTypeName = request.CashAccountTypeName,
|
||||
AccountNumber = request.AccountNumber,
|
||||
CurrencyCode = string.IsNullOrWhiteSpace(request.CurrencyCode) ? DefaultCurrency : request.CurrencyCode!,
|
||||
IsActive = true,
|
||||
IsDefault = false
|
||||
};
|
||||
|
||||
await repo.AddAsync(account);
|
||||
await uow.CommitAsync(ct);
|
||||
return ToResponse(account);
|
||||
}
|
||||
|
||||
public async Task<BankAccountResponse?> UpdateCashAsync(Guid id, UpdateCashAccountRequest request, CancellationToken ct = default)
|
||||
{
|
||||
var account = await repo.GetByIdAsync(id);
|
||||
if (account is null || account.AccountType != BankAccountType.Cash) return null;
|
||||
if (string.IsNullOrWhiteSpace(request.AccountName))
|
||||
throw new InvalidOperationException("AccountName is required");
|
||||
if (await repo.ExistsByNameAsync(BankAccountType.Cash, request.AccountName, excludeId: id))
|
||||
throw new InvalidOperationException("A cash account with this name already exists");
|
||||
|
||||
account.AccountName = request.AccountName;
|
||||
repo.Update(account);
|
||||
await uow.CommitAsync(ct);
|
||||
return ToResponse(account);
|
||||
}
|
||||
|
||||
public async Task<BankAccountResponse?> SetCashStatusAsync(Guid id, SetCashAccountStatusRequest request, CancellationToken ct = default)
|
||||
{
|
||||
var account = await repo.GetByIdAsync(id);
|
||||
if (account is null || account.AccountType != BankAccountType.Cash) return null;
|
||||
if (account.IsDefault && !request.IsActive)
|
||||
throw new InvalidOperationException("The default cash account cannot be deactivated");
|
||||
|
||||
account.IsActive = request.IsActive;
|
||||
repo.Update(account);
|
||||
await uow.CommitAsync(ct);
|
||||
return ToResponse(account);
|
||||
}
|
||||
|
||||
public async Task<bool> DeleteCashAsync(Guid id, CancellationToken ct = default)
|
||||
{
|
||||
var account = await repo.GetByIdAsync(id);
|
||||
if (account is null || account.AccountType != BankAccountType.Cash) return false;
|
||||
if (account.IsDefault)
|
||||
throw new InvalidOperationException("The default cash account cannot be deleted");
|
||||
|
||||
repo.Remove(account);
|
||||
await uow.CommitAsync(ct);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using Distribution.Models;
|
||||
|
||||
namespace Distribution.Features.BankAccount;
|
||||
|
||||
public interface IBankAccountRepository
|
||||
{
|
||||
Task<Models.BankAccount?> GetByIdAsync(Guid id);
|
||||
Task<List<Models.BankAccount>> ListAsync(BankAccountType? accountType);
|
||||
Task<List<string>> ListCashAccountTypeNamesAsync();
|
||||
Task<bool> ExistsByNameAsync(BankAccountType type, string accountName, Guid? excludeId = null);
|
||||
Task AddAsync(Models.BankAccount account);
|
||||
void Update(Models.BankAccount account);
|
||||
void Remove(Models.BankAccount account);
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
using Distribution.Models;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Distribution.Features.ChequeBook;
|
||||
|
||||
[ApiController]
|
||||
[Route("api")]
|
||||
public class ChequeBookController(ChequeBookService service) : ControllerBase
|
||||
{
|
||||
[HttpGet("cheque-books")]
|
||||
public async Task<IActionResult> List(
|
||||
[FromQuery] Guid? bankAccountId,
|
||||
[FromQuery] int? branchId,
|
||||
[FromQuery] ChequeBookStatus? status,
|
||||
[FromQuery] int page = 1,
|
||||
[FromQuery] int pageSize = 50)
|
||||
=> Ok(await service.ListAsync(bankAccountId, branchId, status, page, pageSize));
|
||||
|
||||
[HttpGet("cheque-books/{chequeBookNo}")]
|
||||
public async Task<IActionResult> Get(string chequeBookNo, [FromQuery] string? expand)
|
||||
{
|
||||
var expandPages = string.Equals(expand, "pages", StringComparison.OrdinalIgnoreCase);
|
||||
var result = await service.GetAsync(chequeBookNo, expandPages);
|
||||
return result is null ? NotFound() : Ok(result);
|
||||
}
|
||||
|
||||
[HttpPost("cheque-books")]
|
||||
public async Task<IActionResult> Create(CreateChequeBookRequest request, CancellationToken ct)
|
||||
{
|
||||
try { return Ok(await service.CreateAsync(request, ct)); }
|
||||
catch (InvalidOperationException ex) { return BadRequest(new { error = ex.Message }); }
|
||||
}
|
||||
|
||||
[HttpPut("cheque-pages/{chequeNo}/issue")]
|
||||
public async Task<IActionResult> Issue(string chequeNo, IssueChequePageRequest request, CancellationToken ct)
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = await service.IssueAsync(chequeNo, request, ct);
|
||||
return result is null ? NotFound() : Ok(result);
|
||||
}
|
||||
catch (InvalidOperationException ex) { return BadRequest(new { error = ex.Message }); }
|
||||
}
|
||||
|
||||
[HttpPut("cheque-pages/{chequeNo}/status")]
|
||||
public async Task<IActionResult> UpdateStatus(string chequeNo, UpdateChequePageStatusRequest request, CancellationToken ct)
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = await service.UpdatePageStatusAsync(chequeNo, request, ct);
|
||||
return result is null ? NotFound() : Ok(result);
|
||||
}
|
||||
catch (InvalidOperationException ex) { return BadRequest(new { error = ex.Message }); }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
using Distribution.Models;
|
||||
|
||||
namespace Distribution.Features.ChequeBook;
|
||||
|
||||
public record ChequePageResponse(
|
||||
Guid Id,
|
||||
string ChequeNo,
|
||||
string ChequeBookNo,
|
||||
ChequePageIssueStatus IssueStatus,
|
||||
PayeeType? PayeeType,
|
||||
int? PayeeId,
|
||||
string? PayeeName,
|
||||
DateTime? IssueDate,
|
||||
decimal? Amount,
|
||||
string? CurrencyCode,
|
||||
int? VoucherId,
|
||||
string? ReferenceNo,
|
||||
string? Purpose,
|
||||
bool? IsCrossCheque,
|
||||
bool? IsAccountPayee,
|
||||
bool? IsPostDated,
|
||||
string? Notes,
|
||||
string? PrintedBy,
|
||||
DateTime? PrintedAt,
|
||||
DateTime? ClearedDate,
|
||||
bool? ClearedByBank,
|
||||
string? CancelReason);
|
||||
|
||||
public record ChequeBookResponse(
|
||||
string ChequeBookNo,
|
||||
int BranchId,
|
||||
Guid BankAccountId,
|
||||
string StartChequeNo,
|
||||
string EndChequeNo,
|
||||
int TotalLeaves,
|
||||
DateTime ReceivedDate,
|
||||
string? Description,
|
||||
string? CreatedBy,
|
||||
ChequeBookStatus Status,
|
||||
List<ChequePageResponse> Pages);
|
||||
|
||||
public record PagedResult<T>(IReadOnlyList<T> Items, int TotalCount, int? Page, int? PageSize);
|
||||
|
||||
public record CreateChequeBookRequest(
|
||||
int BranchId,
|
||||
Guid BankAccountId,
|
||||
string ChequeBookNo,
|
||||
string StartChequeNo,
|
||||
string EndChequeNo,
|
||||
int TotalLeaves,
|
||||
DateTime ReceivedDate,
|
||||
string? Description,
|
||||
string? CreatedBy);
|
||||
|
||||
public record IssueChequePageRequest(
|
||||
PayeeType PayeeType,
|
||||
int? PayeeId,
|
||||
string PayeeName,
|
||||
DateTime IssueDate,
|
||||
decimal Amount,
|
||||
string? CurrencyCode,
|
||||
int? VoucherId,
|
||||
string? ReferenceNo,
|
||||
string? Purpose,
|
||||
bool? IsCrossCheque,
|
||||
bool? IsAccountPayee,
|
||||
bool? IsPostDated,
|
||||
string? Notes,
|
||||
string? PrintedBy);
|
||||
|
||||
public record UpdateChequePageStatusRequest(
|
||||
ChequePageStatusAction Action,
|
||||
DateTime? ClearedDate,
|
||||
string? CancelReason,
|
||||
string? PerformedBy);
|
||||
@@ -0,0 +1,46 @@
|
||||
using Distribution.Data;
|
||||
using Distribution.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Distribution.Features.ChequeBook;
|
||||
|
||||
public class ChequeBookRepository(AppDBContext context) : IChequeBookRepository
|
||||
{
|
||||
public async Task<Models.ChequeBook?> GetByNoAsync(string chequeBookNo, bool includePages)
|
||||
{
|
||||
var query = context.ChequeBooks.AsQueryable();
|
||||
if (includePages) query = query.Include(b => b.Pages);
|
||||
return await query.FirstOrDefaultAsync(b => b.ChequeBookNo == chequeBookNo);
|
||||
}
|
||||
|
||||
public async Task<(List<Models.ChequeBook> Items, int TotalCount)> ListAsync(
|
||||
Guid? bankAccountId, int? branchId, ChequeBookStatus? status, int page, int pageSize)
|
||||
{
|
||||
var query = context.ChequeBooks.AsQueryable();
|
||||
if (bankAccountId is not null) query = query.Where(b => b.FK_BankAccountId == bankAccountId);
|
||||
if (branchId is not null) query = query.Where(b => b.BranchId == branchId);
|
||||
if (status is not null) query = query.Where(b => b.Status == status);
|
||||
|
||||
var total = await query.CountAsync();
|
||||
var items = await query
|
||||
.OrderByDescending(b => b.CreatedAt)
|
||||
.Skip((page - 1) * pageSize)
|
||||
.Take(pageSize)
|
||||
.ToListAsync();
|
||||
return (items, total);
|
||||
}
|
||||
|
||||
public async Task<bool> BookNoExistsAsync(string chequeBookNo) =>
|
||||
await context.ChequeBooks.AnyAsync(b => b.ChequeBookNo == chequeBookNo);
|
||||
|
||||
public async Task AddAsync(Models.ChequeBook book) => await context.ChequeBooks.AddAsync(book);
|
||||
|
||||
public void Update(Models.ChequeBook book) => context.ChequeBooks.Update(book);
|
||||
|
||||
public async Task<Models.ChequePage?> GetPageByNoAsync(string chequeNo) =>
|
||||
await context.ChequePages
|
||||
.Include(p => p.ChequeBook)
|
||||
.FirstOrDefaultAsync(p => p.ChequeNo == chequeNo);
|
||||
|
||||
public void UpdatePage(Models.ChequePage page) => context.ChequePages.Update(page);
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
using Distribution.Features.BankAccount;
|
||||
using Distribution.Infra.UoW;
|
||||
using Distribution.Models;
|
||||
|
||||
namespace Distribution.Features.ChequeBook;
|
||||
|
||||
public class ChequeBookService(
|
||||
IChequeBookRepository repo,
|
||||
IBankAccountRepository bankAccountRepo,
|
||||
IUnitOfWork uow)
|
||||
{
|
||||
private const string DefaultCurrency = "LKR";
|
||||
|
||||
private static ChequePageResponse ToPageResponse(Models.ChequePage p, string chequeBookNo) =>
|
||||
new(p.Id, p.ChequeNo, chequeBookNo, p.IssueStatus, p.PayeeType, p.PayeeId, p.PayeeName,
|
||||
p.IssueDate, p.Amount, p.CurrencyCode, p.VoucherId, p.ReferenceNo, p.Purpose,
|
||||
p.IsCrossCheque, p.IsAccountPayee, p.IsPostDated, p.Notes,
|
||||
p.PrintedBy, p.PrintedAt, p.ClearedDate, p.ClearedByBank, p.CancelReason);
|
||||
|
||||
private static ChequeBookResponse ToBookResponse(Models.ChequeBook b, bool includePages) =>
|
||||
new(b.ChequeBookNo, b.BranchId, b.FK_BankAccountId, b.StartChequeNo, b.EndChequeNo,
|
||||
b.TotalLeaves, b.ReceivedDate, b.Description, b.CreatedBy, b.Status,
|
||||
includePages ? b.Pages.OrderBy(p => p.ChequeNo).Select(p => ToPageResponse(p, b.ChequeBookNo)).ToList() : new());
|
||||
|
||||
public async Task<PagedResult<ChequeBookResponse>> ListAsync(
|
||||
Guid? bankAccountId, int? branchId, ChequeBookStatus? status, int page, int pageSize)
|
||||
{
|
||||
if (page < 1) page = 1;
|
||||
if (pageSize < 1 || pageSize > 200) pageSize = 50;
|
||||
|
||||
var (items, total) = await repo.ListAsync(bankAccountId, branchId, status, page, pageSize);
|
||||
return new PagedResult<ChequeBookResponse>(
|
||||
items.Select(b => ToBookResponse(b, includePages: false)).ToList(),
|
||||
total, page, pageSize);
|
||||
}
|
||||
|
||||
public async Task<ChequeBookResponse?> GetAsync(string chequeBookNo, bool expandPages)
|
||||
{
|
||||
var book = await repo.GetByNoAsync(chequeBookNo, includePages: expandPages);
|
||||
return book is null ? null : ToBookResponse(book, includePages: expandPages);
|
||||
}
|
||||
|
||||
public async Task<ChequeBookResponse> CreateAsync(CreateChequeBookRequest request, CancellationToken ct = default)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(request.ChequeBookNo))
|
||||
throw new InvalidOperationException("ChequeBookNo is required");
|
||||
if (request.TotalLeaves <= 0)
|
||||
throw new InvalidOperationException("TotalLeaves must be greater than zero");
|
||||
if (!long.TryParse(request.StartChequeNo, out var start) ||
|
||||
!long.TryParse(request.EndChequeNo, out var end))
|
||||
throw new InvalidOperationException("StartChequeNo and EndChequeNo must be numeric");
|
||||
if (end < start)
|
||||
throw new InvalidOperationException("EndChequeNo must be greater than or equal to StartChequeNo");
|
||||
if (end - start + 1 != request.TotalLeaves)
|
||||
throw new InvalidOperationException("TotalLeaves does not match the Start/End cheque range");
|
||||
if (await repo.BookNoExistsAsync(request.ChequeBookNo))
|
||||
throw new InvalidOperationException("A cheque book with this number already exists");
|
||||
|
||||
var bankAccount = await bankAccountRepo.GetByIdAsync(request.BankAccountId);
|
||||
if (bankAccount is null || bankAccount.AccountType != BankAccountType.Bank)
|
||||
throw new InvalidOperationException("BankAccountId does not reference a valid bank account");
|
||||
|
||||
var book = new Models.ChequeBook
|
||||
{
|
||||
ChequeBookNo = request.ChequeBookNo,
|
||||
BranchId = request.BranchId,
|
||||
FK_BankAccountId = request.BankAccountId,
|
||||
StartChequeNo = request.StartChequeNo,
|
||||
EndChequeNo = request.EndChequeNo,
|
||||
TotalLeaves = request.TotalLeaves,
|
||||
ReceivedDate = request.ReceivedDate,
|
||||
Description = request.Description,
|
||||
CreatedBy = request.CreatedBy,
|
||||
Status = ChequeBookStatus.Active
|
||||
};
|
||||
|
||||
var width = request.StartChequeNo.Length;
|
||||
for (var n = start; n <= end; n++)
|
||||
{
|
||||
book.Pages.Add(new Models.ChequePage
|
||||
{
|
||||
ChequeNo = n.ToString().PadLeft(width, '0'),
|
||||
IssueStatus = ChequePageIssueStatus.Unused
|
||||
});
|
||||
}
|
||||
|
||||
await uow.BeginAsync(ct);
|
||||
try
|
||||
{
|
||||
await repo.AddAsync(book);
|
||||
await uow.CommitAsync(ct);
|
||||
}
|
||||
catch { await uow.RollbackAsync(ct); throw; }
|
||||
|
||||
return ToBookResponse(book, includePages: true);
|
||||
}
|
||||
|
||||
public async Task<ChequePageResponse?> IssueAsync(string chequeNo, IssueChequePageRequest request, CancellationToken ct = default)
|
||||
{
|
||||
var page = await repo.GetPageByNoAsync(chequeNo);
|
||||
if (page is null) return null;
|
||||
if (page.IssueStatus != ChequePageIssueStatus.Unused)
|
||||
throw new InvalidOperationException($"Cannot issue a cheque in status '{page.IssueStatus}'");
|
||||
if (page.ChequeBook is null || page.ChequeBook.Status != ChequeBookStatus.Active)
|
||||
throw new InvalidOperationException("The cheque book is not active");
|
||||
if (request.Amount <= 0)
|
||||
throw new InvalidOperationException("Amount must be greater than zero");
|
||||
if (string.IsNullOrWhiteSpace(request.PayeeName))
|
||||
throw new InvalidOperationException("PayeeName is required");
|
||||
|
||||
page.IssueStatus = ChequePageIssueStatus.Issued;
|
||||
page.PayeeType = request.PayeeType;
|
||||
page.PayeeId = request.PayeeId;
|
||||
page.PayeeName = request.PayeeName;
|
||||
page.IssueDate = request.IssueDate;
|
||||
page.Amount = request.Amount;
|
||||
page.CurrencyCode = string.IsNullOrWhiteSpace(request.CurrencyCode) ? DefaultCurrency : request.CurrencyCode;
|
||||
page.VoucherId = request.VoucherId;
|
||||
page.ReferenceNo = request.ReferenceNo;
|
||||
page.Purpose = request.Purpose;
|
||||
page.IsCrossCheque = request.IsCrossCheque;
|
||||
page.IsAccountPayee = request.IsAccountPayee;
|
||||
page.IsPostDated = request.IsPostDated;
|
||||
page.Notes = request.Notes;
|
||||
page.PrintedBy = request.PrintedBy;
|
||||
page.PrintedAt = DateTime.Now;
|
||||
|
||||
repo.UpdatePage(page);
|
||||
await uow.CommitAsync(ct);
|
||||
return ToPageResponse(page, page.ChequeBook!.ChequeBookNo);
|
||||
}
|
||||
|
||||
public async Task<ChequePageResponse?> UpdatePageStatusAsync(string chequeNo, UpdateChequePageStatusRequest request, CancellationToken ct = default)
|
||||
{
|
||||
var page = await repo.GetPageByNoAsync(chequeNo);
|
||||
if (page is null) return null;
|
||||
|
||||
switch (request.Action)
|
||||
{
|
||||
case ChequePageStatusAction.Clear:
|
||||
if (page.IssueStatus != ChequePageIssueStatus.Issued)
|
||||
throw new InvalidOperationException("Only issued cheques can be cleared");
|
||||
if (request.ClearedDate is null)
|
||||
throw new InvalidOperationException("ClearedDate is required when clearing a cheque");
|
||||
page.IssueStatus = ChequePageIssueStatus.Cleared;
|
||||
page.ClearedDate = request.ClearedDate;
|
||||
page.ClearedByBank = true;
|
||||
break;
|
||||
|
||||
case ChequePageStatusAction.Bounce:
|
||||
if (page.IssueStatus != ChequePageIssueStatus.Issued)
|
||||
throw new InvalidOperationException("Only issued cheques can be bounced");
|
||||
page.IssueStatus = ChequePageIssueStatus.Bounced;
|
||||
break;
|
||||
|
||||
case ChequePageStatusAction.Cancel:
|
||||
if (page.IssueStatus != ChequePageIssueStatus.Issued)
|
||||
throw new InvalidOperationException("Only issued cheques can be cancelled");
|
||||
if (string.IsNullOrWhiteSpace(request.CancelReason))
|
||||
throw new InvalidOperationException("CancelReason is required when cancelling a cheque");
|
||||
page.IssueStatus = ChequePageIssueStatus.Cancelled;
|
||||
page.CancelReason = request.CancelReason;
|
||||
break;
|
||||
|
||||
case ChequePageStatusAction.Void:
|
||||
if (page.IssueStatus != ChequePageIssueStatus.Unused)
|
||||
throw new InvalidOperationException("Only unused cheques can be voided");
|
||||
page.IssueStatus = ChequePageIssueStatus.Void;
|
||||
break;
|
||||
}
|
||||
|
||||
repo.UpdatePage(page);
|
||||
await uow.CommitAsync(ct);
|
||||
return ToPageResponse(page, page.ChequeBook!.ChequeBookNo);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
using Distribution.Models;
|
||||
|
||||
namespace Distribution.Features.ChequeBook;
|
||||
|
||||
public interface IChequeBookRepository
|
||||
{
|
||||
Task<Models.ChequeBook?> GetByNoAsync(string chequeBookNo, bool includePages);
|
||||
Task<(List<Models.ChequeBook> Items, int TotalCount)> ListAsync(
|
||||
Guid? bankAccountId, int? branchId, ChequeBookStatus? status, int page, int pageSize);
|
||||
Task<bool> BookNoExistsAsync(string chequeBookNo);
|
||||
Task AddAsync(Models.ChequeBook book);
|
||||
void Update(Models.ChequeBook book);
|
||||
|
||||
Task<Models.ChequePage?> GetPageByNoAsync(string chequeNo);
|
||||
void UpdatePage(Models.ChequePage page);
|
||||
}
|
||||
@@ -2,21 +2,27 @@ namespace Distribution.Features.DashboardReport;
|
||||
|
||||
public record DashboardFilter(DateTime From, DateTime To, Guid? RegionId, Guid? CategoryId, Guid? WarehouseId);
|
||||
|
||||
public record SalesTotals(decimal GrossSales, decimal ReturnValue, int Orders, decimal CostedSales, decimal CostOfSales);
|
||||
public record SalesTotals(decimal GrossSales, decimal ReturnValue, decimal Discounts, int Orders, decimal CostedSales, decimal CostOfSales);
|
||||
|
||||
public record SalesKpis(
|
||||
decimal GrossSales, decimal ReturnValue, decimal NetSales, int Orders, decimal AverageOrderValue,
|
||||
decimal GrossSales, decimal ReturnValue, decimal Discounts, decimal NetSales, int Orders, decimal AverageOrderValue,
|
||||
decimal GrossProfit, decimal? GrossProfitMarginPercent);
|
||||
|
||||
public record SalesKpiComparison(SalesKpis Current, SalesKpis Previous, DateTime PreviousFrom, DateTime PreviousTo);
|
||||
|
||||
public record ReceivablesTotals(decimal Collected, decimal Outstanding, decimal Overdue);
|
||||
|
||||
public record CashKpis(decimal Collected, decimal Outstanding, decimal Overdue, decimal? CollectionRatePercent);
|
||||
|
||||
public record CashKpiComparison(CashKpis Current, CashKpis Previous);
|
||||
|
||||
public record TrendPoint(string Period, decimal Sales, decimal Returns);
|
||||
|
||||
public record NamedAmount(Guid? Id, string Name, decimal Amount);
|
||||
|
||||
public record SalesSummaryResponse(
|
||||
DateTime From, DateTime To, string Granularity,
|
||||
SalesKpiComparison Kpis, List<TrendPoint> Trend,
|
||||
SalesKpiComparison Kpis, CashKpiComparison Cash, List<TrendPoint> Trend,
|
||||
List<NamedAmount> ByCategory, List<NamedAmount> ByRegion, List<NamedAmount> ByRep);
|
||||
|
||||
public record PeriodAmount(DateTime Date, decimal Amount);
|
||||
|
||||
@@ -19,29 +19,30 @@ public class DashboardReportRepository(AppDBContext db) : IDashboardReportReposi
|
||||
public Guid RepId { get; set; }
|
||||
}
|
||||
|
||||
// Same definition of "sold" as ProductReportRepository: fulfilled lines on non-cancelled POs
|
||||
// whose invoice is Finalized, dated by Invoice.FinalizedAt. 'To' is inclusive (whole day).
|
||||
// Same definition of "sold" as ProductReportRepository: billable (non-removed) invoice lines of
|
||||
// Finalized invoices on non-cancelled POs, dated by Invoice.FinalizedAt. 'To' is inclusive (whole day).
|
||||
// Payment status/method is deliberately ignored: a sale is recognised when invoiced.
|
||||
private IQueryable<SoldLine> SoldLines(DashboardFilter f)
|
||||
{
|
||||
var start = f.From.Date;
|
||||
var end = f.To.Date.AddDays(1);
|
||||
var q =
|
||||
from li in db.ShopPOItems.AsNoTracking()
|
||||
join inv in db.Invoices.AsNoTracking() on li.FK_ShopPOId equals inv.FK_ShopPOId
|
||||
where inv.Status == InvoiceStatus.Finalized
|
||||
&& li.ShopPO.Status != ShopPOStatus.Cancelled
|
||||
&& inv.FinalizedAt >= start && inv.FinalizedAt < end
|
||||
from li in db.InvoiceItems.AsNoTracking()
|
||||
where li.IsActive && !li.IsRemoved
|
||||
&& li.Invoice.Status == InvoiceStatus.Finalized
|
||||
&& li.Invoice.ShopPO.Status != ShopPOStatus.Cancelled
|
||||
&& li.Invoice.FinalizedAt >= start && li.Invoice.FinalizedAt < end
|
||||
select new SoldLine
|
||||
{
|
||||
Date = inv.FinalizedAt!.Value.Date,
|
||||
PoId = li.FK_ShopPOId,
|
||||
Value = li.QuantityFulfilled * li.UnitPrice,
|
||||
Cost = li.Item.CostPrice == null ? null : li.QuantityFulfilled * li.Item.CostPrice,
|
||||
Date = li.Invoice.FinalizedAt!.Value.Date,
|
||||
PoId = li.Invoice.FK_ShopPOId,
|
||||
Value = li.Quantity * li.UnitPrice,
|
||||
Cost = li.Item.CostPrice == null ? null : li.Quantity * li.Item.CostPrice,
|
||||
CategoryId = li.Item.FK_CategoryId,
|
||||
CategoryName = li.Item.Category.Name,
|
||||
RegionId = li.ShopPO.Store.Route.FK_RegionId,
|
||||
RegionName = li.ShopPO.Store.Route.Region.Name,
|
||||
RepId = li.ShopPO.RaisedById,
|
||||
RegionId = li.Invoice.ShopPO.Store.Route.FK_RegionId,
|
||||
RegionName = li.Invoice.ShopPO.Store.Route.Region.Name,
|
||||
RepId = li.Invoice.ShopPO.RaisedById,
|
||||
};
|
||||
if (f.RegionId.HasValue) q = q.Where(l => l.RegionId == f.RegionId.Value);
|
||||
if (f.CategoryId.HasValue) q = q.Where(l => l.CategoryId == f.CategoryId.Value);
|
||||
@@ -52,8 +53,10 @@ public class DashboardReportRepository(AppDBContext db) : IDashboardReportReposi
|
||||
{
|
||||
var start = f.From.Date;
|
||||
var end = f.To.Date.AddDays(1);
|
||||
// Only credit-deduction returns reduce sales; an item exchange is replaced like-for-like (net zero).
|
||||
var q = db.SaleReturnItems.AsNoTracking()
|
||||
.Where(r => r.SaleReturn.CreatedAt >= start && r.SaleReturn.CreatedAt < end);
|
||||
.Where(r => r.SaleReturn.CreatedAt >= start && r.SaleReturn.CreatedAt < end
|
||||
&& r.SaleReturn.Mode == ReturnMode.CreditDeduction);
|
||||
if (f.RegionId.HasValue) q = q.Where(r => r.SaleReturn.ShopPO.Store.Route.FK_RegionId == f.RegionId.Value);
|
||||
if (f.CategoryId.HasValue) q = q.Where(r => r.Item.FK_CategoryId == f.CategoryId.Value);
|
||||
return q;
|
||||
@@ -68,7 +71,59 @@ public class DashboardReportRepository(AppDBContext db) : IDashboardReportReposi
|
||||
var costedSales = await costed.SumAsync(l => (decimal?)l.Value) ?? 0m;
|
||||
var cost = await costed.SumAsync(l => l.Cost) ?? 0m;
|
||||
var returns = await ReturnLines(f).SumAsync(r => (decimal?)(r.Quantity * r.UnitPrice)) ?? 0m;
|
||||
return new SalesTotals(gross, returns, orders, costedSales, cost);
|
||||
return new SalesTotals(gross, returns, await GetDiscountsAsync(f), orders, costedSales, cost);
|
||||
}
|
||||
|
||||
// Discount carried on the invoice (Invoice.DiscountAmount) for invoices finalized in the range, same dating as sales.
|
||||
// A discount belongs to the whole invoice, so it can't be split by category: with a category filter it is 0.
|
||||
private async Task<decimal> GetDiscountsAsync(DashboardFilter f)
|
||||
{
|
||||
if (f.CategoryId.HasValue) return 0m;
|
||||
var start = f.From.Date;
|
||||
var end = f.To.Date.AddDays(1);
|
||||
var q = db.Invoices.AsNoTracking()
|
||||
.Where(i => i.IsActive && i.Status == InvoiceStatus.Finalized && i.ShopPO.Status != ShopPOStatus.Cancelled
|
||||
&& i.FinalizedAt >= start && i.FinalizedAt < end);
|
||||
if (f.RegionId.HasValue) q = q.Where(i => i.ShopPO.Store.Route.FK_RegionId == f.RegionId.Value);
|
||||
return await q.SumAsync(i => (decimal?)i.DiscountAmount) ?? 0m;
|
||||
}
|
||||
|
||||
// Cash view, independent of Net Sales. Credit-method payments are store-credit usage, so they stay
|
||||
// receivable (not collected). Outstanding/Overdue are balances as of the end of the range;
|
||||
// overdue = unpaid balance on invoices finalized more than OverdueDays before that date.
|
||||
private const int OverdueDays = 30;
|
||||
|
||||
public async Task<ReceivablesTotals> GetReceivablesAsync(DashboardFilter f)
|
||||
{
|
||||
var start = f.From.Date;
|
||||
var end = f.To.Date.AddDays(1);
|
||||
|
||||
var pays = db.Payments.AsNoTracking().Where(p => p.IsActive && p.Method != PaymentMethod.Credit);
|
||||
var invs = db.Invoices.AsNoTracking().Where(i => i.IsActive && i.Status == InvoiceStatus.Finalized
|
||||
&& i.ShopPO.Status != ShopPOStatus.Cancelled && i.FinalizedAt < end);
|
||||
if (f.RegionId.HasValue)
|
||||
{
|
||||
pays = pays.Where(p => p.Invoice.ShopPO.Store.Route.FK_RegionId == f.RegionId.Value);
|
||||
invs = invs.Where(i => i.ShopPO.Store.Route.FK_RegionId == f.RegionId.Value);
|
||||
}
|
||||
|
||||
var collected = await pays.Where(p => p.CollectedAt >= start && p.CollectedAt < end)
|
||||
.SumAsync(p => (decimal?)p.Amount) ?? 0m;
|
||||
|
||||
var rows = await invs.Select(i => new
|
||||
{
|
||||
i.FinalizedAt,
|
||||
i.TotalAmount,
|
||||
Paid = i.Payments.Where(p => p.IsActive && p.Method != PaymentMethod.Credit && p.CollectedAt < end)
|
||||
.Sum(p => (decimal?)p.Amount) ?? 0m,
|
||||
}).ToListAsync();
|
||||
|
||||
var balances = rows
|
||||
.Select(r => (Due: r.FinalizedAt!.Value.AddDays(OverdueDays), Balance: Math.Max(0m, r.TotalAmount - r.Paid)))
|
||||
.ToList();
|
||||
return new ReceivablesTotals(collected,
|
||||
balances.Sum(b => b.Balance),
|
||||
balances.Where(b => b.Due < end).Sum(b => b.Balance));
|
||||
}
|
||||
|
||||
public async Task<List<PeriodAmount>> GetDailySalesAsync(DashboardFilter f) =>
|
||||
|
||||
@@ -3,7 +3,7 @@ namespace Distribution.Features.DashboardReport;
|
||||
public class DashboardReportService(IDashboardReportRepository repo)
|
||||
{
|
||||
public const int MaxRangeDays = 366;
|
||||
private const int DailyGranularityMaxDays = 62;
|
||||
private const int DailyGranularityMaxDays = 92; // covers a "last 3 months" daily trend
|
||||
|
||||
public static DashboardFilter BuildFilter(DateTime? from, DateTime? to, Guid? regionId, Guid? categoryId, Guid? warehouseId)
|
||||
{
|
||||
@@ -25,14 +25,18 @@ public class DashboardReportService(IDashboardReportRepository repo)
|
||||
|
||||
private static SalesKpis ToKpis(SalesTotals t)
|
||||
{
|
||||
var net = t.GrossSales - t.ReturnValue;
|
||||
var net = t.GrossSales - t.ReturnValue - t.Discounts;
|
||||
var gp = t.CostedSales - t.CostOfSales;
|
||||
return new SalesKpis(
|
||||
t.GrossSales, t.ReturnValue, net, t.Orders,
|
||||
t.GrossSales, t.ReturnValue, t.Discounts, net, t.Orders,
|
||||
t.Orders > 0 ? Math.Round(t.GrossSales / t.Orders, 2) : 0m,
|
||||
gp, t.CostedSales > 0 ? Math.Round(gp / t.CostedSales * 100, 2) : null);
|
||||
}
|
||||
|
||||
private static CashKpis ToCash(ReceivablesTotals r, decimal grossInvoiced) =>
|
||||
new(r.Collected, r.Outstanding, r.Overdue,
|
||||
grossInvoiced > 0 ? Math.Round(r.Collected / grossInvoiced * 100, 2) : null);
|
||||
|
||||
public async Task<SalesSummaryResponse> GetSalesSummaryAsync(DashboardFilter f)
|
||||
{
|
||||
var days = (f.To - f.From).Days + 1;
|
||||
@@ -41,6 +45,8 @@ public class DashboardReportService(IDashboardReportRepository repo)
|
||||
|
||||
var current = await repo.GetSalesTotalsAsync(f);
|
||||
var previous = await repo.GetSalesTotalsAsync(prev);
|
||||
var currentCash = await repo.GetReceivablesAsync(f);
|
||||
var previousCash = await repo.GetReceivablesAsync(prev);
|
||||
var sales = await repo.GetDailySalesAsync(f);
|
||||
var returns = await repo.GetDailyReturnsAsync(f);
|
||||
|
||||
@@ -55,6 +61,9 @@ public class DashboardReportService(IDashboardReportRepository repo)
|
||||
return new SalesSummaryResponse(
|
||||
f.From, f.To, daily ? "day" : "month",
|
||||
new SalesKpiComparison(ToKpis(current), ToKpis(previous), prev.From, prev.To),
|
||||
new CashKpiComparison(
|
||||
ToCash(currentCash, current.GrossSales - current.Discounts),
|
||||
ToCash(previousCash, previous.GrossSales - previous.Discounts)),
|
||||
trend,
|
||||
(await repo.GetSalesByCategoryAsync(f)).Select(Map).ToList(),
|
||||
(await repo.GetSalesByRegionAsync(f)).Select(Map).ToList(),
|
||||
|
||||
@@ -5,6 +5,7 @@ public record GroupAggregate(Guid? Key, string Name, decimal Amount);
|
||||
public interface IDashboardReportRepository
|
||||
{
|
||||
Task<SalesTotals> GetSalesTotalsAsync(DashboardFilter f);
|
||||
Task<ReceivablesTotals> GetReceivablesAsync(DashboardFilter f);
|
||||
Task<List<PeriodAmount>> GetDailySalesAsync(DashboardFilter f);
|
||||
Task<List<PeriodAmount>> GetDailyReturnsAsync(DashboardFilter f);
|
||||
Task<List<GroupAggregate>> GetSalesByCategoryAsync(DashboardFilter f);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Distribution.Features.LoadingEvent;
|
||||
using Distribution.Features.Notifications;
|
||||
using Distribution.Features.Ride;
|
||||
using Distribution.Features.ShopPO;
|
||||
using Distribution.Features.Stock;
|
||||
@@ -13,8 +14,18 @@ public class DeliveryTaskService(
|
||||
IShopPORepository shopPORepo,
|
||||
ILoadingSheetRepository loadingSheetRepo,
|
||||
StockMover mover,
|
||||
INotificationService notifications,
|
||||
IUnitOfWork uow)
|
||||
{
|
||||
private const string DeliveriesLink = "/operations/deliveries";
|
||||
|
||||
// Route owner + managers whose scope covers the store being delivered to.
|
||||
private static List<RecipientRule> DeliveryRecipients(Guid storeId) =>
|
||||
[
|
||||
RecipientRule.RouteOwner(ScopeKind.Store, storeId),
|
||||
RecipientRule.PermissionInScope("delivery-task.manage", ScopeKind.Store, storeId),
|
||||
];
|
||||
|
||||
private static DeliveryTaskResponse ToResponse(DeliveryTask t) =>
|
||||
new(t.Id,
|
||||
t.FK_RideId,
|
||||
@@ -181,6 +192,13 @@ public class DeliveryTaskService(
|
||||
}
|
||||
catch { await uow.RollbackAsync(ct); throw; }
|
||||
|
||||
if (!fullyDelivered && shopPO.Store is not null)
|
||||
await notifications.NotifyAsync(
|
||||
NotificationTypes.DeliveryTaskPartiallyDelivered,
|
||||
DeliveryRecipients(shopPO.FK_StoreId),
|
||||
new NotificationArgs(new { shopPO.Store.StoreName }, "DeliveryTask", task.Id, DeliveriesLink, userId),
|
||||
ct);
|
||||
|
||||
var saved = await repo.GetByIdAsync(taskId);
|
||||
return ToResponse(saved!);
|
||||
}
|
||||
@@ -204,6 +222,13 @@ public class DeliveryTaskService(
|
||||
}
|
||||
catch { await uow.RollbackAsync(ct); throw; }
|
||||
|
||||
if (task.ShopPO?.Store is not null)
|
||||
await notifications.NotifyAsync(
|
||||
NotificationTypes.DeliveryTaskFailed,
|
||||
DeliveryRecipients(task.ShopPO.FK_StoreId),
|
||||
new NotificationArgs(new { task.ShopPO.Store.StoreName, Note = note }, "DeliveryTask", task.Id, DeliveriesLink),
|
||||
ct);
|
||||
|
||||
return ToResponse(task);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Distribution.Features.Expense;
|
||||
|
||||
[ApiController]
|
||||
public class ExpenseAttachmentController(ExpenseAttachmentService service) : ControllerBase
|
||||
{
|
||||
[HttpPost("api/expenses/{expenseId:guid}/attachments")]
|
||||
[Authorize("expense.create")]
|
||||
[RequestSizeLimit(10 * 1024 * 1024)]
|
||||
public async Task<IActionResult> Upload(Guid expenseId, IFormFile file, CancellationToken ct)
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = await service.UploadAsync(expenseId, file, ct);
|
||||
return result is null ? NotFound() : Ok(result);
|
||||
}
|
||||
catch (InvalidOperationException ex) { return BadRequest(new { error = ex.Message }); }
|
||||
}
|
||||
|
||||
[HttpGet("api/expense-attachments/{id:guid}/download")]
|
||||
[Authorize("expense.view")]
|
||||
public async Task<IActionResult> Download(Guid id, CancellationToken ct)
|
||||
{
|
||||
var result = await service.DownloadAsync(id, ct);
|
||||
if (result is null) return NotFound();
|
||||
return File(result.Value.Stream, result.Value.ContentType, result.Value.FileName);
|
||||
}
|
||||
|
||||
[HttpDelete("api/expense-attachments/{id:guid}")]
|
||||
[Authorize("expense.create")]
|
||||
public async Task<IActionResult> Delete(Guid id, CancellationToken ct) =>
|
||||
await service.DeleteAsync(id, ct) ? NoContent() : NotFound();
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
using Distribution.Data;
|
||||
using Distribution.Infra.Authorization;
|
||||
using Distribution.Infra.UoW;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Distribution.Features.Expense;
|
||||
|
||||
public class ExpenseAttachmentService(
|
||||
AppDBContext context,
|
||||
IConfiguration config,
|
||||
IUnitOfWork uow,
|
||||
ICurrentUserService currentUser)
|
||||
{
|
||||
private const long MaxBytes = 10 * 1024 * 1024;
|
||||
private static readonly string[] AllowedExtensions = [".pdf", ".jpg", ".jpeg", ".png", ".webp"];
|
||||
private static readonly string[] AllowedContentTypes =
|
||||
["application/pdf", "image/jpeg", "image/jpg", "image/png", "image/webp"];
|
||||
|
||||
private string AttachmentsRoot =>
|
||||
config["Expense:AttachmentsRoot"]
|
||||
?? throw new InvalidOperationException("Expense:AttachmentsRoot is not configured");
|
||||
|
||||
public async Task<ExpenseAttachmentResponse?> UploadAsync(Guid expenseId, IFormFile file, CancellationToken ct)
|
||||
{
|
||||
var expense = await context.Expenses.FirstOrDefaultAsync(e => e.Id == expenseId && e.IsActive, ct);
|
||||
if (expense is null) return null;
|
||||
|
||||
if (file is null || file.Length == 0)
|
||||
throw new InvalidOperationException("File is empty");
|
||||
if (file.Length > MaxBytes)
|
||||
throw new InvalidOperationException($"File exceeds the {MaxBytes / (1024 * 1024)}MB limit");
|
||||
|
||||
var ext = Path.GetExtension(file.FileName).ToLowerInvariant();
|
||||
if (!AllowedExtensions.Contains(ext))
|
||||
throw new InvalidOperationException($"File type '{ext}' is not allowed");
|
||||
if (!AllowedContentTypes.Contains(file.ContentType.ToLowerInvariant()))
|
||||
throw new InvalidOperationException($"Content type '{file.ContentType}' is not allowed");
|
||||
|
||||
var userId = currentUser.UserId
|
||||
?? throw new InvalidOperationException("Authenticated user is required");
|
||||
|
||||
var now = DateTime.Now;
|
||||
var relativeDir = Path.Combine(now.Year.ToString("D4"), now.Month.ToString("D2"));
|
||||
var absoluteDir = Path.Combine(AttachmentsRoot, relativeDir);
|
||||
Directory.CreateDirectory(absoluteDir);
|
||||
|
||||
var storedName = $"{Guid.NewGuid()}{ext}";
|
||||
var absolutePath = Path.Combine(absoluteDir, storedName);
|
||||
|
||||
await using (var stream = File.Create(absolutePath))
|
||||
{
|
||||
await file.CopyToAsync(stream, ct);
|
||||
}
|
||||
|
||||
var attachment = new Models.ExpenseAttachment
|
||||
{
|
||||
FK_ExpenseId = expenseId,
|
||||
OriginalFileName = file.FileName,
|
||||
StoredPath = absolutePath,
|
||||
ContentType = file.ContentType,
|
||||
SizeBytes = file.Length,
|
||||
UploadedByUserId = userId
|
||||
};
|
||||
|
||||
await uow.BeginAsync(ct);
|
||||
try
|
||||
{
|
||||
await context.ExpenseAttachments.AddAsync(attachment, ct);
|
||||
await uow.CommitAsync(ct);
|
||||
}
|
||||
catch
|
||||
{
|
||||
await uow.RollbackAsync(ct);
|
||||
try { File.Delete(absolutePath); } catch { }
|
||||
throw;
|
||||
}
|
||||
|
||||
return new ExpenseAttachmentResponse(attachment.Id, attachment.OriginalFileName,
|
||||
attachment.ContentType, attachment.SizeBytes, attachment.CreatedAt);
|
||||
}
|
||||
|
||||
public async Task<(Stream Stream, string ContentType, string FileName)?> DownloadAsync(Guid attachmentId, CancellationToken ct)
|
||||
{
|
||||
var a = await context.ExpenseAttachments.FirstOrDefaultAsync(x => x.Id == attachmentId && x.IsActive, ct);
|
||||
if (a is null) return null;
|
||||
if (!File.Exists(a.StoredPath)) return null;
|
||||
|
||||
var stream = File.OpenRead(a.StoredPath);
|
||||
return (stream, a.ContentType, a.OriginalFileName);
|
||||
}
|
||||
|
||||
public async Task<bool> DeleteAsync(Guid attachmentId, CancellationToken ct)
|
||||
{
|
||||
var a = await context.ExpenseAttachments.FirstOrDefaultAsync(x => x.Id == attachmentId && x.IsActive, ct);
|
||||
if (a is null) return false;
|
||||
|
||||
a.IsActive = false;
|
||||
|
||||
await uow.BeginAsync(ct);
|
||||
try { context.ExpenseAttachments.Update(a); await uow.CommitAsync(ct); }
|
||||
catch { await uow.RollbackAsync(ct); throw; }
|
||||
|
||||
// Best-effort file removal — keep DB soft-delete even if the file is gone.
|
||||
try { File.Delete(a.StoredPath); } catch { }
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
using Distribution.Infra.Authorization;
|
||||
using Distribution.Models;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Distribution.Features.Expense;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/expenses")]
|
||||
public class ExpenseController(
|
||||
ExpenseService service,
|
||||
IPermissionService permissions,
|
||||
ICurrentUserService currentUser) : ControllerBase
|
||||
{
|
||||
private async Task<bool> HasAsync(string code)
|
||||
{
|
||||
if (currentUser.RoleId is not { } roleId) return false;
|
||||
return await permissions.HasPermissionAsync(roleId, code);
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Authorize("expense.view")]
|
||||
public async Task<IActionResult> List(
|
||||
[FromQuery] ExpenseStatus? status,
|
||||
[FromQuery] Guid? categoryId,
|
||||
[FromQuery] Guid? warehouseId,
|
||||
[FromQuery] DateTime? from,
|
||||
[FromQuery] DateTime? to,
|
||||
[FromQuery] int page = 1,
|
||||
[FromQuery] int pageSize = 20) =>
|
||||
Ok(await service.ListAsync(status, categoryId, warehouseId, from, to, page, pageSize));
|
||||
|
||||
[HttpGet("{id:guid}")]
|
||||
[Authorize("expense.view")]
|
||||
public async Task<IActionResult> Get(Guid id)
|
||||
{
|
||||
var result = await service.GetAsync(id);
|
||||
return result is null ? NotFound() : Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create endpoint takes a required ?action= query to drive the 4 form buttons:
|
||||
/// draft → Save draft; post → Save & post (requires expense.post); pay → Save & pay
|
||||
/// (requires expense.post + expense.pay). Draft is the default.
|
||||
/// </summary>
|
||||
[HttpPost]
|
||||
[Authorize("expense.create")]
|
||||
public async Task<IActionResult> Create(
|
||||
CreateExpenseRequest request,
|
||||
[FromQuery] ExpenseAction action = ExpenseAction.Draft,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
if (action is ExpenseAction.Post or ExpenseAction.Pay && !await HasAsync("expense.post"))
|
||||
return Forbid();
|
||||
if (action == ExpenseAction.Pay && !await HasAsync("expense.pay"))
|
||||
return Forbid();
|
||||
|
||||
try { return Ok(await service.CreateAsync(request, action, ct)); }
|
||||
catch (InvalidOperationException ex) { return BadRequest(new { error = ex.Message }); }
|
||||
}
|
||||
|
||||
[HttpPut("{id:guid}")]
|
||||
[Authorize("expense.create")]
|
||||
public async Task<IActionResult> UpdateDraft(Guid id, UpdateExpenseDraftRequest request, CancellationToken ct)
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = await service.UpdateDraftAsync(id, request, ct);
|
||||
return result is null ? NotFound() : Ok(result);
|
||||
}
|
||||
catch (InvalidOperationException ex) { return BadRequest(new { error = ex.Message }); }
|
||||
}
|
||||
|
||||
[HttpPost("{id:guid}/post")]
|
||||
[Authorize("expense.post")]
|
||||
public async Task<IActionResult> Post(Guid id, CancellationToken ct)
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = await service.PostAsync(id, ct);
|
||||
return result is null ? NotFound() : Ok(result);
|
||||
}
|
||||
catch (InvalidOperationException ex) { return BadRequest(new { error = ex.Message }); }
|
||||
}
|
||||
|
||||
[HttpPost("{id:guid}/pay")]
|
||||
[Authorize("expense.pay")]
|
||||
public async Task<IActionResult> Pay(Guid id, ExpensePaymentRequest request, CancellationToken ct)
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = await service.PayAsync(id, request, ct);
|
||||
return result is null ? NotFound() : Ok(result);
|
||||
}
|
||||
catch (InvalidOperationException ex) { return BadRequest(new { error = ex.Message }); }
|
||||
}
|
||||
|
||||
[HttpPost("{id:guid}/void")]
|
||||
[Authorize("expense.void")]
|
||||
public async Task<IActionResult> Void(Guid id, VoidExpenseRequest request, CancellationToken ct)
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = await service.VoidAsync(id, request?.Reason, ct);
|
||||
return result is null ? NotFound() : Ok(result);
|
||||
}
|
||||
catch (InvalidOperationException ex) { return BadRequest(new { error = ex.Message }); }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
using Distribution.Models;
|
||||
|
||||
namespace Distribution.Features.Expense;
|
||||
|
||||
public record ExpensePaymentRequest(
|
||||
PaymentMethod Method,
|
||||
Guid? BankAccountId,
|
||||
Guid? ChequePageId);
|
||||
|
||||
public record CreateExpenseRequest(
|
||||
Guid CategoryId,
|
||||
Guid? WarehouseId,
|
||||
decimal Amount,
|
||||
DateTime BillDate,
|
||||
DateTime? DueDate,
|
||||
DateTime? PeriodCovered,
|
||||
Guid? VendorId,
|
||||
string? Payee,
|
||||
string? BillRef,
|
||||
string? Description,
|
||||
/// <summary>Required when action = Pay. Ignored for Draft / Post.</summary>
|
||||
ExpensePaymentRequest? Payment);
|
||||
|
||||
public record UpdateExpenseDraftRequest(
|
||||
Guid CategoryId,
|
||||
Guid? WarehouseId,
|
||||
decimal Amount,
|
||||
DateTime BillDate,
|
||||
DateTime? DueDate,
|
||||
DateTime? PeriodCovered,
|
||||
Guid? VendorId,
|
||||
string? Payee,
|
||||
string? BillRef,
|
||||
string? Description);
|
||||
|
||||
public record VoidExpenseRequest(string? Reason);
|
||||
|
||||
public record ExpenseAttachmentResponse(
|
||||
Guid Id,
|
||||
string OriginalFileName,
|
||||
string ContentType,
|
||||
long SizeBytes,
|
||||
DateTime CreatedAt);
|
||||
|
||||
public record ExpenseResponse(
|
||||
Guid Id,
|
||||
Guid CategoryId,
|
||||
string CategoryName,
|
||||
Guid? ParentCategoryId,
|
||||
string? ParentCategoryName,
|
||||
Guid? WarehouseId,
|
||||
string? WarehouseName,
|
||||
decimal Amount,
|
||||
DateTime BillDate,
|
||||
DateTime? DueDate,
|
||||
DateTime? PeriodCovered,
|
||||
Guid? VendorId,
|
||||
string? VendorName,
|
||||
string? Payee,
|
||||
string? BillRef,
|
||||
string? Description,
|
||||
ExpenseStatus Status,
|
||||
PaymentMethod? Method,
|
||||
Guid? BankAccountId,
|
||||
Guid? ChequePageId,
|
||||
string? ChequeNo,
|
||||
DateTime? PaidAt,
|
||||
Guid RecordedByUserId,
|
||||
Guid? PostedByUserId,
|
||||
Guid? PaidByUserId,
|
||||
Guid? VoidedByUserId,
|
||||
string? VoidReason,
|
||||
Guid? RecurringTemplateId,
|
||||
List<ExpenseAttachmentResponse> Attachments,
|
||||
DateTime CreatedAt);
|
||||
|
||||
public record PagedExpensesResponse(List<ExpenseResponse> Items, int Page, int PageSize, int Total);
|
||||
@@ -0,0 +1,50 @@
|
||||
using Distribution.Data;
|
||||
using Distribution.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Distribution.Features.Expense;
|
||||
|
||||
public class ExpenseRepository(AppDBContext context) : IExpenseRepository
|
||||
{
|
||||
private IQueryable<Models.Expense> Base() =>
|
||||
context.Expenses
|
||||
.Include(e => e.Category!).ThenInclude(c => c.Parent)
|
||||
.Include(e => e.Warehouse)
|
||||
.Include(e => e.Vendor)
|
||||
.Include(e => e.ChequePage)
|
||||
.Include(e => e.Attachments.Where(a => a.IsActive));
|
||||
|
||||
public async Task<Models.Expense?> GetByIdAsync(Guid id) =>
|
||||
await Base().FirstOrDefaultAsync(e => e.Id == id && e.IsActive);
|
||||
|
||||
private IQueryable<Models.Expense> Filtered(
|
||||
ExpenseStatus? status, Guid? categoryId, Guid? warehouseId, DateTime? from, DateTime? to) =>
|
||||
Base()
|
||||
.Where(e => e.IsActive)
|
||||
.Where(e => !status.HasValue || e.Status == status)
|
||||
.Where(e => !categoryId.HasValue
|
||||
|| e.FK_CategoryId == categoryId
|
||||
|| e.Category!.FK_ParentCategoryId == categoryId)
|
||||
.Where(e => !warehouseId.HasValue || e.FK_WarehouseId == warehouseId)
|
||||
.Where(e => !from.HasValue || e.BillDate >= from)
|
||||
.Where(e => !to.HasValue || e.BillDate <= to);
|
||||
|
||||
public async Task<List<Models.Expense>> ListAsync(
|
||||
ExpenseStatus? status, Guid? categoryId, Guid? warehouseId, DateTime? from, DateTime? to,
|
||||
int page, int pageSize) =>
|
||||
await Filtered(status, categoryId, warehouseId, from, to)
|
||||
.OrderByDescending(e => e.BillDate)
|
||||
.Skip((page - 1) * pageSize)
|
||||
.Take(pageSize)
|
||||
.ToListAsync();
|
||||
|
||||
public async Task<int> CountAsync(
|
||||
ExpenseStatus? status, Guid? categoryId, Guid? warehouseId, DateTime? from, DateTime? to) =>
|
||||
await Filtered(status, categoryId, warehouseId, from, to).CountAsync();
|
||||
|
||||
public async Task AddAsync(Models.Expense entity) =>
|
||||
await context.Expenses.AddAsync(entity);
|
||||
|
||||
public void Update(Models.Expense entity) =>
|
||||
context.Expenses.Update(entity);
|
||||
}
|
||||
@@ -0,0 +1,269 @@
|
||||
using Distribution.Data;
|
||||
using Distribution.Features.BankAccount;
|
||||
using Distribution.Features.ChequeBook;
|
||||
using Distribution.Infra.Authorization;
|
||||
using Distribution.Infra.UoW;
|
||||
using Distribution.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Distribution.Features.Expense;
|
||||
|
||||
public class ExpenseService(
|
||||
AppDBContext context,
|
||||
IExpenseRepository repo,
|
||||
IBankAccountRepository bankAccountRepo,
|
||||
IChequeBookRepository chequeBookRepo,
|
||||
IUnitOfWork uow,
|
||||
ICurrentUserService currentUser)
|
||||
{
|
||||
private static ExpenseAttachmentResponse MapAttachment(Models.ExpenseAttachment a) =>
|
||||
new(a.Id, a.OriginalFileName, a.ContentType, a.SizeBytes, a.CreatedAt);
|
||||
|
||||
private static ExpenseResponse ToResponse(Models.Expense e) =>
|
||||
new(e.Id,
|
||||
e.FK_CategoryId, e.Category?.Name ?? string.Empty,
|
||||
e.Category?.FK_ParentCategoryId, e.Category?.Parent?.Name,
|
||||
e.FK_WarehouseId, e.Warehouse?.Name,
|
||||
e.Amount, e.BillDate, e.DueDate, e.PeriodCovered,
|
||||
e.FK_VendorId, e.Vendor?.Name,
|
||||
e.Payee, e.BillRef, e.Description,
|
||||
e.Status, e.Method, e.FK_BankAccountId, e.FK_ChequePageId, e.ChequePage?.ChequeNo,
|
||||
e.PaidAt,
|
||||
e.RecordedByUserId, e.PostedByUserId, e.PaidByUserId, e.VoidedByUserId, e.VoidReason,
|
||||
e.FK_RecurringTemplateId,
|
||||
e.Attachments.Where(a => a.IsActive).Select(MapAttachment).ToList(),
|
||||
e.CreatedAt);
|
||||
|
||||
public async Task<PagedExpensesResponse> ListAsync(
|
||||
ExpenseStatus? status, Guid? categoryId, Guid? warehouseId, DateTime? from, DateTime? to,
|
||||
int page, int pageSize)
|
||||
{
|
||||
if (page < 1) page = 1;
|
||||
if (pageSize < 1 || pageSize > 200) pageSize = 20;
|
||||
|
||||
var items = await repo.ListAsync(status, categoryId, warehouseId, from, to, page, pageSize);
|
||||
var total = await repo.CountAsync(status, categoryId, warehouseId, from, to);
|
||||
return new PagedExpensesResponse(items.Select(ToResponse).ToList(), page, pageSize, total);
|
||||
}
|
||||
|
||||
public async Task<ExpenseResponse?> GetAsync(Guid id)
|
||||
{
|
||||
var e = await repo.GetByIdAsync(id);
|
||||
return e is null ? null : ToResponse(e);
|
||||
}
|
||||
|
||||
public async Task<ExpenseResponse> CreateAsync(CreateExpenseRequest request, ExpenseAction action, CancellationToken ct = default)
|
||||
{
|
||||
ValidateBase(request);
|
||||
|
||||
var category = await context.ExpenseCategories.Include(c => c.Parent)
|
||||
.FirstOrDefaultAsync(c => c.Id == request.CategoryId && c.IsActive, ct)
|
||||
?? throw new InvalidOperationException("Category not found");
|
||||
if (category.FK_ParentCategoryId is null)
|
||||
throw new InvalidOperationException("Pick a specific title under a category, not the category itself");
|
||||
|
||||
var userId = currentUser.UserId
|
||||
?? throw new InvalidOperationException("Authenticated user is required");
|
||||
|
||||
var entity = new Models.Expense
|
||||
{
|
||||
FK_CategoryId = request.CategoryId,
|
||||
FK_WarehouseId = request.WarehouseId,
|
||||
Amount = request.Amount,
|
||||
BillDate = request.BillDate.Date,
|
||||
DueDate = request.DueDate?.Date,
|
||||
PeriodCovered = request.PeriodCovered?.Date,
|
||||
FK_VendorId = request.VendorId,
|
||||
Payee = request.Payee,
|
||||
BillRef = request.BillRef,
|
||||
Description = request.Description,
|
||||
RecordedByUserId = userId,
|
||||
Status = ExpenseStatus.Draft
|
||||
};
|
||||
|
||||
await uow.BeginAsync(ct);
|
||||
try
|
||||
{
|
||||
await repo.AddAsync(entity);
|
||||
|
||||
if (action is ExpenseAction.Post or ExpenseAction.Pay)
|
||||
{
|
||||
entity.Status = ExpenseStatus.Posted;
|
||||
entity.PostedByUserId = userId;
|
||||
entity.PostedAt = DateTime.Now;
|
||||
}
|
||||
|
||||
if (action == ExpenseAction.Pay)
|
||||
{
|
||||
if (request.Payment is null)
|
||||
throw new InvalidOperationException("Payment details are required when action = Pay");
|
||||
await ApplyPaymentAsync(entity, request.Payment, userId, ct);
|
||||
}
|
||||
|
||||
await uow.CommitAsync(ct);
|
||||
}
|
||||
catch { await uow.RollbackAsync(ct); throw; }
|
||||
|
||||
return ToResponse((await repo.GetByIdAsync(entity.Id))!);
|
||||
}
|
||||
|
||||
public async Task<ExpenseResponse?> UpdateDraftAsync(Guid id, UpdateExpenseDraftRequest request, CancellationToken ct = default)
|
||||
{
|
||||
var entity = await repo.GetByIdAsync(id);
|
||||
if (entity is null) return null;
|
||||
if (entity.Status != ExpenseStatus.Draft)
|
||||
throw new InvalidOperationException("Only Draft expenses can be edited");
|
||||
|
||||
if (request.Amount <= 0) throw new InvalidOperationException("Amount must be greater than zero");
|
||||
|
||||
entity.FK_CategoryId = request.CategoryId;
|
||||
entity.FK_WarehouseId = request.WarehouseId;
|
||||
entity.Amount = request.Amount;
|
||||
entity.BillDate = request.BillDate.Date;
|
||||
entity.DueDate = request.DueDate?.Date;
|
||||
entity.PeriodCovered = request.PeriodCovered?.Date;
|
||||
entity.FK_VendorId = request.VendorId;
|
||||
entity.Payee = request.Payee;
|
||||
entity.BillRef = request.BillRef;
|
||||
entity.Description = request.Description;
|
||||
|
||||
await uow.BeginAsync(ct);
|
||||
try { repo.Update(entity); await uow.CommitAsync(ct); }
|
||||
catch { await uow.RollbackAsync(ct); throw; }
|
||||
|
||||
return ToResponse((await repo.GetByIdAsync(id))!);
|
||||
}
|
||||
|
||||
public async Task<ExpenseResponse?> PostAsync(Guid id, CancellationToken ct = default)
|
||||
{
|
||||
var entity = await repo.GetByIdAsync(id);
|
||||
if (entity is null) return null;
|
||||
if (entity.Status != ExpenseStatus.Draft)
|
||||
throw new InvalidOperationException("Only Draft expenses can be posted");
|
||||
|
||||
var userId = currentUser.UserId
|
||||
?? throw new InvalidOperationException("Authenticated user is required");
|
||||
|
||||
entity.Status = ExpenseStatus.Posted;
|
||||
entity.PostedByUserId = userId;
|
||||
entity.PostedAt = DateTime.Now;
|
||||
|
||||
await uow.BeginAsync(ct);
|
||||
try { repo.Update(entity); await uow.CommitAsync(ct); }
|
||||
catch { await uow.RollbackAsync(ct); throw; }
|
||||
|
||||
return ToResponse((await repo.GetByIdAsync(id))!);
|
||||
}
|
||||
|
||||
public async Task<ExpenseResponse?> PayAsync(Guid id, ExpensePaymentRequest payment, CancellationToken ct = default)
|
||||
{
|
||||
var entity = await repo.GetByIdAsync(id);
|
||||
if (entity is null) return null;
|
||||
if (entity.Status != ExpenseStatus.Posted)
|
||||
throw new InvalidOperationException("Only Posted expenses can be paid");
|
||||
|
||||
var userId = currentUser.UserId
|
||||
?? throw new InvalidOperationException("Authenticated user is required");
|
||||
|
||||
await uow.BeginAsync(ct);
|
||||
try
|
||||
{
|
||||
await ApplyPaymentAsync(entity, payment, userId, ct);
|
||||
await uow.CommitAsync(ct);
|
||||
}
|
||||
catch { await uow.RollbackAsync(ct); throw; }
|
||||
|
||||
return ToResponse((await repo.GetByIdAsync(id))!);
|
||||
}
|
||||
|
||||
public async Task<ExpenseResponse?> VoidAsync(Guid id, string? reason, CancellationToken ct = default)
|
||||
{
|
||||
var entity = await repo.GetByIdAsync(id);
|
||||
if (entity is null) return null;
|
||||
if (entity.Status == ExpenseStatus.Void)
|
||||
throw new InvalidOperationException("Already voided");
|
||||
if (entity.Status == ExpenseStatus.Paid)
|
||||
throw new InvalidOperationException("Paid expenses cannot be voided — issue a reversing entry instead");
|
||||
|
||||
var userId = currentUser.UserId
|
||||
?? throw new InvalidOperationException("Authenticated user is required");
|
||||
|
||||
entity.Status = ExpenseStatus.Void;
|
||||
entity.VoidedByUserId = userId;
|
||||
entity.VoidedAt = DateTime.Now;
|
||||
entity.VoidReason = reason;
|
||||
|
||||
await uow.BeginAsync(ct);
|
||||
try { repo.Update(entity); await uow.CommitAsync(ct); }
|
||||
catch { await uow.RollbackAsync(ct); throw; }
|
||||
|
||||
return ToResponse((await repo.GetByIdAsync(id))!);
|
||||
}
|
||||
|
||||
// ─── helpers ──────────────────────────────────────────────────────────────
|
||||
|
||||
private static void ValidateBase(CreateExpenseRequest request)
|
||||
{
|
||||
if (request.Amount <= 0) throw new InvalidOperationException("Amount must be greater than zero");
|
||||
if (request.DueDate.HasValue && request.DueDate.Value.Date < request.BillDate.Date)
|
||||
throw new InvalidOperationException("Due date cannot be earlier than bill date");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Settles payment: Cash → debit a BankAccount (incl. petty cash which is just a Cash account);
|
||||
/// Cheque → consume an Unused ChequePage from an Active cheque book.
|
||||
/// </summary>
|
||||
private async Task ApplyPaymentAsync(Models.Expense entity, ExpensePaymentRequest payment, Guid userId, CancellationToken ct)
|
||||
{
|
||||
if (payment.Method == PaymentMethod.Credit)
|
||||
throw new InvalidOperationException("Credit is not a valid method for expense payments");
|
||||
|
||||
if (payment.Method == PaymentMethod.Cash)
|
||||
{
|
||||
if (payment.BankAccountId is null)
|
||||
throw new InvalidOperationException("BankAccountId is required when Method is Cash");
|
||||
|
||||
var account = await bankAccountRepo.GetByIdAsync(payment.BankAccountId.Value)
|
||||
?? throw new InvalidOperationException("BankAccountId does not reference a valid account");
|
||||
if (account.IsActive is false)
|
||||
throw new InvalidOperationException("The selected account is inactive");
|
||||
|
||||
entity.FK_BankAccountId = account.Id;
|
||||
}
|
||||
else // Cheque
|
||||
{
|
||||
if (payment.ChequePageId is null)
|
||||
throw new InvalidOperationException("ChequePageId is required when Method is Cheque");
|
||||
|
||||
var page = await context.ChequePages
|
||||
.Include(p => p.ChequeBook)
|
||||
.FirstOrDefaultAsync(p => p.Id == payment.ChequePageId.Value, ct)
|
||||
?? throw new InvalidOperationException("ChequePageId does not reference a valid cheque page");
|
||||
if (page.IssueStatus != ChequePageIssueStatus.Unused)
|
||||
throw new InvalidOperationException($"Cannot issue a cheque in status '{page.IssueStatus}'");
|
||||
if (page.ChequeBook is null || page.ChequeBook.Status != ChequeBookStatus.Active)
|
||||
throw new InvalidOperationException("The cheque book is not active");
|
||||
|
||||
page.IssueStatus = ChequePageIssueStatus.Issued;
|
||||
page.PayeeType = PayeeType.Other;
|
||||
page.PayeeName = entity.Payee ?? entity.Vendor?.Name ?? "Expense";
|
||||
page.IssueDate = DateTime.Now;
|
||||
page.Amount = entity.Amount;
|
||||
page.CurrencyCode = "LKR";
|
||||
page.PrintedAt = DateTime.Now;
|
||||
|
||||
chequeBookRepo.UpdatePage(page);
|
||||
entity.FK_ChequePageId = page.Id;
|
||||
entity.ChequePage = page;
|
||||
}
|
||||
|
||||
entity.Method = payment.Method;
|
||||
entity.Status = ExpenseStatus.Paid;
|
||||
entity.PaidByUserId = userId;
|
||||
entity.PaidAt = DateTime.Now;
|
||||
|
||||
// TODO ledger: when accounting is wired in, post a journal entry here —
|
||||
// Debit Expense (category)
|
||||
// Credit Cash/Bank (BankAccount) OR Cheques-outstanding (for Cheque payments)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
using Distribution.Models;
|
||||
|
||||
namespace Distribution.Features.Expense;
|
||||
|
||||
public interface IExpenseRepository
|
||||
{
|
||||
Task<Models.Expense?> GetByIdAsync(Guid id);
|
||||
Task<List<Models.Expense>> ListAsync(
|
||||
ExpenseStatus? status,
|
||||
Guid? categoryId,
|
||||
Guid? warehouseId,
|
||||
DateTime? from,
|
||||
DateTime? to,
|
||||
int page,
|
||||
int pageSize);
|
||||
Task<int> CountAsync(
|
||||
ExpenseStatus? status,
|
||||
Guid? categoryId,
|
||||
Guid? warehouseId,
|
||||
DateTime? from,
|
||||
DateTime? to);
|
||||
Task AddAsync(Models.Expense entity);
|
||||
void Update(Models.Expense entity);
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Distribution.Features.ExpenseCategory;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/expense-categories")]
|
||||
public class ExpenseCategoryController(ExpenseCategoryService service) : ControllerBase
|
||||
{
|
||||
[HttpGet]
|
||||
[Authorize("expense.view")]
|
||||
public async Task<IActionResult> Tree() => Ok(await service.GetTreeAsync());
|
||||
|
||||
[HttpPost]
|
||||
[Authorize("expense.manage_categories")]
|
||||
public async Task<IActionResult> Create(CreateExpenseCategoryRequest request, CancellationToken ct)
|
||||
{
|
||||
try { return Ok(await service.CreateAsync(request, ct)); }
|
||||
catch (InvalidOperationException ex) { return BadRequest(new { error = ex.Message }); }
|
||||
}
|
||||
|
||||
[HttpPut("{id:guid}")]
|
||||
[Authorize("expense.manage_categories")]
|
||||
public async Task<IActionResult> Update(Guid id, UpdateExpenseCategoryRequest request, CancellationToken ct)
|
||||
{
|
||||
var result = await service.UpdateAsync(id, request, ct);
|
||||
return result is null ? NotFound() : Ok(result);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
namespace Distribution.Features.ExpenseCategory;
|
||||
|
||||
public record ExpenseCategoryResponse(
|
||||
Guid Id,
|
||||
string Name,
|
||||
string? Code,
|
||||
Guid? ParentCategoryId,
|
||||
bool IsActive,
|
||||
List<ExpenseCategoryResponse> Children);
|
||||
|
||||
public record CreateExpenseCategoryRequest(
|
||||
string Name,
|
||||
string? Code,
|
||||
Guid? ParentCategoryId);
|
||||
|
||||
public record UpdateExpenseCategoryRequest(
|
||||
string Name,
|
||||
string? Code,
|
||||
bool IsActive);
|
||||
@@ -0,0 +1,19 @@
|
||||
using Distribution.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Distribution.Features.ExpenseCategory;
|
||||
|
||||
public class ExpenseCategoryRepository(AppDBContext context) : IExpenseCategoryRepository
|
||||
{
|
||||
public async Task<Models.ExpenseCategory?> GetByIdAsync(Guid id) =>
|
||||
await context.ExpenseCategories.FirstOrDefaultAsync(c => c.Id == id);
|
||||
|
||||
public async Task<List<Models.ExpenseCategory>> GetAllAsync() =>
|
||||
await context.ExpenseCategories.OrderBy(c => c.FK_ParentCategoryId).ThenBy(c => c.Name).ToListAsync();
|
||||
|
||||
public async Task AddAsync(Models.ExpenseCategory entity) =>
|
||||
await context.ExpenseCategories.AddAsync(entity);
|
||||
|
||||
public void Update(Models.ExpenseCategory entity) =>
|
||||
context.ExpenseCategories.Update(entity);
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
using Distribution.Infra.UoW;
|
||||
|
||||
namespace Distribution.Features.ExpenseCategory;
|
||||
|
||||
public class ExpenseCategoryService(IExpenseCategoryRepository repo, IUnitOfWork uow)
|
||||
{
|
||||
public async Task<List<ExpenseCategoryResponse>> GetTreeAsync()
|
||||
{
|
||||
var all = await repo.GetAllAsync();
|
||||
var byParent = all.ToLookup(c => c.FK_ParentCategoryId);
|
||||
|
||||
return all
|
||||
.Where(c => c.FK_ParentCategoryId is null)
|
||||
.Select(c => Map(c, byParent))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private static ExpenseCategoryResponse Map(Models.ExpenseCategory c, ILookup<Guid?, Models.ExpenseCategory> byParent) =>
|
||||
new(c.Id, c.Name, c.Code, c.FK_ParentCategoryId, c.IsActive,
|
||||
byParent[c.Id].Select(child => Map(child, byParent)).ToList());
|
||||
|
||||
public async Task<ExpenseCategoryResponse> CreateAsync(CreateExpenseCategoryRequest request, CancellationToken ct = default)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(request.Name))
|
||||
throw new InvalidOperationException("Name is required");
|
||||
|
||||
if (request.ParentCategoryId is { } parentId)
|
||||
{
|
||||
var parent = await repo.GetByIdAsync(parentId)
|
||||
?? throw new InvalidOperationException("Parent category not found");
|
||||
if (parent.FK_ParentCategoryId is not null)
|
||||
throw new InvalidOperationException("Only two levels are allowed — cannot nest under a title");
|
||||
}
|
||||
|
||||
var entity = new Models.ExpenseCategory
|
||||
{
|
||||
Name = request.Name.Trim(),
|
||||
Code = string.IsNullOrWhiteSpace(request.Code) ? null : request.Code.Trim(),
|
||||
FK_ParentCategoryId = request.ParentCategoryId
|
||||
};
|
||||
|
||||
await uow.BeginAsync(ct);
|
||||
try { await repo.AddAsync(entity); await uow.CommitAsync(ct); }
|
||||
catch { await uow.RollbackAsync(ct); throw; }
|
||||
|
||||
return new ExpenseCategoryResponse(entity.Id, entity.Name, entity.Code, entity.FK_ParentCategoryId, entity.IsActive, []);
|
||||
}
|
||||
|
||||
public async Task<ExpenseCategoryResponse?> UpdateAsync(Guid id, UpdateExpenseCategoryRequest request, CancellationToken ct = default)
|
||||
{
|
||||
var entity = await repo.GetByIdAsync(id);
|
||||
if (entity is null) return null;
|
||||
|
||||
entity.Name = request.Name.Trim();
|
||||
entity.Code = string.IsNullOrWhiteSpace(request.Code) ? null : request.Code.Trim();
|
||||
entity.IsActive = request.IsActive;
|
||||
|
||||
await uow.BeginAsync(ct);
|
||||
try { repo.Update(entity); await uow.CommitAsync(ct); }
|
||||
catch { await uow.RollbackAsync(ct); throw; }
|
||||
|
||||
return new ExpenseCategoryResponse(entity.Id, entity.Name, entity.Code, entity.FK_ParentCategoryId, entity.IsActive, []);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
using Distribution.Models;
|
||||
|
||||
namespace Distribution.Features.ExpenseCategory;
|
||||
|
||||
public interface IExpenseCategoryRepository
|
||||
{
|
||||
Task<Models.ExpenseCategory?> GetByIdAsync(Guid id);
|
||||
Task<List<Models.ExpenseCategory>> GetAllAsync();
|
||||
Task AddAsync(Models.ExpenseCategory entity);
|
||||
void Update(Models.ExpenseCategory entity);
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Distribution.Features.ExpenseVendor;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/expense-vendors")]
|
||||
public class ExpenseVendorController(ExpenseVendorService service) : ControllerBase
|
||||
{
|
||||
[HttpGet]
|
||||
[Authorize("expense.view")]
|
||||
public async Task<IActionResult> List(
|
||||
[FromQuery] string? search,
|
||||
[FromQuery] int page = 1,
|
||||
[FromQuery] int pageSize = 50) =>
|
||||
Ok(await service.ListAsync(search, page, pageSize));
|
||||
|
||||
[HttpPost]
|
||||
[Authorize("expense.manage_categories")]
|
||||
public async Task<IActionResult> Create(CreateExpenseVendorRequest request, CancellationToken ct)
|
||||
{
|
||||
try { return Ok(await service.CreateAsync(request, ct)); }
|
||||
catch (InvalidOperationException ex) { return BadRequest(new { error = ex.Message }); }
|
||||
}
|
||||
|
||||
[HttpPut("{id:guid}")]
|
||||
[Authorize("expense.manage_categories")]
|
||||
public async Task<IActionResult> Update(Guid id, UpdateExpenseVendorRequest request, CancellationToken ct)
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = await service.UpdateAsync(id, request, ct);
|
||||
return result is null ? NotFound() : Ok(result);
|
||||
}
|
||||
catch (InvalidOperationException ex) { return BadRequest(new { error = ex.Message }); }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace Distribution.Features.ExpenseVendor;
|
||||
|
||||
public record ExpenseVendorResponse(Guid Id, string Name, string? ContactInfo, bool IsActive);
|
||||
|
||||
public record CreateExpenseVendorRequest(string Name, string? ContactInfo);
|
||||
|
||||
public record UpdateExpenseVendorRequest(string Name, string? ContactInfo, bool IsActive);
|
||||
@@ -0,0 +1,31 @@
|
||||
using Distribution.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Distribution.Features.ExpenseVendor;
|
||||
|
||||
public class ExpenseVendorRepository(AppDBContext context) : IExpenseVendorRepository
|
||||
{
|
||||
public async Task<Models.ExpenseVendor?> GetByIdAsync(Guid id) =>
|
||||
await context.ExpenseVendors.FirstOrDefaultAsync(v => v.Id == id);
|
||||
|
||||
public async Task<List<Models.ExpenseVendor>> ListAsync(string? search, int page, int pageSize) =>
|
||||
await context.ExpenseVendors
|
||||
.Where(v => v.IsActive && (search == null || v.Name.Contains(search)))
|
||||
.OrderBy(v => v.Name)
|
||||
.Skip((page - 1) * pageSize)
|
||||
.Take(pageSize)
|
||||
.ToListAsync();
|
||||
|
||||
public async Task<int> CountAsync(string? search) =>
|
||||
await context.ExpenseVendors
|
||||
.CountAsync(v => v.IsActive && (search == null || v.Name.Contains(search)));
|
||||
|
||||
public async Task<bool> NameExistsAsync(string name, Guid? excludeId) =>
|
||||
await context.ExpenseVendors.AnyAsync(v => v.Name == name && (excludeId == null || v.Id != excludeId));
|
||||
|
||||
public async Task AddAsync(Models.ExpenseVendor entity) =>
|
||||
await context.ExpenseVendors.AddAsync(entity);
|
||||
|
||||
public void Update(Models.ExpenseVendor entity) =>
|
||||
context.ExpenseVendors.Update(entity);
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
using Distribution.Infra.UoW;
|
||||
|
||||
namespace Distribution.Features.ExpenseVendor;
|
||||
|
||||
public record PagedExpenseVendorsResponse(List<ExpenseVendorResponse> Items, int Page, int PageSize, int Total);
|
||||
|
||||
public class ExpenseVendorService(IExpenseVendorRepository repo, IUnitOfWork uow)
|
||||
{
|
||||
private static ExpenseVendorResponse Map(Models.ExpenseVendor v) =>
|
||||
new(v.Id, v.Name, v.ContactInfo, v.IsActive);
|
||||
|
||||
public async Task<PagedExpenseVendorsResponse> ListAsync(string? search, int page, int pageSize)
|
||||
{
|
||||
if (page < 1) page = 1;
|
||||
if (pageSize < 1 || pageSize > 200) pageSize = 50;
|
||||
|
||||
search = string.IsNullOrWhiteSpace(search) ? null : search.Trim();
|
||||
var items = await repo.ListAsync(search, page, pageSize);
|
||||
var total = await repo.CountAsync(search);
|
||||
return new PagedExpenseVendorsResponse(items.Select(Map).ToList(), page, pageSize, total);
|
||||
}
|
||||
|
||||
public async Task<ExpenseVendorResponse> CreateAsync(CreateExpenseVendorRequest request, CancellationToken ct = default)
|
||||
{
|
||||
var name = (request.Name ?? "").Trim();
|
||||
if (string.IsNullOrEmpty(name)) throw new InvalidOperationException("Name is required");
|
||||
if (await repo.NameExistsAsync(name, null))
|
||||
throw new InvalidOperationException("A vendor with this name already exists");
|
||||
|
||||
var entity = new Models.ExpenseVendor { Name = name, ContactInfo = request.ContactInfo };
|
||||
|
||||
await uow.BeginAsync(ct);
|
||||
try { await repo.AddAsync(entity); await uow.CommitAsync(ct); }
|
||||
catch { await uow.RollbackAsync(ct); throw; }
|
||||
|
||||
return Map(entity);
|
||||
}
|
||||
|
||||
public async Task<ExpenseVendorResponse?> UpdateAsync(Guid id, UpdateExpenseVendorRequest request, CancellationToken ct = default)
|
||||
{
|
||||
var entity = await repo.GetByIdAsync(id);
|
||||
if (entity is null) return null;
|
||||
|
||||
var name = (request.Name ?? "").Trim();
|
||||
if (string.IsNullOrEmpty(name)) throw new InvalidOperationException("Name is required");
|
||||
if (await repo.NameExistsAsync(name, id))
|
||||
throw new InvalidOperationException("A vendor with this name already exists");
|
||||
|
||||
entity.Name = name;
|
||||
entity.ContactInfo = request.ContactInfo;
|
||||
entity.IsActive = request.IsActive;
|
||||
|
||||
await uow.BeginAsync(ct);
|
||||
try { repo.Update(entity); await uow.CommitAsync(ct); }
|
||||
catch { await uow.RollbackAsync(ct); throw; }
|
||||
|
||||
return Map(entity);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace Distribution.Features.ExpenseVendor;
|
||||
|
||||
public interface IExpenseVendorRepository
|
||||
{
|
||||
Task<Models.ExpenseVendor?> GetByIdAsync(Guid id);
|
||||
Task<List<Models.ExpenseVendor>> ListAsync(string? search, int page, int pageSize);
|
||||
Task<int> CountAsync(string? search);
|
||||
Task<bool> NameExistsAsync(string name, Guid? excludeId);
|
||||
Task AddAsync(Models.ExpenseVendor entity);
|
||||
void Update(Models.ExpenseVendor entity);
|
||||
}
|
||||
@@ -9,6 +9,12 @@ public interface IInvoiceRepository
|
||||
Task<List<Models.Invoice>> GetAllAsync(Guid? agentId, Guid? routeId, Guid? shopId, InvoiceStatus? status, PaymentStatus? paymentStatus, int page, int pageSize);
|
||||
Task<int> GetCountAsync(Guid? agentId, Guid? routeId, Guid? shopId, InvoiceStatus? status, PaymentStatus? paymentStatus);
|
||||
Task<List<Models.Invoice>> GetOutstandingAsync();
|
||||
Task<decimal> GetSchemeDiscountAsync(Guid shopPOId);
|
||||
Task AddAsync(Models.Invoice invoice);
|
||||
Task<Models.Item?> GetItemAsync(Guid itemId);
|
||||
Task AddItemsAsync(IEnumerable<InvoiceItem> items);
|
||||
Task AddVersionAsync(InvoiceVersion version);
|
||||
Task<int> GetLatestVersionNumberAsync(Guid invoiceId);
|
||||
Task<List<InvoiceVersion>> GetVersionsAsync(Guid invoiceId);
|
||||
void Update(Models.Invoice invoice);
|
||||
}
|
||||
|
||||
@@ -36,6 +36,52 @@ public class InvoiceController(InvoiceService service, ICurrentUserService curre
|
||||
return result is null ? NotFound() : Ok(result);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> Create([FromBody] CreateInvoiceRequest request, CancellationToken ct)
|
||||
{
|
||||
// TEMPORARY: currentUser.UserId is always unset due to the broken auth middleware
|
||||
// (testbug.md #28) — fall back to the explicitly supplied user until that's fixed.
|
||||
var userId = currentUser.UserId ?? request.CreatedByUserId;
|
||||
if (userId is null) return BadRequest(new { error = "CreatedByUserId is required while auth is unavailable" });
|
||||
try
|
||||
{
|
||||
var result = await service.CreateAsync(request, userId.Value, ct);
|
||||
return CreatedAtAction(nameof(GetById), new { id = result.Id }, result);
|
||||
}
|
||||
catch (KeyNotFoundException) { return NotFound(); }
|
||||
catch (InvalidOperationException ex) { return BadRequest(new { error = ex.Message }); }
|
||||
}
|
||||
|
||||
[HttpPost("{id:guid}/preview-totals")]
|
||||
public async Task<IActionResult> PreviewTotals(Guid id, [FromBody] PreviewInvoiceTotalsRequest request)
|
||||
{
|
||||
var result = await service.PreviewTotalsAsync(id, request);
|
||||
return result is null ? NotFound() : Ok(result);
|
||||
}
|
||||
|
||||
[HttpGet("{id:guid}/versions")]
|
||||
public async Task<IActionResult> GetVersions(Guid id)
|
||||
{
|
||||
var result = await service.GetVersionsAsync(id);
|
||||
return result is null ? NotFound() : Ok(result);
|
||||
}
|
||||
|
||||
[HttpPut("{id:guid}/items")]
|
||||
public async Task<IActionResult> UpdateItems(Guid id, [FromBody] UpdateInvoiceItemsRequest request, CancellationToken ct)
|
||||
{
|
||||
// TEMPORARY: currentUser.UserId is always unset due to the broken auth middleware
|
||||
// (testbug.md #28) — fall back to the explicitly supplied user until that's fixed.
|
||||
var userId = currentUser.UserId ?? request.EditedByUserId;
|
||||
if (userId is null) return BadRequest(new { error = "EditedByUserId is required while auth is unavailable" });
|
||||
try
|
||||
{
|
||||
var result = await service.UpdateItemsAsync(id, request, userId.Value, ct);
|
||||
return Ok(result);
|
||||
}
|
||||
catch (KeyNotFoundException) { return NotFound(); }
|
||||
catch (InvalidOperationException ex) { return BadRequest(new { error = ex.Message }); }
|
||||
}
|
||||
|
||||
[HttpPost("{id:guid}/print")]
|
||||
public async Task<IActionResult> Print(Guid id, [FromBody] PrintInvoiceRequest? request, CancellationToken ct)
|
||||
{
|
||||
|
||||
@@ -1,15 +1,52 @@
|
||||
using Distribution.Models;
|
||||
using Distribution.Features.ShopPO;
|
||||
using Distribution.Models;
|
||||
|
||||
namespace Distribution.Features.Invoice;
|
||||
|
||||
public record CreateInvoiceRequest(Guid ShopPOId);
|
||||
public record InvoiceItemAdjustment(Guid ShopPOItemId, int AdjustedQuantity, string? Note = null);
|
||||
public record CreateInvoiceRequest(Guid ShopPOId, Guid? CreatedByUserId = null);
|
||||
public record InvoiceItemAdjustment(Guid InvoiceItemId, int AdjustedQuantity, bool IsRemoved = false, string? Note = null);
|
||||
public record FinalizeInvoiceRequest(List<InvoiceItemAdjustment>? Adjustments = null, Guid? FinalizedByUserId = null);
|
||||
public record PrintInvoiceRequest(Guid? PrintedByUserId = null);
|
||||
|
||||
// InvoiceItemId set = edit/remove/restore an existing line; ItemId set (InvoiceItemId null) = add a new line.
|
||||
public record InvoiceItemEdit(Guid? InvoiceItemId, Guid? ItemId, int Quantity, decimal? UnitPrice = null, bool IsRemoved = false);
|
||||
// DiscountSchemeIds: the percentage-discount scheme chosen in the editor (at most one); null = leave as is.
|
||||
public record UpdateInvoiceItemsRequest(List<InvoiceItemEdit> Items, Guid? EditedByUserId = null, List<Guid>? DiscountSchemeIds = null);
|
||||
|
||||
public record PreviewLine(Guid ItemId, int Quantity, decimal UnitPrice, bool IsFree = false, bool IsRemoved = false, Guid? SchemeId = null);
|
||||
public record PreviewInvoiceTotalsRequest(List<PreviewLine> Items, List<Guid>? DiscountSchemeIds = null);
|
||||
// Quantity = how many free units the scheme still entitles for the lines as typed.
|
||||
public record FreeEntitlement(Guid SchemeId, int Quantity);
|
||||
public record InvoiceTotalsPreview(decimal DiscountAmount, decimal TotalAmount, List<FreeEntitlement> FreeEntitlements);
|
||||
|
||||
public record InvoiceVersionItemResponse(Guid? ShopPOItemId, Guid ItemId, string ItemName, int Quantity, decimal UnitPrice, bool IsRemoved);
|
||||
|
||||
public record InvoiceVersionResponse(
|
||||
Guid Id,
|
||||
int VersionNumber,
|
||||
InvoiceVersionStage Stage,
|
||||
decimal TotalAmount,
|
||||
Guid CreatedBy,
|
||||
DateTime CreatedAt,
|
||||
List<InvoiceVersionItemResponse> Items);
|
||||
|
||||
public record InvoiceItemResponse(
|
||||
Guid Id,
|
||||
Guid? ShopPOItemId,
|
||||
Guid ItemId,
|
||||
string ItemName,
|
||||
int? OrderedQuantity,
|
||||
int Quantity,
|
||||
decimal UnitPrice,
|
||||
bool IsRemoved,
|
||||
bool IsFree = false,
|
||||
Guid? SchemeId = null,
|
||||
string? SchemeName = null);
|
||||
|
||||
public record InvoiceResponse(
|
||||
Guid Id,
|
||||
Guid ShopPOId,
|
||||
Guid StoreId,
|
||||
string StoreName,
|
||||
InvoiceStatus Status,
|
||||
PaymentStatus PaymentStatus,
|
||||
@@ -19,7 +56,11 @@ public record InvoiceResponse(
|
||||
DateTime? FinalizedAt,
|
||||
Guid? FinalizedBy,
|
||||
decimal TotalAmount,
|
||||
decimal DiscountAmount,
|
||||
decimal PaidAmount,
|
||||
bool IsActive,
|
||||
DateTime CreatedAt);
|
||||
DateTime CreatedAt,
|
||||
List<InvoiceItemResponse> Items,
|
||||
List<ShopPODiscountResponse>? Discounts = null);
|
||||
|
||||
public record OutstandingPaymentsResponse(int InvoiceCount, decimal OutstandingAmount);
|
||||
|
||||
@@ -10,6 +10,8 @@ public class InvoiceRepository(AppDBContext context) : IInvoiceRepository
|
||||
await context.Invoices
|
||||
.Include(i => i.ShopPO).ThenInclude(p => p.Store)
|
||||
.Include(i => i.ShopPO).ThenInclude(p => p.Items.Where(x => x.IsActive))
|
||||
.Include(i => i.Items.Where(x => x.IsActive)).ThenInclude(x => x.Item)
|
||||
.Include(i => i.Items.Where(x => x.IsActive)).ThenInclude(x => x.ShopPOItem)
|
||||
.Include(i => i.Payments.Where(p => p.IsActive))
|
||||
.FirstOrDefaultAsync(i => i.Id == id && i.IsActive);
|
||||
|
||||
@@ -21,6 +23,7 @@ public class InvoiceRepository(AppDBContext context) : IInvoiceRepository
|
||||
public async Task<List<Models.Invoice>> GetAllAsync(Guid? agentId, Guid? routeId, Guid? shopId, InvoiceStatus? status, PaymentStatus? paymentStatus, int page, int pageSize) =>
|
||||
await context.Invoices
|
||||
.Include(i => i.ShopPO).ThenInclude(p => p.Store)
|
||||
.Include(i => i.Payments.Where(p => p.IsActive))
|
||||
.Where(i => i.IsActive
|
||||
&& (agentId == null || i.ShopPO.RaisedById == agentId)
|
||||
&& (routeId == null || i.ShopPO.Store.FK_RouteId == routeId)
|
||||
@@ -48,9 +51,35 @@ public class InvoiceRepository(AppDBContext context) : IInvoiceRepository
|
||||
.Where(i => i.IsActive && i.PaymentStatus != PaymentStatus.Paid)
|
||||
.ToListAsync();
|
||||
|
||||
public async Task<decimal> GetSchemeDiscountAsync(Guid shopPOId) =>
|
||||
await context.SchemeRedemptions
|
||||
.Where(r => r.FK_ShopPOId == shopPOId)
|
||||
.SumAsync(r => r.DiscountAmount) ?? 0m;
|
||||
|
||||
public async Task AddAsync(Models.Invoice invoice) =>
|
||||
await context.Invoices.AddAsync(invoice);
|
||||
|
||||
public async Task<Models.Item?> GetItemAsync(Guid itemId) =>
|
||||
await context.Items.FirstOrDefaultAsync(i => i.Id == itemId && i.IsActive);
|
||||
|
||||
public async Task AddItemsAsync(IEnumerable<InvoiceItem> items) =>
|
||||
await context.InvoiceItems.AddRangeAsync(items);
|
||||
|
||||
public async Task AddVersionAsync(InvoiceVersion version) =>
|
||||
await context.InvoiceVersions.AddAsync(version);
|
||||
|
||||
public async Task<int> GetLatestVersionNumberAsync(Guid invoiceId) =>
|
||||
await context.InvoiceVersions
|
||||
.Where(v => v.FK_InvoiceId == invoiceId)
|
||||
.MaxAsync(v => (int?)v.VersionNumber) ?? 0;
|
||||
|
||||
public async Task<List<InvoiceVersion>> GetVersionsAsync(Guid invoiceId) =>
|
||||
await context.InvoiceVersions
|
||||
.Include(v => v.Items).ThenInclude(i => i.Item)
|
||||
.Where(v => v.FK_InvoiceId == invoiceId && v.IsActive)
|
||||
.OrderBy(v => v.VersionNumber)
|
||||
.ToListAsync();
|
||||
|
||||
public void Update(Models.Invoice invoice) =>
|
||||
context.Invoices.Update(invoice);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
using Distribution.Features.Stock;
|
||||
using Distribution.Features.Scheme;
|
||||
using Distribution.Features.ShopPO;
|
||||
using Distribution.Features.Stock;
|
||||
using Distribution.Infra.Authorization;
|
||||
using Distribution.Infra.UoW;
|
||||
using Distribution.Models;
|
||||
@@ -7,21 +9,126 @@ namespace Distribution.Features.Invoice;
|
||||
|
||||
public class InvoiceService(
|
||||
IInvoiceRepository repo,
|
||||
IShopPORepository shopPoRepo,
|
||||
SchemeService schemeService,
|
||||
IUnitOfWork uow,
|
||||
ICurrentUserService currentUser)
|
||||
{
|
||||
private static InvoiceResponse ToResponse(Models.Invoice i) =>
|
||||
new(i.Id, i.FK_ShopPOId,
|
||||
i.ShopPO?.FK_StoreId ?? Guid.Empty,
|
||||
i.ShopPO?.Store?.StoreName ?? string.Empty,
|
||||
i.Status, i.PaymentStatus, i.CreatedBy,
|
||||
i.PrintedAt, i.PrintedBy,
|
||||
i.FinalizedAt, i.FinalizedBy,
|
||||
i.TotalAmount, i.IsActive, i.CreatedAt);
|
||||
i.TotalAmount, i.DiscountAmount,
|
||||
i.Payments?.Where(p => p.IsActive).Sum(p => p.Amount) ?? 0m,
|
||||
i.IsActive, i.CreatedAt,
|
||||
i.Items.Where(x => x.IsActive).Select(x =>
|
||||
new InvoiceItemResponse(x.Id, x.FK_ShopPOItemId, x.FK_ItemId,
|
||||
x.Item?.Name ?? string.Empty, x.ShopPOItem?.Quantity,
|
||||
x.Quantity, x.UnitPrice, x.IsRemoved, x.IsFree, x.FK_SchemeId, x.SchemeName)).ToList());
|
||||
|
||||
private static decimal BillableTotal(IEnumerable<InvoiceItem> items) =>
|
||||
items.Where(i => i.IsActive && !i.IsRemoved).Sum(i => i.Quantity * i.UnitPrice);
|
||||
|
||||
// The lines that count toward a percentage discount: active, not struck out. Free issues carry the flag so the
|
||||
// scheme rules ignore them.
|
||||
private static List<ShopPOItem> DiscountableLines(IEnumerable<InvoiceItem> items) =>
|
||||
items.Where(i => i.IsActive && !i.IsRemoved)
|
||||
.Select(i => new ShopPOItem { FK_ItemId = i.FK_ItemId, Quantity = i.Quantity, UnitPrice = i.UnitPrice, IsFree = i.IsFree })
|
||||
.ToList();
|
||||
|
||||
// TotalAmount = billable lines - scheme discount. The discount is re-evaluated against the current lines
|
||||
// (so adding/removing/re-pricing items moves it, and a slab level can be gained or lost) whenever lines change
|
||||
// and at print/finalize. The redemption amount is updated with it.
|
||||
private async Task ApplyTotalsAsync(Models.Invoice invoice, IEnumerable<InvoiceItem> items)
|
||||
{
|
||||
var lines = items.ToList();
|
||||
// Lowering/removing the paid item that earned a free issue lowers the free quantity with it.
|
||||
await schemeService.ReconcileInvoiceFreeLinesAsync(invoice.FK_ShopPOId, lines);
|
||||
var billable = BillableTotal(lines);
|
||||
var discount = await schemeService.CalculateDiscountAsync(invoice.FK_ShopPOId, DiscountableLines(lines), persist: true);
|
||||
invoice.DiscountAmount = Math.Min(discount, billable);
|
||||
invoice.TotalAmount = billable - invoice.DiscountAmount;
|
||||
}
|
||||
|
||||
// Live preview for the draft editor: what the discount and total would be for these unsaved lines. Changes nothing.
|
||||
public async Task<InvoiceTotalsPreview?> PreviewTotalsAsync(Guid id, PreviewInvoiceTotalsRequest request)
|
||||
{
|
||||
var invoice = await repo.GetByIdAsync(id);
|
||||
if (invoice is null) return null;
|
||||
|
||||
var lines = request.Items.Where(l => !l.IsRemoved)
|
||||
.Select(l => new ShopPOItem { FK_ItemId = l.ItemId, Quantity = l.Quantity, UnitPrice = l.UnitPrice, IsFree = l.IsFree, FK_SchemeId = l.SchemeId })
|
||||
.ToList();
|
||||
var billable = lines.Sum(l => l.Quantity * l.UnitPrice);
|
||||
var chosen = request.DiscountSchemeIds is null
|
||||
? await schemeService.CalculateDiscountAsync(invoice.FK_ShopPOId, lines, persist: false)
|
||||
: await schemeService.CalculateDiscountForSchemesAsync(request.DiscountSchemeIds, lines);
|
||||
var discount = Math.Min(chosen, billable);
|
||||
|
||||
var schemeIds = lines.Where(l => l.IsFree && l.FK_SchemeId.HasValue).Select(l => l.FK_SchemeId!.Value).ToList();
|
||||
var entitlements = (await schemeService.GetFreeEntitlementsAsync(schemeIds, lines))
|
||||
.Select(e => new FreeEntitlement(e.Key, e.Value)).ToList();
|
||||
return new InvoiceTotalsPreview(discount, billable - discount, entitlements);
|
||||
}
|
||||
|
||||
// Immutable copy of the invoice lines as they stand right now.
|
||||
public static InvoiceVersion BuildVersion(Models.Invoice invoice, IEnumerable<InvoiceItem> items, int versionNumber, InvoiceVersionStage stage, Guid userId) =>
|
||||
new()
|
||||
{
|
||||
FK_InvoiceId = invoice.Id,
|
||||
VersionNumber = versionNumber,
|
||||
Stage = stage,
|
||||
TotalAmount = invoice.TotalAmount,
|
||||
CreatedBy = userId,
|
||||
Items = items.Select(i => new InvoiceVersionItem
|
||||
{
|
||||
FK_ShopPOItemId = i.FK_ShopPOItemId,
|
||||
FK_ItemId = i.FK_ItemId,
|
||||
Quantity = i.Quantity,
|
||||
UnitPrice = i.UnitPrice,
|
||||
IsRemoved = i.IsRemoved
|
||||
}).ToList()
|
||||
};
|
||||
|
||||
public async Task<List<InvoiceVersionResponse>?> GetVersionsAsync(Guid id)
|
||||
{
|
||||
if (await repo.GetByIdAsync(id) is null) return null;
|
||||
var versions = await repo.GetVersionsAsync(id);
|
||||
return versions.Select(v => new InvoiceVersionResponse(
|
||||
v.Id, v.VersionNumber, v.Stage, v.TotalAmount, v.CreatedBy, v.CreatedAt,
|
||||
v.Items.Select(i => new InvoiceVersionItemResponse(
|
||||
i.FK_ShopPOItemId, i.FK_ItemId, i.Item?.Name ?? string.Empty, i.Quantity, i.UnitPrice, i.IsRemoved)).ToList())).ToList();
|
||||
}
|
||||
|
||||
private async Task AddVersionAsync(Models.Invoice invoice, InvoiceVersionStage stage, Guid userId, IEnumerable<InvoiceItem>? addedLines = null)
|
||||
{
|
||||
var next = await repo.GetLatestVersionNumberAsync(invoice.Id) + 1;
|
||||
// Added lines may or may not be in invoice.Items yet, so merge by reference.
|
||||
var lines = invoice.Items.Concat(addedLines ?? []).Where(i => i.IsActive).Distinct();
|
||||
await repo.AddVersionAsync(BuildVersion(invoice, lines, next, stage, userId));
|
||||
}
|
||||
|
||||
public static List<InvoiceItem> BuildItemsFromShopPO(Models.ShopPO po, Guid invoiceId) =>
|
||||
po.Items.Where(i => i.IsActive).Select(i => new InvoiceItem
|
||||
{
|
||||
FK_InvoiceId = invoiceId,
|
||||
FK_ShopPOItemId = i.Id,
|
||||
FK_ItemId = i.FK_ItemId,
|
||||
Quantity = i.Quantity,
|
||||
UnitPrice = i.UnitPrice,
|
||||
IsFree = i.IsFree,
|
||||
FK_SchemeId = i.FK_SchemeId,
|
||||
SchemeName = i.SchemeName
|
||||
}).ToList();
|
||||
|
||||
public async Task<InvoiceResponse?> GetByIdAsync(Guid id)
|
||||
{
|
||||
var invoice = await repo.GetByIdAsync(id);
|
||||
return invoice is null ? null : ToResponse(invoice);
|
||||
if (invoice is null) return null;
|
||||
return ToResponse(invoice) with { Discounts = await schemeService.GetDiscountsForShopPOAsync(invoice.FK_ShopPOId) };
|
||||
}
|
||||
|
||||
public async Task<PagedResponse<InvoiceResponse>> GetAllAsync(Guid? agentId, Guid? routeId, Guid? shopId, InvoiceStatus? status, PaymentStatus? paymentStatus, int page, int pageSize)
|
||||
@@ -39,6 +146,131 @@ public class InvoiceService(
|
||||
return new OutstandingPaymentsResponse(count, outstanding);
|
||||
}
|
||||
|
||||
public async Task<InvoiceResponse> CreateAsync(CreateInvoiceRequest request, Guid createdBy, CancellationToken ct = default)
|
||||
{
|
||||
var po = await shopPoRepo.GetByIdAsync(request.ShopPOId)
|
||||
?? throw new KeyNotFoundException("Shop PO not found");
|
||||
|
||||
if (po.ConfirmedAt is null)
|
||||
throw new InvalidOperationException("Shop PO must be confirmed before an invoice can be created");
|
||||
if (po.Status == ShopPOStatus.Cancelled)
|
||||
throw new InvalidOperationException("Cannot create an invoice for a cancelled Shop PO");
|
||||
if (po.InvoiceId is not null)
|
||||
throw new InvalidOperationException("An invoice already exists for this Shop PO");
|
||||
|
||||
var invoice = new Models.Invoice
|
||||
{
|
||||
FK_ShopPOId = po.Id,
|
||||
Status = InvoiceStatus.Draft,
|
||||
PaymentStatus = PaymentStatus.Pending,
|
||||
CreatedBy = createdBy
|
||||
};
|
||||
var lines = BuildItemsFromShopPO(po, invoice.Id);
|
||||
await ApplyTotalsAsync(invoice, lines);
|
||||
|
||||
await uow.BeginAsync(ct);
|
||||
try
|
||||
{
|
||||
await repo.AddAsync(invoice);
|
||||
await repo.AddItemsAsync(lines);
|
||||
await repo.AddVersionAsync(BuildVersion(invoice, lines, 1, InvoiceVersionStage.Created, createdBy));
|
||||
po.InvoiceId = invoice.Id;
|
||||
shopPoRepo.Update(po);
|
||||
await uow.CommitAsync(ct);
|
||||
}
|
||||
catch { await uow.RollbackAsync(ct); throw; }
|
||||
|
||||
var saved = await repo.GetByIdAsync(invoice.Id);
|
||||
return ToResponse(saved!);
|
||||
}
|
||||
|
||||
public async Task<InvoiceResponse> UpdateItemsAsync(Guid id, UpdateInvoiceItemsRequest request, Guid editedBy, CancellationToken ct = default)
|
||||
{
|
||||
var invoice = await repo.GetByIdAsync(id)
|
||||
?? throw new KeyNotFoundException("Invoice not found");
|
||||
|
||||
if (invoice.Status != InvoiceStatus.Draft)
|
||||
throw new InvalidOperationException($"Items can only be edited while the invoice is Draft (current: {invoice.Status})");
|
||||
|
||||
var newLines = new List<InvoiceItem>();
|
||||
|
||||
foreach (var edit in request.Items)
|
||||
{
|
||||
if (edit.UnitPrice is < 0)
|
||||
throw new InvalidOperationException("Unit price cannot be negative");
|
||||
|
||||
if (edit.InvoiceItemId is Guid lineId)
|
||||
{
|
||||
// Existing line: edit quantity/price, or strike it (IsRemoved) / restore it.
|
||||
var item = invoice.Items.FirstOrDefault(i => i.Id == lineId && i.IsActive)
|
||||
?? throw new InvalidOperationException($"Invoice item {lineId} not found on this invoice");
|
||||
|
||||
item.IsRemoved = edit.IsRemoved;
|
||||
if (!edit.IsRemoved)
|
||||
{
|
||||
ValidateQuantity(item, edit.Quantity);
|
||||
item.Quantity = edit.Quantity;
|
||||
}
|
||||
if (edit.UnitPrice is decimal price)
|
||||
{
|
||||
if (item.IsFree && price != 0m)
|
||||
throw new InvalidOperationException("A free-issue line must stay at zero price");
|
||||
item.UnitPrice = price;
|
||||
}
|
||||
}
|
||||
else if (edit.ItemId is Guid itemId)
|
||||
{
|
||||
// New line for an item that was not on the PO.
|
||||
if (edit.IsRemoved) continue;
|
||||
if (edit.Quantity <= 0)
|
||||
throw new InvalidOperationException("Quantity for an added item must be greater than zero");
|
||||
if (edit.UnitPrice is null)
|
||||
throw new InvalidOperationException("Unit price is required for an added item");
|
||||
if (invoice.Items.Concat(newLines).Any(i => i.IsActive && !i.IsRemoved && i.FK_ItemId == itemId))
|
||||
throw new InvalidOperationException("This item is already on the invoice — edit its existing line instead");
|
||||
|
||||
var catalogItem = await repo.GetItemAsync(itemId)
|
||||
?? throw new InvalidOperationException($"Item {itemId} not found");
|
||||
|
||||
newLines.Add(new InvoiceItem
|
||||
{
|
||||
FK_InvoiceId = invoice.Id,
|
||||
FK_ItemId = catalogItem.Id,
|
||||
Item = catalogItem,
|
||||
Quantity = edit.Quantity,
|
||||
UnitPrice = edit.UnitPrice.Value
|
||||
});
|
||||
}
|
||||
else
|
||||
throw new InvalidOperationException("Each edit needs an invoiceItemId (existing line) or an itemId (new line)");
|
||||
}
|
||||
|
||||
var allLines = invoice.Items.Concat(newLines).ToList();
|
||||
await schemeService.ApplyInvoiceDiscountSelectionAsync(
|
||||
invoice.FK_ShopPOId, invoice.ShopPO?.FK_StoreId ?? Guid.Empty, allLines, request.DiscountSchemeIds);
|
||||
await ApplyTotalsAsync(invoice, allLines);
|
||||
|
||||
await uow.BeginAsync(ct);
|
||||
try
|
||||
{
|
||||
repo.Update(invoice);
|
||||
if (newLines.Count > 0) await repo.AddItemsAsync(newLines);
|
||||
await AddVersionAsync(invoice, InvoiceVersionStage.DraftEdited, editedBy, newLines);
|
||||
await uow.CommitAsync(ct);
|
||||
}
|
||||
catch { await uow.RollbackAsync(ct); throw; }
|
||||
|
||||
return (await GetByIdAsync(id))!;
|
||||
}
|
||||
|
||||
private static void ValidateQuantity(InvoiceItem item, int quantity)
|
||||
{
|
||||
var ordered = item.ShopPOItem?.Quantity ?? int.MaxValue;
|
||||
if (quantity < 0 || quantity > ordered)
|
||||
throw new InvalidOperationException(
|
||||
$"Quantity for item {item.Id} must be between 0 and {ordered}");
|
||||
}
|
||||
|
||||
public async Task<InvoiceResponse> PrintAsync(Guid id, Guid printedBy, CancellationToken ct = default)
|
||||
{
|
||||
var invoice = await repo.GetByIdAsync(id)
|
||||
@@ -47,12 +279,21 @@ public class InvoiceService(
|
||||
if (invoice.Status != InvoiceStatus.Draft)
|
||||
throw new InvalidOperationException($"Cannot print an invoice in {invoice.Status} status");
|
||||
|
||||
// Printing fixes the amount the customer sees, so pick up any scheme discount applied since the last edit.
|
||||
var lines = invoice.Items.Where(i => i.IsActive).ToList();
|
||||
if (lines.Count > 0) await ApplyTotalsAsync(invoice, lines);
|
||||
|
||||
invoice.Status = InvoiceStatus.Printed;
|
||||
invoice.PrintedAt = DateTime.Now;
|
||||
invoice.PrintedBy = printedBy;
|
||||
|
||||
await uow.BeginAsync(ct);
|
||||
try { repo.Update(invoice); await uow.CommitAsync(ct); }
|
||||
try
|
||||
{
|
||||
repo.Update(invoice);
|
||||
await AddVersionAsync(invoice, InvoiceVersionStage.Printed, printedBy);
|
||||
await uow.CommitAsync(ct);
|
||||
}
|
||||
catch { await uow.RollbackAsync(ct); throw; }
|
||||
|
||||
return ToResponse(invoice);
|
||||
@@ -66,31 +307,37 @@ public class InvoiceService(
|
||||
if (invoice.Status != InvoiceStatus.Printed)
|
||||
throw new InvalidOperationException($"Cannot finalize an invoice in {invoice.Status} status");
|
||||
|
||||
var items = invoice.Items.Where(i => i.IsActive).ToList();
|
||||
var adjustments = request.Adjustments ?? [];
|
||||
if (adjustments.Count > 0)
|
||||
|
||||
// Agent's handwritten edits from the printed copy are written onto the invoice lines.
|
||||
foreach (var adjustment in adjustments)
|
||||
{
|
||||
var poItems = invoice.ShopPO.Items.Where(i => i.IsActive).ToList();
|
||||
decimal total = 0;
|
||||
foreach (var item in poItems)
|
||||
var item = items.FirstOrDefault(i => i.Id == adjustment.InvoiceItemId)
|
||||
?? throw new InvalidOperationException($"Invoice item {adjustment.InvoiceItemId} is not on this invoice");
|
||||
|
||||
item.IsRemoved = adjustment.IsRemoved;
|
||||
if (!adjustment.IsRemoved)
|
||||
{
|
||||
var adjustment = adjustments.FirstOrDefault(a => a.ShopPOItemId == item.Id);
|
||||
var billedQuantity = adjustment is not null ? adjustment.AdjustedQuantity : item.Quantity;
|
||||
|
||||
if (billedQuantity < 0 || billedQuantity > item.Quantity)
|
||||
throw new InvalidOperationException(
|
||||
$"Adjusted quantity for item {item.Id} must be between 0 and {item.Quantity}");
|
||||
|
||||
total += billedQuantity * item.UnitPrice;
|
||||
ValidateQuantity(item, adjustment.AdjustedQuantity);
|
||||
item.Quantity = adjustment.AdjustedQuantity;
|
||||
}
|
||||
invoice.TotalAmount = total;
|
||||
}
|
||||
|
||||
if (items.Count > 0)
|
||||
await ApplyTotalsAsync(invoice, items);
|
||||
|
||||
invoice.Status = InvoiceStatus.Finalized;
|
||||
invoice.FinalizedAt = DateTime.Now;
|
||||
invoice.FinalizedBy = finalizedBy;
|
||||
|
||||
await uow.BeginAsync(ct);
|
||||
try { repo.Update(invoice); await uow.CommitAsync(ct); }
|
||||
try
|
||||
{
|
||||
repo.Update(invoice);
|
||||
await AddVersionAsync(invoice, InvoiceVersionStage.Finalized, finalizedBy);
|
||||
await uow.CommitAsync(ct);
|
||||
}
|
||||
catch { await uow.RollbackAsync(ct); throw; }
|
||||
|
||||
return ToResponse(invoice);
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
namespace Distribution.Features.Notifications;
|
||||
|
||||
public interface INotificationRepository
|
||||
{
|
||||
// ── Inbox ────────────────────────────────────────────────────────────
|
||||
Task AddAsync(Models.Notification notification);
|
||||
/// <summary>Stops tracking a notification (and its recipients) whose save failed.</summary>
|
||||
void Discard(Models.Notification notification);
|
||||
|
||||
Task<int> GetInboxCountAsync(Guid userId, bool unreadOnly);
|
||||
Task<List<Models.NotificationRecipient>> GetInboxAsync(Guid userId, bool unreadOnly, int page, int pageSize);
|
||||
Task<int> GetUnreadCountAsync(Guid userId);
|
||||
Task<Models.NotificationRecipient?> GetRecipientAsync(Guid userId, Guid recipientId);
|
||||
Task<List<Models.NotificationRecipient>> GetUnreadRecipientsAsync(Guid userId);
|
||||
void RemoveRecipient(Models.NotificationRecipient recipient);
|
||||
|
||||
// ── Recipient resolution ─────────────────────────────────────────────
|
||||
Task<List<Guid>> FilterActiveUserIdsAsync(IEnumerable<Guid> userIds);
|
||||
Task<List<Guid>> GetUserIdsByPermissionAsync(string permissionCode);
|
||||
Task<List<Guid>> GetUserIdsByScopeAsync(ScopeKind kind, Guid id);
|
||||
Task<Guid?> GetReportsToUserIdAsync(Guid userId);
|
||||
Task<Guid?> GetRouteOwnerUserIdAsync(ScopeKind kind, Guid id);
|
||||
|
||||
// ── Retention ────────────────────────────────────────────────────────
|
||||
Task<List<Models.NotificationRecipient>> GetExpiredReadRecipientsAsync(DateTime cutoff, int take);
|
||||
Task<List<Models.Notification>> GetOrphanNotificationsAsync(int take);
|
||||
void RemoveRecipients(IEnumerable<Models.NotificationRecipient> recipients);
|
||||
void RemoveNotifications(IEnumerable<Models.Notification> notifications);
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
using Distribution.Infra.UoW;
|
||||
|
||||
namespace Distribution.Features.Notifications;
|
||||
|
||||
/// <summary>
|
||||
/// Daily retention purge: deletes read inbox rows older than <c>Notifications:RetentionDays</c>
|
||||
/// (default 90), then any notification left with no recipients.
|
||||
/// </summary>
|
||||
public class NotificationCleanupJob(
|
||||
IServiceScopeFactory scopes,
|
||||
IConfiguration config,
|
||||
ILogger<NotificationCleanupJob> logger) : BackgroundService
|
||||
{
|
||||
private const int BatchSize = 500;
|
||||
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
// Let the app finish starting before the first run.
|
||||
await Task.Delay(TimeSpan.FromMinutes(1), stoppingToken);
|
||||
|
||||
while (!stoppingToken.IsCancellationRequested)
|
||||
{
|
||||
try { await RunOnceAsync(stoppingToken); }
|
||||
catch (OperationCanceledException) { break; }
|
||||
catch (Exception ex) { logger.LogError(ex, "Notification cleanup failed."); }
|
||||
|
||||
await Task.Delay(TimeSpan.FromHours(24), stoppingToken);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RunOnceAsync(CancellationToken ct)
|
||||
{
|
||||
var retentionDays = config.GetValue("Notifications:RetentionDays", 90);
|
||||
var cutoff = DateTime.Now.AddDays(-retentionDays);
|
||||
|
||||
using var scope = scopes.CreateScope();
|
||||
var repo = scope.ServiceProvider.GetRequiredService<INotificationRepository>();
|
||||
var uow = scope.ServiceProvider.GetRequiredService<IUnitOfWork>();
|
||||
|
||||
var removed = 0;
|
||||
while (true)
|
||||
{
|
||||
var expired = await repo.GetExpiredReadRecipientsAsync(cutoff, BatchSize);
|
||||
if (expired.Count == 0) break;
|
||||
|
||||
repo.RemoveRecipients(expired);
|
||||
await uow.CommitAsync(ct);
|
||||
removed += expired.Count;
|
||||
}
|
||||
|
||||
while (true)
|
||||
{
|
||||
var orphans = await repo.GetOrphanNotificationsAsync(BatchSize);
|
||||
if (orphans.Count == 0) break;
|
||||
|
||||
repo.RemoveNotifications(orphans);
|
||||
await uow.CommitAsync(ct);
|
||||
}
|
||||
|
||||
if (removed > 0)
|
||||
logger.LogInformation("Notification cleanup removed {Count} read inbox rows older than {Days} days.", removed, retentionDays);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
using Distribution.Infra.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Distribution.Features.Notifications;
|
||||
|
||||
/// <summary>Every endpoint acts on the signed-in user's own notifications only.</summary>
|
||||
[ApiController]
|
||||
[Route("api/notifications")]
|
||||
public class NotificationController(NotificationService service, ICurrentUserService currentUser) : ControllerBase
|
||||
{
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> GetInbox(
|
||||
[FromQuery] bool unreadOnly = false, [FromQuery] int page = 1, [FromQuery] int pageSize = 20)
|
||||
{
|
||||
if (currentUser.UserId is not { } userId) return Unauthorized();
|
||||
return Ok(await service.GetInboxAsync(userId, unreadOnly, page, pageSize));
|
||||
}
|
||||
|
||||
[HttpGet("unread-count")]
|
||||
public async Task<IActionResult> GetUnreadCount()
|
||||
{
|
||||
if (currentUser.UserId is not { } userId) return Unauthorized();
|
||||
return Ok(await service.GetUnreadCountAsync(userId));
|
||||
}
|
||||
|
||||
[HttpPatch("{id:guid}/read")]
|
||||
public async Task<IActionResult> MarkRead(Guid id, CancellationToken ct)
|
||||
{
|
||||
if (currentUser.UserId is not { } userId) return Unauthorized();
|
||||
try
|
||||
{
|
||||
await service.MarkReadAsync(userId, id, ct);
|
||||
return NoContent();
|
||||
}
|
||||
catch (KeyNotFoundException ex) { return NotFound(new { error = ex.Message }); }
|
||||
}
|
||||
|
||||
[HttpPatch("read-all")]
|
||||
public async Task<IActionResult> MarkAllRead(CancellationToken ct)
|
||||
{
|
||||
if (currentUser.UserId is not { } userId) return Unauthorized();
|
||||
await service.MarkAllReadAsync(userId, ct);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpDelete("{id:guid}")]
|
||||
public async Task<IActionResult> Delete(Guid id, CancellationToken ct)
|
||||
{
|
||||
if (currentUser.UserId is not { } userId) return Unauthorized();
|
||||
try
|
||||
{
|
||||
await service.DeleteAsync(userId, id, ct);
|
||||
return NoContent();
|
||||
}
|
||||
catch (KeyNotFoundException ex) { return NotFound(new { error = ex.Message }); }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
using Distribution.Models;
|
||||
|
||||
namespace Distribution.Features.Notifications;
|
||||
|
||||
/// <param name="Values">Object whose public properties fill the {Token} placeholders in the type's templates.</param>
|
||||
/// <param name="CreatedByUserId">The acting user; excluded from the recipients.</param>
|
||||
public record NotificationArgs(
|
||||
object? Values,
|
||||
string EntityType,
|
||||
Guid EntityId,
|
||||
string Link,
|
||||
Guid? CreatedByUserId = null);
|
||||
|
||||
/// <param name="Id">The caller's own recipient row id — use it for read / delete.</param>
|
||||
public record NotificationResponse(
|
||||
Guid Id,
|
||||
string Type,
|
||||
NotificationSeverity Severity,
|
||||
string Title,
|
||||
string Message,
|
||||
string EntityType,
|
||||
Guid EntityId,
|
||||
string Link,
|
||||
DateTime CreatedAt,
|
||||
bool IsRead,
|
||||
DateTime? ReadAt);
|
||||
|
||||
public record UnreadCountResponse(int Count);
|
||||
@@ -0,0 +1,22 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
|
||||
namespace Distribution.Features.Notifications;
|
||||
|
||||
/// <summary>
|
||||
/// Server → client only. The server pushes a "notification" event to each recipient; clients never
|
||||
/// invoke hub methods. Users are addressed by their id (see <see cref="NotificationUserIdProvider"/>).
|
||||
/// </summary>
|
||||
[Authorize]
|
||||
public class NotificationHub : Hub
|
||||
{
|
||||
public const string Path = "/hubs/notifications";
|
||||
public const string ClientEvent = "notification";
|
||||
}
|
||||
|
||||
/// <summary>Maps a SignalR connection to the "UserId" claim that <c>AuthMiddleware</c> puts on the principal.</summary>
|
||||
public class NotificationUserIdProvider : IUserIdProvider
|
||||
{
|
||||
public string? GetUserId(HubConnectionContext connection) =>
|
||||
connection.User?.FindFirst("UserId")?.Value;
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
namespace Distribution.Features.Notifications;
|
||||
|
||||
public class NotificationRecipientResolver(INotificationRepository repo)
|
||||
{
|
||||
/// <summary>
|
||||
/// Expands the rules into a de-duplicated list of active user ids, never including
|
||||
/// <paramref name="actingUserId"/>.
|
||||
/// </summary>
|
||||
public async Task<List<Guid>> ResolveAsync(IEnumerable<RecipientRule> rules, Guid? actingUserId)
|
||||
{
|
||||
var ids = new HashSet<Guid>();
|
||||
|
||||
foreach (var rule in rules)
|
||||
ids.UnionWith(await ResolveRuleAsync(rule));
|
||||
|
||||
if (actingUserId is { } actor)
|
||||
ids.Remove(actor);
|
||||
|
||||
if (ids.Count == 0) return [];
|
||||
return await repo.FilterActiveUserIdsAsync(ids);
|
||||
}
|
||||
|
||||
private async Task<IEnumerable<Guid>> ResolveRuleAsync(RecipientRule rule)
|
||||
{
|
||||
switch (rule)
|
||||
{
|
||||
case UsersRule r:
|
||||
return r.Ids;
|
||||
|
||||
case PermissionRule r:
|
||||
return await repo.GetUserIdsByPermissionAsync(r.Code);
|
||||
|
||||
case ScopeRule r:
|
||||
return await repo.GetUserIdsByScopeAsync(r.Kind, r.Id);
|
||||
|
||||
case PermissionInScopeRule r:
|
||||
var withPermission = await repo.GetUserIdsByPermissionAsync(r.Code);
|
||||
var inScope = await repo.GetUserIdsByScopeAsync(r.Kind, r.Id);
|
||||
return withPermission.Intersect(inScope);
|
||||
|
||||
case ReportsToRule r:
|
||||
var supervisor = await repo.GetReportsToUserIdAsync(r.UserId);
|
||||
return supervisor is { } s ? [s] : [];
|
||||
|
||||
case RouteOwnerRule r:
|
||||
var owner = await repo.GetRouteOwnerUserIdAsync(r.Kind, r.Id);
|
||||
return owner is { } o ? [o] : [];
|
||||
|
||||
default:
|
||||
throw new NotSupportedException($"Unknown recipient rule {rule.GetType().Name}.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
using Distribution.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Distribution.Features.Notifications;
|
||||
|
||||
public class NotificationRepository(AppDBContext context) : INotificationRepository
|
||||
{
|
||||
// ── Inbox ────────────────────────────────────────────────────────────
|
||||
|
||||
public async Task AddAsync(Models.Notification notification) =>
|
||||
await context.Notifications.AddAsync(notification);
|
||||
|
||||
public void Discard(Models.Notification notification)
|
||||
{
|
||||
foreach (var recipient in notification.Recipients)
|
||||
context.Entry(recipient).State = EntityState.Detached;
|
||||
context.Entry(notification).State = EntityState.Detached;
|
||||
}
|
||||
|
||||
private IQueryable<Models.NotificationRecipient> InboxQuery(Guid userId, bool unreadOnly)
|
||||
{
|
||||
var query = context.NotificationRecipients.Where(r => r.FK_UserId == userId && r.IsActive);
|
||||
return unreadOnly ? query.Where(r => !r.IsRead) : query;
|
||||
}
|
||||
|
||||
public Task<int> GetInboxCountAsync(Guid userId, bool unreadOnly) =>
|
||||
InboxQuery(userId, unreadOnly).CountAsync();
|
||||
|
||||
public Task<List<Models.NotificationRecipient>> GetInboxAsync(Guid userId, bool unreadOnly, int page, int pageSize) =>
|
||||
InboxQuery(userId, unreadOnly)
|
||||
.Include(r => r.Notification)
|
||||
.OrderByDescending(r => r.Notification.CreatedAt)
|
||||
.Skip((page - 1) * pageSize)
|
||||
.Take(pageSize)
|
||||
.AsNoTracking()
|
||||
.ToListAsync();
|
||||
|
||||
public Task<int> GetUnreadCountAsync(Guid userId) =>
|
||||
InboxQuery(userId, unreadOnly: true).CountAsync();
|
||||
|
||||
public Task<Models.NotificationRecipient?> GetRecipientAsync(Guid userId, Guid recipientId) =>
|
||||
context.NotificationRecipients
|
||||
.FirstOrDefaultAsync(r => r.Id == recipientId && r.FK_UserId == userId && r.IsActive);
|
||||
|
||||
public Task<List<Models.NotificationRecipient>> GetUnreadRecipientsAsync(Guid userId) =>
|
||||
InboxQuery(userId, unreadOnly: true).ToListAsync();
|
||||
|
||||
public void RemoveRecipient(Models.NotificationRecipient recipient) =>
|
||||
context.NotificationRecipients.Remove(recipient);
|
||||
|
||||
// ── Recipient resolution ─────────────────────────────────────────────
|
||||
|
||||
public Task<List<Guid>> FilterActiveUserIdsAsync(IEnumerable<Guid> userIds)
|
||||
{
|
||||
var ids = userIds.ToList();
|
||||
return context.Users
|
||||
.Where(u => u.IsActive && ids.Contains(u.Id))
|
||||
.Select(u => u.Id)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public Task<List<Guid>> GetUserIdsByPermissionAsync(string permissionCode) =>
|
||||
context.Users
|
||||
.Where(u => u.IsActive && context.RolePermissions.Any(rp =>
|
||||
rp.FK_RoleId == u.FK_RoleId && rp.IsActive &&
|
||||
rp.Permission.IsActive && rp.Permission.Code == permissionCode))
|
||||
.Select(u => u.Id)
|
||||
.ToListAsync();
|
||||
|
||||
public async Task<List<Guid>> GetUserIdsByScopeAsync(ScopeKind kind, Guid id)
|
||||
{
|
||||
Guid? regionId = null;
|
||||
Guid? routeId = null;
|
||||
Guid? storeId = null;
|
||||
|
||||
switch (kind)
|
||||
{
|
||||
case ScopeKind.Region:
|
||||
regionId = id;
|
||||
break;
|
||||
case ScopeKind.Route:
|
||||
routeId = id;
|
||||
regionId = await context.Routes.Where(r => r.Id == id)
|
||||
.Select(r => (Guid?)r.FK_RegionId).FirstOrDefaultAsync();
|
||||
break;
|
||||
case ScopeKind.Store:
|
||||
storeId = id;
|
||||
routeId = await context.Stores.Where(s => s.Id == id)
|
||||
.Select(s => (Guid?)s.FK_RouteId).FirstOrDefaultAsync();
|
||||
if (routeId is { } rid)
|
||||
regionId = await context.Routes.Where(r => r.Id == rid)
|
||||
.Select(r => (Guid?)r.FK_RegionId).FirstOrDefaultAsync();
|
||||
break;
|
||||
}
|
||||
|
||||
var userIds = new HashSet<Guid>();
|
||||
|
||||
if (regionId is { } region)
|
||||
userIds.UnionWith(await context.UserRegionScopes
|
||||
.Where(s => s.IsActive && s.FK_RegionId == region)
|
||||
.Select(s => s.FK_UserId).ToListAsync());
|
||||
|
||||
if (routeId is { } route)
|
||||
userIds.UnionWith(await context.UserRouteScopes
|
||||
.Where(s => s.IsActive && s.FK_RouteId == route)
|
||||
.Select(s => s.FK_UserId).ToListAsync());
|
||||
|
||||
if (storeId is { } store)
|
||||
userIds.UnionWith(await context.UserStoreScopes
|
||||
.Where(s => s.IsActive && s.FK_StoreId == store)
|
||||
.Select(s => s.FK_UserId).ToListAsync());
|
||||
|
||||
return userIds.ToList();
|
||||
}
|
||||
|
||||
public Task<Guid?> GetReportsToUserIdAsync(Guid userId) =>
|
||||
context.Users.Where(u => u.Id == userId)
|
||||
.Select(u => u.FK_ReportsToUserId)
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
public async Task<Guid?> GetRouteOwnerUserIdAsync(ScopeKind kind, Guid id)
|
||||
{
|
||||
Guid? routeId = kind switch
|
||||
{
|
||||
ScopeKind.Route => id,
|
||||
ScopeKind.Store => await context.Stores.Where(s => s.Id == id)
|
||||
.Select(s => (Guid?)s.FK_RouteId).FirstOrDefaultAsync(),
|
||||
_ => null
|
||||
};
|
||||
if (routeId is not { } rid) return null;
|
||||
|
||||
return await context.Routes.Where(r => r.Id == rid && r.IsActive)
|
||||
.Select(r => r.FK_OwnerUserId)
|
||||
.FirstOrDefaultAsync();
|
||||
}
|
||||
|
||||
// ── Retention ────────────────────────────────────────────────────────
|
||||
|
||||
public Task<List<Models.NotificationRecipient>> GetExpiredReadRecipientsAsync(DateTime cutoff, int take) =>
|
||||
context.NotificationRecipients
|
||||
.Where(r => r.IsRead && r.ReadAt != null && r.ReadAt < cutoff)
|
||||
.Take(take)
|
||||
.ToListAsync();
|
||||
|
||||
public Task<List<Models.Notification>> GetOrphanNotificationsAsync(int take) =>
|
||||
context.Notifications
|
||||
.Where(n => !n.Recipients.Any())
|
||||
.Take(take)
|
||||
.ToListAsync();
|
||||
|
||||
public void RemoveRecipients(IEnumerable<Models.NotificationRecipient> recipients) =>
|
||||
context.NotificationRecipients.RemoveRange(recipients);
|
||||
|
||||
public void RemoveNotifications(IEnumerable<Models.Notification> notifications) =>
|
||||
context.Notifications.RemoveRange(notifications);
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
using System.Text.RegularExpressions;
|
||||
using Distribution.Features.Stock;
|
||||
using Distribution.Infra.UoW;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
|
||||
namespace Distribution.Features.Notifications;
|
||||
|
||||
public interface INotificationService
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a notification for every resolved recipient and pushes it live. Best-effort: this method
|
||||
/// never throws, so a notification problem can never fail the business action that triggered it.
|
||||
/// Call it only AFTER the owning service's <c>uow.CommitAsync()</c> has succeeded.
|
||||
/// </summary>
|
||||
Task NotifyAsync(string type, IReadOnlyList<RecipientRule> recipients, NotificationArgs args, CancellationToken ct = default);
|
||||
}
|
||||
|
||||
public class NotificationService(
|
||||
INotificationRepository repo,
|
||||
NotificationRecipientResolver resolver,
|
||||
IHubContext<NotificationHub> hub,
|
||||
IUnitOfWork uow,
|
||||
ILogger<NotificationService> logger) : INotificationService
|
||||
{
|
||||
private static readonly Regex TokenPattern = new(@"\{(\w+)\}", RegexOptions.Compiled);
|
||||
|
||||
// ── Trigger side ─────────────────────────────────────────────────────
|
||||
|
||||
public async Task NotifyAsync(string type, IReadOnlyList<RecipientRule> recipients, NotificationArgs args, CancellationToken ct = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!NotificationTypes.All.TryGetValue(type, out var definition))
|
||||
{
|
||||
logger.LogError("Notification type {Type} is not registered in NotificationTypes; skipped.", type);
|
||||
return;
|
||||
}
|
||||
|
||||
var userIds = await resolver.ResolveAsync(recipients, args.CreatedByUserId);
|
||||
if (userIds.Count == 0) return;
|
||||
|
||||
var values = ToDictionary(args.Values);
|
||||
var notification = new Models.Notification
|
||||
{
|
||||
Type = type,
|
||||
Severity = definition.Severity,
|
||||
Title = Truncate(Render(definition.TitleTemplate, values), 150),
|
||||
Message = Truncate(Render(definition.MessageTemplate, values), 500),
|
||||
EntityType = args.EntityType,
|
||||
EntityId = args.EntityId,
|
||||
Link = args.Link,
|
||||
CreatedByUserId = args.CreatedByUserId,
|
||||
CreatedAt = DateTime.Now,
|
||||
Recipients = userIds.Select(id => new Models.NotificationRecipient { FK_UserId = id }).ToList()
|
||||
};
|
||||
|
||||
await repo.AddAsync(notification);
|
||||
try
|
||||
{
|
||||
await uow.CommitAsync(ct);
|
||||
}
|
||||
catch
|
||||
{
|
||||
repo.Discard(notification);
|
||||
throw;
|
||||
}
|
||||
|
||||
await PushAsync(notification);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Failed to create {Type} notification for {EntityType} {EntityId}.",
|
||||
type, args.EntityType, args.EntityId);
|
||||
}
|
||||
}
|
||||
|
||||
// Separate from persistence: a failed push still leaves the notification in the inbox.
|
||||
private async Task PushAsync(Models.Notification notification)
|
||||
{
|
||||
foreach (var recipient in notification.Recipients)
|
||||
{
|
||||
try
|
||||
{
|
||||
await hub.Clients.User(recipient.FK_UserId.ToString())
|
||||
.SendAsync(NotificationHub.ClientEvent, ToResponse(notification, recipient));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogWarning(ex, "Failed to push notification {NotificationId} to user {UserId}.",
|
||||
notification.Id, recipient.FK_UserId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Inbox side ───────────────────────────────────────────────────────
|
||||
|
||||
public async Task<PagedResponse<NotificationResponse>> GetInboxAsync(Guid userId, bool unreadOnly, int page, int pageSize)
|
||||
{
|
||||
page = Math.Max(1, page);
|
||||
pageSize = Math.Clamp(pageSize, 1, 100);
|
||||
|
||||
var total = await repo.GetInboxCountAsync(userId, unreadOnly);
|
||||
var rows = await repo.GetInboxAsync(userId, unreadOnly, page, pageSize);
|
||||
return new PagedResponse<NotificationResponse>(page, pageSize, total,
|
||||
rows.Select(r => ToResponse(r.Notification, r)).ToList());
|
||||
}
|
||||
|
||||
public async Task<UnreadCountResponse> GetUnreadCountAsync(Guid userId) =>
|
||||
new(await repo.GetUnreadCountAsync(userId));
|
||||
|
||||
public async Task MarkReadAsync(Guid userId, Guid recipientId, CancellationToken ct = default)
|
||||
{
|
||||
var recipient = await repo.GetRecipientAsync(userId, recipientId)
|
||||
?? throw new KeyNotFoundException("Notification not found.");
|
||||
if (recipient.IsRead) return;
|
||||
|
||||
recipient.IsRead = true;
|
||||
recipient.ReadAt = DateTime.Now;
|
||||
await uow.CommitAsync(ct);
|
||||
}
|
||||
|
||||
public async Task MarkAllReadAsync(Guid userId, CancellationToken ct = default)
|
||||
{
|
||||
var unread = await repo.GetUnreadRecipientsAsync(userId);
|
||||
if (unread.Count == 0) return;
|
||||
|
||||
var now = DateTime.Now;
|
||||
foreach (var recipient in unread)
|
||||
{
|
||||
recipient.IsRead = true;
|
||||
recipient.ReadAt = now;
|
||||
}
|
||||
await uow.CommitAsync(ct);
|
||||
}
|
||||
|
||||
public async Task DeleteAsync(Guid userId, Guid recipientId, CancellationToken ct = default)
|
||||
{
|
||||
var recipient = await repo.GetRecipientAsync(userId, recipientId)
|
||||
?? throw new KeyNotFoundException("Notification not found.");
|
||||
|
||||
repo.RemoveRecipient(recipient);
|
||||
await uow.CommitAsync(ct);
|
||||
}
|
||||
|
||||
// ── Helpers ──────────────────────────────────────────────────────────
|
||||
|
||||
private static NotificationResponse ToResponse(Models.Notification n, Models.NotificationRecipient r) =>
|
||||
new(r.Id, n.Type, n.Severity, n.Title, n.Message, n.EntityType, n.EntityId, n.Link,
|
||||
n.CreatedAt, r.IsRead, r.ReadAt);
|
||||
|
||||
private static Dictionary<string, string> ToDictionary(object? values) =>
|
||||
values is null
|
||||
? []
|
||||
: values.GetType().GetProperties()
|
||||
.ToDictionary(p => p.Name, p => p.GetValue(values)?.ToString() ?? string.Empty);
|
||||
|
||||
private static string Render(string template, Dictionary<string, string> values) =>
|
||||
TokenPattern.Replace(template, m => values.TryGetValue(m.Groups[1].Value, out var v) ? v : string.Empty).Trim();
|
||||
|
||||
private static string Truncate(string value, int max) =>
|
||||
value.Length <= max ? value : value[..(max - 1)] + "…";
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
using Distribution.Models;
|
||||
|
||||
namespace Distribution.Features.Notifications;
|
||||
|
||||
public record NotificationTypeDefinition(NotificationSeverity Severity, string TitleTemplate, string MessageTemplate);
|
||||
|
||||
/// <summary>
|
||||
/// Registry of every notification type. Services must pass one of these constants to
|
||||
/// <see cref="INotificationService.NotifyAsync"/> — never a string literal. Templates use {Token}
|
||||
/// placeholders filled from <see cref="NotificationArgs.Values"/>.
|
||||
/// </summary>
|
||||
public static class NotificationTypes
|
||||
{
|
||||
public const string RideCancelled = "RideCancelled";
|
||||
public const string RideDriverAssigned = "RideDriverAssigned";
|
||||
public const string DeliveryTaskFailed = "DeliveryTaskFailed";
|
||||
public const string DeliveryTaskPartiallyDelivered = "DeliveryTaskPartiallyDelivered";
|
||||
public const string SupplierClaimApproved = "SupplierClaimApproved";
|
||||
public const string SupplierClaimRejected = "SupplierClaimRejected";
|
||||
public const string ShopPOConfirmed = "ShopPOConfirmed";
|
||||
public const string ShopPOCancelled = "ShopPOCancelled";
|
||||
public const string StockLow = "StockLow";
|
||||
public const string CreditLimitBreached = "CreditLimitBreached";
|
||||
|
||||
public static readonly IReadOnlyDictionary<string, NotificationTypeDefinition> All =
|
||||
new Dictionary<string, NotificationTypeDefinition>
|
||||
{
|
||||
[RideCancelled] = new(NotificationSeverity.Critical,
|
||||
"Ride cancelled", "A ride was cancelled ({CancelType}). {Reason}"),
|
||||
[RideDriverAssigned] = new(NotificationSeverity.Info,
|
||||
"Ride assigned", "You have been assigned to a ride."),
|
||||
[DeliveryTaskFailed] = new(NotificationSeverity.Warning,
|
||||
"Delivery failed", "Delivery to {StoreName} failed. {Note}"),
|
||||
[DeliveryTaskPartiallyDelivered] = new(NotificationSeverity.Warning,
|
||||
"Partial delivery", "Delivery to {StoreName} was only partly delivered."),
|
||||
[SupplierClaimApproved] = new(NotificationSeverity.Success,
|
||||
"Supplier claim approved", "Your supplier claim was approved."),
|
||||
[SupplierClaimRejected] = new(NotificationSeverity.Warning,
|
||||
"Supplier claim rejected", "Your supplier claim was rejected."),
|
||||
[ShopPOConfirmed] = new(NotificationSeverity.Info,
|
||||
"Shop order confirmed", "Shop order for {StoreName} was confirmed."),
|
||||
[ShopPOCancelled] = new(NotificationSeverity.Warning,
|
||||
"Shop order cancelled", "Shop order for {StoreName} was cancelled: {Reason}"),
|
||||
[StockLow] = new(NotificationSeverity.Warning,
|
||||
"Low stock", "{ItemName} is down to {Quantity} at {Location}."),
|
||||
[CreditLimitBreached] = new(NotificationSeverity.Warning,
|
||||
"Credit limit exceeded", "{StoreName} has exceeded its credit limit."),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
namespace Distribution.Features.Notifications;
|
||||
|
||||
public enum ScopeKind { Region, Route, Store }
|
||||
|
||||
/// <summary>
|
||||
/// Who a notification goes to. There is deliberately no "everyone" rule — every trigger must say who
|
||||
/// needs to know. Scope coverage follows Region → Route → Store: a region scope covers its routes
|
||||
/// and their stores, a route scope covers its stores.
|
||||
/// </summary>
|
||||
public abstract record RecipientRule
|
||||
{
|
||||
/// <summary>These specific users (creator, driver, approver, route owner …).</summary>
|
||||
public static RecipientRule Users(params Guid[] ids) => new UsersRule(ids);
|
||||
|
||||
/// <summary>Every active user whose role holds the permission code.</summary>
|
||||
public static RecipientRule Permission(string code) => new PermissionRule(code);
|
||||
|
||||
/// <summary>Every active user whose scope covers the region / route / store.</summary>
|
||||
public static RecipientRule Scope(ScopeKind kind, Guid id) => new ScopeRule(kind, id);
|
||||
|
||||
/// <summary>Users who hold the permission AND whose scope covers the region / route / store.</summary>
|
||||
public static RecipientRule PermissionInScope(string code, ScopeKind kind, Guid id) =>
|
||||
new PermissionInScopeRule(code, kind, id);
|
||||
|
||||
/// <summary>The user's supervisor (User.FK_ReportsToUserId).</summary>
|
||||
public static RecipientRule ReportsTo(Guid userId) => new ReportsToRule(userId);
|
||||
|
||||
/// <summary>The owner of the route (<see cref="ScopeKind.Route"/>) or of the store's route (<see cref="ScopeKind.Store"/>).</summary>
|
||||
public static RecipientRule RouteOwner(ScopeKind kind, Guid id) => new RouteOwnerRule(kind, id);
|
||||
}
|
||||
|
||||
public sealed record UsersRule(IReadOnlyCollection<Guid> Ids) : RecipientRule;
|
||||
public sealed record PermissionRule(string Code) : RecipientRule;
|
||||
public sealed record ScopeRule(ScopeKind Kind, Guid Id) : RecipientRule;
|
||||
public sealed record PermissionInScopeRule(string Code, ScopeKind Kind, Guid Id) : RecipientRule;
|
||||
public sealed record ReportsToRule(Guid UserId) : RecipientRule;
|
||||
public sealed record RouteOwnerRule(ScopeKind Kind, Guid Id) : RecipientRule;
|
||||
@@ -1,4 +1,4 @@
|
||||
using Distribution.Models;
|
||||
using Distribution.Models;
|
||||
|
||||
namespace Distribution.Features.Payment;
|
||||
|
||||
@@ -6,8 +6,14 @@ public record CreatePaymentRequest(
|
||||
decimal Amount,
|
||||
PaymentMethod Method,
|
||||
EntryMode EntryMode = EntryMode.RealTime,
|
||||
// Cash: required. Cheque: optional (used as the deposit account hint if provided).
|
||||
Guid? BankAccountId = null,
|
||||
// Cheque: required.
|
||||
string? ChequeNumber = null,
|
||||
DateTime? ChequeDate = null,
|
||||
// Cheque: optional metadata forwarded to the auto-created ReceivedCheque.
|
||||
string? DrawerBankName = null,
|
||||
string? AccountHolderName = null,
|
||||
Guid? CollectedByUserId = null);
|
||||
|
||||
public record PaymentResponse(
|
||||
@@ -16,6 +22,8 @@ public record PaymentResponse(
|
||||
decimal Amount,
|
||||
PaymentMethod Method,
|
||||
EntryMode EntryMode,
|
||||
Guid? BankAccountId,
|
||||
Guid? ReceivedChequeId,
|
||||
string? ChequeNumber,
|
||||
DateTime? ChequeDate,
|
||||
DateTime CollectedAt,
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
using Distribution.Features.Invoice;
|
||||
using Distribution.Features.BankAccount;
|
||||
using Distribution.Features.Invoice;
|
||||
using Distribution.Features.ReceivedCheque;
|
||||
using Distribution.Features.StoreCredit;
|
||||
using Distribution.Infra.Authorization;
|
||||
using Distribution.Infra.UoW;
|
||||
using Distribution.Models;
|
||||
@@ -8,11 +11,16 @@ namespace Distribution.Features.Payment;
|
||||
public class PaymentService(
|
||||
IPaymentRepository repo,
|
||||
IInvoiceRepository invoiceRepo,
|
||||
IBankAccountRepository bankAccountRepo,
|
||||
IReceivedChequeRepository receivedChequeRepo,
|
||||
IStoreCreditMovementRepository creditRepo,
|
||||
IUnitOfWork uow,
|
||||
ICurrentUserService currentUser)
|
||||
{
|
||||
private static PaymentResponse ToResponse(Models.Payment p) =>
|
||||
new(p.Id, p.FK_InvoiceId, p.Amount, p.Method, p.EntryMode, p.ChequeNumber, p.ChequeDate,
|
||||
new(p.Id, p.FK_InvoiceId, p.Amount, p.Method, p.EntryMode,
|
||||
p.FK_BankAccountId, p.FK_ReceivedChequeId,
|
||||
p.ChequeNumber, p.ChequeDate,
|
||||
p.CollectedAt, p.CollectedBy, p.RecordedBy, p.CreatedAt);
|
||||
|
||||
public async Task<List<PaymentResponse>?> GetByInvoiceIdAsync(Guid invoiceId)
|
||||
@@ -28,13 +36,46 @@ public class PaymentService(
|
||||
{
|
||||
var invoice = await invoiceRepo.GetByIdAsync(invoiceId);
|
||||
if (invoice is null || !invoice.IsActive) return null;
|
||||
if (invoice.Status == InvoiceStatus.Finalized)
|
||||
throw new InvalidOperationException("Cannot record a payment against a finalized invoice");
|
||||
|
||||
if (request.Amount <= 0)
|
||||
throw new InvalidOperationException("Amount must be greater than zero");
|
||||
|
||||
var existingPaid = (await repo.GetByInvoiceIdAsync(invoiceId)).Sum(p => p.Amount);
|
||||
var remaining = invoice.TotalAmount - existingPaid;
|
||||
if (request.Amount > remaining)
|
||||
throw new InvalidOperationException(
|
||||
$"Amount exceeds the outstanding balance of {remaining:0.##}");
|
||||
|
||||
if (request.Method == PaymentMethod.Cheque &&
|
||||
(string.IsNullOrWhiteSpace(request.ChequeNumber) || request.ChequeDate is null))
|
||||
throw new InvalidOperationException("ChequeNumber and ChequeDate are required when Method is Cheque");
|
||||
|
||||
if (request.Method == PaymentMethod.Cash && request.BankAccountId is null)
|
||||
throw new InvalidOperationException("BankAccountId is required when Method is Cash");
|
||||
|
||||
if (request.Method == PaymentMethod.Credit)
|
||||
{
|
||||
var store = invoice.ShopPO?.Store
|
||||
?? throw new InvalidOperationException("Invoice is missing the owning store; cannot record a credit payment");
|
||||
var currentOutstanding = await creditRepo.GetOutstandingForStoreAsync(store.Id);
|
||||
if (currentOutstanding + request.Amount > store.CreditLimit)
|
||||
{
|
||||
var available = store.CreditLimit - currentOutstanding;
|
||||
throw new InvalidOperationException(
|
||||
$"Credit limit exceeded. Available credit: {available:0.##}");
|
||||
}
|
||||
}
|
||||
|
||||
Models.BankAccount? bankAccount = null;
|
||||
if (request.BankAccountId is not null)
|
||||
{
|
||||
bankAccount = await bankAccountRepo.GetByIdAsync(request.BankAccountId.Value);
|
||||
if (bankAccount is null)
|
||||
throw new InvalidOperationException("BankAccountId does not reference a valid account");
|
||||
if (bankAccount.IsActive is false)
|
||||
throw new InvalidOperationException("The selected account is inactive");
|
||||
}
|
||||
|
||||
var recorderId = currentUser.UserId ?? request.CollectedByUserId
|
||||
?? throw new InvalidOperationException("CollectedByUserId is required while auth is unavailable");
|
||||
var collectedBy = request.CollectedByUserId ?? recorderId;
|
||||
@@ -50,6 +91,7 @@ public class PaymentService(
|
||||
EntryMode = request.EntryMode,
|
||||
ChequeNumber = request.Method == PaymentMethod.Cheque ? request.ChequeNumber : null,
|
||||
ChequeDate = request.Method == PaymentMethod.Cheque ? request.ChequeDate : null,
|
||||
FK_BankAccountId = request.BankAccountId,
|
||||
CollectedAt = DateTime.Now,
|
||||
CollectedBy = collectedBy,
|
||||
RecordedBy = recordedBy
|
||||
@@ -58,11 +100,48 @@ public class PaymentService(
|
||||
await uow.BeginAsync(ct);
|
||||
try
|
||||
{
|
||||
// Auto-create a ReceivedCheque for the physical cheque. Status starts at Received;
|
||||
// bookkeeping of the deposit/clear transitions happens through the Received Cheques screen.
|
||||
if (request.Method == PaymentMethod.Cheque)
|
||||
{
|
||||
var cheque = new Models.ReceivedCheque
|
||||
{
|
||||
CompanyId = null,
|
||||
BranchId = null,
|
||||
ReceivedFromType = ReceivedFromType.Customer,
|
||||
ReceivedFromId = null,
|
||||
ReceivedFromName = $"Invoice {invoice.Id}",
|
||||
DrawerBankName = request.DrawerBankName,
|
||||
AccountHolderName = request.AccountHolderName,
|
||||
ChequeNo = request.ChequeNumber!,
|
||||
ChequeDate = request.ChequeDate!.Value,
|
||||
Amount = request.Amount,
|
||||
ReceivedDate = DateTime.Now,
|
||||
ReferenceType = "Invoice",
|
||||
Status = ReceivedChequeStatus.Received
|
||||
};
|
||||
await receivedChequeRepo.AddAsync(cheque);
|
||||
payment.FK_ReceivedChequeId = cheque.Id;
|
||||
}
|
||||
|
||||
await repo.AddAsync(payment);
|
||||
|
||||
var allPayments = await repo.GetByInvoiceIdAsync(invoiceId);
|
||||
var total = allPayments.Sum(p => p.Amount) + request.Amount;
|
||||
if (request.Method == PaymentMethod.Credit)
|
||||
{
|
||||
var movement = new StoreCreditMovement
|
||||
{
|
||||
FK_StoreId = invoice.ShopPO!.Store!.Id,
|
||||
Direction = StoreCreditMovementDirection.Usage,
|
||||
Amount = request.Amount,
|
||||
FK_InvoiceId = invoice.Id,
|
||||
FK_PaymentId = payment.Id,
|
||||
OccurredAt = DateTime.Now,
|
||||
RecordedBy = recorderId
|
||||
};
|
||||
await creditRepo.AddAsync(movement);
|
||||
}
|
||||
|
||||
var total = existingPaid + request.Amount;
|
||||
invoice.PaymentStatus = total == 0
|
||||
? PaymentStatus.Pending
|
||||
: total < invoice.TotalAmount
|
||||
@@ -70,6 +149,11 @@ public class PaymentService(
|
||||
: PaymentStatus.Paid;
|
||||
|
||||
invoiceRepo.Update(invoice);
|
||||
|
||||
// TODO ledger: when accounting is wired in, post a journal entry here —
|
||||
// Debit Cash/Bank (BankAccount) OR Cheques-in-hand (for Cheque payments)
|
||||
// Credit Accounts Receivable (invoice.Store)
|
||||
|
||||
await uow.CommitAsync(ct);
|
||||
}
|
||||
catch { await uow.RollbackAsync(ct); throw; }
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
namespace Distribution.Features.PaymentReport;
|
||||
|
||||
public interface IPaymentReportRepository
|
||||
{
|
||||
/// <summary>Active payments collected within [From, To] (whole days).</summary>
|
||||
Task<List<PaymentRow>> GetPaymentsAsync(PaymentReportFilter f);
|
||||
|
||||
/// <summary>Finalized, non-cancelled invoices up to the end of 'To', each with cash/cheque paid as of then.</summary>
|
||||
Task<List<InvoiceBalanceRow>> GetInvoiceBalancesAsync(PaymentReportFilter f);
|
||||
|
||||
/// <summary>Sum of TotalAmount of invoices finalized within [From, To].</summary>
|
||||
Task<decimal> GetInvoicedAsync(PaymentReportFilter f);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Distribution.Features.PaymentReport;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/reports/payments")]
|
||||
public class PaymentReportController(PaymentReportService service) : ControllerBase
|
||||
{
|
||||
[HttpGet("analytics")]
|
||||
public async Task<IActionResult> Analytics(
|
||||
[FromQuery] DateTime? from, [FromQuery] DateTime? to, [FromQuery] Guid? regionId)
|
||||
{
|
||||
var f = PaymentReportService.BuildFilter(from, to, regionId);
|
||||
var error = PaymentReportService.Validate(f);
|
||||
return error is null ? Ok(await service.GetAnalyticsAsync(f)) : BadRequest(new { message = error });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
using Distribution.Models;
|
||||
|
||||
namespace Distribution.Features.PaymentReport;
|
||||
|
||||
public record PaymentReportFilter(DateTime From, DateTime To, Guid? RegionId);
|
||||
|
||||
public record PaymentSummary(
|
||||
decimal Invoiced,
|
||||
decimal Collected,
|
||||
decimal CreditExtended,
|
||||
decimal Outstanding,
|
||||
decimal Overdue,
|
||||
decimal? CollectionRatePct,
|
||||
int PaymentCount);
|
||||
|
||||
public record MethodBreakdown(string Method, int Count, decimal Amount, decimal SharePct);
|
||||
|
||||
public record PaymentTrendPoint(string Period, decimal Cash, decimal Cheque, decimal Credit);
|
||||
|
||||
public record AgingBucket(string Bucket, int Invoices, decimal Balance);
|
||||
|
||||
public record StoreOutstanding(Guid StoreId, string StoreName, int Invoices, decimal Balance, decimal Overdue);
|
||||
|
||||
public record AccountCollection(Guid? AccountId, string AccountName, int Count, decimal Amount);
|
||||
|
||||
public record ChequePipeline(string Status, int Count, decimal Amount);
|
||||
|
||||
public record PaymentAnalyticsResponse(
|
||||
DateTime From,
|
||||
DateTime To,
|
||||
PaymentSummary Summary,
|
||||
List<MethodBreakdown> ByMethod,
|
||||
List<PaymentTrendPoint> Trend,
|
||||
List<AgingBucket> Aging,
|
||||
List<StoreOutstanding> TopOutstanding,
|
||||
List<AccountCollection> ByAccount,
|
||||
List<ChequePipeline> Cheques);
|
||||
|
||||
// Repository row shapes
|
||||
public record PaymentRow(PaymentMethod Method, decimal Amount, DateTime CollectedAt, Guid? AccountId, string? AccountName, string? ChequeStatus);
|
||||
|
||||
public record InvoiceBalanceRow(Guid StoreId, string StoreName, DateTime FinalizedAt, decimal Total, decimal Paid);
|
||||
@@ -0,0 +1,59 @@
|
||||
using Distribution.Data;
|
||||
using Distribution.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Distribution.Features.PaymentReport;
|
||||
|
||||
public class PaymentReportRepository(AppDBContext db) : IPaymentReportRepository
|
||||
{
|
||||
public async Task<List<PaymentRow>> GetPaymentsAsync(PaymentReportFilter f)
|
||||
{
|
||||
var start = f.From.Date;
|
||||
var end = f.To.Date.AddDays(1);
|
||||
var q = db.Payments.AsNoTracking()
|
||||
.Where(p => p.IsActive && p.CollectedAt >= start && p.CollectedAt < end
|
||||
&& p.Invoice.ShopPO.Status != ShopPOStatus.Cancelled);
|
||||
if (f.RegionId.HasValue)
|
||||
q = q.Where(p => p.Invoice.ShopPO.Store.Route.FK_RegionId == f.RegionId.Value);
|
||||
|
||||
return await q.Select(p => new PaymentRow(
|
||||
p.Method, p.Amount, p.CollectedAt,
|
||||
p.FK_BankAccountId,
|
||||
p.BankAccount == null ? null : p.BankAccount.AccountName,
|
||||
p.ReceivedCheque == null ? null : p.ReceivedCheque.Status.ToString()))
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<List<InvoiceBalanceRow>> GetInvoiceBalancesAsync(PaymentReportFilter f)
|
||||
{
|
||||
var end = f.To.Date.AddDays(1);
|
||||
var q = db.Invoices.AsNoTracking()
|
||||
.Where(i => i.IsActive && i.Status == InvoiceStatus.Finalized
|
||||
&& i.ShopPO.Status != ShopPOStatus.Cancelled && i.FinalizedAt < end);
|
||||
if (f.RegionId.HasValue)
|
||||
q = q.Where(i => i.ShopPO.Store.Route.FK_RegionId == f.RegionId.Value);
|
||||
|
||||
// Credit payments do not reduce the receivable (same rule as the dashboard).
|
||||
return await q.Select(i => new InvoiceBalanceRow(
|
||||
i.ShopPO.Store.Id,
|
||||
i.ShopPO.Store.StoreName,
|
||||
i.FinalizedAt!.Value,
|
||||
i.TotalAmount,
|
||||
i.Payments.Where(p => p.IsActive && p.Method != PaymentMethod.Credit && p.CollectedAt < end)
|
||||
.Sum(p => (decimal?)p.Amount) ?? 0m))
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<decimal> GetInvoicedAsync(PaymentReportFilter f)
|
||||
{
|
||||
var start = f.From.Date;
|
||||
var end = f.To.Date.AddDays(1);
|
||||
var q = db.Invoices.AsNoTracking()
|
||||
.Where(i => i.IsActive && i.Status == InvoiceStatus.Finalized
|
||||
&& i.ShopPO.Status != ShopPOStatus.Cancelled
|
||||
&& i.FinalizedAt >= start && i.FinalizedAt < end);
|
||||
if (f.RegionId.HasValue)
|
||||
q = q.Where(i => i.ShopPO.Store.Route.FK_RegionId == f.RegionId.Value);
|
||||
return await q.SumAsync(i => (decimal?)i.TotalAmount) ?? 0m;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
using Distribution.Models;
|
||||
|
||||
namespace Distribution.Features.PaymentReport;
|
||||
|
||||
public class PaymentReportService(IPaymentReportRepository repo)
|
||||
{
|
||||
public const int MaxRangeDays = 366;
|
||||
private const int DailyGranularityMaxDays = 92;
|
||||
private const int OverdueDays = 30; // matches the dashboard receivables definition
|
||||
private const int TopStores = 10;
|
||||
|
||||
public static PaymentReportFilter BuildFilter(DateTime? from, DateTime? to, Guid? regionId)
|
||||
{
|
||||
var end = (to ?? DateTime.Now).Date;
|
||||
var start = (from ?? end.AddDays(-29)).Date;
|
||||
return new PaymentReportFilter(start, end, regionId);
|
||||
}
|
||||
|
||||
public static string? Validate(PaymentReportFilter f)
|
||||
{
|
||||
if (f.To < f.From) return "'to' must not be before 'from'.";
|
||||
if ((f.To - f.From).TotalDays > MaxRangeDays) return $"Date range cannot exceed {MaxRangeDays} days.";
|
||||
return null;
|
||||
}
|
||||
|
||||
private static decimal Pct(decimal part, decimal whole) => whole > 0 ? Math.Round(part / whole * 100, 2) : 0m;
|
||||
|
||||
public async Task<PaymentAnalyticsResponse> GetAnalyticsAsync(PaymentReportFilter f)
|
||||
{
|
||||
var payments = await repo.GetPaymentsAsync(f);
|
||||
var balances = await repo.GetInvoiceBalancesAsync(f);
|
||||
var invoiced = await repo.GetInvoicedAsync(f);
|
||||
var asOf = f.To.Date.AddDays(1);
|
||||
|
||||
var collected = payments.Where(p => p.Method != PaymentMethod.Credit).Sum(p => p.Amount);
|
||||
var credit = payments.Where(p => p.Method == PaymentMethod.Credit).Sum(p => p.Amount);
|
||||
var total = payments.Sum(p => p.Amount);
|
||||
|
||||
var open = balances
|
||||
.Select(b => (b.StoreId, b.StoreName, Age: (asOf - b.FinalizedAt).TotalDays, Balance: Math.Max(0m, b.Total - b.Paid)))
|
||||
.Where(b => b.Balance > 0)
|
||||
.ToList();
|
||||
var outstanding = open.Sum(b => b.Balance);
|
||||
var overdue = open.Where(b => b.Age > OverdueDays).Sum(b => b.Balance);
|
||||
|
||||
var summary = new PaymentSummary(
|
||||
invoiced, collected, credit, outstanding, overdue,
|
||||
invoiced > 0 ? Math.Round(collected / invoiced * 100, 2) : null,
|
||||
payments.Count);
|
||||
|
||||
var byMethod = Enum.GetValues<PaymentMethod>().Select(m =>
|
||||
{
|
||||
var rows = payments.Where(p => p.Method == m).ToList();
|
||||
var amount = rows.Sum(p => p.Amount);
|
||||
return new MethodBreakdown(m.ToString(), rows.Count, amount, Pct(amount, total));
|
||||
}).ToList();
|
||||
|
||||
var daily = (f.To - f.From).TotalDays <= DailyGranularityMaxDays;
|
||||
var trend = payments
|
||||
.GroupBy(p => daily ? p.CollectedAt.ToString("yyyy-MM-dd") : p.CollectedAt.ToString("yyyy-MM"))
|
||||
.OrderBy(g => g.Key)
|
||||
.Select(g => new PaymentTrendPoint(g.Key,
|
||||
g.Where(p => p.Method == PaymentMethod.Cash).Sum(p => p.Amount),
|
||||
g.Where(p => p.Method == PaymentMethod.Cheque).Sum(p => p.Amount),
|
||||
g.Where(p => p.Method == PaymentMethod.Credit).Sum(p => p.Amount)))
|
||||
.ToList();
|
||||
|
||||
var buckets = new (string Name, Func<double, bool> In)[]
|
||||
{
|
||||
("0-30 days", a => a <= 30),
|
||||
("31-60 days", a => a > 30 && a <= 60),
|
||||
("61-90 days", a => a > 60 && a <= 90),
|
||||
("90+ days", a => a > 90),
|
||||
};
|
||||
var aging = buckets.Select(b =>
|
||||
{
|
||||
var rows = open.Where(o => b.In(o.Age)).ToList();
|
||||
return new AgingBucket(b.Name, rows.Count, rows.Sum(r => r.Balance));
|
||||
}).ToList();
|
||||
|
||||
var topOutstanding = open
|
||||
.GroupBy(o => (o.StoreId, o.StoreName))
|
||||
.Select(g => new StoreOutstanding(g.Key.StoreId, g.Key.StoreName, g.Count(),
|
||||
g.Sum(x => x.Balance), g.Where(x => x.Age > OverdueDays).Sum(x => x.Balance)))
|
||||
.OrderByDescending(s => s.Balance)
|
||||
.Take(TopStores)
|
||||
.ToList();
|
||||
|
||||
var byAccount = payments
|
||||
.Where(p => p.Method == PaymentMethod.Cash)
|
||||
.GroupBy(p => (p.AccountId, Name: p.AccountName ?? "Unassigned"))
|
||||
.Select(g => new AccountCollection(g.Key.AccountId, g.Key.Name, g.Count(), g.Sum(p => p.Amount)))
|
||||
.OrderByDescending(a => a.Amount)
|
||||
.ToList();
|
||||
|
||||
var cheques = payments
|
||||
.Where(p => p.Method == PaymentMethod.Cheque)
|
||||
.GroupBy(p => p.ChequeStatus ?? "Unknown")
|
||||
.Select(g => new ChequePipeline(g.Key, g.Count(), g.Sum(p => p.Amount)))
|
||||
.OrderByDescending(c => c.Amount)
|
||||
.ToList();
|
||||
|
||||
return new PaymentAnalyticsResponse(f.From, f.To, summary, byMethod, trend, aging, topOutstanding, byAccount, cheques);
|
||||
}
|
||||
}
|
||||
@@ -60,24 +60,24 @@ public class ProductReportRepository(AppDBContext db) : IProductReportRepository
|
||||
return (rows, total);
|
||||
}
|
||||
|
||||
// Sold = shop PO lines on non-cancelled POs whose invoice is Finalized, dated by Invoice.FinalizedAt.
|
||||
private IQueryable<ShopPOItem> SoldLines(ProductReportFilter f)
|
||||
// Sold = billable (non-removed) invoice lines of Finalized invoices on non-cancelled POs, dated by Invoice.FinalizedAt.
|
||||
private IQueryable<InvoiceItem> SoldLines(ProductReportFilter f)
|
||||
{
|
||||
var lines =
|
||||
from li in db.ShopPOItems.AsNoTracking()
|
||||
join inv in db.Invoices.AsNoTracking() on li.FK_ShopPOId equals inv.FK_ShopPOId
|
||||
where inv.Status == InvoiceStatus.Finalized
|
||||
&& li.ShopPO.Status != ShopPOStatus.Cancelled
|
||||
&& (!f.From.HasValue || inv.FinalizedAt >= f.From.Value)
|
||||
&& (!f.To.HasValue || inv.FinalizedAt < f.To.Value.Date.AddDays(1))
|
||||
from li in db.InvoiceItems.AsNoTracking()
|
||||
where li.IsActive && !li.IsRemoved
|
||||
&& li.Invoice.Status == InvoiceStatus.Finalized
|
||||
&& li.Invoice.ShopPO.Status != ShopPOStatus.Cancelled
|
||||
&& (!f.From.HasValue || li.Invoice.FinalizedAt >= f.From.Value)
|
||||
&& (!f.To.HasValue || li.Invoice.FinalizedAt < f.To.Value.Date.AddDays(1))
|
||||
select li;
|
||||
|
||||
if (f.BrandId.HasValue) lines = lines.Where(l => l.Item.FK_BrandId == f.BrandId.Value);
|
||||
if (f.CategoryId.HasValue) lines = lines.Where(l => l.Item.FK_CategoryId == f.CategoryId.Value);
|
||||
if (f.SubCategoryId.HasValue) lines = lines.Where(l => l.Item.FK_SubCategoryId == f.SubCategoryId.Value);
|
||||
if (f.StoreId.HasValue) lines = lines.Where(l => l.ShopPO.FK_StoreId == f.StoreId.Value);
|
||||
if (f.RouteId.HasValue) lines = lines.Where(l => l.ShopPO.Store.FK_RouteId == f.RouteId.Value);
|
||||
if (f.RegionId.HasValue) lines = lines.Where(l => l.ShopPO.Store.Route.FK_RegionId == f.RegionId.Value);
|
||||
if (f.StoreId.HasValue) lines = lines.Where(l => l.Invoice.ShopPO.FK_StoreId == f.StoreId.Value);
|
||||
if (f.RouteId.HasValue) lines = lines.Where(l => l.Invoice.ShopPO.Store.FK_RouteId == f.RouteId.Value);
|
||||
if (f.RegionId.HasValue) lines = lines.Where(l => l.Invoice.ShopPO.Store.Route.FK_RegionId == f.RegionId.Value);
|
||||
if (!string.IsNullOrWhiteSpace(f.Search))
|
||||
{
|
||||
var s = f.Search.Trim();
|
||||
@@ -93,33 +93,36 @@ public class ProductReportRepository(AppDBContext db) : IProductReportRepository
|
||||
.Select(g => new
|
||||
{
|
||||
g.Key.FK_ItemId, g.Key.Sku, g.Key.Name, g.Key.CostPrice,
|
||||
Orders = g.Select(x => x.FK_ShopPOId).Distinct().Count(),
|
||||
QtyOrdered = g.Sum(x => (decimal)x.Quantity),
|
||||
QtyFulfilled = g.Sum(x => (decimal)x.QuantityFulfilled),
|
||||
SalesValue = g.Sum(x => x.QuantityFulfilled * x.UnitPrice),
|
||||
QtyOrdered = g.Sum(x => (decimal)(x.ShopPOItem != null ? x.ShopPOItem.Quantity : x.Quantity)),
|
||||
QtyFulfilled = g.Sum(x => (decimal)x.Quantity),
|
||||
SalesValue = g.Sum(x => x.Quantity * x.UnitPrice),
|
||||
});
|
||||
|
||||
var total = await query.CountAsync();
|
||||
var page_ = await query.OrderByDescending(r => r.SalesValue).ThenBy(r => r.Name)
|
||||
.Skip((page - 1) * pageSize).Take(pageSize).ToListAsync();
|
||||
|
||||
// Distinct stores per item is counted separately: MySQL can't run the correlated
|
||||
// Distinct orders and stores per item are counted separately: MySQL can't run the correlated
|
||||
// "SELECT DISTINCT ... WHERE outer.col" derived table EF generates inside a GroupBy.
|
||||
var ids = page_.Select(r => r.FK_ItemId).ToList();
|
||||
var storePairs = await SoldLines(f)
|
||||
var pairs = await SoldLines(f)
|
||||
.Where(l => ids.Contains(l.FK_ItemId))
|
||||
.Select(l => new { l.FK_ItemId, l.ShopPO.FK_StoreId })
|
||||
.Select(l => new { l.FK_ItemId, PoId = l.Invoice.FK_ShopPOId, l.Invoice.ShopPO.FK_StoreId })
|
||||
.Distinct()
|
||||
.ToListAsync();
|
||||
var storeCounts = storePairs.GroupBy(p => p.FK_ItemId).ToDictionary(g => g.Key, g => g.Count());
|
||||
var orderCounts = pairs.GroupBy(p => p.FK_ItemId)
|
||||
.ToDictionary(g => g.Key, g => g.Select(p => p.PoId).Distinct().Count());
|
||||
var storeCounts = pairs.GroupBy(p => p.FK_ItemId)
|
||||
.ToDictionary(g => g.Key, g => g.Select(p => p.FK_StoreId).Distinct().Count());
|
||||
|
||||
var rows = page_.Select(r => new SalesAggregate(r.FK_ItemId, r.Sku, r.Name, r.CostPrice,
|
||||
r.Orders, storeCounts.GetValueOrDefault(r.FK_ItemId), r.QtyOrdered, r.QtyFulfilled, r.SalesValue)).ToList();
|
||||
orderCounts.GetValueOrDefault(r.FK_ItemId), storeCounts.GetValueOrDefault(r.FK_ItemId),
|
||||
r.QtyOrdered, r.QtyFulfilled, r.SalesValue)).ToList();
|
||||
return (rows, total);
|
||||
}
|
||||
|
||||
public Task<decimal> GetTotalSalesAsync(ProductReportFilter f) =>
|
||||
SoldLines(f).SumAsync(x => x.QuantityFulfilled * x.UnitPrice);
|
||||
SoldLines(f).SumAsync(x => x.Quantity * x.UnitPrice);
|
||||
|
||||
public async Task<Dictionary<Guid, decimal>> GetSoldQtyAsync(ProductReportFilter f, IEnumerable<Guid> itemIds)
|
||||
{
|
||||
@@ -127,7 +130,7 @@ public class ProductReportRepository(AppDBContext db) : IProductReportRepository
|
||||
return await SoldLines(f with { Search = null })
|
||||
.Where(l => ids.Contains(l.FK_ItemId))
|
||||
.GroupBy(l => l.FK_ItemId)
|
||||
.Select(g => new { g.Key, Qty = g.Sum(x => (decimal)x.QuantityFulfilled) })
|
||||
.Select(g => new { g.Key, Qty = g.Sum(x => (decimal)x.Quantity) })
|
||||
.ToDictionaryAsync(x => x.Key, x => x.Qty);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
using Distribution.Models;
|
||||
|
||||
namespace Distribution.Features.ReceivedCheque;
|
||||
|
||||
public interface IReceivedChequeRepository
|
||||
{
|
||||
Task<Models.ReceivedCheque?> GetByIdAsync(Guid id);
|
||||
Task<(List<Models.ReceivedCheque> Items, int TotalCount)> ListAsync(
|
||||
int? companyId, int? branchId, ReceivedChequeStatus? status, ReceivedFromType? receivedFromType,
|
||||
int page, int pageSize);
|
||||
Task AddAsync(Models.ReceivedCheque cheque);
|
||||
void Update(Models.ReceivedCheque cheque);
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
using Distribution.Models;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Distribution.Features.ReceivedCheque;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/received-cheques")]
|
||||
public class ReceivedChequeController(ReceivedChequeService service) : ControllerBase
|
||||
{
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> List(
|
||||
[FromQuery] int? companyId,
|
||||
[FromQuery] int? branchId,
|
||||
[FromQuery] ReceivedChequeStatus? status,
|
||||
[FromQuery] ReceivedFromType? receivedFromType,
|
||||
[FromQuery] int page = 1,
|
||||
[FromQuery] int pageSize = 50)
|
||||
=> Ok(await service.ListAsync(companyId, branchId, status, receivedFromType, page, pageSize));
|
||||
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> Create(CreateReceivedChequeRequest request, CancellationToken ct)
|
||||
{
|
||||
try { return Ok(await service.CreateAsync(request, ct)); }
|
||||
catch (InvalidOperationException ex) { return BadRequest(new { error = ex.Message }); }
|
||||
}
|
||||
|
||||
[HttpPut("{id:guid}/status")]
|
||||
public async Task<IActionResult> UpdateStatus(Guid id, UpdateReceivedChequeStatusRequest request, CancellationToken ct)
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = await service.UpdateStatusAsync(id, request, ct);
|
||||
return result is null ? NotFound() : Ok(result);
|
||||
}
|
||||
catch (InvalidOperationException ex) { return BadRequest(new { error = ex.Message }); }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
using Distribution.Features.ChequeBook;
|
||||
using Distribution.Models;
|
||||
|
||||
namespace Distribution.Features.ReceivedCheque;
|
||||
|
||||
public record ReceivedChequeResponse(
|
||||
Guid ReceivedChequeId,
|
||||
int? CompanyId,
|
||||
int? BranchId,
|
||||
ReceivedFromType ReceivedFromType,
|
||||
int? ReceivedFromId,
|
||||
string ReceivedFromName,
|
||||
string? DrawerBankName,
|
||||
string? DrawerBankBranch,
|
||||
string? AccountHolderName,
|
||||
string ChequeNo,
|
||||
DateTime ChequeDate,
|
||||
decimal Amount,
|
||||
DateTime ReceivedDate,
|
||||
string? ReferenceType,
|
||||
int? ReferenceId,
|
||||
string? Notes,
|
||||
string? CreatedBy,
|
||||
ReceivedChequeStatus Status,
|
||||
Guid? DepositBankAccountId,
|
||||
DateTime? DepositDate);
|
||||
|
||||
public record CreateReceivedChequeRequest(
|
||||
int CompanyId,
|
||||
int? BranchId,
|
||||
ReceivedFromType ReceivedFromType,
|
||||
int? ReceivedFromId,
|
||||
string ReceivedFromName,
|
||||
string? DrawerBankName,
|
||||
string? DrawerBankBranch,
|
||||
string? AccountHolderName,
|
||||
string ChequeNo,
|
||||
DateTime ChequeDate,
|
||||
decimal Amount,
|
||||
DateTime ReceivedDate,
|
||||
string? ReferenceType,
|
||||
int? ReferenceId,
|
||||
string? Notes,
|
||||
string? CreatedBy);
|
||||
|
||||
public record UpdateReceivedChequeStatusRequest(
|
||||
ReceivedChequeStatusAction Action,
|
||||
Guid? DepositBankAccountId,
|
||||
DateTime? DepositDate,
|
||||
string? Notes,
|
||||
string? PerformedBy);
|
||||
@@ -0,0 +1,34 @@
|
||||
using Distribution.Data;
|
||||
using Distribution.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Distribution.Features.ReceivedCheque;
|
||||
|
||||
public class ReceivedChequeRepository(AppDBContext context) : IReceivedChequeRepository
|
||||
{
|
||||
public async Task<Models.ReceivedCheque?> GetByIdAsync(Guid id) =>
|
||||
await context.ReceivedCheques.FirstOrDefaultAsync(c => c.Id == id);
|
||||
|
||||
public async Task<(List<Models.ReceivedCheque> Items, int TotalCount)> ListAsync(
|
||||
int? companyId, int? branchId, ReceivedChequeStatus? status, ReceivedFromType? receivedFromType,
|
||||
int page, int pageSize)
|
||||
{
|
||||
var query = context.ReceivedCheques.AsQueryable();
|
||||
if (companyId is not null) query = query.Where(c => c.CompanyId == companyId);
|
||||
if (branchId is not null) query = query.Where(c => c.BranchId == branchId);
|
||||
if (status is not null) query = query.Where(c => c.Status == status);
|
||||
if (receivedFromType is not null) query = query.Where(c => c.ReceivedFromType == receivedFromType);
|
||||
|
||||
var total = await query.CountAsync();
|
||||
var items = await query
|
||||
.OrderByDescending(c => c.ReceivedDate)
|
||||
.Skip((page - 1) * pageSize)
|
||||
.Take(pageSize)
|
||||
.ToListAsync();
|
||||
return (items, total);
|
||||
}
|
||||
|
||||
public async Task AddAsync(Models.ReceivedCheque cheque) => await context.ReceivedCheques.AddAsync(cheque);
|
||||
|
||||
public void Update(Models.ReceivedCheque cheque) => context.ReceivedCheques.Update(cheque);
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
using Distribution.Features.BankAccount;
|
||||
using Distribution.Features.ChequeBook;
|
||||
using Distribution.Infra.UoW;
|
||||
using Distribution.Models;
|
||||
|
||||
namespace Distribution.Features.ReceivedCheque;
|
||||
|
||||
public class ReceivedChequeService(
|
||||
IReceivedChequeRepository repo,
|
||||
IBankAccountRepository bankAccountRepo,
|
||||
IUnitOfWork uow)
|
||||
{
|
||||
private static ReceivedChequeResponse ToResponse(Models.ReceivedCheque c) =>
|
||||
new(c.Id, c.CompanyId, c.BranchId, c.ReceivedFromType, c.ReceivedFromId, c.ReceivedFromName,
|
||||
c.DrawerBankName, c.DrawerBankBranch, c.AccountHolderName,
|
||||
c.ChequeNo, c.ChequeDate, c.Amount, c.ReceivedDate,
|
||||
c.ReferenceType, c.ReferenceId, c.Notes, c.CreatedBy,
|
||||
c.Status, c.FK_DepositBankAccountId, c.DepositDate);
|
||||
|
||||
public async Task<PagedResult<ReceivedChequeResponse>> ListAsync(
|
||||
int? companyId, int? branchId, ReceivedChequeStatus? status, ReceivedFromType? receivedFromType,
|
||||
int page, int pageSize)
|
||||
{
|
||||
if (page < 1) page = 1;
|
||||
if (pageSize < 1 || pageSize > 200) pageSize = 50;
|
||||
|
||||
var (items, total) = await repo.ListAsync(companyId, branchId, status, receivedFromType, page, pageSize);
|
||||
return new PagedResult<ReceivedChequeResponse>(
|
||||
items.Select(ToResponse).ToList(), total, page, pageSize);
|
||||
}
|
||||
|
||||
public async Task<ReceivedChequeResponse> CreateAsync(CreateReceivedChequeRequest request, CancellationToken ct = default)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(request.ReceivedFromName))
|
||||
throw new InvalidOperationException("ReceivedFromName is required");
|
||||
if (string.IsNullOrWhiteSpace(request.ChequeNo))
|
||||
throw new InvalidOperationException("ChequeNo is required");
|
||||
if (request.Amount <= 0)
|
||||
throw new InvalidOperationException("Amount must be greater than zero");
|
||||
|
||||
var cheque = new Models.ReceivedCheque
|
||||
{
|
||||
CompanyId = request.CompanyId,
|
||||
BranchId = request.BranchId,
|
||||
ReceivedFromType = request.ReceivedFromType,
|
||||
ReceivedFromId = request.ReceivedFromId,
|
||||
ReceivedFromName = request.ReceivedFromName,
|
||||
DrawerBankName = request.DrawerBankName,
|
||||
DrawerBankBranch = request.DrawerBankBranch,
|
||||
AccountHolderName = request.AccountHolderName,
|
||||
ChequeNo = request.ChequeNo,
|
||||
ChequeDate = request.ChequeDate,
|
||||
Amount = request.Amount,
|
||||
ReceivedDate = request.ReceivedDate,
|
||||
ReferenceType = request.ReferenceType,
|
||||
ReferenceId = request.ReferenceId,
|
||||
Notes = request.Notes,
|
||||
CreatedBy = request.CreatedBy,
|
||||
Status = ReceivedChequeStatus.Received
|
||||
};
|
||||
|
||||
await repo.AddAsync(cheque);
|
||||
await uow.CommitAsync(ct);
|
||||
return ToResponse(cheque);
|
||||
}
|
||||
|
||||
public async Task<ReceivedChequeResponse?> UpdateStatusAsync(Guid id, UpdateReceivedChequeStatusRequest request, CancellationToken ct = default)
|
||||
{
|
||||
var cheque = await repo.GetByIdAsync(id);
|
||||
if (cheque is null) return null;
|
||||
|
||||
switch (request.Action)
|
||||
{
|
||||
case ReceivedChequeStatusAction.Deposit:
|
||||
if (cheque.Status != ReceivedChequeStatus.Received)
|
||||
throw new InvalidOperationException("Only received cheques can be deposited");
|
||||
if (request.DepositBankAccountId is null)
|
||||
throw new InvalidOperationException("DepositBankAccountId is required when depositing");
|
||||
if (request.DepositDate is null)
|
||||
throw new InvalidOperationException("DepositDate is required when depositing");
|
||||
var bankAccount = await bankAccountRepo.GetByIdAsync(request.DepositBankAccountId.Value);
|
||||
if (bankAccount is null || bankAccount.AccountType != BankAccountType.Bank)
|
||||
throw new InvalidOperationException("DepositBankAccountId does not reference a valid bank account");
|
||||
cheque.Status = ReceivedChequeStatus.Deposited;
|
||||
cheque.FK_DepositBankAccountId = request.DepositBankAccountId;
|
||||
cheque.DepositDate = request.DepositDate;
|
||||
break;
|
||||
|
||||
case ReceivedChequeStatusAction.Clear:
|
||||
if (cheque.Status != ReceivedChequeStatus.Deposited)
|
||||
throw new InvalidOperationException("Only deposited cheques can be cleared");
|
||||
cheque.Status = ReceivedChequeStatus.Cleared;
|
||||
break;
|
||||
|
||||
case ReceivedChequeStatusAction.Return:
|
||||
if (cheque.Status != ReceivedChequeStatus.Deposited)
|
||||
throw new InvalidOperationException("Only deposited cheques can be returned");
|
||||
cheque.Status = ReceivedChequeStatus.Returned;
|
||||
break;
|
||||
|
||||
case ReceivedChequeStatusAction.Cancel:
|
||||
if (cheque.Status != ReceivedChequeStatus.Received)
|
||||
throw new InvalidOperationException("Only received cheques can be cancelled");
|
||||
cheque.Status = ReceivedChequeStatus.Cancelled;
|
||||
break;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(request.Notes))
|
||||
cheque.Notes = request.Notes;
|
||||
|
||||
repo.Update(cheque);
|
||||
await uow.CommitAsync(ct);
|
||||
return ToResponse(cheque);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace Distribution.Features.RecurringExpense;
|
||||
|
||||
public interface IRecurringExpenseRepository
|
||||
{
|
||||
Task<Models.RecurringExpenseTemplate?> GetByIdAsync(Guid id);
|
||||
Task<List<Models.RecurringExpenseTemplate>> ListAsync(int page, int pageSize);
|
||||
Task<int> CountAsync();
|
||||
Task<List<Models.RecurringExpenseTemplate>> ListDueAsync(DateTime asOf);
|
||||
Task AddAsync(Models.RecurringExpenseTemplate entity);
|
||||
void Update(Models.RecurringExpenseTemplate entity);
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Distribution.Features.RecurringExpense;
|
||||
|
||||
public record ToggleRequest(bool Paused);
|
||||
|
||||
[ApiController]
|
||||
[Route("api/recurring-expenses")]
|
||||
public class RecurringExpenseController(RecurringExpenseService service) : ControllerBase
|
||||
{
|
||||
[HttpGet]
|
||||
[Authorize("expense.view")]
|
||||
public async Task<IActionResult> List([FromQuery] int page = 1, [FromQuery] int pageSize = 20) =>
|
||||
Ok(await service.ListAsync(page, pageSize));
|
||||
|
||||
[HttpPost]
|
||||
[Authorize("expense.manage_recurring")]
|
||||
public async Task<IActionResult> Create(CreateRecurringExpenseRequest request, CancellationToken ct)
|
||||
{
|
||||
try { return Ok(await service.CreateAsync(request, ct)); }
|
||||
catch (InvalidOperationException ex) { return BadRequest(new { error = ex.Message }); }
|
||||
}
|
||||
|
||||
[HttpPut("{id:guid}")]
|
||||
[Authorize("expense.manage_recurring")]
|
||||
public async Task<IActionResult> Update(Guid id, UpdateRecurringExpenseRequest request, CancellationToken ct)
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = await service.UpdateAsync(id, request, ct);
|
||||
return result is null ? NotFound() : Ok(result);
|
||||
}
|
||||
catch (InvalidOperationException ex) { return BadRequest(new { error = ex.Message }); }
|
||||
}
|
||||
|
||||
[HttpPost("{id:guid}/toggle")]
|
||||
[Authorize("expense.manage_recurring")]
|
||||
public async Task<IActionResult> Toggle(Guid id, ToggleRequest body, CancellationToken ct)
|
||||
{
|
||||
var result = await service.ToggleAsync(id, body.Paused, ct);
|
||||
return result is null ? NotFound() : Ok(result);
|
||||
}
|
||||
|
||||
[HttpPost("run-due")]
|
||||
[Authorize("expense.manage_recurring")]
|
||||
public async Task<IActionResult> RunDue(CancellationToken ct)
|
||||
{
|
||||
try { return Ok(await service.RunDueAsync(ct)); }
|
||||
catch (InvalidOperationException ex) { return BadRequest(new { error = ex.Message }); }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
using Distribution.Models;
|
||||
|
||||
namespace Distribution.Features.RecurringExpense;
|
||||
|
||||
public record CreateRecurringExpenseRequest(
|
||||
string Name,
|
||||
Guid CategoryId,
|
||||
Guid? WarehouseId,
|
||||
Guid? VendorId,
|
||||
decimal Amount,
|
||||
string? Payee,
|
||||
string? Description,
|
||||
RecurringFrequency Frequency,
|
||||
int? DayOfMonth,
|
||||
int? DayOfWeek,
|
||||
DateTime StartDate,
|
||||
DateTime? EndDate,
|
||||
bool AutoPost);
|
||||
|
||||
public record UpdateRecurringExpenseRequest(
|
||||
string Name,
|
||||
Guid CategoryId,
|
||||
Guid? WarehouseId,
|
||||
Guid? VendorId,
|
||||
decimal Amount,
|
||||
string? Payee,
|
||||
string? Description,
|
||||
RecurringFrequency Frequency,
|
||||
int? DayOfMonth,
|
||||
int? DayOfWeek,
|
||||
DateTime StartDate,
|
||||
DateTime? EndDate,
|
||||
bool AutoPost);
|
||||
|
||||
public record RecurringExpenseResponse(
|
||||
Guid Id,
|
||||
string Name,
|
||||
Guid CategoryId,
|
||||
string CategoryName,
|
||||
Guid? WarehouseId,
|
||||
string? WarehouseName,
|
||||
Guid? VendorId,
|
||||
string? VendorName,
|
||||
decimal Amount,
|
||||
string? Payee,
|
||||
string? Description,
|
||||
RecurringFrequency Frequency,
|
||||
int? DayOfMonth,
|
||||
int? DayOfWeek,
|
||||
DateTime StartDate,
|
||||
DateTime? EndDate,
|
||||
DateTime NextRunDate,
|
||||
bool AutoPost,
|
||||
bool IsPaused,
|
||||
bool IsActive,
|
||||
DateTime CreatedAt);
|
||||
|
||||
public record PagedRecurringExpensesResponse(List<RecurringExpenseResponse> Items, int Page, int PageSize, int Total);
|
||||
|
||||
public record RunDueResponse(int Generated, List<Guid> ExpenseIds);
|
||||
@@ -0,0 +1,39 @@
|
||||
using Distribution.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Distribution.Features.RecurringExpense;
|
||||
|
||||
public class RecurringExpenseRepository(AppDBContext context) : IRecurringExpenseRepository
|
||||
{
|
||||
private IQueryable<Models.RecurringExpenseTemplate> Base() =>
|
||||
context.RecurringExpenseTemplates
|
||||
.Include(t => t.Category)
|
||||
.Include(t => t.Warehouse)
|
||||
.Include(t => t.Vendor);
|
||||
|
||||
public async Task<Models.RecurringExpenseTemplate?> GetByIdAsync(Guid id) =>
|
||||
await Base().FirstOrDefaultAsync(t => t.Id == id);
|
||||
|
||||
public async Task<List<Models.RecurringExpenseTemplate>> ListAsync(int page, int pageSize) =>
|
||||
await Base()
|
||||
.Where(t => t.IsActive)
|
||||
.OrderBy(t => t.IsPaused).ThenBy(t => t.NextRunDate)
|
||||
.Skip((page - 1) * pageSize)
|
||||
.Take(pageSize)
|
||||
.ToListAsync();
|
||||
|
||||
public async Task<int> CountAsync() =>
|
||||
await context.RecurringExpenseTemplates.CountAsync(t => t.IsActive);
|
||||
|
||||
public async Task<List<Models.RecurringExpenseTemplate>> ListDueAsync(DateTime asOf) =>
|
||||
await Base()
|
||||
.Where(t => t.IsActive && !t.IsPaused && t.NextRunDate <= asOf
|
||||
&& (t.EndDate == null || t.EndDate >= asOf))
|
||||
.ToListAsync();
|
||||
|
||||
public async Task AddAsync(Models.RecurringExpenseTemplate entity) =>
|
||||
await context.RecurringExpenseTemplates.AddAsync(entity);
|
||||
|
||||
public void Update(Models.RecurringExpenseTemplate entity) =>
|
||||
context.RecurringExpenseTemplates.Update(entity);
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
using Distribution.Data;
|
||||
using Distribution.Infra.Authorization;
|
||||
using Distribution.Infra.UoW;
|
||||
using Distribution.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Distribution.Features.RecurringExpense;
|
||||
|
||||
public class RecurringExpenseService(
|
||||
AppDBContext context,
|
||||
IRecurringExpenseRepository repo,
|
||||
IUnitOfWork uow,
|
||||
ICurrentUserService currentUser)
|
||||
{
|
||||
private static RecurringExpenseResponse Map(Models.RecurringExpenseTemplate t) =>
|
||||
new(t.Id, t.Name,
|
||||
t.FK_CategoryId, t.Category?.Name ?? string.Empty,
|
||||
t.FK_WarehouseId, t.Warehouse?.Name,
|
||||
t.FK_VendorId, t.Vendor?.Name,
|
||||
t.Amount, t.Payee, t.Description,
|
||||
t.Frequency, t.DayOfMonth, t.DayOfWeek,
|
||||
t.StartDate, t.EndDate, t.NextRunDate, t.AutoPost, t.IsPaused, t.IsActive, t.CreatedAt);
|
||||
|
||||
public async Task<PagedRecurringExpensesResponse> ListAsync(int page, int pageSize)
|
||||
{
|
||||
if (page < 1) page = 1;
|
||||
if (pageSize < 1 || pageSize > 200) pageSize = 20;
|
||||
var items = await repo.ListAsync(page, pageSize);
|
||||
var total = await repo.CountAsync();
|
||||
return new PagedRecurringExpensesResponse(items.Select(Map).ToList(), page, pageSize, total);
|
||||
}
|
||||
|
||||
public async Task<RecurringExpenseResponse> CreateAsync(CreateRecurringExpenseRequest request, CancellationToken ct = default)
|
||||
{
|
||||
ValidateSchedule(request.Frequency, request.DayOfMonth, request.DayOfWeek);
|
||||
if (request.Amount <= 0) throw new InvalidOperationException("Amount must be greater than zero");
|
||||
|
||||
var category = await context.ExpenseCategories.FirstOrDefaultAsync(c => c.Id == request.CategoryId && c.IsActive, ct)
|
||||
?? throw new InvalidOperationException("Category not found");
|
||||
if (category.FK_ParentCategoryId is null)
|
||||
throw new InvalidOperationException("Pick a specific title under a category");
|
||||
|
||||
var entity = new Models.RecurringExpenseTemplate
|
||||
{
|
||||
Name = request.Name.Trim(),
|
||||
FK_CategoryId = request.CategoryId,
|
||||
FK_WarehouseId = request.WarehouseId,
|
||||
FK_VendorId = request.VendorId,
|
||||
Amount = request.Amount,
|
||||
Payee = request.Payee,
|
||||
Description = request.Description,
|
||||
Frequency = request.Frequency,
|
||||
DayOfMonth = request.DayOfMonth,
|
||||
DayOfWeek = request.DayOfWeek,
|
||||
StartDate = request.StartDate.Date,
|
||||
EndDate = request.EndDate?.Date,
|
||||
NextRunDate = request.StartDate.Date,
|
||||
AutoPost = request.AutoPost
|
||||
};
|
||||
|
||||
await uow.BeginAsync(ct);
|
||||
try { await repo.AddAsync(entity); await uow.CommitAsync(ct); }
|
||||
catch { await uow.RollbackAsync(ct); throw; }
|
||||
|
||||
return Map((await repo.GetByIdAsync(entity.Id))!);
|
||||
}
|
||||
|
||||
public async Task<RecurringExpenseResponse?> UpdateAsync(Guid id, UpdateRecurringExpenseRequest request, CancellationToken ct = default)
|
||||
{
|
||||
var entity = await repo.GetByIdAsync(id);
|
||||
if (entity is null) return null;
|
||||
|
||||
ValidateSchedule(request.Frequency, request.DayOfMonth, request.DayOfWeek);
|
||||
if (request.Amount <= 0) throw new InvalidOperationException("Amount must be greater than zero");
|
||||
|
||||
entity.Name = request.Name.Trim();
|
||||
entity.FK_CategoryId = request.CategoryId;
|
||||
entity.FK_WarehouseId = request.WarehouseId;
|
||||
entity.FK_VendorId = request.VendorId;
|
||||
entity.Amount = request.Amount;
|
||||
entity.Payee = request.Payee;
|
||||
entity.Description = request.Description;
|
||||
entity.Frequency = request.Frequency;
|
||||
entity.DayOfMonth = request.DayOfMonth;
|
||||
entity.DayOfWeek = request.DayOfWeek;
|
||||
entity.StartDate = request.StartDate.Date;
|
||||
entity.EndDate = request.EndDate?.Date;
|
||||
entity.AutoPost = request.AutoPost;
|
||||
|
||||
await uow.BeginAsync(ct);
|
||||
try { repo.Update(entity); await uow.CommitAsync(ct); }
|
||||
catch { await uow.RollbackAsync(ct); throw; }
|
||||
|
||||
return Map((await repo.GetByIdAsync(id))!);
|
||||
}
|
||||
|
||||
public async Task<RecurringExpenseResponse?> ToggleAsync(Guid id, bool paused, CancellationToken ct = default)
|
||||
{
|
||||
var entity = await repo.GetByIdAsync(id);
|
||||
if (entity is null) return null;
|
||||
entity.IsPaused = paused;
|
||||
|
||||
await uow.BeginAsync(ct);
|
||||
try { repo.Update(entity); await uow.CommitAsync(ct); }
|
||||
catch { await uow.RollbackAsync(ct); throw; }
|
||||
|
||||
return Map(entity);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Materializes every active, non-paused template whose NextRunDate ≤ today
|
||||
/// into an Expense row, then advances NextRunDate by one period. Idempotent
|
||||
/// per call — advances only after a successful insert.
|
||||
/// </summary>
|
||||
public async Task<RunDueResponse> RunDueAsync(CancellationToken ct = default)
|
||||
{
|
||||
var today = DateTime.Today;
|
||||
var due = await repo.ListDueAsync(today);
|
||||
if (due.Count == 0) return new RunDueResponse(0, []);
|
||||
|
||||
var userId = currentUser.UserId
|
||||
?? throw new InvalidOperationException("Authenticated user is required");
|
||||
|
||||
var createdIds = new List<Guid>();
|
||||
|
||||
await uow.BeginAsync(ct);
|
||||
try
|
||||
{
|
||||
foreach (var t in due)
|
||||
{
|
||||
var expense = new Models.Expense
|
||||
{
|
||||
FK_CategoryId = t.FK_CategoryId,
|
||||
FK_WarehouseId = t.FK_WarehouseId,
|
||||
FK_VendorId = t.FK_VendorId,
|
||||
Amount = t.Amount,
|
||||
BillDate = t.NextRunDate,
|
||||
PeriodCovered = new DateTime(t.NextRunDate.Year, t.NextRunDate.Month, 1),
|
||||
Payee = t.Payee,
|
||||
Description = t.Description,
|
||||
Status = t.AutoPost ? ExpenseStatus.Posted : ExpenseStatus.Draft,
|
||||
RecordedByUserId = userId,
|
||||
PostedByUserId = t.AutoPost ? userId : null,
|
||||
PostedAt = t.AutoPost ? DateTime.Now : null,
|
||||
FK_RecurringTemplateId = t.Id
|
||||
};
|
||||
await context.Expenses.AddAsync(expense, ct);
|
||||
createdIds.Add(expense.Id);
|
||||
|
||||
t.NextRunDate = AdvanceNext(t.NextRunDate, t.Frequency);
|
||||
repo.Update(t);
|
||||
}
|
||||
|
||||
await uow.CommitAsync(ct);
|
||||
}
|
||||
catch { await uow.RollbackAsync(ct); throw; }
|
||||
|
||||
return new RunDueResponse(createdIds.Count, createdIds);
|
||||
}
|
||||
|
||||
private static DateTime AdvanceNext(DateTime from, RecurringFrequency freq) => freq switch
|
||||
{
|
||||
RecurringFrequency.Weekly => from.AddDays(7),
|
||||
RecurringFrequency.Monthly => from.AddMonths(1),
|
||||
RecurringFrequency.Quarterly => from.AddMonths(3),
|
||||
RecurringFrequency.Yearly => from.AddYears(1),
|
||||
_ => throw new InvalidOperationException($"Unknown frequency {freq}")
|
||||
};
|
||||
|
||||
private static void ValidateSchedule(RecurringFrequency freq, int? dayOfMonth, int? dayOfWeek)
|
||||
{
|
||||
if (freq == RecurringFrequency.Weekly)
|
||||
{
|
||||
if (dayOfWeek is null or < 0 or > 6)
|
||||
throw new InvalidOperationException("DayOfWeek (0-6) is required for Weekly");
|
||||
}
|
||||
else
|
||||
{
|
||||
if (dayOfMonth is null or < 1 or > 31)
|
||||
throw new InvalidOperationException("DayOfMonth (1-31) is required for Monthly/Quarterly/Yearly");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
using Distribution.Features.Stock;
|
||||
using Distribution.Features.DeliveryRide;
|
||||
using Distribution.Features.Notifications;
|
||||
using Distribution.Features.Stock;
|
||||
using Distribution.Features.DriverComplianceConfig;
|
||||
using Distribution.Features.DriverProfile;
|
||||
using Distribution.Infra.UoW;
|
||||
@@ -10,9 +12,14 @@ public class RideService(
|
||||
IRideRepository rideRepo,
|
||||
IDriverProfileRepository driverRepo,
|
||||
IDriverComplianceConfigRepository complianceRepo,
|
||||
IDeliveryTaskRepository taskRepo,
|
||||
StockMover mover,
|
||||
INotificationService notifications,
|
||||
ILogger<RideService> logger,
|
||||
IUnitOfWork uow)
|
||||
{
|
||||
private const string RidesLink = "/operations/rides";
|
||||
|
||||
public async Task<PagedResponse<RideResponse>> GetAllAsync(RideStatus? status, Guid? driverProfileId, Guid? vehicleId, int page, int pageSize)
|
||||
{
|
||||
var total = await rideRepo.GetCountAsync(status, driverProfileId, vehicleId);
|
||||
@@ -72,6 +79,12 @@ public class RideService(
|
||||
try { rideRepo.Update(ride); await uow.CommitAsync(ct); }
|
||||
catch { await uow.RollbackAsync(ct); throw; }
|
||||
|
||||
await notifications.NotifyAsync(
|
||||
NotificationTypes.RideDriverAssigned,
|
||||
[RecipientRule.Users(driver.FK_UserId)],
|
||||
new NotificationArgs(null, "Ride", rideId, RidesLink),
|
||||
ct);
|
||||
|
||||
var saved = await rideRepo.GetByIdAsync(rideId);
|
||||
return ToResponse(saved!);
|
||||
}
|
||||
@@ -125,9 +138,46 @@ public class RideService(
|
||||
}
|
||||
catch { await uow.RollbackAsync(ct); throw; }
|
||||
|
||||
await NotifyRideCancelledAsync(ride, request, userId, ct);
|
||||
|
||||
return ToResponse(ride);
|
||||
}
|
||||
|
||||
// A ride has no route of its own; the people to tell are the owners and managers of the routes
|
||||
// its delivery tasks go to. Best-effort: the ride is already cancelled, so nothing here may throw.
|
||||
private async Task NotifyRideCancelledAsync(DeliveryRun ride, CancelRideRequest request, Guid userId, CancellationToken ct)
|
||||
{
|
||||
try
|
||||
{
|
||||
var tasks = await taskRepo.GetByRideIdAsync(ride.Id);
|
||||
var routeIds = tasks
|
||||
.Where(t => t.ShopPO?.Store is not null)
|
||||
.Select(t => t.ShopPO.Store.FK_RouteId)
|
||||
.Distinct()
|
||||
.ToList();
|
||||
|
||||
var recipients = new List<RecipientRule>();
|
||||
foreach (var routeId in routeIds)
|
||||
{
|
||||
recipients.Add(RecipientRule.RouteOwner(ScopeKind.Route, routeId));
|
||||
recipients.Add(RecipientRule.PermissionInScope("delivery-task.manage", ScopeKind.Route, routeId));
|
||||
}
|
||||
if (recipients.Count == 0) return;
|
||||
|
||||
await notifications.NotifyAsync(
|
||||
NotificationTypes.RideCancelled,
|
||||
recipients,
|
||||
new NotificationArgs(
|
||||
new { CancelType = request.TypeCode, Reason = request.Reason },
|
||||
"Ride", ride.Id, RidesLink, userId),
|
||||
ct);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Failed to build RideCancelled notification for ride {RideId}.", ride.Id);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task CheckDriverComplianceAsync(Guid driverProfileId)
|
||||
{
|
||||
var driver = await driverRepo.GetByIdAsync(driverProfileId)
|
||||
|
||||
@@ -10,7 +10,10 @@ public interface ISchemeRepository
|
||||
|
||||
Task<List<Models.SchemeRedemption>> GetRedemptionsAsync(Guid schemeId);
|
||||
Task AddRedemptionAsync(Models.SchemeRedemption entity);
|
||||
Task<List<Models.SchemeRedemption>> GetRedemptionsByShopPOAsync(Guid shopPOId);
|
||||
void RemoveRedemption(Models.SchemeRedemption entity);
|
||||
Task<bool> HasRedemptionAsync(Guid schemeId, Guid orderId);
|
||||
Task<bool> HasLockedInvoiceAsync(Guid shopPOId);
|
||||
|
||||
Task<Models.SchemeBundleComponent?> GetBundleComponentAsync(Guid schemeId, Guid itemId);
|
||||
Task AddBundleComponentAsync(Models.SchemeBundleComponent entity);
|
||||
|
||||
@@ -112,6 +112,14 @@ public class SchemeController(SchemeService service) : ControllerBase
|
||||
return result is null ? NotFound() : Ok(result);
|
||||
}
|
||||
|
||||
[HttpPost("suggest")]
|
||||
[Authorize("scheme.view")]
|
||||
public async Task<IActionResult> Suggest(SuggestSchemesRequest request)
|
||||
{
|
||||
var result = await service.SuggestAsync(request);
|
||||
return result is null ? NotFound() : Ok(result);
|
||||
}
|
||||
|
||||
[HttpGet("{id:guid}/spend")]
|
||||
[Authorize("scheme.view")]
|
||||
public async Task<IActionResult> GetSpend(Guid id)
|
||||
|
||||
@@ -94,7 +94,13 @@ public record SchemeEligibilityResponse(
|
||||
string? Reason,
|
||||
decimal? DiscountAmount,
|
||||
Guid? FreeItemId,
|
||||
decimal? FreeQty);
|
||||
decimal? FreeQty,
|
||||
string? FreeItemName = null);
|
||||
|
||||
/// <summary>A cart line for scheme suggestion before the Shop PO exists. UnitPrice null = resolve from the price list.</summary>
|
||||
public record SuggestLine(Guid ItemId, int Quantity, decimal? UnitPrice = null, bool IsFree = false);
|
||||
|
||||
public record SuggestSchemesRequest(Guid StoreId, List<SuggestLine> Items);
|
||||
|
||||
public record SchemeRedemptionResponse(
|
||||
Guid Id,
|
||||
|
||||
@@ -49,9 +49,25 @@ public class SchemeRepository(AppDBContext context) : ISchemeRepository
|
||||
public async Task AddRedemptionAsync(Models.SchemeRedemption entity) =>
|
||||
await context.SchemeRedemptions.AddAsync(entity);
|
||||
|
||||
public async Task<List<Models.SchemeRedemption>> GetRedemptionsByShopPOAsync(Guid shopPOId)
|
||||
{
|
||||
var list = await context.SchemeRedemptions.Include(r => r.Scheme).Where(r => r.FK_ShopPOId == shopPOId).ToListAsync();
|
||||
// Rows added earlier in this unit of work are not in the database yet; rows removed are still there.
|
||||
foreach (var added in context.SchemeRedemptions.Local.Where(r => r.FK_ShopPOId == shopPOId && !list.Contains(r)))
|
||||
list.Add(added);
|
||||
return list.Where(r => context.Entry(r).State != EntityState.Deleted).ToList();
|
||||
}
|
||||
|
||||
public void RemoveRedemption(Models.SchemeRedemption entity) =>
|
||||
context.SchemeRedemptions.Remove(entity);
|
||||
|
||||
public async Task<bool> HasRedemptionAsync(Guid schemeId, Guid shopPOId) =>
|
||||
await context.SchemeRedemptions.AnyAsync(r => r.FK_SchemeId == schemeId && r.FK_ShopPOId == shopPOId);
|
||||
|
||||
// An invoice past Draft (printed or finalized) has a fixed amount, so it can no longer take a discount.
|
||||
public async Task<bool> HasLockedInvoiceAsync(Guid shopPOId) =>
|
||||
await context.Invoices.AnyAsync(i => i.FK_ShopPOId == shopPOId && i.IsActive && i.Status != Models.InvoiceStatus.Draft);
|
||||
|
||||
public async Task<Models.SchemeBundleComponent?> GetBundleComponentAsync(Guid schemeId, Guid itemId) =>
|
||||
await context.SchemeBundleComponents
|
||||
.FirstOrDefaultAsync(c => c.FK_SchemeId == schemeId && c.FK_ItemId == itemId);
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
using System.Text.Json;
|
||||
using Distribution.Features.Invoice;
|
||||
using Distribution.Features.Item;
|
||||
using Distribution.Features.PriceList;
|
||||
using Distribution.Features.ShopPO;
|
||||
using Distribution.Features.Stock;
|
||||
using Distribution.Features.Store;
|
||||
@@ -14,6 +16,8 @@ public class SchemeService(
|
||||
IItemRepository itemRepo,
|
||||
IStoreRepository storeRepo,
|
||||
IStockRepository stockRepo,
|
||||
IInvoiceRepository invoiceRepo,
|
||||
PriceListService priceListService,
|
||||
IUnitOfWork uow)
|
||||
{
|
||||
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web);
|
||||
@@ -261,12 +265,7 @@ public class SchemeService(
|
||||
|
||||
foreach (var scheme in candidates)
|
||||
{
|
||||
if (scheme.FK_RegionId.HasValue && scheme.FK_RegionId != regionId)
|
||||
continue;
|
||||
if (!string.IsNullOrEmpty(scheme.EligibleSegment) &&
|
||||
!string.Equals(scheme.EligibleSegment, store.Segment, StringComparison.OrdinalIgnoreCase))
|
||||
continue;
|
||||
if (shopPO.CreatedAt < scheme.DateRangeFrom || shopPO.CreatedAt > scheme.DateRangeTo)
|
||||
if (!IsInScope(scheme, store.Segment, regionId, shopPO.CreatedAt))
|
||||
continue;
|
||||
|
||||
result.Add(EvaluateRule(scheme, items));
|
||||
@@ -275,6 +274,328 @@ public class SchemeService(
|
||||
return result;
|
||||
}
|
||||
|
||||
private static bool IsInScope(Models.Scheme scheme, string? storeSegment, Guid? storeRegionId, DateTime asOf)
|
||||
{
|
||||
if (scheme.FK_RegionId.HasValue && scheme.FK_RegionId != storeRegionId) return false;
|
||||
if (!string.IsNullOrEmpty(scheme.EligibleSegment) &&
|
||||
!string.Equals(scheme.EligibleSegment, storeSegment, StringComparison.OrdinalIgnoreCase)) return false;
|
||||
return asOf >= scheme.DateRangeFrom && asOf.Date <= scheme.DateRangeTo.Date;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Suggestions for a Shop PO that is still being built (nothing saved yet). Returns eligible schemes,
|
||||
/// plus BuyXGetY/FreeBundle schemes the cart is part-way to, so the UI can nudge ("add 4 more").
|
||||
/// </summary>
|
||||
public async Task<List<SchemeEligibilityResponse>?> SuggestAsync(SuggestSchemesRequest request)
|
||||
{
|
||||
var store = await storeRepo.GetByIdWithRegionAsync(request.StoreId);
|
||||
if (store is null || !store.IsActive) return null;
|
||||
|
||||
var lines = new List<ShopPOItem>();
|
||||
foreach (var l in request.Items.Where(i => !i.IsFree))
|
||||
{
|
||||
var price = l.UnitPrice
|
||||
?? (await priceListService.ResolveByStoreAsync(l.ItemId, request.StoreId, DateTime.Now))?.Price
|
||||
?? 0m;
|
||||
lines.Add(new ShopPOItem { FK_ItemId = l.ItemId, Quantity = l.Quantity, UnitPrice = price });
|
||||
}
|
||||
|
||||
var regionId = store.Route?.FK_RegionId;
|
||||
var inCart = lines.Select(l => l.FK_ItemId).ToHashSet();
|
||||
var result = new List<SchemeEligibilityResponse>();
|
||||
|
||||
foreach (var scheme in (await repo.GetAllAsync()).Where(s => s.IsActive))
|
||||
{
|
||||
if (!IsInScope(scheme, store.Segment, regionId, DateTime.Now)) continue;
|
||||
|
||||
var eval = EvaluateRule(scheme, lines);
|
||||
var relevant = eval.Eligible || scheme.RuleType switch
|
||||
{
|
||||
SchemeRuleType.BuyXGetY => scheme.FK_TriggerItemId.HasValue && inCart.Contains(scheme.FK_TriggerItemId.Value),
|
||||
SchemeRuleType.FreeBundle => scheme.BundleComponents.Any(c => inCart.Contains(c.FK_ItemId)),
|
||||
_ => false
|
||||
};
|
||||
if (!relevant) continue;
|
||||
|
||||
result.Add(eval with { FreeItemName = scheme.FreeItem?.Name });
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks the free lines of a Shop PO that is about to be saved: each must reference an in-scope scheme the
|
||||
/// paid lines still qualify for, be the scheme's free item, stay within the entitled qty, and be in stock.
|
||||
/// Stamps the scheme-name snapshot and forces UnitPrice to 0. Read-only; throws InvalidOperationException.
|
||||
/// </summary>
|
||||
public async Task ValidateFreeLinesAsync(Guid storeId, DateTime asOf, List<ShopPOItem> lines)
|
||||
{
|
||||
var freeLines = lines.Where(l => l.IsFree).ToList();
|
||||
if (freeLines.Count == 0) return;
|
||||
|
||||
if (freeLines.Any(l => l.FK_SchemeId is null))
|
||||
throw new InvalidOperationException("A free-issue line must reference the scheme that granted it.");
|
||||
if (freeLines.Any(l => l.Quantity <= 0))
|
||||
throw new InvalidOperationException("Free-issue quantity must be greater than zero (remove the line instead).");
|
||||
|
||||
var store = await storeRepo.GetByIdWithRegionAsync(storeId)
|
||||
?? throw new InvalidOperationException("Store not found.");
|
||||
var regionId = store.Route?.FK_RegionId;
|
||||
|
||||
foreach (var group in freeLines.GroupBy(l => l.FK_SchemeId!.Value))
|
||||
{
|
||||
var scheme = await repo.GetByIdAsync(group.Key);
|
||||
if (scheme is null || !scheme.IsActive)
|
||||
throw new InvalidOperationException("A free-issue line references a scheme that no longer exists.");
|
||||
if (!IsInScope(scheme, store.Segment, regionId, asOf))
|
||||
throw new InvalidOperationException($"Scheme '{scheme.Name}' is not available for this store or date.");
|
||||
|
||||
var eval = EvaluateRule(scheme, lines);
|
||||
if (!eval.Eligible || eval.FreeItemId is null || eval.FreeQty is null)
|
||||
throw new InvalidOperationException(eval.Reason ?? $"Order no longer qualifies for scheme '{scheme.Name}'.");
|
||||
if (group.Any(l => l.FK_ItemId != eval.FreeItemId))
|
||||
throw new InvalidOperationException($"Scheme '{scheme.Name}' grants a different free item.");
|
||||
|
||||
var qty = group.Sum(l => l.Quantity);
|
||||
if (qty > eval.FreeQty.Value)
|
||||
throw new InvalidOperationException($"Free quantity {qty} exceeds the {eval.FreeQty.Value:0.##} entitled by scheme '{scheme.Name}'.");
|
||||
|
||||
var available = (await stockRepo.GetBalancesByItemAsync(eval.FreeItemId.Value)).Sum(b => b.Quantity);
|
||||
if (available < qty)
|
||||
throw new InvalidOperationException($"Insufficient stock to give {qty} free of scheme '{scheme.Name}'.");
|
||||
|
||||
foreach (var l in group)
|
||||
{
|
||||
l.UnitPrice = 0m;
|
||||
l.SchemeName = scheme.Name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks the percentage-discount schemes (FlatPercent / SlabDiscount) the user accepted for a Shop PO that is
|
||||
/// about to be saved: one at most, in scope, and the paid lines must still qualify. Read-only; throws.
|
||||
/// </summary>
|
||||
public async Task ValidateDiscountSchemesAsync(Guid storeId, DateTime asOf, List<ShopPOItem> lines, List<Guid>? schemeIds)
|
||||
{
|
||||
if (schemeIds is not { Count: > 0 }) return;
|
||||
if (schemeIds.Distinct().Count() > 1)
|
||||
throw new InvalidOperationException("Only one discount scheme can be applied to an order.");
|
||||
|
||||
var store = await storeRepo.GetByIdWithRegionAsync(storeId)
|
||||
?? throw new InvalidOperationException("Store not found.");
|
||||
|
||||
var scheme = await repo.GetByIdAsync(schemeIds[0]);
|
||||
if (scheme is null || !scheme.IsActive)
|
||||
throw new InvalidOperationException("The selected discount scheme no longer exists.");
|
||||
if (scheme.RuleType is not (SchemeRuleType.FlatPercent or SchemeRuleType.SlabDiscount))
|
||||
throw new InvalidOperationException($"Scheme '{scheme.Name}' is not a discount scheme.");
|
||||
if (!IsInScope(scheme, store.Segment, store.Route?.FK_RegionId, asOf))
|
||||
throw new InvalidOperationException($"Scheme '{scheme.Name}' is not available for this store or date.");
|
||||
|
||||
var eval = EvaluateRule(scheme, lines);
|
||||
if (!eval.Eligible)
|
||||
throw new InvalidOperationException(eval.Reason ?? $"Order does not qualify for scheme '{scheme.Name}'.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Re-evaluates the percentage discounts redeemed against a Shop PO for the given (invoice) lines and returns
|
||||
/// their total. With persist = true the redemption amounts are updated too (the caller commits), so spend/ROI and
|
||||
/// the invoice stay in step; a discount the lines no longer qualify for drops to 0 and comes back if they requalify.
|
||||
/// </summary>
|
||||
public async Task<decimal> CalculateDiscountAsync(Guid shopPOId, IEnumerable<ShopPOItem> lines, bool persist)
|
||||
{
|
||||
var priced = lines.ToList();
|
||||
decimal total = 0m;
|
||||
foreach (var r in (await repo.GetRedemptionsByShopPOAsync(shopPOId)).Where(r => r.DiscountAmount.HasValue))
|
||||
{
|
||||
var scheme = await repo.GetByIdAsync(r.FK_SchemeId);
|
||||
var eval = scheme is null ? null : EvaluateRule(scheme, priced);
|
||||
var amount = eval is { Eligible: true, DiscountAmount: not null } ? eval.DiscountAmount.Value : 0m;
|
||||
if (persist) r.DiscountAmount = amount;
|
||||
total += amount;
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
// How many free units each scheme currently entitles for these paid lines (0 when no longer eligible).
|
||||
private async Task<Dictionary<Guid, int>> FreeEntitlementsAsync(IEnumerable<Guid> schemeIds, List<ShopPOItem> paidLines)
|
||||
{
|
||||
var result = new Dictionary<Guid, int>();
|
||||
foreach (var id in schemeIds.Distinct())
|
||||
{
|
||||
var scheme = await repo.GetByIdAsync(id);
|
||||
var eval = scheme is null ? null : EvaluateRule(scheme, paidLines);
|
||||
result[id] = eval is { Eligible: true, FreeQty: not null } ? (int)Math.Floor(eval.FreeQty.Value) : 0;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Free-issue entitlement per scheme for unsaved invoice lines (editor preview). Lines carry IsFree / scheme id;
|
||||
/// only the paid ones count toward the scheme rules.
|
||||
/// </summary>
|
||||
public async Task<Dictionary<Guid, int>> GetFreeEntitlementsAsync(IEnumerable<Guid> schemeIds, IEnumerable<ShopPOItem> lines) =>
|
||||
await FreeEntitlementsAsync(schemeIds, lines.Where(l => !l.IsFree).ToList());
|
||||
|
||||
/// <summary>
|
||||
/// Keeps an invoice's free-issue lines within what the remaining paid lines still earn: when the paid quantity of the
|
||||
/// qualifying item is lowered (or removed), the free quantity is reduced to match (a line that earns nothing is struck
|
||||
/// out) and the PO's redemption is resized. Stages changes only. Returns true if any free line was changed.
|
||||
/// </summary>
|
||||
public async Task<bool> ReconcileInvoiceFreeLinesAsync(Guid shopPOId, List<InvoiceItem> lines)
|
||||
{
|
||||
var freeLines = lines.Where(l => l.IsActive && l.IsFree && l.FK_SchemeId.HasValue).ToList();
|
||||
if (freeLines.Count == 0) return false;
|
||||
|
||||
var paid = lines.Where(l => l.IsActive && !l.IsRemoved && !l.IsFree)
|
||||
.Select(l => new ShopPOItem { FK_ItemId = l.FK_ItemId, Quantity = l.Quantity, UnitPrice = l.UnitPrice })
|
||||
.ToList();
|
||||
var entitled = await FreeEntitlementsAsync(freeLines.Select(l => l.FK_SchemeId!.Value), paid);
|
||||
var redemptions = await repo.GetRedemptionsByShopPOAsync(shopPOId);
|
||||
var changed = false;
|
||||
|
||||
foreach (var group in freeLines.GroupBy(l => l.FK_SchemeId!.Value))
|
||||
{
|
||||
var remaining = entitled[group.Key];
|
||||
foreach (var line in group.Where(l => !l.IsRemoved).OrderBy(l => l.CreatedAt))
|
||||
{
|
||||
if (line.Quantity <= remaining) { remaining -= line.Quantity; continue; }
|
||||
|
||||
if (remaining == 0) line.IsRemoved = true;
|
||||
else line.Quantity = remaining;
|
||||
remaining = 0;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
var redemption = redemptions.FirstOrDefault(r => r.FK_SchemeId == group.Key && r.FK_FreeItemId.HasValue);
|
||||
if (redemption is not null)
|
||||
redemption.FreeQty = group.Where(l => !l.IsRemoved).Sum(l => l.Quantity);
|
||||
}
|
||||
|
||||
return changed;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Discount the given schemes would give for these lines, without touching any redemption (editor preview).
|
||||
/// </summary>
|
||||
public async Task<decimal> CalculateDiscountForSchemesAsync(IEnumerable<Guid> schemeIds, IEnumerable<ShopPOItem> lines)
|
||||
{
|
||||
var priced = lines.ToList();
|
||||
decimal total = 0m;
|
||||
foreach (var id in schemeIds.Distinct())
|
||||
{
|
||||
var scheme = await repo.GetByIdAsync(id);
|
||||
var eval = scheme is null ? null : EvaluateRule(scheme, priced);
|
||||
if (eval is { Eligible: true, DiscountAmount: not null }) total += eval.DiscountAmount.Value;
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies the discount choice made on a Draft invoice (the percentage scheme the user accepted, or none) to the
|
||||
/// PO's redemptions: newly chosen schemes are validated and added, unchosen ones removed. The amount itself is
|
||||
/// then set by CalculateDiscountAsync when the totals are re-derived. Stages changes only. Null = leave as is.
|
||||
/// </summary>
|
||||
public async Task ApplyInvoiceDiscountSelectionAsync(Guid shopPOId, Guid storeId, List<InvoiceItem> lines, List<Guid>? schemeIds)
|
||||
{
|
||||
if (schemeIds is null) return;
|
||||
if (schemeIds.Distinct().Count() > 1)
|
||||
throw new InvalidOperationException("Only one discount scheme can be applied to an order.");
|
||||
|
||||
var existing = (await repo.GetRedemptionsByShopPOAsync(shopPOId)).Where(r => r.DiscountAmount.HasValue).ToList();
|
||||
foreach (var r in existing.Where(r => !schemeIds.Contains(r.FK_SchemeId)))
|
||||
repo.RemoveRedemption(r);
|
||||
|
||||
var paid = lines.Where(l => l.IsActive && !l.IsRemoved && !l.IsFree)
|
||||
.Select(l => new ShopPOItem { FK_ItemId = l.FK_ItemId, Quantity = l.Quantity, UnitPrice = l.UnitPrice })
|
||||
.ToList();
|
||||
var added = schemeIds.Where(id => existing.All(r => r.FK_SchemeId != id)).ToList();
|
||||
await ValidateDiscountSchemesAsync(storeId, DateTime.Now, paid, added);
|
||||
|
||||
foreach (var id in added)
|
||||
await repo.AddRedemptionAsync(new SchemeRedemption { FK_SchemeId = id, FK_ShopPOId = shopPOId, DiscountAmount = 0m });
|
||||
}
|
||||
|
||||
/// <summary>Discounts already redeemed against a Shop PO, for display and for editing.</summary>
|
||||
public async Task<List<ShopPODiscountResponse>> GetDiscountsForShopPOAsync(Guid shopPOId) =>
|
||||
(await repo.GetRedemptionsByShopPOAsync(shopPOId))
|
||||
.Where(r => r.DiscountAmount.HasValue)
|
||||
.Select(r => new ShopPODiscountResponse(r.FK_SchemeId, r.Scheme?.Name ?? string.Empty, r.DiscountAmount!.Value))
|
||||
.ToList();
|
||||
|
||||
/// <summary>
|
||||
/// Brings the Shop PO's scheme redemptions in line with its saved lines: free-goods redemptions follow the
|
||||
/// free lines (created, resized or removed), discount redemptions are recomputed or dropped if the order no
|
||||
/// longer qualifies. Stages changes only — the caller owns the commit.
|
||||
/// </summary>
|
||||
public async Task SyncRedemptionsAsync(Guid shopPOId, List<ShopPOItem> lines, List<Guid>? discountSchemeIds = null)
|
||||
{
|
||||
var existing = await repo.GetRedemptionsByShopPOAsync(shopPOId);
|
||||
var freeBySchemes = lines.Where(l => l.IsFree && l.FK_SchemeId.HasValue)
|
||||
.GroupBy(l => l.FK_SchemeId!.Value).ToDictionary(g => g.Key, g => g.ToList());
|
||||
|
||||
foreach (var r in existing)
|
||||
{
|
||||
if (r.FK_FreeItemId.HasValue)
|
||||
{
|
||||
if (!freeBySchemes.ContainsKey(r.FK_SchemeId)) repo.RemoveRedemption(r);
|
||||
}
|
||||
else if (r.DiscountAmount.HasValue)
|
||||
{
|
||||
// A null list means "leave the accepted discounts alone" (just recompute them).
|
||||
if (discountSchemeIds is not null && !discountSchemeIds.Contains(r.FK_SchemeId))
|
||||
{
|
||||
repo.RemoveRedemption(r);
|
||||
continue;
|
||||
}
|
||||
|
||||
var scheme = await repo.GetByIdAsync(r.FK_SchemeId);
|
||||
var eval = scheme is null ? null : EvaluateRule(scheme, lines);
|
||||
if (eval is { Eligible: true, DiscountAmount: not null }) r.DiscountAmount = eval.DiscountAmount;
|
||||
else repo.RemoveRedemption(r);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var schemeId in discountSchemeIds ?? [])
|
||||
{
|
||||
if (existing.Any(r => r.FK_SchemeId == schemeId && r.DiscountAmount.HasValue)) continue;
|
||||
var scheme = await repo.GetByIdAsync(schemeId);
|
||||
var eval = scheme is null ? null : EvaluateRule(scheme, lines);
|
||||
if (eval is { Eligible: true, DiscountAmount: not null })
|
||||
await repo.AddRedemptionAsync(new SchemeRedemption
|
||||
{
|
||||
FK_SchemeId = schemeId,
|
||||
FK_ShopPOId = shopPOId,
|
||||
DiscountAmount = eval.DiscountAmount
|
||||
});
|
||||
}
|
||||
|
||||
foreach (var (schemeId, group) in freeBySchemes)
|
||||
{
|
||||
var freeItem = await itemRepo.GetByIdAsync(group[0].FK_ItemId)
|
||||
?? throw new InvalidOperationException("Free item not found.");
|
||||
var qty = group.Sum(l => l.Quantity);
|
||||
var redemption = existing.FirstOrDefault(r => r.FK_SchemeId == schemeId && r.FK_FreeItemId.HasValue);
|
||||
|
||||
if (redemption is null)
|
||||
await repo.AddRedemptionAsync(new SchemeRedemption
|
||||
{
|
||||
FK_SchemeId = schemeId,
|
||||
FK_ShopPOId = shopPOId,
|
||||
FK_FreeItemId = freeItem.Id,
|
||||
FreeQty = qty,
|
||||
FreeItemCostValue = freeItem.CostPrice ?? 0m
|
||||
});
|
||||
else
|
||||
{
|
||||
redemption.FK_FreeItemId = freeItem.Id;
|
||||
redemption.FreeQty = qty;
|
||||
redemption.FreeItemCostValue = freeItem.CostPrice ?? 0m;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Scope-only check (region, segment, date range as of today) with no ShopPO to evaluate line
|
||||
/// items against — used by Store Management's `checkStoreSchemeEligibility(storeId, schemeId)`
|
||||
@@ -299,14 +620,18 @@ public class SchemeService(
|
||||
return (false, $"Scheme is only available to the '{scheme.EligibleSegment}' segment.");
|
||||
|
||||
var today = DateTime.Now;
|
||||
if (today < scheme.DateRangeFrom || today > scheme.DateRangeTo)
|
||||
if (today < scheme.DateRangeFrom || today.Date > scheme.DateRangeTo.Date)
|
||||
return (false, "Scheme is not currently active (outside its date range).");
|
||||
|
||||
return (true, null);
|
||||
}
|
||||
|
||||
private static SchemeEligibilityResponse EvaluateRule(Models.Scheme scheme, List<ShopPOItem> items)
|
||||
private static SchemeEligibilityResponse EvaluateRule(Models.Scheme scheme, List<ShopPOItem> allItems)
|
||||
{
|
||||
// Free-issue lines (added by a previous redemption) must not count toward a trigger/bundle
|
||||
// quantity, otherwise the free units inflate the multiplier.
|
||||
var items = allItems.Where(i => !i.IsFree && i.UnitPrice > 0).ToList();
|
||||
|
||||
switch (scheme.RuleType)
|
||||
{
|
||||
case SchemeRuleType.BuyXGetY:
|
||||
@@ -383,6 +708,9 @@ public class SchemeService(
|
||||
if (await repo.HasRedemptionAsync(schemeId, shopPOId))
|
||||
throw new InvalidOperationException("This scheme has already been redeemed against this ShopPO.");
|
||||
|
||||
if (await repo.HasLockedInvoiceAsync(shopPOId))
|
||||
throw new InvalidOperationException("This order's invoice is already printed or finalized, so a scheme can no longer be applied. Apply schemes before the invoice is printed.");
|
||||
|
||||
var items = await shopPORepo.GetActiveItemsByShopPOIdAsync(shopPOId);
|
||||
var eligibility = EvaluateRule(scheme, items);
|
||||
if (!eligibility.Eligible)
|
||||
@@ -411,7 +739,10 @@ public class SchemeService(
|
||||
FK_ShopPOId = shopPOId,
|
||||
FK_ItemId = freeItem.Id,
|
||||
Quantity = (int)eligibility.FreeQty.Value,
|
||||
UnitPrice = 0m
|
||||
UnitPrice = 0m,
|
||||
IsFree = true,
|
||||
FK_SchemeId = scheme.Id,
|
||||
SchemeName = scheme.Name
|
||||
});
|
||||
|
||||
redemption.FK_FreeItemId = freeItem.Id;
|
||||
@@ -422,7 +753,27 @@ public class SchemeService(
|
||||
await uow.BeginAsync(ct);
|
||||
try
|
||||
{
|
||||
if (newItems.Count > 0) await shopPORepo.AddItemsAsync(newItems);
|
||||
if (newItems.Count > 0)
|
||||
{
|
||||
await shopPORepo.AddItemsAsync(newItems);
|
||||
|
||||
// A still-draft invoice was copied from the PO before this line existed, so mirror it there.
|
||||
if (shopPO.InvoiceId is Guid invoiceId &&
|
||||
await invoiceRepo.GetByIdAsync(invoiceId) is { Status: InvoiceStatus.Draft })
|
||||
{
|
||||
await invoiceRepo.AddItemsAsync(newItems.Select(n => new InvoiceItem
|
||||
{
|
||||
FK_InvoiceId = invoiceId,
|
||||
FK_ShopPOItemId = n.Id,
|
||||
FK_ItemId = n.FK_ItemId,
|
||||
Quantity = n.Quantity,
|
||||
UnitPrice = 0m,
|
||||
IsFree = true,
|
||||
FK_SchemeId = n.FK_SchemeId,
|
||||
SchemeName = n.SchemeName
|
||||
}));
|
||||
}
|
||||
}
|
||||
await repo.AddRedemptionAsync(redemption);
|
||||
await uow.CommitAsync(ct);
|
||||
}
|
||||
@@ -484,6 +835,8 @@ public class SchemeService(
|
||||
|
||||
if (freeItemId is null || freeQty is null or <= 0)
|
||||
throw new InvalidOperationException("FreeItemId and a positive FreeQty are required.");
|
||||
if (freeQty.Value != decimal.Truncate(freeQty.Value))
|
||||
throw new InvalidOperationException("FreeQty must be a whole number (order lines hold whole units).");
|
||||
if (await itemRepo.GetByIdAsync(freeItemId.Value) is null)
|
||||
throw new InvalidOperationException("Free item not found.");
|
||||
}
|
||||
|
||||
@@ -30,6 +30,16 @@ public class ShopPOController(ShopPOService service, ICurrentUserService current
|
||||
return result is null ? NotFound() : Ok(result);
|
||||
}
|
||||
|
||||
[HttpGet("confirmed-without-invoice")]
|
||||
public async Task<IActionResult> GetConfirmedWithoutInvoice(
|
||||
[FromQuery] Guid? agentId,
|
||||
[FromQuery] Guid? routeId,
|
||||
[FromQuery] Guid? shopId)
|
||||
{
|
||||
var result = await service.GetConfirmedWithoutInvoiceAsync(agentId, routeId, shopId);
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> Create(CreateShopPORequest request, CancellationToken ct)
|
||||
{
|
||||
|
||||
@@ -2,13 +2,18 @@
|
||||
|
||||
namespace Distribution.Features.ShopPO;
|
||||
|
||||
public record CreateShopPORequest(Guid StoreId, List<ShopPOItemRequest> Items, Guid? RaisedByUserId = null);
|
||||
public record ShopPOItemRequest(Guid ItemId, int Quantity, decimal? UnitPrice = null);
|
||||
public record UpdateShopPOItemsRequest(List<ShopPOItemRequest> Items);
|
||||
// DiscountSchemeIds: the FlatPercent/SlabDiscount scheme the user accepted (at most one). Null = leave as is.
|
||||
public record CreateShopPORequest(Guid StoreId, List<ShopPOItemRequest> Items, Guid? RaisedByUserId = null, List<Guid>? DiscountSchemeIds = null);
|
||||
// IsFree + SchemeId mark a free-issue line the user accepted from a scheme suggestion; its price is forced to 0.
|
||||
public record ShopPOItemRequest(Guid ItemId, int Quantity, decimal? UnitPrice = null, bool IsFree = false, Guid? SchemeId = null);
|
||||
public record UpdateShopPOItemsRequest(List<ShopPOItemRequest> Items, List<Guid>? DiscountSchemeIds = null);
|
||||
public record ConfirmShopPORequest(Guid? ConfirmedByUserId = null);
|
||||
public record CancelShopPORequest(string Reason, Guid? CancelledByUserId = null);
|
||||
|
||||
public record ShopPOItemResponse(Guid Id, Guid ItemId, string ItemName, int Quantity, int QuantityFulfilled, decimal UnitPrice, bool IsActive);
|
||||
public record ShopPOItemResponse(Guid Id, Guid ItemId, string ItemName, int Quantity, int QuantityFulfilled, decimal UnitPrice, bool IsActive,
|
||||
bool IsFree = false, Guid? SchemeId = null, string? SchemeName = null);
|
||||
|
||||
public record ShopPODiscountResponse(Guid SchemeId, string SchemeName, decimal Amount);
|
||||
|
||||
public record ShopPOResponse(
|
||||
Guid Id,
|
||||
@@ -34,4 +39,5 @@ public record ShopPOResponse(
|
||||
DateTime? CancelledAt = null,
|
||||
string? CancelReason = null,
|
||||
string? CancelledByName = null,
|
||||
string? CancelledByRole = null);
|
||||
string? CancelledByRole = null,
|
||||
List<ShopPODiscountResponse>? Discounts = null);
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
using Distribution.Features.Invoice;
|
||||
using Distribution.Features.Notifications;
|
||||
using Distribution.Features.PriceList;
|
||||
using Distribution.Features.Scheme;
|
||||
using Distribution.Features.Stock;
|
||||
using Distribution.Infra.UoW;
|
||||
using Distribution.Models;
|
||||
@@ -10,16 +12,47 @@ public class ShopPOService(
|
||||
IShopPORepository repo,
|
||||
IInvoiceRepository invoiceRepo,
|
||||
PriceListService priceListService,
|
||||
SchemeService schemeService,
|
||||
INotificationService notifications,
|
||||
IUnitOfWork uow)
|
||||
{
|
||||
private static ShopPOResponse ToResponse(Models.ShopPO p, Dictionary<Guid, (string Name, string Role)>? users = null) =>
|
||||
// Paid lines are priced from the request or the price list; accepted free-issue lines are always priced 0.
|
||||
private async Task<ShopPOItem> BuildLineAsync(ShopPOItemRequest line, Guid storeId)
|
||||
{
|
||||
if (line.IsFree)
|
||||
return new ShopPOItem
|
||||
{
|
||||
FK_ItemId = line.ItemId,
|
||||
Quantity = line.Quantity,
|
||||
UnitPrice = 0m,
|
||||
IsFree = true,
|
||||
FK_SchemeId = line.SchemeId
|
||||
};
|
||||
|
||||
var unitPrice = line.UnitPrice
|
||||
?? (await priceListService.ResolveByStoreAsync(line.ItemId, storeId, DateTime.Now))?.Price
|
||||
?? throw new InvalidOperationException($"No resolvable price for item {line.ItemId}");
|
||||
|
||||
return new ShopPOItem { FK_ItemId = line.ItemId, Quantity = line.Quantity, UnitPrice = unitPrice };
|
||||
}
|
||||
|
||||
// Tells the person who raised the order (and their supervisor) what happened to it, unless they did it themselves.
|
||||
private Task NotifyOrderAsync(string type, Models.ShopPO po, Guid actingUserId, string? reason, CancellationToken ct) =>
|
||||
notifications.NotifyAsync(
|
||||
type,
|
||||
[RecipientRule.Users(po.RaisedById), RecipientRule.ReportsTo(po.RaisedById)],
|
||||
new NotificationArgs(new { StoreName = po.Store?.StoreName, Reason = reason }, "ShopPO", po.Id, "/sales/orders", actingUserId),
|
||||
ct);
|
||||
|
||||
private static ShopPOResponse ToResponse(Models.ShopPO p, Dictionary<Guid, (string Name, string Role)>? users = null, List<ShopPODiscountResponse>? discounts = null) =>
|
||||
new(p.Id, p.FK_StoreId, p.Store?.StoreName ?? string.Empty,
|
||||
p.Store?.StoreAddress, p.Store?.ContactNumber,
|
||||
p.RaisedById, p.InvoiceId, p.TotalAmount, p.Status,
|
||||
p.ConfirmedAt, p.ConfirmedBy, p.FulfilledAt,
|
||||
p.Items.Where(i => i.IsActive).Select(i =>
|
||||
new ShopPOItemResponse(i.Id, i.FK_ItemId, i.Item?.Name ?? string.Empty,
|
||||
i.Quantity, i.QuantityFulfilled, i.UnitPrice, i.IsActive)).ToList(),
|
||||
i.Quantity, i.QuantityFulfilled, i.UnitPrice, i.IsActive,
|
||||
i.IsFree, i.FK_SchemeId, i.SchemeName)).ToList(),
|
||||
p.IsActive, p.CreatedAt,
|
||||
users != null && users.TryGetValue(p.RaisedById, out var u) ? u.Name : null,
|
||||
users != null && users.TryGetValue(p.RaisedById, out var r) ? r.Role : null,
|
||||
@@ -27,7 +60,8 @@ public class ShopPOService(
|
||||
users != null && p.ConfirmedBy is Guid cb2 && users.TryGetValue(cb2, out var c2) ? c2.Role : null,
|
||||
p.CancelledBy, p.CancelledAt, p.CancelReason,
|
||||
users != null && p.CancelledBy is Guid xb && users.TryGetValue(xb, out var x) ? x.Name : null,
|
||||
users != null && p.CancelledBy is Guid xb2 && users.TryGetValue(xb2, out var x2) ? x2.Role : null);
|
||||
users != null && p.CancelledBy is Guid xb2 && users.TryGetValue(xb2, out var x2) ? x2.Role : null,
|
||||
discounts ?? []);
|
||||
|
||||
private static IEnumerable<Guid> UserIds(Models.ShopPO p)
|
||||
{
|
||||
@@ -52,7 +86,14 @@ public class ShopPOService(
|
||||
var po = await repo.GetByIdAsync(id);
|
||||
if (po is null) return null;
|
||||
var users = await repo.GetUserInfoAsync(UserIds(po));
|
||||
return ToResponse(po, users);
|
||||
return ToResponse(po, users, await schemeService.GetDiscountsForShopPOAsync(po.Id));
|
||||
}
|
||||
|
||||
public async Task<List<ShopPOResponse>> GetConfirmedWithoutInvoiceAsync(Guid? agentId, Guid? routeId, Guid? shopId)
|
||||
{
|
||||
var items = await repo.GetConfirmedWithoutInvoiceAsync(agentId, routeId, shopId);
|
||||
var users = await repo.GetUserInfoAsync(items.SelectMany(UserIds));
|
||||
return items.Select(p => ToResponse(p, users)).ToList();
|
||||
}
|
||||
|
||||
public async Task<ShopPOResponse> CreateAsync(CreateShopPORequest request, Guid raisedById, CancellationToken ct = default)
|
||||
@@ -65,20 +106,14 @@ public class ShopPOService(
|
||||
|
||||
foreach (var line in request.Items)
|
||||
{
|
||||
var unitPrice = line.UnitPrice
|
||||
?? (await priceListService.ResolveByStoreAsync(line.ItemId, request.StoreId, DateTime.Now))?.Price
|
||||
?? throw new InvalidOperationException($"No resolvable price for item {line.ItemId}");
|
||||
|
||||
items.Add(new ShopPOItem
|
||||
{
|
||||
FK_ItemId = line.ItemId,
|
||||
Quantity = line.Quantity,
|
||||
QuantityFulfilled = 0,
|
||||
UnitPrice = unitPrice
|
||||
});
|
||||
total += line.Quantity * unitPrice;
|
||||
var item = await BuildLineAsync(line, request.StoreId);
|
||||
items.Add(item);
|
||||
total += item.Quantity * item.UnitPrice;
|
||||
}
|
||||
|
||||
await schemeService.ValidateFreeLinesAsync(request.StoreId, DateTime.Now, items);
|
||||
await schemeService.ValidateDiscountSchemesAsync(request.StoreId, DateTime.Now, items, request.DiscountSchemeIds);
|
||||
|
||||
var po = new Models.ShopPO
|
||||
{
|
||||
FK_StoreId = request.StoreId,
|
||||
@@ -93,6 +128,7 @@ public class ShopPOService(
|
||||
await repo.AddAsync(po);
|
||||
foreach (var item in items) item.FK_ShopPOId = po.Id;
|
||||
await repo.AddItemsAsync(items);
|
||||
await schemeService.SyncRedemptionsAsync(po.Id, items, request.DiscountSchemeIds);
|
||||
await uow.CommitAsync(ct);
|
||||
}
|
||||
catch { await uow.RollbackAsync(ct); throw; }
|
||||
@@ -120,27 +156,22 @@ public class ShopPOService(
|
||||
|
||||
foreach (var line in request.Items)
|
||||
{
|
||||
var unitPrice = line.UnitPrice
|
||||
?? (await priceListService.ResolveByStoreAsync(line.ItemId, po.FK_StoreId, DateTime.Now))?.Price
|
||||
?? throw new InvalidOperationException($"No resolvable price for item {line.ItemId}");
|
||||
|
||||
newItems.Add(new ShopPOItem
|
||||
{
|
||||
FK_ShopPOId = id,
|
||||
FK_ItemId = line.ItemId,
|
||||
Quantity = line.Quantity,
|
||||
QuantityFulfilled = 0,
|
||||
UnitPrice = unitPrice
|
||||
});
|
||||
total += line.Quantity * unitPrice;
|
||||
var item = await BuildLineAsync(line, po.FK_StoreId);
|
||||
item.FK_ShopPOId = id;
|
||||
newItems.Add(item);
|
||||
total += item.Quantity * item.UnitPrice;
|
||||
}
|
||||
|
||||
await schemeService.ValidateFreeLinesAsync(po.FK_StoreId, po.CreatedAt, newItems);
|
||||
await schemeService.ValidateDiscountSchemesAsync(po.FK_StoreId, po.CreatedAt, newItems, request.DiscountSchemeIds);
|
||||
|
||||
po.TotalAmount = total;
|
||||
|
||||
await uow.BeginAsync(ct);
|
||||
try
|
||||
{
|
||||
await repo.ReplaceItemsAsync(id, newItems);
|
||||
await schemeService.SyncRedemptionsAsync(id, newItems, request.DiscountSchemeIds);
|
||||
repo.Update(po);
|
||||
await uow.CommitAsync(ct);
|
||||
}
|
||||
@@ -168,6 +199,8 @@ public class ShopPOService(
|
||||
try { repo.Update(po); await uow.CommitAsync(ct); }
|
||||
catch { await uow.RollbackAsync(ct); throw; }
|
||||
|
||||
await NotifyOrderAsync(NotificationTypes.ShopPOConfirmed, po, confirmedBy, null, ct);
|
||||
|
||||
return await ToResponseWithUserAsync(po);
|
||||
}
|
||||
|
||||
@@ -193,17 +226,25 @@ public class ShopPOService(
|
||||
CreatedBy = userId,
|
||||
TotalAmount = po.TotalAmount
|
||||
};
|
||||
// Start the invoice at the discounted total so it matches what print/finalize will recompute.
|
||||
invoice.DiscountAmount = Math.Min(await invoiceRepo.GetSchemeDiscountAsync(po.Id), po.TotalAmount);
|
||||
invoice.TotalAmount = po.TotalAmount - invoice.DiscountAmount;
|
||||
|
||||
await uow.BeginAsync(ct);
|
||||
try
|
||||
{
|
||||
await invoiceRepo.AddAsync(invoice);
|
||||
var lines = InvoiceService.BuildItemsFromShopPO(po, invoice.Id);
|
||||
await invoiceRepo.AddItemsAsync(lines);
|
||||
await invoiceRepo.AddVersionAsync(InvoiceService.BuildVersion(invoice, lines, 1, InvoiceVersionStage.Created, userId));
|
||||
po.InvoiceId = invoice.Id;
|
||||
repo.Update(po);
|
||||
await uow.CommitAsync(ct);
|
||||
}
|
||||
catch { await uow.RollbackAsync(ct); throw; }
|
||||
|
||||
await NotifyOrderAsync(NotificationTypes.ShopPOConfirmed, po, userId, null, ct);
|
||||
|
||||
var saved = await repo.GetByIdAsync(id);
|
||||
return await ToResponseWithUserAsync(saved!);
|
||||
}
|
||||
@@ -231,6 +272,8 @@ public class ShopPOService(
|
||||
try { repo.Update(po); await uow.CommitAsync(ct); }
|
||||
catch { await uow.RollbackAsync(ct); throw; }
|
||||
|
||||
await NotifyOrderAsync(NotificationTypes.ShopPOCancelled, po, cancelledBy, po.CancelReason, ct);
|
||||
|
||||
return await ToResponseWithUserAsync(po);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
using Distribution.Features.Notifications;
|
||||
using Distribution.Features.Scheme;
|
||||
using Distribution.Features.Stock;
|
||||
using Distribution.Features.StoreCredit;
|
||||
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, SchemeService schemeService, Route.IRouteRepository routeRepo)
|
||||
public class StoreService(IStoreRepository repo, IUnitOfWork uow, ICurrentUserService currentUser, IScopeAccessService scopeAccess, SchemeService schemeService, Route.IRouteRepository routeRepo, INotificationService notifications, IStoreCreditMovementRepository creditRepo)
|
||||
{
|
||||
private const int GlobalScopeRoleLevel = 50;
|
||||
private const int CreditApproverRoleLevel = 20;
|
||||
@@ -147,6 +149,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;
|
||||
var wasWithinLimit = store.OutstandingBalance <= store.CreditLimit;
|
||||
store.OutstandingBalance = request.NewBalance;
|
||||
await uow.BeginAsync(ct);
|
||||
try
|
||||
@@ -159,6 +162,18 @@ public class StoreService(IStoreRepository repo, IUnitOfWork uow, ICurrentUserSe
|
||||
await uow.RollbackAsync(ct);
|
||||
throw;
|
||||
}
|
||||
|
||||
// Only on the crossing, so repeated updates while over the limit don't re-alert.
|
||||
if (store.CreditLimit > 0 && wasWithinLimit && store.OutstandingBalance > store.CreditLimit)
|
||||
await notifications.NotifyAsync(
|
||||
NotificationTypes.CreditLimitBreached,
|
||||
[
|
||||
RecipientRule.RouteOwner(ScopeKind.Store, store.Id),
|
||||
RecipientRule.PermissionInScope("store.manage", ScopeKind.Store, store.Id),
|
||||
],
|
||||
new NotificationArgs(new { store.StoreName }, "Store", store.Id, $"/stores/{store.Id}", currentUser.UserId),
|
||||
ct);
|
||||
|
||||
return ToResponse(store);
|
||||
}
|
||||
|
||||
@@ -167,7 +182,8 @@ 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 OutstandingBalanceResponse(store.Id, store.CreditLimit, store.OutstandingBalance, store.CreditLimit - store.OutstandingBalance);
|
||||
var outstanding = await creditRepo.GetOutstandingForStoreAsync(id);
|
||||
return new OutstandingBalanceResponse(store.Id, store.CreditLimit, outstanding, store.CreditLimit - outstanding);
|
||||
}
|
||||
public async Task<StoreResponse?> ClassifySegmentAsync(Guid id, ClassifySegmentRequest request, CancellationToken ct = default)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
using Distribution.Models;
|
||||
|
||||
namespace Distribution.Features.StoreCredit;
|
||||
|
||||
public interface IStoreCreditMovementRepository
|
||||
{
|
||||
Task<decimal> GetOutstandingForStoreAsync(Guid storeId);
|
||||
Task<List<StoreCreditMovement>> GetByStoreAsync(Guid storeId, int page, int pageSize);
|
||||
Task<int> GetCountByStoreAsync(Guid storeId);
|
||||
Task AddAsync(StoreCreditMovement movement);
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
using Distribution.Data;
|
||||
using Distribution.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Distribution.Features.StoreCredit;
|
||||
|
||||
public class StoreCreditMovementRepository(AppDBContext context) : IStoreCreditMovementRepository
|
||||
{
|
||||
public async Task<decimal> GetOutstandingForStoreAsync(Guid storeId)
|
||||
{
|
||||
var usage = await context.StoreCreditMovements
|
||||
.Where(m => m.IsActive && m.FK_StoreId == storeId && m.Direction == StoreCreditMovementDirection.Usage)
|
||||
.SumAsync(m => (decimal?)m.Amount) ?? 0m;
|
||||
|
||||
var settlements = await context.StoreCreditMovements
|
||||
.Where(m => m.IsActive && m.FK_StoreId == storeId && m.Direction == StoreCreditMovementDirection.Settlement)
|
||||
.SumAsync(m => (decimal?)m.Amount) ?? 0m;
|
||||
|
||||
return usage - settlements;
|
||||
}
|
||||
|
||||
public async Task<List<StoreCreditMovement>> GetByStoreAsync(Guid storeId, int page, int pageSize) =>
|
||||
await context.StoreCreditMovements
|
||||
.Where(m => m.IsActive && m.FK_StoreId == storeId)
|
||||
.OrderByDescending(m => m.OccurredAt)
|
||||
.Skip((page - 1) * pageSize)
|
||||
.Take(pageSize)
|
||||
.ToListAsync();
|
||||
|
||||
public async Task<int> GetCountByStoreAsync(Guid storeId) =>
|
||||
await context.StoreCreditMovements
|
||||
.CountAsync(m => m.IsActive && m.FK_StoreId == storeId);
|
||||
|
||||
public async Task AddAsync(StoreCreditMovement movement) =>
|
||||
await context.StoreCreditMovements.AddAsync(movement);
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using Distribution.Features.Grn;
|
||||
using Distribution.Features.Notifications;
|
||||
using Distribution.Features.Po;
|
||||
using Distribution.Infra.UoW;
|
||||
using Distribution.Models;
|
||||
@@ -9,6 +10,7 @@ public class SupplierClaimService(
|
||||
ISupplierClaimRepository repo,
|
||||
IPoRepository poRepo,
|
||||
IGrnRepository grnRepo,
|
||||
INotificationService notifications,
|
||||
IUnitOfWork uow)
|
||||
{
|
||||
private static SupplierClaimItemResponse MapItem(SupplierClaimItem i) =>
|
||||
@@ -149,9 +151,19 @@ public class SupplierClaimService(
|
||||
}
|
||||
catch { await uow.RollbackAsync(ct); throw; }
|
||||
|
||||
await NotifyDecisionAsync(NotificationTypes.SupplierClaimApproved, claim, approverId, ct);
|
||||
|
||||
return Map(claim);
|
||||
}
|
||||
|
||||
// Tells whoever raised the claim how it was decided (never the person who decided it).
|
||||
private Task NotifyDecisionAsync(string type, SupplierClaim claim, Guid decidedBy, CancellationToken ct) =>
|
||||
notifications.NotifyAsync(
|
||||
type,
|
||||
[RecipientRule.Users(claim.RaisedByUserId)],
|
||||
new NotificationArgs(null, "SupplierClaim", claim.Id, "/returns-claims/supplier-claims", decidedBy),
|
||||
ct);
|
||||
|
||||
public async Task<SupplierClaimResponse> RejectAsync(Guid id, RejectSupplierClaimRequest request, Guid approverId, CancellationToken ct = default)
|
||||
{
|
||||
var claim = await repo.GetByIdAsync(id)
|
||||
@@ -172,6 +184,8 @@ public class SupplierClaimService(
|
||||
}
|
||||
catch { await uow.RollbackAsync(ct); throw; }
|
||||
|
||||
await NotifyDecisionAsync(NotificationTypes.SupplierClaimRejected, claim, approverId, ct);
|
||||
|
||||
return Map(claim);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
using Distribution.Models;
|
||||
|
||||
namespace Distribution.Features.SupplierPayment;
|
||||
|
||||
public interface ISupplierPaymentRepository
|
||||
{
|
||||
Task<Models.SupplierPayment?> GetByIdAsync(Guid id);
|
||||
Task<List<Models.SupplierPayment>> ListBySupplierAsync(Guid supplierId, int page, int pageSize);
|
||||
Task<int> CountBySupplierAsync(Guid supplierId);
|
||||
Task AddAsync(Models.SupplierPayment payment);
|
||||
|
||||
/// <summary>Sum of currently-allocated amounts to a GRN, across all active SupplierPayments.</summary>
|
||||
Task<decimal> GetAllocatedTotalForGrnAsync(Guid goodsReceiptId);
|
||||
|
||||
/// <summary>Load a GRN with its items so the total can be computed.</summary>
|
||||
Task<Models.GoodsReceipt?> GetGrnWithItemsAsync(Guid goodsReceiptId);
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Distribution.Features.SupplierPayment;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/suppliers/{supplierId:guid}/payments")]
|
||||
public class SupplierPaymentController(SupplierPaymentService service) : ControllerBase
|
||||
{
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> List(Guid supplierId, [FromQuery] int page = 1, [FromQuery] int pageSize = 20)
|
||||
{
|
||||
var result = await service.ListBySupplierAsync(supplierId, page, pageSize);
|
||||
return result is null ? NotFound() : Ok(result);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> Create(Guid supplierId, CreateSupplierPaymentRequest request, CancellationToken ct)
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = await service.CreateAsync(supplierId, request, ct);
|
||||
return result is null ? NotFound() : Ok(result);
|
||||
}
|
||||
catch (InvalidOperationException ex) { return BadRequest(new { error = ex.Message }); }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
using Distribution.Models;
|
||||
|
||||
namespace Distribution.Features.SupplierPayment;
|
||||
|
||||
public record SupplierPaymentAllocationRequest(
|
||||
Guid GoodsReceiptId,
|
||||
decimal AllocatedAmount);
|
||||
|
||||
public record CreateSupplierPaymentRequest(
|
||||
decimal Amount,
|
||||
PaymentMethod Method,
|
||||
// One or more GRNs this payment covers. Sum of AllocatedAmount must equal Amount.
|
||||
List<SupplierPaymentAllocationRequest> Allocations,
|
||||
// Cash: required.
|
||||
Guid? BankAccountId = null,
|
||||
// Cheque: required — an Unused page from an Active cheque book.
|
||||
Guid? ChequePageId = null,
|
||||
string? Notes = null,
|
||||
Guid? PaidByUserId = null);
|
||||
|
||||
public record SupplierPaymentAllocationResponse(
|
||||
Guid GoodsReceiptId,
|
||||
decimal AllocatedAmount);
|
||||
|
||||
public record SupplierPaymentResponse(
|
||||
Guid Id,
|
||||
Guid SupplierId,
|
||||
decimal Amount,
|
||||
PaymentMethod Method,
|
||||
Guid? BankAccountId,
|
||||
Guid? ChequePageId,
|
||||
string? ChequeNo,
|
||||
DateTime PaidAt,
|
||||
Guid? PaidByUserId,
|
||||
string? Notes,
|
||||
List<SupplierPaymentAllocationResponse> Allocations,
|
||||
DateTime CreatedAt);
|
||||
|
||||
public record PagedSupplierPaymentsResponse(
|
||||
List<SupplierPaymentResponse> Items,
|
||||
int Page,
|
||||
int PageSize,
|
||||
int Total);
|
||||
@@ -0,0 +1,43 @@
|
||||
using Distribution.Data;
|
||||
using Distribution.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Distribution.Features.SupplierPayment;
|
||||
|
||||
public class SupplierPaymentRepository(AppDBContext context) : ISupplierPaymentRepository
|
||||
{
|
||||
public async Task<Models.SupplierPayment?> GetByIdAsync(Guid id) =>
|
||||
await context.SupplierPayments
|
||||
.Include(p => p.ChequePage)
|
||||
.Include(p => p.Allocations)
|
||||
.FirstOrDefaultAsync(p => p.Id == id);
|
||||
|
||||
public async Task<List<Models.SupplierPayment>> ListBySupplierAsync(Guid supplierId, int page, int pageSize) =>
|
||||
await context.SupplierPayments
|
||||
.Include(p => p.ChequePage)
|
||||
.Include(p => p.Allocations)
|
||||
.Where(p => p.FK_SupplierId == supplierId && p.IsActive)
|
||||
.OrderByDescending(p => p.PaidAt)
|
||||
.Skip((page - 1) * pageSize)
|
||||
.Take(pageSize)
|
||||
.ToListAsync();
|
||||
|
||||
public async Task<int> CountBySupplierAsync(Guid supplierId) =>
|
||||
await context.SupplierPayments
|
||||
.CountAsync(p => p.FK_SupplierId == supplierId && p.IsActive);
|
||||
|
||||
public async Task AddAsync(Models.SupplierPayment payment) =>
|
||||
await context.SupplierPayments.AddAsync(payment);
|
||||
|
||||
public async Task<decimal> GetAllocatedTotalForGrnAsync(Guid goodsReceiptId) =>
|
||||
await context.SupplierPaymentGRNs
|
||||
.Where(a => a.FK_GoodsReceiptId == goodsReceiptId
|
||||
&& a.IsActive
|
||||
&& a.SupplierPayment!.IsActive)
|
||||
.SumAsync(a => (decimal?)a.AllocatedAmount) ?? 0m;
|
||||
|
||||
public async Task<Models.GoodsReceipt?> GetGrnWithItemsAsync(Guid goodsReceiptId) =>
|
||||
await context.GoodsReceipts
|
||||
.Include(g => g.Items)
|
||||
.FirstOrDefaultAsync(g => g.Id == goodsReceiptId && g.IsActive);
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
using Distribution.Data;
|
||||
using Distribution.Features.BankAccount;
|
||||
using Distribution.Features.ChequeBook;
|
||||
using Distribution.Features.Grn;
|
||||
using Distribution.Features.Supplier;
|
||||
using Distribution.Infra.Authorization;
|
||||
using Distribution.Infra.UoW;
|
||||
using Distribution.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Distribution.Features.SupplierPayment;
|
||||
|
||||
public class SupplierPaymentService(
|
||||
AppDBContext context,
|
||||
ISupplierPaymentRepository repo,
|
||||
ISupplierRepository supplierRepo,
|
||||
IBankAccountRepository bankAccountRepo,
|
||||
IChequeBookRepository chequeBookRepo,
|
||||
IUnitOfWork uow,
|
||||
ICurrentUserService currentUser)
|
||||
{
|
||||
private static SupplierPaymentResponse ToResponse(Models.SupplierPayment p) =>
|
||||
new(p.Id, p.FK_SupplierId, p.Amount, p.Method,
|
||||
p.FK_BankAccountId, p.FK_ChequePageId, p.ChequePage?.ChequeNo,
|
||||
p.PaidAt, p.PaidByUserId, p.Notes,
|
||||
p.Allocations
|
||||
.Where(a => a.IsActive)
|
||||
.Select(a => new SupplierPaymentAllocationResponse(a.FK_GoodsReceiptId, a.AllocatedAmount))
|
||||
.ToList(),
|
||||
p.CreatedAt);
|
||||
|
||||
/// <summary>GRN total = sum per line of (gross - discount + NBT + VAT). Rejected qty never costs.</summary>
|
||||
private static decimal GrnTotal(Models.GoodsReceipt grn) =>
|
||||
grn.Items.Where(i => i.IsActive).Sum(i =>
|
||||
GrnPricing.Line(i.AcceptedQuantity, i.UnitCost, i.DiscountPercent, i.NbtPercent, i.VatPercent).Total);
|
||||
|
||||
public async Task<PagedSupplierPaymentsResponse?> ListBySupplierAsync(Guid supplierId, int page, int pageSize)
|
||||
{
|
||||
var supplier = await supplierRepo.GetByIdAsync(supplierId);
|
||||
if (supplier is null) return null;
|
||||
|
||||
if (page < 1) page = 1;
|
||||
if (pageSize < 1) pageSize = 20;
|
||||
if (pageSize > 200) pageSize = 200;
|
||||
|
||||
var payments = await repo.ListBySupplierAsync(supplierId, page, pageSize);
|
||||
var total = await repo.CountBySupplierAsync(supplierId);
|
||||
return new PagedSupplierPaymentsResponse(payments.Select(ToResponse).ToList(), page, pageSize, total);
|
||||
}
|
||||
|
||||
public async Task<SupplierPaymentResponse?> CreateAsync(Guid supplierId, CreateSupplierPaymentRequest request, CancellationToken ct = default)
|
||||
{
|
||||
var supplier = await supplierRepo.GetByIdAsync(supplierId);
|
||||
if (supplier is null) return null;
|
||||
|
||||
if (request.Amount <= 0)
|
||||
throw new InvalidOperationException("Amount must be greater than zero");
|
||||
if (request.Method == PaymentMethod.Credit)
|
||||
throw new InvalidOperationException("Credit is not a valid outgoing payment method");
|
||||
|
||||
if (request.Allocations is null || request.Allocations.Count == 0)
|
||||
throw new InvalidOperationException("At least one GRN allocation is required");
|
||||
|
||||
var duplicateGrn = request.Allocations.GroupBy(a => a.GoodsReceiptId).FirstOrDefault(g => g.Count() > 1);
|
||||
if (duplicateGrn is not null)
|
||||
throw new InvalidOperationException($"GRN {duplicateGrn.Key} is allocated more than once");
|
||||
|
||||
if (request.Allocations.Any(a => a.AllocatedAmount <= 0))
|
||||
throw new InvalidOperationException("Each allocation amount must be greater than zero");
|
||||
|
||||
var allocationSum = request.Allocations.Sum(a => a.AllocatedAmount);
|
||||
if (allocationSum != request.Amount)
|
||||
throw new InvalidOperationException($"Allocations sum ({allocationSum}) must equal Amount ({request.Amount})");
|
||||
|
||||
// Validate each GRN exists, belongs to this supplier, and the new allocation does not push cumulative paid over the GRN total.
|
||||
foreach (var alloc in request.Allocations)
|
||||
{
|
||||
var grn = await repo.GetGrnWithItemsAsync(alloc.GoodsReceiptId)
|
||||
?? throw new InvalidOperationException($"GRN {alloc.GoodsReceiptId} not found");
|
||||
|
||||
var grnSupplierId = grn.FK_SupplierId
|
||||
?? grn.PurchaseOrder?.FK_SupplierId;
|
||||
if (grnSupplierId is null)
|
||||
{
|
||||
// PO-based GRN without the PO loaded — re-query to confirm supplier match.
|
||||
var po = await context.PurchaseOrders.FindAsync([grn.FK_PurchaseOrderId], ct);
|
||||
grnSupplierId = po?.FK_SupplierId;
|
||||
}
|
||||
if (grnSupplierId != supplierId)
|
||||
throw new InvalidOperationException($"GRN {alloc.GoodsReceiptId} does not belong to the specified supplier");
|
||||
|
||||
var grnTotal = GrnTotal(grn);
|
||||
var alreadyAllocated = await repo.GetAllocatedTotalForGrnAsync(grn.Id);
|
||||
if (alreadyAllocated + alloc.AllocatedAmount > grnTotal)
|
||||
throw new InvalidOperationException(
|
||||
$"Allocation to GRN {grn.Id} ({alloc.AllocatedAmount}) exceeds outstanding balance " +
|
||||
$"(GRN total {grnTotal}, already paid {alreadyAllocated})");
|
||||
}
|
||||
|
||||
var payerId = currentUser.UserId ?? request.PaidByUserId;
|
||||
|
||||
var payment = new Models.SupplierPayment
|
||||
{
|
||||
FK_SupplierId = supplierId,
|
||||
Amount = request.Amount,
|
||||
Method = request.Method,
|
||||
PaidAt = DateTime.Now,
|
||||
PaidByUserId = payerId,
|
||||
Notes = request.Notes,
|
||||
Allocations = request.Allocations.Select(a => new Models.SupplierPaymentGRN
|
||||
{
|
||||
FK_GoodsReceiptId = a.GoodsReceiptId,
|
||||
AllocatedAmount = a.AllocatedAmount
|
||||
}).ToList()
|
||||
};
|
||||
|
||||
await uow.BeginAsync(ct);
|
||||
try
|
||||
{
|
||||
if (request.Method == PaymentMethod.Cash)
|
||||
{
|
||||
if (request.BankAccountId is null)
|
||||
throw new InvalidOperationException("BankAccountId is required when Method is Cash");
|
||||
|
||||
var account = await bankAccountRepo.GetByIdAsync(request.BankAccountId.Value);
|
||||
if (account is null)
|
||||
throw new InvalidOperationException("BankAccountId does not reference a valid account");
|
||||
if (account.IsActive is false)
|
||||
throw new InvalidOperationException("The selected account is inactive");
|
||||
|
||||
payment.FK_BankAccountId = account.Id;
|
||||
}
|
||||
else // Cheque
|
||||
{
|
||||
if (request.ChequePageId is null)
|
||||
throw new InvalidOperationException("ChequePageId is required when Method is Cheque");
|
||||
|
||||
var page = await context.ChequePages
|
||||
.Include(p => p.ChequeBook)
|
||||
.FirstOrDefaultAsync(p => p.Id == request.ChequePageId.Value, ct);
|
||||
|
||||
if (page is null)
|
||||
throw new InvalidOperationException("ChequePageId does not reference a valid cheque page");
|
||||
if (page.IssueStatus != ChequePageIssueStatus.Unused)
|
||||
throw new InvalidOperationException($"Cannot issue a cheque in status '{page.IssueStatus}'");
|
||||
if (page.ChequeBook is null || page.ChequeBook.Status != ChequeBookStatus.Active)
|
||||
throw new InvalidOperationException("The cheque book is not active");
|
||||
|
||||
page.IssueStatus = ChequePageIssueStatus.Issued;
|
||||
page.PayeeType = PayeeType.Supplier;
|
||||
page.PayeeName = supplier.Name;
|
||||
page.IssueDate = DateTime.Now;
|
||||
page.Amount = request.Amount;
|
||||
page.CurrencyCode = "LKR";
|
||||
page.PrintedAt = DateTime.Now;
|
||||
|
||||
chequeBookRepo.UpdatePage(page);
|
||||
payment.FK_ChequePageId = page.Id;
|
||||
payment.ChequePage = page;
|
||||
}
|
||||
|
||||
await repo.AddAsync(payment);
|
||||
|
||||
// TODO ledger: when accounting is wired in, post a journal entry here —
|
||||
// Debit Accounts Payable (supplier)
|
||||
// Credit Cash/Bank (BankAccount) OR Cheques-outstanding (for Cheque payments)
|
||||
|
||||
await uow.CommitAsync(ct);
|
||||
}
|
||||
catch { await uow.RollbackAsync(ct); throw; }
|
||||
|
||||
return ToResponse(payment);
|
||||
}
|
||||
}
|
||||
+5546
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,926 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Distribution.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddPaymentServiceModule : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "BankAccounts",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
|
||||
AccountType = table.Column<int>(type: "int", nullable: false),
|
||||
AccountName = table.Column<string>(type: "varchar(200)", maxLength: 200, nullable: false)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
BankName = table.Column<string>(type: "varchar(200)", maxLength: 200, nullable: true)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
CashAccountTypeName = table.Column<string>(type: "varchar(100)", maxLength: 100, nullable: true)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
AccountNumber = table.Column<string>(type: "varchar(100)", maxLength: 100, nullable: true)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
CurrencyCode = table.Column<string>(type: "varchar(10)", maxLength: 10, nullable: false)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
IsDefault = table.Column<bool>(type: "tinyint(1)", 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_BankAccounts", x => x.Id);
|
||||
})
|
||||
.Annotation("MySql:CharSet", "utf8mb4");
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "ChequeBooks",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
|
||||
ChequeBookNo = table.Column<string>(type: "varchar(100)", maxLength: 100, nullable: false)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
BranchId = table.Column<int>(type: "int", nullable: false),
|
||||
FK_BankAccountId = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
|
||||
StartChequeNo = table.Column<string>(type: "varchar(50)", maxLength: 50, nullable: false)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
EndChequeNo = table.Column<string>(type: "varchar(50)", maxLength: 50, nullable: false)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
TotalLeaves = table.Column<int>(type: "int", nullable: false),
|
||||
ReceivedDate = table.Column<DateTime>(type: "datetime(6)", nullable: false),
|
||||
Description = table.Column<string>(type: "longtext", nullable: true)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
CreatedBy = table.Column<string>(type: "longtext", nullable: true)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
Status = 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_ChequeBooks", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_ChequeBooks_BankAccounts_FK_BankAccountId",
|
||||
column: x => x.FK_BankAccountId,
|
||||
principalTable: "BankAccounts",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
})
|
||||
.Annotation("MySql:CharSet", "utf8mb4");
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "ReceivedCheques",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
|
||||
CompanyId = table.Column<int>(type: "int", nullable: false),
|
||||
BranchId = table.Column<int>(type: "int", nullable: true),
|
||||
ReceivedFromType = table.Column<int>(type: "int", nullable: false),
|
||||
ReceivedFromId = table.Column<int>(type: "int", nullable: true),
|
||||
ReceivedFromName = table.Column<string>(type: "varchar(200)", maxLength: 200, nullable: false)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
DrawerBankName = table.Column<string>(type: "longtext", nullable: true)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
DrawerBankBranch = table.Column<string>(type: "longtext", nullable: true)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
AccountHolderName = table.Column<string>(type: "longtext", nullable: true)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
ChequeNo = table.Column<string>(type: "varchar(50)", maxLength: 50, nullable: false)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
ChequeDate = table.Column<DateTime>(type: "datetime(6)", nullable: false),
|
||||
Amount = table.Column<decimal>(type: "decimal(65,30)", nullable: false),
|
||||
ReceivedDate = table.Column<DateTime>(type: "datetime(6)", nullable: false),
|
||||
ReferenceType = table.Column<string>(type: "longtext", nullable: true)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
ReferenceId = table.Column<int>(type: "int", nullable: true),
|
||||
Notes = table.Column<string>(type: "longtext", nullable: true)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
CreatedBy = table.Column<string>(type: "longtext", nullable: true)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
Status = table.Column<int>(type: "int", nullable: false),
|
||||
FK_DepositBankAccountId = table.Column<Guid>(type: "char(36)", nullable: true, collation: "ascii_general_ci"),
|
||||
DepositDate = 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_ReceivedCheques", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_ReceivedCheques_BankAccounts_FK_DepositBankAccountId",
|
||||
column: x => x.FK_DepositBankAccountId,
|
||||
principalTable: "BankAccounts",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
})
|
||||
.Annotation("MySql:CharSet", "utf8mb4");
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "ChequePages",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
|
||||
ChequeNo = table.Column<string>(type: "varchar(50)", maxLength: 50, nullable: false)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
FK_ChequeBookId = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
|
||||
IssueStatus = table.Column<int>(type: "int", nullable: false),
|
||||
PayeeType = table.Column<int>(type: "int", nullable: true),
|
||||
PayeeId = table.Column<int>(type: "int", nullable: true),
|
||||
PayeeName = table.Column<string>(type: "longtext", nullable: true)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
IssueDate = table.Column<DateTime>(type: "datetime(6)", nullable: true),
|
||||
Amount = table.Column<decimal>(type: "decimal(65,30)", nullable: true),
|
||||
CurrencyCode = table.Column<string>(type: "varchar(10)", maxLength: 10, nullable: true)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
VoucherId = table.Column<int>(type: "int", nullable: true),
|
||||
ReferenceNo = table.Column<string>(type: "longtext", nullable: true)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
Purpose = table.Column<string>(type: "longtext", nullable: true)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
IsCrossCheque = table.Column<bool>(type: "tinyint(1)", nullable: true),
|
||||
IsAccountPayee = table.Column<bool>(type: "tinyint(1)", nullable: true),
|
||||
IsPostDated = table.Column<bool>(type: "tinyint(1)", nullable: true),
|
||||
Notes = table.Column<string>(type: "longtext", nullable: true)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
PrintedBy = table.Column<string>(type: "longtext", nullable: true)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
PrintedAt = table.Column<DateTime>(type: "datetime(6)", nullable: true),
|
||||
ClearedDate = table.Column<DateTime>(type: "datetime(6)", nullable: true),
|
||||
ClearedByBank = table.Column<bool>(type: "tinyint(1)", nullable: true),
|
||||
CancelReason = table.Column<string>(type: "longtext", nullable: true)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
CreatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false),
|
||||
IsActive = table.Column<bool>(type: "tinyint(1)", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_ChequePages", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_ChequePages_ChequeBooks_FK_ChequeBookId",
|
||||
column: x => x.FK_ChequeBookId,
|
||||
principalTable: "ChequeBooks",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
})
|
||||
.Annotation("MySql:CharSet", "utf8mb4");
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000001"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("8c43c584-0f65-496d-a77c-a7df46bb175d"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000002"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("25576a9d-38cb-4ff5-b8ca-08df43d8a5a6"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000003"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("e47016c1-a014-476f-ba87-4b3d438de62a"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000004"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("a24c0e9e-0194-4f96-823c-bdbdcd122d85"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000005"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("e76ef6e2-67ba-462f-a954-b1dbc5a95d8c"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000006"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("bba4fdaa-fd0e-43dc-aedf-331212bd06cd"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000007"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("bc66d450-af6b-426e-9d05-266ceba21fac"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000008"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("9ee2196c-1e4b-452d-aaf6-7c55e3d2be2b"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000009"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("63508590-7a30-4640-9fc9-d3d54978f581"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000010"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("82802a3f-23bc-45db-a531-d7e880dd7970"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000011"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("65ccf5b6-754a-431a-81bf-d8e0587c956e"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000012"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("6c713413-362b-475a-846b-9f3382cf7a35"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000013"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("6dfe68ff-f5a3-41a8-981b-ac108a81408e"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000014"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("09c6b812-dc14-44a2-9362-429dfef3942d"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000015"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("bf29a1bb-234e-47a3-922f-f0fb3a1daba0"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000016"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("0d2f3497-4852-453e-a451-275e945b6c1d"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000017"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("41c13dbc-4f51-4134-891c-b6955f728d65"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000018"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("29bc0291-7a91-4fc5-840b-9420b64b86fc"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000019"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("8ae0c386-d206-4580-890f-c1673c1dfeb2"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000020"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("5db4bd09-9481-4608-9dec-3412ba7f3f69"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000021"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("c1840bd7-88fd-496a-8d88-aef3d886e1ec"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000022"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("af97d659-c040-4fdc-beeb-62fc8334f95c"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000023"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("a6443517-3c2a-4407-a3bd-5497e053b0d6"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000024"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("2f540d56-bff4-41fe-9181-4a423c1da51c"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000025"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("db18cfb8-a56c-46bc-baff-298e6204c455"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000026"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("fe209c12-1189-485b-920e-ce6db3c132b9"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000027"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("fedef395-394d-4237-b5aa-8254603ed304"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000028"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("7cc47626-65b8-44b0-83fb-a5dff96c39f6"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000029"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("cf04aea5-3f47-4717-95aa-8dab547f9cfb"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000030"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("c2906d62-9eea-4d08-b904-86a3f8b04a20"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000031"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("7a546bf9-c2f3-4d23-a38e-a068da53dfeb"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000032"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("19c916c3-7b17-42e0-b4b6-89b8cb6475f4"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000033"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("e6805dbe-a931-4e2a-b08f-83e8760d4968"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000034"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("7d991f5c-16db-46c5-863c-93063c2e7382"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000035"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("513c50d0-5d7b-426d-beef-f14bdea626bc"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000036"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("638a6a60-25da-425c-bdec-683431c26d62"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000037"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("b00bfae6-92bb-48cb-99d9-f2814baae47d"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000038"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("6562c9b4-0673-4ce0-8805-03734bc889ce"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000039"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("6601e0fe-83dd-4307-be91-38dae998cc9c"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000040"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("c4848449-9678-4fa6-8a00-db4e504b57d6"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000041"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("91c02c24-12a3-48d5-9d19-174c8e764152"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000042"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("0f00477f-5120-40ba-a0e6-0406e47d1c80"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000043"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("db135a10-6335-478d-b2f8-3b5934cd142b"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000044"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("5c219dd9-9ff3-4541-a708-cc7e4bd4dd25"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000045"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("086e7b93-079d-49d0-90e9-95899b53be52"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000046"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("f0b3d172-cb32-4606-8272-00feaf4067a2"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000047"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("75a882c0-b2e9-4efa-a64c-b5d530d42194"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000048"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("03540f63-d811-4436-869f-2f76995bbb19"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000049"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("73894d9f-a823-4b5b-b809-5cc0aa716a70"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000050"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("1ad4bfb8-cde6-4623-a21d-9a78af0026d6"));
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_BankAccounts_AccountType_AccountName",
|
||||
table: "BankAccounts",
|
||||
columns: new[] { "AccountType", "AccountName" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ChequeBooks_ChequeBookNo",
|
||||
table: "ChequeBooks",
|
||||
column: "ChequeBookNo",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ChequeBooks_FK_BankAccountId",
|
||||
table: "ChequeBooks",
|
||||
column: "FK_BankAccountId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ChequePages_ChequeNo",
|
||||
table: "ChequePages",
|
||||
column: "ChequeNo",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ChequePages_FK_ChequeBookId",
|
||||
table: "ChequePages",
|
||||
column: "FK_ChequeBookId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ReceivedCheques_ChequeNo",
|
||||
table: "ReceivedCheques",
|
||||
column: "ChequeNo");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ReceivedCheques_FK_DepositBankAccountId",
|
||||
table: "ReceivedCheques",
|
||||
column: "FK_DepositBankAccountId");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "ChequePages");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "ReceivedCheques");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "ChequeBooks");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "BankAccounts");
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000001"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("db785a23-5337-4fc8-b460-80c30b707d79"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000002"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("b07b73f9-2c8b-4dff-af5c-42622b026321"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000003"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("2e867ba5-2739-4ce4-b8d7-25b14ed392c1"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000004"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("203d855d-f2d0-43d0-9d4f-9c4d0e8e7566"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000005"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("edd2a0ee-1ca9-47ff-9df6-7254929aef80"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000006"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("644b5ee5-f2c5-4187-a3d4-322cdd5c95d7"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000007"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("70fdf921-f3fd-4e66-bfd2-cb7bb156fa34"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000008"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("e2de5341-4146-4c84-a584-8788774c14df"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000009"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("ece07436-4a0b-435c-888b-a22c14612dc4"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000010"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("b72f8737-7a21-48c0-809a-bc0d3fdd7063"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000011"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("fe1efed0-271f-45e3-a58b-244f1115246f"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000012"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("1b58e809-e4a0-4108-af6e-fd0dac7b3163"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000013"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("881e8e67-e2ee-47be-94f7-77da73a05365"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000014"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("e5d0baa8-9def-4b81-8727-5886b2906eaf"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000015"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("a46ec771-5e79-4595-bb40-91e8a954a792"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000016"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("ef8b1808-3b14-43a8-ba75-30551e643cc5"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000017"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("50142904-8d40-424b-aeb3-53e8cb110445"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000018"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("4bd67f53-7f84-492c-83f9-fd6488fa88b7"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000019"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("d9a0eb59-600e-47e9-8e07-8b7bffc93f8b"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000020"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("e5a9d4da-9497-4e7e-8fb9-796927cbb510"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000021"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("ffa29833-505e-4f56-9a4d-58f5df844af2"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000022"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("4e54994b-7ce1-4b99-aa98-de00c1d205fa"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000023"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("f45b7352-f42a-4bb3-9d1e-d7cbe2665c0c"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000024"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("27744965-334b-4da2-8a2d-5d7433d11b9d"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000025"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("cec1991b-ea93-45b0-b380-01cf0d36fc17"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000026"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("25675bb4-2ab5-437c-bee8-1b1ec1741736"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000027"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("5fc66d40-1960-4028-af48-cf51a80bff04"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000028"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("bd4e1740-a8b6-4a53-8633-a68c9858b3f5"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000029"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("9de97081-77d7-43c3-90b9-af3bc9d4bbef"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000030"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("6ae05e16-95e5-4d43-bfa0-56991393d76f"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000031"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("eb096aea-92c9-4556-ba4f-3084da223c7d"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000032"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("6c9dac72-200a-46eb-b49e-699b1ca775b5"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000033"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("8c8d0c74-40f6-4164-a6a7-6d2374ef162a"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000034"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("4a0d8cea-c33d-45a6-bd42-d8a51ce321c8"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000035"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("a0ea0280-8e40-45bc-88e4-c3feb92e36d4"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000036"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("61da4c55-4f19-4ce4-bfc9-3fa9073365a2"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000037"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("fbab8f2e-f224-42a0-be16-b7ea4e24eec5"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000038"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("bcf7f883-e6c8-4a97-8a27-6814b0ed9144"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000039"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("13c01d81-198b-45cb-9de9-f25130e16313"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000040"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("667c3022-ea0d-4d1c-8ae4-b64e2f104295"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000041"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("78607299-2f5c-4edb-9143-c4a83395f731"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000042"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("e53313fd-c6d4-4d52-827f-14cfd55401eb"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000043"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("c2521113-60f4-41fa-8ded-6b6a6b91a584"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000044"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("14532efd-d9f9-4a46-a7c3-fbfbe85a6f13"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000045"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("438bbd52-ee61-4afc-9d59-7d0407da4f1d"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000046"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("3e3d0cd9-f95e-4c1c-b829-cd02f1bad987"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000047"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("84daf87e-d3e1-4f4e-9a57-ec9996fff41d"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000048"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("ddcb8caf-106e-4072-b48e-a2542de51211"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000049"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("95d39d28-c5c7-4e82-87eb-40570db1c1f5"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000050"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("b8e7b327-33c9-4420-b4d1-0071c58a5c95"));
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,729 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Distribution.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class SeedDefaultCashAccount : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.InsertData(
|
||||
table: "BankAccounts",
|
||||
columns: new[] { "Id", "AccountName", "AccountNumber", "AccountType", "BankName", "CashAccountTypeName", "CreatedAt", "CurrencyCode", "IsActive", "IsDefault" },
|
||||
values: new object[] { new Guid("11111111-1111-1111-1111-000000000001"), "Petty Cash", null, 0, null, "Petty Cash", new DateTime(2026, 10, 19, 0, 0, 0, 0, DateTimeKind.Unspecified), "LKR", true, true });
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000001"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("ec13fdfa-d848-4004-b749-4b455ba0a77e"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000002"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("66f41359-cf33-4bd9-a936-816f03b23da7"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000003"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("369efbe7-f200-4b2e-bc15-858b6d2ca882"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000004"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("adc14999-08af-4b4c-937d-3ecd7c35b709"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000005"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("f1bf4ccb-45c6-4915-b941-6b507eb1e44f"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000006"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("1cbd132a-ea3d-46b2-8150-c5a077b6b36a"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000007"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("362118bd-1d4c-4576-95f6-7da899bf7368"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000008"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("15e94675-d2ae-4f5a-8198-6297a26d7a2c"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000009"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("4af2ab93-d3ba-4bda-b440-167f672cc166"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000010"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("bc5ba5f8-f20a-432a-985b-95b9500ce693"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000011"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("4f43f82b-8e6a-4729-9ded-c78c7dde24a7"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000012"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("7ac3df8c-f5c3-4d57-b537-5d5ca4ec1e11"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000013"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("44ec8d0f-f540-4786-9d9a-9194fa091286"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000014"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("143c4280-21be-4852-adc5-7cd17ab595fd"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000015"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("d00b144e-bf2d-4a04-9c85-0c14544edf48"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000016"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("9a72b7e4-1197-4032-8ec6-ebb3442a267f"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000017"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("5ec2af6e-d1db-4811-9c37-94a260e6ef9b"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000018"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("a71aceba-5199-4824-8b4e-998b108c082c"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000019"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("4bd840bf-da52-46fd-b065-40253f915c7c"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000020"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("8b4d8558-dba9-45bb-b263-f066580a55e9"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000021"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("0ee1a2e0-e4d2-4af3-9cca-ffad05bc1e20"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000022"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("700be5ec-0c11-497b-808a-80e7a854d2b4"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000023"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("b9978e5a-14b1-462a-9b7b-c9c32c4decb9"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000024"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("1e7cc9d4-f65e-434b-90e5-f78b4fda39b8"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000025"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("bfb3f80b-32c3-43b0-b216-f05e55f09360"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000026"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("fcfe2242-93ad-4c6d-8eec-3ec1f1270d0a"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000027"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("8e6eeb90-966f-4417-a1ae-148cdaa64904"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000028"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("6df60aea-63f3-465e-96fb-9a7304ead3c2"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000029"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("c0e21519-a095-4c7f-af42-e26efdcdd31e"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000030"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("496bbf8d-d84d-4c21-b190-bf95a06b4113"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000031"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("36f55476-d749-459b-92d9-839919499460"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000032"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("d90301fa-8990-49cf-8fa0-228ea7e684bc"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000033"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("8da4cd2a-1fd2-4417-bb6d-9e91174193f5"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000034"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("cc9bed46-a230-4611-88b4-d568abca857e"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000035"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("4d791413-ac44-4165-87b8-c28dbfa49230"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000036"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("2bb28028-8b12-4937-9dc6-68e35b100007"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000037"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("fdc8d3d9-a32d-4322-8144-93ff677beea3"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000038"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("b1ea0901-08e8-4dea-8ac3-15b522a2f274"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000039"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("116ac7bf-741e-4d4f-a313-2e7946a50044"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000040"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("7fd97ca6-a14b-4e42-8474-5471c0ef79a9"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000041"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("e58f35b2-8308-4fbb-b3ca-db9d037347cd"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000042"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("dc6b2750-083b-4653-a986-7af7aaec8136"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000043"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("6629888a-eb6b-433f-bbfe-ee5d947fb5b2"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000044"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("94b97271-7593-4cec-9290-802e9a26f0d8"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000045"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("0dc88aca-4e86-4712-988f-8db6a466531d"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000046"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("c8839187-4358-4218-bf3d-5ea4ed758661"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000047"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("5c731906-8d81-4f85-abad-230050af0bd0"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000048"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("d6e3db40-ca38-4caf-88d8-ce87475b4d87"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000049"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("398f1c4d-5d7d-4af4-9aa7-a88d7abf9a33"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000050"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("0f6c66e6-833c-4b52-ba2c-09da8dbead6f"));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DeleteData(
|
||||
table: "BankAccounts",
|
||||
keyColumn: "Id",
|
||||
keyValue: new Guid("11111111-1111-1111-1111-000000000001"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000001"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("8c43c584-0f65-496d-a77c-a7df46bb175d"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000002"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("25576a9d-38cb-4ff5-b8ca-08df43d8a5a6"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000003"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("e47016c1-a014-476f-ba87-4b3d438de62a"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000004"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("a24c0e9e-0194-4f96-823c-bdbdcd122d85"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000005"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("e76ef6e2-67ba-462f-a954-b1dbc5a95d8c"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000006"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("bba4fdaa-fd0e-43dc-aedf-331212bd06cd"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000007"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("bc66d450-af6b-426e-9d05-266ceba21fac"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000008"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("9ee2196c-1e4b-452d-aaf6-7c55e3d2be2b"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000009"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("63508590-7a30-4640-9fc9-d3d54978f581"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000010"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("82802a3f-23bc-45db-a531-d7e880dd7970"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000011"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("65ccf5b6-754a-431a-81bf-d8e0587c956e"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000012"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("6c713413-362b-475a-846b-9f3382cf7a35"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000013"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("6dfe68ff-f5a3-41a8-981b-ac108a81408e"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000014"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("09c6b812-dc14-44a2-9362-429dfef3942d"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000015"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("bf29a1bb-234e-47a3-922f-f0fb3a1daba0"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000016"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("0d2f3497-4852-453e-a451-275e945b6c1d"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000017"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("41c13dbc-4f51-4134-891c-b6955f728d65"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000018"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("29bc0291-7a91-4fc5-840b-9420b64b86fc"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000019"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("8ae0c386-d206-4580-890f-c1673c1dfeb2"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000020"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("5db4bd09-9481-4608-9dec-3412ba7f3f69"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000021"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("c1840bd7-88fd-496a-8d88-aef3d886e1ec"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000022"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("af97d659-c040-4fdc-beeb-62fc8334f95c"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000023"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("a6443517-3c2a-4407-a3bd-5497e053b0d6"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000024"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("2f540d56-bff4-41fe-9181-4a423c1da51c"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000025"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("db18cfb8-a56c-46bc-baff-298e6204c455"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000026"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("fe209c12-1189-485b-920e-ce6db3c132b9"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000027"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("fedef395-394d-4237-b5aa-8254603ed304"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000028"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("7cc47626-65b8-44b0-83fb-a5dff96c39f6"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000029"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("cf04aea5-3f47-4717-95aa-8dab547f9cfb"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000030"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("c2906d62-9eea-4d08-b904-86a3f8b04a20"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000031"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("7a546bf9-c2f3-4d23-a38e-a068da53dfeb"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000032"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("19c916c3-7b17-42e0-b4b6-89b8cb6475f4"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000033"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("e6805dbe-a931-4e2a-b08f-83e8760d4968"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000034"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("7d991f5c-16db-46c5-863c-93063c2e7382"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000035"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("513c50d0-5d7b-426d-beef-f14bdea626bc"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000036"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("638a6a60-25da-425c-bdec-683431c26d62"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000037"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("b00bfae6-92bb-48cb-99d9-f2814baae47d"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000038"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("6562c9b4-0673-4ce0-8805-03734bc889ce"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000039"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("6601e0fe-83dd-4307-be91-38dae998cc9c"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000040"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("c4848449-9678-4fa6-8a00-db4e504b57d6"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000041"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("91c02c24-12a3-48d5-9d19-174c8e764152"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000042"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("0f00477f-5120-40ba-a0e6-0406e47d1c80"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000043"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("db135a10-6335-478d-b2f8-3b5934cd142b"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000044"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("5c219dd9-9ff3-4541-a708-cc7e4bd4dd25"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000045"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("086e7b93-079d-49d0-90e9-95899b53be52"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000046"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("f0b3d172-cb32-4606-8272-00feaf4067a2"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000047"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("75a882c0-b2e9-4efa-a64c-b5d530d42194"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000048"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("03540f63-d811-4436-869f-2f76995bbb19"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000049"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("73894d9f-a823-4b5b-b809-5cc0aa716a70"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000050"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("1ad4bfb8-cde6-4623-a21d-9a78af0026d6"));
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,928 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Distribution.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddSupplierPayments : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AlterColumn<int>(
|
||||
name: "CompanyId",
|
||||
table: "ReceivedCheques",
|
||||
type: "int",
|
||||
nullable: true,
|
||||
oldClrType: typeof(int),
|
||||
oldType: "int");
|
||||
|
||||
migrationBuilder.AddColumn<Guid>(
|
||||
name: "FK_BankAccountId",
|
||||
table: "Payments",
|
||||
type: "char(36)",
|
||||
nullable: true,
|
||||
collation: "ascii_general_ci");
|
||||
|
||||
migrationBuilder.AddColumn<Guid>(
|
||||
name: "FK_ReceivedChequeId",
|
||||
table: "Payments",
|
||||
type: "char(36)",
|
||||
nullable: true,
|
||||
collation: "ascii_general_ci");
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "SupplierPayments",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
|
||||
FK_SupplierId = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
|
||||
Amount = table.Column<decimal>(type: "decimal(18,2)", precision: 18, scale: 2, nullable: false),
|
||||
Method = table.Column<int>(type: "int", nullable: false),
|
||||
FK_BankAccountId = table.Column<Guid>(type: "char(36)", nullable: true, collation: "ascii_general_ci"),
|
||||
FK_ChequePageId = table.Column<Guid>(type: "char(36)", nullable: true, collation: "ascii_general_ci"),
|
||||
PaidAt = table.Column<DateTime>(type: "datetime(6)", nullable: false),
|
||||
PaidByUserId = table.Column<Guid>(type: "char(36)", nullable: true, collation: "ascii_general_ci"),
|
||||
Notes = table.Column<string>(type: "longtext", nullable: true)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
CreatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false),
|
||||
IsActive = table.Column<bool>(type: "tinyint(1)", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_SupplierPayments", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_SupplierPayments_BankAccounts_FK_BankAccountId",
|
||||
column: x => x.FK_BankAccountId,
|
||||
principalTable: "BankAccounts",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
table.ForeignKey(
|
||||
name: "FK_SupplierPayments_ChequePages_FK_ChequePageId",
|
||||
column: x => x.FK_ChequePageId,
|
||||
principalTable: "ChequePages",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
table.ForeignKey(
|
||||
name: "FK_SupplierPayments_Suppliers_FK_SupplierId",
|
||||
column: x => x.FK_SupplierId,
|
||||
principalTable: "Suppliers",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
})
|
||||
.Annotation("MySql:CharSet", "utf8mb4");
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "SupplierPaymentGRNs",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
|
||||
FK_SupplierPaymentId = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
|
||||
FK_GoodsReceiptId = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
|
||||
AllocatedAmount = table.Column<decimal>(type: "decimal(18,2)", precision: 18, scale: 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_SupplierPaymentGRNs", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_SupplierPaymentGRNs_GoodsReceipts_FK_GoodsReceiptId",
|
||||
column: x => x.FK_GoodsReceiptId,
|
||||
principalTable: "GoodsReceipts",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
table.ForeignKey(
|
||||
name: "FK_SupplierPaymentGRNs_SupplierPayments_FK_SupplierPaymentId",
|
||||
column: x => x.FK_SupplierPaymentId,
|
||||
principalTable: "SupplierPayments",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
})
|
||||
.Annotation("MySql:CharSet", "utf8mb4");
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "Payments",
|
||||
keyColumn: "Id",
|
||||
keyValue: new Guid("47000000-0000-0000-0000-000000000001"),
|
||||
columns: new[] { "FK_BankAccountId", "FK_ReceivedChequeId" },
|
||||
values: new object[] { null, null });
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "Payments",
|
||||
keyColumn: "Id",
|
||||
keyValue: new Guid("47000000-0000-0000-0000-000000000002"),
|
||||
columns: new[] { "FK_BankAccountId", "FK_ReceivedChequeId" },
|
||||
values: new object[] { null, null });
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "Payments",
|
||||
keyColumn: "Id",
|
||||
keyValue: new Guid("47000000-0000-0000-0000-000000000003"),
|
||||
columns: new[] { "FK_BankAccountId", "FK_ReceivedChequeId" },
|
||||
values: new object[] { null, null });
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "Payments",
|
||||
keyColumn: "Id",
|
||||
keyValue: new Guid("47000000-0000-0000-0000-000000000004"),
|
||||
columns: new[] { "FK_BankAccountId", "FK_ReceivedChequeId" },
|
||||
values: new object[] { null, null });
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000001"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("30f658ac-2c92-4cd8-ad47-10b0def2699c"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000002"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("42ab2c52-eaf8-4a17-b67a-ccd66569e56a"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000003"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("8afec825-27b3-4153-ad06-f8bcdbf93da8"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000004"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("4b427620-e99d-4068-a416-c3788d5bd9b5"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000005"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("361f44ef-1cd1-48a9-be4d-4c068aacef1e"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000006"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("11fc03ae-92ee-4336-ae57-29aac731ad51"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000007"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("0b1f2b73-ef60-4634-864f-22a6c0cd1373"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000008"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("2380abf3-2bcd-4a9f-b447-5f1685df2fc8"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000009"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("240e4d6a-8306-40ba-aaec-8e05cd0347db"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000010"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("cfde0aa6-e859-4f9c-8291-b0d886fcd4c9"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000011"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("dae772c8-26ec-4b5c-8d7a-f3196510bf2c"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000012"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("3512c0a4-a5ae-4002-a779-7d3cc600ab61"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000013"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("f9e2a348-159e-47a2-b7be-a60f0254873f"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000014"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("c8b4c181-5b08-45d2-818b-74bceeab78a8"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000015"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("8fb4ff4c-c5bd-4afd-a08e-02861d63cb7b"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000016"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("edb3b711-d623-439d-811e-afaef8d0680d"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000017"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("f33e4c62-d69f-4a91-8b62-acbd5351ea2a"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000018"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("c7c9d16c-e343-43e9-9e27-f38f903a5d00"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000019"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("f5f4760a-8a45-46ca-8093-743c95b0313f"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000020"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("c5434801-146a-4fd9-9c29-a1145e7f7f00"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000021"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("6c085135-78a5-4e65-a380-ee090b07e2bb"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000022"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("bbde2785-a3df-4f81-bad1-0bc5afe5de57"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000023"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("4421a0fb-bdd4-45a9-8dc0-d9b70f775e97"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000024"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("dba9dcb4-3b94-4ddc-89f3-552cfc109478"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000025"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("d8f74160-c150-4d9c-aba6-e45e628c684e"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000026"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("495fb68b-9f01-4538-9561-fefce0d39558"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000027"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("3bdece9b-a798-4223-a23c-9c9dd10d042d"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000028"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("6fa616eb-99fe-46b9-8305-908765d037fd"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000029"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("7a07148a-1327-4e97-befc-b2045eacad21"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000030"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("852e86d7-aa57-4a77-81c0-054f3fb4d7f7"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000031"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("3b767929-c973-41fb-a340-4ee5feb65619"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000032"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("63619bbd-0352-41bd-9e1c-a3f7908fb5d2"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000033"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("f6b4c8bf-a9b3-4179-9f57-a8584678c0d1"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000034"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("bb47b8ca-f7cf-40fe-8945-fc532a439b53"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000035"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("52126271-ae3b-4b58-a9d2-50e2dd894087"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000036"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("981e3358-85bb-47fe-a1f5-3a3a02cf47b1"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000037"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("749408da-fcee-458d-9144-8d8dee928ba0"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000038"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("c157857c-fb0e-41fd-a414-c0f57a44b120"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000039"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("3217cdbc-76c9-4505-b9f3-23de91a6212b"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000040"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("63526c52-18ba-40e0-9889-3bd3b7fc8161"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000041"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("260cf2fa-1ca6-4cd5-92ae-8c479a80e2f0"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000042"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("130ab09c-8a36-4705-8235-9d00f75dd684"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000043"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("41794774-e919-4f4e-a42a-5ae408d3407f"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000044"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("2ff23d04-d1f6-4aa3-a48e-1c82bb9ad12a"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000045"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("d36c81c0-8e9b-41ea-8e09-464a327a16d2"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000046"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("6c51dfc1-63e0-46a6-b2bb-27c21c708b51"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000047"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("076333d5-b1f3-4b89-86fd-5a61a4451c88"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000048"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("7e834f3e-1424-4aaa-816e-d62da6df672b"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000049"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("699fa06b-a7ff-4dbd-98a7-a29c2c361665"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000050"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("00df5735-54c6-4cdf-8e2c-a64cd90ede15"));
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Payments_FK_BankAccountId",
|
||||
table: "Payments",
|
||||
column: "FK_BankAccountId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Payments_FK_ReceivedChequeId",
|
||||
table: "Payments",
|
||||
column: "FK_ReceivedChequeId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_SupplierPaymentGRNs_FK_GoodsReceiptId",
|
||||
table: "SupplierPaymentGRNs",
|
||||
column: "FK_GoodsReceiptId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_SupplierPaymentGRNs_FK_SupplierPaymentId",
|
||||
table: "SupplierPaymentGRNs",
|
||||
column: "FK_SupplierPaymentId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_SupplierPayments_FK_BankAccountId",
|
||||
table: "SupplierPayments",
|
||||
column: "FK_BankAccountId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_SupplierPayments_FK_ChequePageId",
|
||||
table: "SupplierPayments",
|
||||
column: "FK_ChequePageId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_SupplierPayments_FK_SupplierId",
|
||||
table: "SupplierPayments",
|
||||
column: "FK_SupplierId");
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_Payments_BankAccounts_FK_BankAccountId",
|
||||
table: "Payments",
|
||||
column: "FK_BankAccountId",
|
||||
principalTable: "BankAccounts",
|
||||
principalColumn: "Id");
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_Payments_ReceivedCheques_FK_ReceivedChequeId",
|
||||
table: "Payments",
|
||||
column: "FK_ReceivedChequeId",
|
||||
principalTable: "ReceivedCheques",
|
||||
principalColumn: "Id");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_Payments_BankAccounts_FK_BankAccountId",
|
||||
table: "Payments");
|
||||
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_Payments_ReceivedCheques_FK_ReceivedChequeId",
|
||||
table: "Payments");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "SupplierPaymentGRNs");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "SupplierPayments");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_Payments_FK_BankAccountId",
|
||||
table: "Payments");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_Payments_FK_ReceivedChequeId",
|
||||
table: "Payments");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "FK_BankAccountId",
|
||||
table: "Payments");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "FK_ReceivedChequeId",
|
||||
table: "Payments");
|
||||
|
||||
migrationBuilder.AlterColumn<int>(
|
||||
name: "CompanyId",
|
||||
table: "ReceivedCheques",
|
||||
type: "int",
|
||||
nullable: false,
|
||||
defaultValue: 0,
|
||||
oldClrType: typeof(int),
|
||||
oldType: "int",
|
||||
oldNullable: true);
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000001"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("ec13fdfa-d848-4004-b749-4b455ba0a77e"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000002"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("66f41359-cf33-4bd9-a936-816f03b23da7"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000003"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("369efbe7-f200-4b2e-bc15-858b6d2ca882"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000004"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("adc14999-08af-4b4c-937d-3ecd7c35b709"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000005"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("f1bf4ccb-45c6-4915-b941-6b507eb1e44f"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000006"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("1cbd132a-ea3d-46b2-8150-c5a077b6b36a"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000007"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("362118bd-1d4c-4576-95f6-7da899bf7368"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000008"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("15e94675-d2ae-4f5a-8198-6297a26d7a2c"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000009"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("4af2ab93-d3ba-4bda-b440-167f672cc166"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000010"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("bc5ba5f8-f20a-432a-985b-95b9500ce693"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000011"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("4f43f82b-8e6a-4729-9ded-c78c7dde24a7"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000012"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("7ac3df8c-f5c3-4d57-b537-5d5ca4ec1e11"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000013"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("44ec8d0f-f540-4786-9d9a-9194fa091286"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000014"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("143c4280-21be-4852-adc5-7cd17ab595fd"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000015"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("d00b144e-bf2d-4a04-9c85-0c14544edf48"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000016"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("9a72b7e4-1197-4032-8ec6-ebb3442a267f"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000017"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("5ec2af6e-d1db-4811-9c37-94a260e6ef9b"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000018"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("a71aceba-5199-4824-8b4e-998b108c082c"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000019"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("4bd840bf-da52-46fd-b065-40253f915c7c"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000020"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("8b4d8558-dba9-45bb-b263-f066580a55e9"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000021"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("0ee1a2e0-e4d2-4af3-9cca-ffad05bc1e20"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000022"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("700be5ec-0c11-497b-808a-80e7a854d2b4"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000023"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("b9978e5a-14b1-462a-9b7b-c9c32c4decb9"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000024"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("1e7cc9d4-f65e-434b-90e5-f78b4fda39b8"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000025"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("bfb3f80b-32c3-43b0-b216-f05e55f09360"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000026"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("fcfe2242-93ad-4c6d-8eec-3ec1f1270d0a"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000027"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("8e6eeb90-966f-4417-a1ae-148cdaa64904"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000028"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("6df60aea-63f3-465e-96fb-9a7304ead3c2"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000029"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("c0e21519-a095-4c7f-af42-e26efdcdd31e"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000030"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("496bbf8d-d84d-4c21-b190-bf95a06b4113"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000031"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("36f55476-d749-459b-92d9-839919499460"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000032"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("d90301fa-8990-49cf-8fa0-228ea7e684bc"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000033"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("8da4cd2a-1fd2-4417-bb6d-9e91174193f5"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000034"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("cc9bed46-a230-4611-88b4-d568abca857e"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000035"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("4d791413-ac44-4165-87b8-c28dbfa49230"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000036"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("2bb28028-8b12-4937-9dc6-68e35b100007"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000037"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("fdc8d3d9-a32d-4322-8144-93ff677beea3"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000038"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("b1ea0901-08e8-4dea-8ac3-15b522a2f274"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000039"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("116ac7bf-741e-4d4f-a313-2e7946a50044"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000040"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("7fd97ca6-a14b-4e42-8474-5471c0ef79a9"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000041"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("e58f35b2-8308-4fbb-b3ca-db9d037347cd"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000042"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("dc6b2750-083b-4653-a986-7af7aaec8136"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000043"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("6629888a-eb6b-433f-bbfe-ee5d947fb5b2"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000044"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("94b97271-7593-4cec-9290-802e9a26f0d8"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000045"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("0dc88aca-4e86-4712-988f-8db6a466531d"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000046"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("c8839187-4358-4218-bf3d-5ea4ed758661"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000047"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("5c731906-8d81-4f85-abad-230050af0bd0"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000048"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("d6e3db40-ca38-4caf-88d8-ce87475b4d87"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000049"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("398f1c4d-5d7d-4af4-9aa7-a88d7abf9a33"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000050"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("0f6c66e6-833c-4b52-ba2c-09da8dbead6f"));
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,778 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Distribution.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class credittable : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "StoreCreditMovements",
|
||||
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"),
|
||||
Direction = table.Column<int>(type: "int", nullable: false),
|
||||
Amount = table.Column<decimal>(type: "decimal(18,2)", precision: 18, scale: 2, nullable: false),
|
||||
FK_InvoiceId = table.Column<Guid>(type: "char(36)", nullable: true, collation: "ascii_general_ci"),
|
||||
FK_PaymentId = table.Column<Guid>(type: "char(36)", nullable: true, collation: "ascii_general_ci"),
|
||||
OccurredAt = table.Column<DateTime>(type: "datetime(6)", nullable: false),
|
||||
RecordedBy = table.Column<Guid>(type: "char(36)", nullable: true, collation: "ascii_general_ci"),
|
||||
Notes = table.Column<string>(type: "longtext", nullable: true)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
CreatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false),
|
||||
IsActive = table.Column<bool>(type: "tinyint(1)", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_StoreCreditMovements", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_StoreCreditMovements_Invoices_FK_InvoiceId",
|
||||
column: x => x.FK_InvoiceId,
|
||||
principalTable: "Invoices",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
table.ForeignKey(
|
||||
name: "FK_StoreCreditMovements_Payments_FK_PaymentId",
|
||||
column: x => x.FK_PaymentId,
|
||||
principalTable: "Payments",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
table.ForeignKey(
|
||||
name: "FK_StoreCreditMovements_Stores_FK_StoreId",
|
||||
column: x => x.FK_StoreId,
|
||||
principalTable: "Stores",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
})
|
||||
.Annotation("MySql:CharSet", "utf8mb4");
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000001"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("be712eda-98a5-4a30-820a-1737e84d1e4b"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000002"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("47ea9420-d6a0-4376-a2b8-211639d95b27"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000003"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("369eed1f-df4c-4131-a3c6-ebbe63d2dc91"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000004"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("14b6944f-2d4a-4378-a974-c6fc0f9dfb81"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000005"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("14a01e0b-7be5-4c65-ae6a-899f9aa7ad28"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000006"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("5d486ccd-3866-4e7d-8ef1-fb177fb637c1"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000007"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("d830e59b-f451-4bde-9a77-78daea5f9a61"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000008"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("096d3837-a649-4184-8987-2ea0dfa2d4a1"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000009"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("5e43bbde-689b-4d07-baf9-0764078b5586"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000010"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("f317d30c-1644-44f0-b0aa-6bcf1fa66f44"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000011"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("31ffac6f-2aa8-48ee-bc87-213f35933de4"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000012"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("fb525bc4-9f16-455a-bfd5-ca6f3e7ad141"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000013"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("2388918f-6392-4384-b269-0a1469f5bf4c"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000014"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("7159012e-5f25-4881-b5e8-5300e382332e"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000015"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("cc4b2e82-ad42-4440-92ba-9b2100bb5f84"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000016"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("6b15c133-ea0e-473d-b58a-7f140e9f03bf"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000017"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("c2dd52a9-380f-479f-8e5e-6b56bfc4738f"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000018"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("bf5f6c7a-2fde-4a01-83f4-aacf91333c4c"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000019"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("162a0346-c4c0-4c4e-96e9-aa8154d25b39"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000020"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("5cc76807-25d1-45e5-9da3-cfd6d6f3ad7c"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000021"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("51a2653e-4b1a-4f6f-80c2-f533b26109ea"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000022"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("e2a382e2-d712-4a97-a7ec-cd6232d77a78"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000023"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("b60260b5-70b0-4f9e-9ab9-ba4abbbdfb02"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000024"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("a73fe794-383c-4157-89cf-227c4e487a6d"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000025"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("a9743e96-15b2-4c1d-b410-698cd6db0b7b"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000026"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("a94d92ab-66d8-4dd9-8ba3-c242eb450e68"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000027"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("9cb1a028-a98e-492a-aafc-70456fe46c96"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000028"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("5bc99a62-f5d7-48fe-b25c-518e4774614c"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000029"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("eceee678-5cee-4ee9-b16d-8d8d21fbb5ea"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000030"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("3a049ed7-75b7-4b1d-b3bb-402ca77c350f"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000031"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("aeaab85c-5b02-4bc2-b0e3-318c0f5b07d3"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000032"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("b79800e0-baa6-4add-9374-7d1e2ed650e8"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000033"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("0da6bf0b-26ff-46b9-bb8c-f9c641b37a89"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000034"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("ff84aeeb-93c0-465d-9a4a-8b686ba7bc75"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000035"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("921f8073-7a66-49bb-ae23-cf8fcde12c9c"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000036"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("bff05501-4988-4c8a-b61d-e32389e795a8"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000037"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("2c4f7432-e697-4d65-97df-069f8913a662"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000038"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("18d27746-3178-4c57-afd5-b75f0061637f"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000039"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("a5d1978b-c497-4314-857b-a7e3b8323a53"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000040"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("88300763-b8c3-4bd2-803b-df0e43bc691e"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000041"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("7aaad5a9-4497-429f-b3dc-9b59664e2e19"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000042"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("2db130c1-4b6f-4719-b3bb-7bd8a0662d75"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000043"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("be349d14-38b5-4a13-a152-9ea055de26b1"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000044"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("5292e02d-9a7d-4a18-be14-86c2f3351cac"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000045"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("97021474-1218-4d21-9115-4b9b2726e38b"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000046"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("56f68355-7b1b-49e4-829c-1a4d47fc93de"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000047"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("3017c8cb-f9e4-41c8-ac2e-f03d23fe944a"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000048"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("619a08e5-f6cb-4515-8b4d-a094d19277d8"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000049"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("cb8b4e96-7026-4485-8b52-3145b5289350"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000050"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("f0f57dfd-21a5-4a0b-a77e-e641f00300fa"));
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_StoreCreditMovements_FK_InvoiceId",
|
||||
table: "StoreCreditMovements",
|
||||
column: "FK_InvoiceId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_StoreCreditMovements_FK_PaymentId",
|
||||
table: "StoreCreditMovements",
|
||||
column: "FK_PaymentId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_StoreCreditMovements_FK_StoreId_OccurredAt",
|
||||
table: "StoreCreditMovements",
|
||||
columns: new[] { "FK_StoreId", "OccurredAt" });
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "StoreCreditMovements");
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000001"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("30f658ac-2c92-4cd8-ad47-10b0def2699c"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000002"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("42ab2c52-eaf8-4a17-b67a-ccd66569e56a"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000003"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("8afec825-27b3-4153-ad06-f8bcdbf93da8"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000004"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("4b427620-e99d-4068-a416-c3788d5bd9b5"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000005"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("361f44ef-1cd1-48a9-be4d-4c068aacef1e"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000006"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("11fc03ae-92ee-4336-ae57-29aac731ad51"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000007"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("0b1f2b73-ef60-4634-864f-22a6c0cd1373"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000008"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("2380abf3-2bcd-4a9f-b447-5f1685df2fc8"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000009"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("240e4d6a-8306-40ba-aaec-8e05cd0347db"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000010"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("cfde0aa6-e859-4f9c-8291-b0d886fcd4c9"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000011"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("dae772c8-26ec-4b5c-8d7a-f3196510bf2c"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000012"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("3512c0a4-a5ae-4002-a779-7d3cc600ab61"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000013"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("f9e2a348-159e-47a2-b7be-a60f0254873f"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000014"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("c8b4c181-5b08-45d2-818b-74bceeab78a8"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000015"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("8fb4ff4c-c5bd-4afd-a08e-02861d63cb7b"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000016"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("edb3b711-d623-439d-811e-afaef8d0680d"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000017"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("f33e4c62-d69f-4a91-8b62-acbd5351ea2a"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000018"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("c7c9d16c-e343-43e9-9e27-f38f903a5d00"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000019"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("f5f4760a-8a45-46ca-8093-743c95b0313f"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000020"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("c5434801-146a-4fd9-9c29-a1145e7f7f00"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000021"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("6c085135-78a5-4e65-a380-ee090b07e2bb"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000022"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("bbde2785-a3df-4f81-bad1-0bc5afe5de57"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000023"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("4421a0fb-bdd4-45a9-8dc0-d9b70f775e97"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000024"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("dba9dcb4-3b94-4ddc-89f3-552cfc109478"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000025"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("d8f74160-c150-4d9c-aba6-e45e628c684e"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000026"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("495fb68b-9f01-4538-9561-fefce0d39558"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000027"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("3bdece9b-a798-4223-a23c-9c9dd10d042d"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000028"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("6fa616eb-99fe-46b9-8305-908765d037fd"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000029"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("7a07148a-1327-4e97-befc-b2045eacad21"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000030"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("852e86d7-aa57-4a77-81c0-054f3fb4d7f7"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000031"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("3b767929-c973-41fb-a340-4ee5feb65619"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000032"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("63619bbd-0352-41bd-9e1c-a3f7908fb5d2"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000033"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("f6b4c8bf-a9b3-4179-9f57-a8584678c0d1"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000034"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("bb47b8ca-f7cf-40fe-8945-fc532a439b53"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000035"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("52126271-ae3b-4b58-a9d2-50e2dd894087"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000036"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("981e3358-85bb-47fe-a1f5-3a3a02cf47b1"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000037"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("749408da-fcee-458d-9144-8d8dee928ba0"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000038"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("c157857c-fb0e-41fd-a414-c0f57a44b120"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000039"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("3217cdbc-76c9-4505-b9f3-23de91a6212b"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000040"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("63526c52-18ba-40e0-9889-3bd3b7fc8161"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000041"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("260cf2fa-1ca6-4cd5-92ae-8c479a80e2f0"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000042"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("130ab09c-8a36-4705-8235-9d00f75dd684"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000043"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("41794774-e919-4f4e-a42a-5ae408d3407f"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000044"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("2ff23d04-d1f6-4aa3-a48e-1c82bb9ad12a"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000045"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("d36c81c0-8e9b-41ea-8e09-464a327a16d2"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000046"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("6c51dfc1-63e0-46a6-b2bb-27c21c708b51"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000047"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("076333d5-b1f3-4b89-86fd-5a61a4451c88"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000048"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("7e834f3e-1424-4aaa-816e-d62da6df672b"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000049"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("699fa06b-a7ff-4dbd-98a7-a29c2c361665"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000050"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("00df5735-54c6-4cdf-8e2c-a64cd90ede15"));
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,99 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Distribution.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddNotifications : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Notifications",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
|
||||
Type = table.Column<string>(type: "varchar(60)", maxLength: 60, nullable: false)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
Severity = table.Column<int>(type: "int", nullable: false),
|
||||
Title = table.Column<string>(type: "varchar(150)", maxLength: 150, nullable: false)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
Message = table.Column<string>(type: "varchar(500)", maxLength: 500, nullable: false)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
EntityType = table.Column<string>(type: "varchar(50)", maxLength: 50, nullable: false)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
EntityId = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
|
||||
Link = table.Column<string>(type: "varchar(200)", maxLength: 200, nullable: false)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
CreatedByUserId = table.Column<Guid>(type: "char(36)", nullable: true, 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_Notifications", x => x.Id);
|
||||
})
|
||||
.Annotation("MySql:CharSet", "utf8mb4");
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "NotificationRecipients",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
|
||||
FK_NotificationId = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
|
||||
FK_UserId = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
|
||||
IsRead = table.Column<bool>(type: "tinyint(1)", nullable: false),
|
||||
ReadAt = 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_NotificationRecipients", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_NotificationRecipients_Notifications_FK_NotificationId",
|
||||
column: x => x.FK_NotificationId,
|
||||
principalTable: "Notifications",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_NotificationRecipients_Users_FK_UserId",
|
||||
column: x => x.FK_UserId,
|
||||
principalTable: "Users",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
})
|
||||
.Annotation("MySql:CharSet", "utf8mb4");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_NotificationRecipients_FK_NotificationId_FK_UserId",
|
||||
table: "NotificationRecipients",
|
||||
columns: new[] { "FK_NotificationId", "FK_UserId" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_NotificationRecipients_FK_UserId_IsRead",
|
||||
table: "NotificationRecipients",
|
||||
columns: new[] { "FK_UserId", "IsRead" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Notifications_EntityType_EntityId",
|
||||
table: "Notifications",
|
||||
columns: new[] { "EntityType", "EntityId" });
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "NotificationRecipients");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Notifications");
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,81 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Distribution.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddInvoiceItems : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "InvoiceItems",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
|
||||
FK_InvoiceId = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
|
||||
FK_ShopPOItemId = 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<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_InvoiceItems", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_InvoiceItems_Invoices_FK_InvoiceId",
|
||||
column: x => x.FK_InvoiceId,
|
||||
principalTable: "Invoices",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
table.ForeignKey(
|
||||
name: "FK_InvoiceItems_Items_FK_ItemId",
|
||||
column: x => x.FK_ItemId,
|
||||
principalTable: "Items",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
table.ForeignKey(
|
||||
name: "FK_InvoiceItems_ShopPOItems_FK_ShopPOItemId",
|
||||
column: x => x.FK_ShopPOItemId,
|
||||
principalTable: "ShopPOItems",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
})
|
||||
.Annotation("MySql:CharSet", "utf8mb4");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_InvoiceItems_FK_InvoiceId",
|
||||
table: "InvoiceItems",
|
||||
column: "FK_InvoiceId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_InvoiceItems_FK_ItemId",
|
||||
table: "InvoiceItems",
|
||||
column: "FK_ItemId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_InvoiceItems_FK_ShopPOItemId",
|
||||
table: "InvoiceItems",
|
||||
column: "FK_ShopPOItemId");
|
||||
|
||||
// Backfill: snapshot each existing invoice's PO lines so older invoices can be edited/finalized.
|
||||
migrationBuilder.Sql(@"
|
||||
INSERT INTO InvoiceItems (Id, FK_InvoiceId, FK_ShopPOItemId, FK_ItemId, Quantity, UnitPrice, CreatedAt, IsActive)
|
||||
SELECT UUID(), inv.Id, poi.Id, poi.FK_ItemId, poi.Quantity, poi.UnitPrice, NOW(6), 1
|
||||
FROM Invoices inv
|
||||
JOIN ShopPOItems poi ON poi.FK_ShopPOId = inv.FK_ShopPOId AND poi.IsActive = 1;");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "InvoiceItems");
|
||||
}
|
||||
}
|
||||
}
|
||||
+6080
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user