576 lines
21 KiB
C#
576 lines
21 KiB
C#
using AutoMapper;
|
|
using Koogle.Application.DTOs;
|
|
using Koogle.Application.Interfaces;
|
|
using Koogle.Application.Services;
|
|
using Koogle.Domain.Entities;
|
|
using Koogle.Domain.Enums;
|
|
using Koogle.Domain.Interfaces;
|
|
using KoogleApp.Data;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using Moq;
|
|
using FluentAssertions;
|
|
|
|
namespace Koogle.Tests.Integration;
|
|
|
|
/// <summary>
|
|
/// Integration tests for CashBook functionality with in-memory database.
|
|
/// </summary>
|
|
public class CashBookIntegrationTests : IAsyncLifetime
|
|
{
|
|
private AppDbContext _context = null!;
|
|
private CashBookService _cashBookService = null!;
|
|
private BookingCategoryService _categoryService = null!;
|
|
private Mock<ICurrentClubContext> _clubContextMock = null!;
|
|
private Guid _clubId;
|
|
private Club _club = null!;
|
|
|
|
public async Task InitializeAsync()
|
|
{
|
|
_clubId = Guid.NewGuid();
|
|
_clubContextMock = new Mock<ICurrentClubContext>();
|
|
_clubContextMock.Setup(c => c.ClubId).Returns(_clubId);
|
|
|
|
var options = new DbContextOptionsBuilder<AppDbContext>()
|
|
.UseInMemoryDatabase($"CashBookIntegration_{Guid.NewGuid()}")
|
|
.Options;
|
|
|
|
_context = new AppDbContext(options);
|
|
|
|
// Create club
|
|
_club = new Club
|
|
{
|
|
Id = _clubId,
|
|
Name = "Test Club",
|
|
LoginName = "testclub",
|
|
MonthlyMembershipFee = 25m,
|
|
CreatedAt = DateTime.UtcNow
|
|
};
|
|
await _context.Clubs.AddAsync(_club);
|
|
await _context.SaveChangesAsync();
|
|
|
|
// Setup services
|
|
var mapper = CreateMapper();
|
|
var factoryMock = new Mock<IDbContextFactory<AppDbContext>>();
|
|
factoryMock.Setup(f => f.CreateDbContextAsync(It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync(() => new AppDbContext(options));
|
|
|
|
var categoryRepository = new TestBookingCategoryRepository(_context);
|
|
var entryRepository = new TestCashBookEntryRepository(_context);
|
|
var personRepository = new TestPersonRepository(_context);
|
|
var clubRepository = new TestClubRepository(_context);
|
|
var personExpenseRepository = new TestPersonExpenseRepository(_context);
|
|
|
|
_categoryService = new BookingCategoryService(
|
|
categoryRepository,
|
|
_clubContextMock.Object,
|
|
mapper);
|
|
|
|
_cashBookService = new CashBookService(
|
|
entryRepository,
|
|
categoryRepository,
|
|
personExpenseRepository,
|
|
personRepository,
|
|
clubRepository,
|
|
_clubContextMock.Object,
|
|
factoryMock.Object,
|
|
mapper);
|
|
|
|
// Seed system categories
|
|
await _categoryService.EnsureSystemCategoriesAsync(_clubId);
|
|
}
|
|
|
|
public async Task DisposeAsync()
|
|
{
|
|
await _context.DisposeAsync();
|
|
}
|
|
|
|
private static IMapper CreateMapper()
|
|
{
|
|
var mapperMock = new Mock<IMapper>();
|
|
|
|
mapperMock.Setup(m => m.Map<BookingCategoryDto>(It.IsAny<BookingCategory>()))
|
|
.Returns((BookingCategory c) => new BookingCategoryDto
|
|
{
|
|
Id = c.Id,
|
|
Name = c.Name,
|
|
Description = c.Description,
|
|
CategoryType = c.CategoryType,
|
|
IsSystemCategory = c.IsSystemCategory,
|
|
Color = c.Color,
|
|
Icon = c.Icon,
|
|
IsActive = c.IsActive
|
|
});
|
|
|
|
mapperMock.Setup(m => m.Map<List<BookingCategoryDto>>(It.IsAny<List<BookingCategory>>()))
|
|
.Returns((List<BookingCategory> cats) => cats.Select(c => new BookingCategoryDto
|
|
{
|
|
Id = c.Id,
|
|
Name = c.Name,
|
|
Description = c.Description,
|
|
CategoryType = c.CategoryType,
|
|
IsSystemCategory = c.IsSystemCategory,
|
|
Color = c.Color,
|
|
Icon = c.Icon,
|
|
IsActive = c.IsActive
|
|
}).ToList());
|
|
|
|
mapperMock.Setup(m => m.Map<CashBookEntryDto>(It.IsAny<CashBookEntry>()))
|
|
.Returns((CashBookEntry e) => new CashBookEntryDto
|
|
{
|
|
Id = e.Id,
|
|
CategoryId = e.CategoryId,
|
|
CategoryName = e.Category?.Name ?? "",
|
|
EntryType = e.EntryType,
|
|
Amount = e.Amount,
|
|
BookingDate = e.BookingDate,
|
|
Comment = e.Comment,
|
|
PersonId = e.PersonId,
|
|
PersonName = e.Person?.Name,
|
|
CreatedAt = e.CreatedAt
|
|
});
|
|
|
|
mapperMock.Setup(m => m.Map<List<CashBookEntryDto>>(It.IsAny<List<CashBookEntry>>()))
|
|
.Returns((List<CashBookEntry> entries) => entries.Select(e => new CashBookEntryDto
|
|
{
|
|
Id = e.Id,
|
|
CategoryId = e.CategoryId,
|
|
CategoryName = e.Category?.Name ?? "",
|
|
EntryType = e.EntryType,
|
|
Amount = e.Amount,
|
|
BookingDate = e.BookingDate,
|
|
Comment = e.Comment,
|
|
PersonId = e.PersonId,
|
|
PersonName = e.Person?.Name,
|
|
CreatedAt = e.CreatedAt
|
|
}).ToList());
|
|
|
|
return mapperMock.Object;
|
|
}
|
|
|
|
#region Category Integration Tests
|
|
|
|
[Fact]
|
|
public async Task EnsureSystemCategories_CreatesAllSystemCategories()
|
|
{
|
|
// Act
|
|
var categories = await _categoryService.GetAllAsync();
|
|
|
|
// Assert
|
|
categories.Should().HaveCount(4);
|
|
categories.Should().Contain(c => c.Name == "Spielstrafe");
|
|
categories.Should().Contain(c => c.Name == "Mitgliedsbeitrag");
|
|
categories.Should().Contain(c => c.Name == "Korrekturbuchung");
|
|
categories.Should().Contain(c => c.Name == "Saldoanpassung");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CreateCategory_PersistsToDatabase()
|
|
{
|
|
// Arrange
|
|
var dto = new CreateBookingCategoryDto
|
|
{
|
|
Name = "Spenden",
|
|
Description = "Einnahmen aus Spenden",
|
|
CategoryType = BookingCategoryType.Income,
|
|
Color = "#00FF00",
|
|
Icon = "Favorite"
|
|
};
|
|
|
|
// Act
|
|
var result = await _categoryService.CreateAsync(dto);
|
|
|
|
// Assert
|
|
result.Should().NotBeNull();
|
|
result.Name.Should().Be("Spenden");
|
|
result.IsSystemCategory.Should().BeFalse();
|
|
|
|
// Verify in database
|
|
var dbCategory = await _context.BookingCategories.FindAsync(result.Id);
|
|
dbCategory.Should().NotBeNull();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task DeleteSystemCategory_ThrowsException()
|
|
{
|
|
// Arrange
|
|
var categories = await _categoryService.GetAllAsync();
|
|
var systemCategory = categories.First(c => c.IsSystemCategory);
|
|
|
|
// Act
|
|
var act = () => _categoryService.DeleteAsync(systemCategory.Id);
|
|
|
|
// Assert
|
|
await act.Should().ThrowAsync<InvalidOperationException>()
|
|
.WithMessage("System categories cannot be deleted.");
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region CashBook Entry Integration Tests
|
|
|
|
[Fact]
|
|
public async Task CreateEntry_PersistsToDatabase()
|
|
{
|
|
// Arrange
|
|
var categories = await _categoryService.GetAllAsync();
|
|
var category = categories.First();
|
|
|
|
var dto = new CreateCashBookEntryDto
|
|
{
|
|
CategoryId = category.Id,
|
|
EntryType = CashBookEntryType.Income,
|
|
Amount = 150m,
|
|
BookingDate = DateTime.Today,
|
|
Comment = "Test entry"
|
|
};
|
|
|
|
// Act
|
|
var result = await _cashBookService.CreateAsync(dto);
|
|
|
|
// Assert
|
|
result.Should().NotBeNull();
|
|
result.Amount.Should().Be(150m);
|
|
|
|
var dbEntry = await _context.CashBookEntries.FindAsync(result.Id);
|
|
dbEntry.Should().NotBeNull();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetBalance_CalculatesCorrectly()
|
|
{
|
|
// Arrange
|
|
var categories = await _categoryService.GetAllAsync();
|
|
var incomeCategory = categories.First(c => c.CategoryType == BookingCategoryType.Income);
|
|
|
|
// Add income entries
|
|
await _cashBookService.CreateAsync(new CreateCashBookEntryDto
|
|
{
|
|
CategoryId = incomeCategory.Id,
|
|
EntryType = CashBookEntryType.Income,
|
|
Amount = 100m,
|
|
BookingDate = DateTime.Today
|
|
});
|
|
await _cashBookService.CreateAsync(new CreateCashBookEntryDto
|
|
{
|
|
CategoryId = incomeCategory.Id,
|
|
EntryType = CashBookEntryType.Income,
|
|
Amount = 50m,
|
|
BookingDate = DateTime.Today
|
|
});
|
|
await _cashBookService.CreateAsync(new CreateCashBookEntryDto
|
|
{
|
|
CategoryId = incomeCategory.Id,
|
|
EntryType = CashBookEntryType.Expense,
|
|
Amount = 30m,
|
|
BookingDate = DateTime.Today
|
|
});
|
|
|
|
// Act
|
|
var balance = await _cashBookService.GetBalanceAsync();
|
|
|
|
// Assert - 100 + 50 - 30 = 120
|
|
balance.Should().Be(120m);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Report Integration Tests
|
|
|
|
[Fact]
|
|
public async Task GetReport_ReturnsCorrectSummary()
|
|
{
|
|
// Arrange
|
|
var categories = await _categoryService.GetAllAsync();
|
|
var incomeCategory = categories.First(c => c.Name == "Spielstrafe");
|
|
var from = DateTime.Today.AddDays(-7);
|
|
var to = DateTime.Today;
|
|
|
|
await _cashBookService.CreateAsync(new CreateCashBookEntryDto
|
|
{
|
|
CategoryId = incomeCategory.Id,
|
|
EntryType = CashBookEntryType.Income,
|
|
Amount = 200m,
|
|
BookingDate = DateTime.Today
|
|
});
|
|
|
|
// Act
|
|
var report = await _cashBookService.GetReportAsync(from, to);
|
|
|
|
// Assert
|
|
report.TotalIncome.Should().Be(200m);
|
|
report.TotalExpense.Should().Be(0m);
|
|
report.IncomeByCategory.Should().ContainSingle(c => c.CategoryName == "Spielstrafe");
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Membership Fee Integration Tests
|
|
|
|
[Fact]
|
|
public async Task CreateMembershipFees_CreatesEntriesForAllMembers()
|
|
{
|
|
// Arrange
|
|
var members = new[]
|
|
{
|
|
new Person { Id = Guid.NewGuid(), ClubId = _clubId, Name = "Max", PersonStatus = PersonStatus.Member, CreatedAt = DateTime.UtcNow },
|
|
new Person { Id = Guid.NewGuid(), ClubId = _clubId, Name = "Anna", PersonStatus = PersonStatus.Member, CreatedAt = DateTime.UtcNow },
|
|
new Person { Id = Guid.NewGuid(), ClubId = _clubId, Name = "Guest", PersonStatus = PersonStatus.Guest, CreatedAt = DateTime.UtcNow }
|
|
};
|
|
await _context.Persons.AddRangeAsync(members);
|
|
await _context.SaveChangesAsync();
|
|
|
|
var dto = new CreateMembershipFeesDto { Year = 2024, Month = 6 };
|
|
|
|
// Act
|
|
var result = await _cashBookService.CreateMembershipFeesAsync(dto);
|
|
|
|
// Assert - 2 members, not guest
|
|
result.Should().HaveCount(2);
|
|
result.Should().OnlyContain(e => e.Amount == 25m);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CreateMembershipFees_ThrowsOnDuplicate()
|
|
{
|
|
// Arrange
|
|
var member = new Person { Id = Guid.NewGuid(), ClubId = _clubId, Name = "Max", PersonStatus = PersonStatus.Member, CreatedAt = DateTime.UtcNow };
|
|
await _context.Persons.AddAsync(member);
|
|
await _context.SaveChangesAsync();
|
|
|
|
var dto = new CreateMembershipFeesDto { Year = 2024, Month = 7 };
|
|
await _cashBookService.CreateMembershipFeesAsync(dto);
|
|
|
|
// Act
|
|
var act = () => _cashBookService.CreateMembershipFeesAsync(dto);
|
|
|
|
// Assert
|
|
await act.Should().ThrowAsync<InvalidOperationException>();
|
|
}
|
|
|
|
#endregion
|
|
}
|
|
|
|
#region Simple Test Repositories
|
|
|
|
internal class TestBookingCategoryRepository : IBookingCategoryRepository
|
|
{
|
|
private readonly AppDbContext _context;
|
|
|
|
public TestBookingCategoryRepository(AppDbContext context) => _context = context;
|
|
|
|
public async Task<List<BookingCategory>> GetByClubIdAsync(Guid clubId, bool includeInactive = false, CancellationToken ct = default)
|
|
{
|
|
var query = _context.BookingCategories.Where(c => c.ClubId == clubId && !c.IsDeleted);
|
|
if (!includeInactive)
|
|
query = query.Where(c => c.IsActive);
|
|
return await query.ToListAsync(ct);
|
|
}
|
|
|
|
public async Task<BookingCategory?> GetByIdAsync(Guid id, CancellationToken ct = default)
|
|
=> await _context.BookingCategories.FirstOrDefaultAsync(c => c.Id == id && !c.IsDeleted, ct);
|
|
|
|
public async Task<BookingCategory?> GetSystemCategoryAsync(Guid clubId, string name, CancellationToken ct = default)
|
|
=> await _context.BookingCategories.FirstOrDefaultAsync(c => c.ClubId == clubId && c.Name == name && c.IsSystemCategory && !c.IsDeleted, ct);
|
|
|
|
public async Task<BookingCategory> AddAsync(BookingCategory entity, CancellationToken ct = default)
|
|
{
|
|
await _context.BookingCategories.AddAsync(entity, ct);
|
|
await _context.SaveChangesAsync(ct);
|
|
return entity;
|
|
}
|
|
|
|
public async Task<BookingCategory> UpdateAsync(BookingCategory entity, CancellationToken ct = default)
|
|
{
|
|
_context.BookingCategories.Update(entity);
|
|
await _context.SaveChangesAsync(ct);
|
|
return entity;
|
|
}
|
|
}
|
|
|
|
internal class TestCashBookEntryRepository : ICashBookEntryRepository
|
|
{
|
|
private readonly AppDbContext _context;
|
|
|
|
public TestCashBookEntryRepository(AppDbContext context) => _context = context;
|
|
|
|
public async Task<List<CashBookEntry>> GetByClubIdAsync(Guid clubId, DateTime? from, DateTime? to, CancellationToken ct = default)
|
|
{
|
|
var query = _context.CashBookEntries
|
|
.Include(e => e.Category)
|
|
.Include(e => e.Person)
|
|
.Where(e => e.ClubId == clubId && !e.IsDeleted);
|
|
|
|
if (from.HasValue) query = query.Where(e => e.BookingDate >= from.Value);
|
|
if (to.HasValue) query = query.Where(e => e.BookingDate <= to.Value);
|
|
|
|
return await query.OrderByDescending(e => e.BookingDate).ToListAsync(ct);
|
|
}
|
|
|
|
public async Task<CashBookEntry?> GetByIdAsync(Guid id, CancellationToken ct = default)
|
|
=> await _context.CashBookEntries.Include(e => e.Category).Include(e => e.Person).FirstOrDefaultAsync(e => e.Id == id && !e.IsDeleted, ct);
|
|
|
|
public async Task<decimal> GetBalanceAsync(Guid clubId, DateTime? asOfDate = null, CancellationToken ct = default)
|
|
{
|
|
var query = _context.CashBookEntries.Where(e => e.ClubId == clubId && !e.IsDeleted);
|
|
if (asOfDate.HasValue) query = query.Where(e => e.BookingDate <= asOfDate.Value);
|
|
|
|
var income = await query.Where(e => e.EntryType == CashBookEntryType.Income).SumAsync(e => e.Amount, ct);
|
|
var expense = await query.Where(e => e.EntryType == CashBookEntryType.Expense).SumAsync(e => e.Amount, ct);
|
|
return income - expense;
|
|
}
|
|
|
|
public async Task<CashBookEntry> AddAsync(CashBookEntry entity, CancellationToken ct = default)
|
|
{
|
|
await _context.CashBookEntries.AddAsync(entity, ct);
|
|
await _context.SaveChangesAsync(ct);
|
|
return entity;
|
|
}
|
|
|
|
public async Task<List<CashBookEntry>> GetByDayIdAsync(Guid dayId, CancellationToken ct = default)
|
|
=> await _context.CashBookEntries.Where(e => e.DayId == dayId && !e.IsDeleted).ToListAsync(ct);
|
|
|
|
public async Task<bool> ExistsMembershipFeeForMonthAsync(Guid clubId, int year, int month, CancellationToken ct = default)
|
|
{
|
|
var startOfMonth = new DateTime(year, month, 1);
|
|
var endOfMonth = startOfMonth.AddMonths(1).AddDays(-1);
|
|
return await _context.CashBookEntries
|
|
.Include(e => e.Category)
|
|
.AnyAsync(e => e.ClubId == clubId
|
|
&& e.Category.Name == "Mitgliedsbeitrag"
|
|
&& e.BookingDate >= startOfMonth
|
|
&& e.BookingDate <= endOfMonth
|
|
&& !e.IsDeleted, ct);
|
|
}
|
|
}
|
|
|
|
internal class TestPersonRepository : IPersonRepository
|
|
{
|
|
private readonly AppDbContext _context;
|
|
|
|
public TestPersonRepository(AppDbContext context) => _context = context;
|
|
|
|
public async Task<List<Person>> GetByClubIdAsync(Guid clubId, CancellationToken ct = default)
|
|
=> await _context.Persons.Where(p => p.ClubId == clubId && !p.IsDeleted).ToListAsync(ct);
|
|
|
|
public Task<Person?> GetByIdAsync(Guid id, CancellationToken ct = default)
|
|
=> _context.Persons.FirstOrDefaultAsync(p => p.Id == id && !p.IsDeleted, ct);
|
|
|
|
public async Task<Person> AddAsync(Person entity, CancellationToken ct = default)
|
|
{
|
|
await _context.Persons.AddAsync(entity, ct);
|
|
await _context.SaveChangesAsync(ct);
|
|
return entity;
|
|
}
|
|
|
|
public async Task<Person> UpdateAsync(Person entity, CancellationToken ct = default)
|
|
{
|
|
_context.Persons.Update(entity);
|
|
await _context.SaveChangesAsync(ct);
|
|
return entity;
|
|
}
|
|
|
|
public async Task<bool> DeleteAsync(Guid id, CancellationToken ct = default)
|
|
{
|
|
var person = await _context.Persons.FindAsync([id], ct);
|
|
if (person == null) return false;
|
|
person.IsDeleted = true;
|
|
await _context.SaveChangesAsync(ct);
|
|
return true;
|
|
}
|
|
|
|
public Task<Person?> GetByNameAsync(Guid clubId, string personName, Guid? excludeGuid, CancellationToken ct = default)
|
|
=> _context.Persons.FirstOrDefaultAsync(p => p.ClubId == clubId && p.Name == personName && !p.IsDeleted && (excludeGuid == null || p.Id != excludeGuid), ct);
|
|
}
|
|
|
|
internal class TestClubRepository : IClubRepository
|
|
{
|
|
private readonly AppDbContext _context;
|
|
|
|
public TestClubRepository(AppDbContext context) => _context = context;
|
|
|
|
public Task<List<Club>> GetAllAsync(CancellationToken ct = default)
|
|
=> _context.Clubs.Where(c => !c.IsDeleted).ToListAsync(ct);
|
|
|
|
public Task<Club?> GetByIdAsync(Guid id, CancellationToken ct = default)
|
|
=> _context.Clubs.FirstOrDefaultAsync(c => c.Id == id && !c.IsDeleted, ct);
|
|
|
|
public async Task<Club> AddAsync(Club entity, CancellationToken ct = default)
|
|
{
|
|
await _context.Clubs.AddAsync(entity, ct);
|
|
await _context.SaveChangesAsync(ct);
|
|
return entity;
|
|
}
|
|
|
|
public async Task<Club> UpdateAsync(Club entity, CancellationToken ct = default)
|
|
{
|
|
_context.Clubs.Update(entity);
|
|
await _context.SaveChangesAsync(ct);
|
|
return entity;
|
|
}
|
|
|
|
public async Task<bool> DeleteAsync(Guid id, CancellationToken ct = default)
|
|
{
|
|
var club = await _context.Clubs.FindAsync([id], ct);
|
|
if (club == null) return false;
|
|
club.IsDeleted = true;
|
|
await _context.SaveChangesAsync(ct);
|
|
return true;
|
|
}
|
|
|
|
public Task<Club?> GetByNameAsync(string clubName, Guid? excludeGuid, CancellationToken ct = default)
|
|
=> _context.Clubs.FirstOrDefaultAsync(c => c.Name == clubName && !c.IsDeleted && (excludeGuid == null || c.Id != excludeGuid), ct);
|
|
|
|
public Task<Club?> GetByLoginNameAsync(string clubLoginName, Guid? excludeGuid, CancellationToken ct = default)
|
|
=> _context.Clubs.FirstOrDefaultAsync(c => c.LoginName == clubLoginName && !c.IsDeleted && (excludeGuid == null || c.Id != excludeGuid), ct);
|
|
}
|
|
|
|
internal class TestPersonExpenseRepository : IPersonExpenseRepository
|
|
{
|
|
private readonly AppDbContext _context;
|
|
|
|
public TestPersonExpenseRepository(AppDbContext context) => _context = context;
|
|
|
|
public Task<List<PersonExpense>> GetByClubIdAsync(Guid clubId, CancellationToken ct = default)
|
|
=> _context.PersonExpenses.Where(pe => pe.ClubId == clubId && !pe.IsDeleted).ToListAsync(ct);
|
|
|
|
public Task<PersonExpense?> GetByIdAsync(Guid id, CancellationToken ct = default)
|
|
=> _context.PersonExpenses.FirstOrDefaultAsync(pe => pe.Id == id && !pe.IsDeleted, ct);
|
|
|
|
public Task<List<PersonExpense>> GetByDayIdAsync(Guid dayId, CancellationToken ct = default)
|
|
=> _context.PersonExpenses.Where(pe => pe.DayId == dayId && !pe.IsDeleted).ToListAsync(ct);
|
|
|
|
public Task<List<PersonExpense>> GetByPersonIdAsync(Guid personId, CancellationToken ct = default)
|
|
=> _context.PersonExpenses.Where(pe => pe.PersonId == personId && !pe.IsDeleted).ToListAsync(ct);
|
|
|
|
public async Task<PersonExpense> AddAsync(PersonExpense entity, CancellationToken ct = default)
|
|
{
|
|
await _context.PersonExpenses.AddAsync(entity, ct);
|
|
await _context.SaveChangesAsync(ct);
|
|
return entity;
|
|
}
|
|
|
|
public async Task<PersonExpense> UpdateAsync(PersonExpense entity, CancellationToken ct = default)
|
|
{
|
|
_context.PersonExpenses.Update(entity);
|
|
await _context.SaveChangesAsync(ct);
|
|
return entity;
|
|
}
|
|
|
|
public async Task<bool> DeleteAsync(Guid id, CancellationToken ct = default)
|
|
{
|
|
var pe = await _context.PersonExpenses.FindAsync([id], ct);
|
|
if (pe == null) return false;
|
|
pe.IsDeleted = true;
|
|
await _context.SaveChangesAsync(ct);
|
|
return true;
|
|
}
|
|
|
|
public async Task AddRangeAsync(IEnumerable<PersonExpense> entities, CancellationToken ct = default)
|
|
{
|
|
await _context.PersonExpenses.AddRangeAsync(entities, ct);
|
|
await _context.SaveChangesAsync(ct);
|
|
}
|
|
}
|
|
|
|
#endregion
|