Files
TakeoutSaaS.TenantApi/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfTenantInvoiceRepository.cs

216 lines
7.3 KiB
C#

using Microsoft.EntityFrameworkCore;
using TakeoutSaaS.Domain.Tenants.Entities;
using TakeoutSaaS.Domain.Tenants.Enums;
using TakeoutSaaS.Domain.Tenants.Repositories;
using TakeoutSaaS.Infrastructure.App.Persistence;
namespace TakeoutSaaS.Infrastructure.App.Repositories;
/// <summary>
/// 租户发票仓储 EF Core 实现。
/// </summary>
public sealed class EfTenantInvoiceRepository(TakeoutAppDbContext context) : ITenantInvoiceRepository
{
/// <inheritdoc />
public Task<TenantInvoiceSetting?> GetSettingAsync(long tenantId, CancellationToken cancellationToken = default)
{
return context.TenantInvoiceSettings
.Where(item => item.TenantId == tenantId)
.FirstOrDefaultAsync(cancellationToken);
}
/// <inheritdoc />
public Task AddSettingAsync(TenantInvoiceSetting entity, CancellationToken cancellationToken = default)
{
return context.TenantInvoiceSettings.AddAsync(entity, cancellationToken).AsTask();
}
/// <inheritdoc />
public Task UpdateSettingAsync(TenantInvoiceSetting entity, CancellationToken cancellationToken = default)
{
context.TenantInvoiceSettings.Update(entity);
return Task.CompletedTask;
}
/// <inheritdoc />
public async Task<(IReadOnlyList<TenantInvoiceRecord> Items, int TotalCount)> SearchRecordsAsync(
long tenantId,
DateTime? startUtc,
DateTime? endUtc,
TenantInvoiceStatus? status,
TenantInvoiceType? invoiceType,
string? keyword,
int page,
int pageSize,
CancellationToken cancellationToken = default)
{
var normalizedPage = Math.Max(1, page);
var normalizedPageSize = Math.Clamp(pageSize, 1, 500);
var query = BuildRecordQuery(tenantId, startUtc, endUtc, status, invoiceType, keyword);
var totalCount = await query.CountAsync(cancellationToken);
if (totalCount == 0)
{
return ([], 0);
}
var items = await query
.OrderByDescending(item => item.AppliedAt)
.ThenByDescending(item => item.Id)
.Skip((normalizedPage - 1) * normalizedPageSize)
.Take(normalizedPageSize)
.ToListAsync(cancellationToken);
return (items, totalCount);
}
/// <inheritdoc />
public async Task<TenantInvoiceRecordStatsSnapshot> GetStatsAsync(
long tenantId,
DateTime nowUtc,
CancellationToken cancellationToken = default)
{
var utcNow = NormalizeUtc(nowUtc);
var monthStart = new DateTime(utcNow.Year, utcNow.Month, 1, 0, 0, 0, DateTimeKind.Utc);
var summary = await context.TenantInvoiceRecords
.AsNoTracking()
.Where(item => item.TenantId == tenantId)
.GroupBy(_ => 1)
.Select(group => new
{
CurrentMonthIssuedAmount = group
.Where(item =>
item.Status == TenantInvoiceStatus.Issued &&
item.IssuedAt.HasValue &&
item.IssuedAt.Value >= monthStart &&
item.IssuedAt.Value <= utcNow)
.Sum(item => item.Amount),
CurrentMonthIssuedCount = group
.Count(item =>
item.Status == TenantInvoiceStatus.Issued &&
item.IssuedAt.HasValue &&
item.IssuedAt.Value >= monthStart &&
item.IssuedAt.Value <= utcNow),
PendingCount = group.Count(item => item.Status == TenantInvoiceStatus.Pending),
VoidedCount = group.Count(item => item.Status == TenantInvoiceStatus.Voided)
})
.FirstOrDefaultAsync(cancellationToken);
if (summary is null)
{
return new TenantInvoiceRecordStatsSnapshot();
}
return new TenantInvoiceRecordStatsSnapshot
{
CurrentMonthIssuedAmount = summary.CurrentMonthIssuedAmount,
CurrentMonthIssuedCount = summary.CurrentMonthIssuedCount,
PendingCount = summary.PendingCount,
VoidedCount = summary.VoidedCount
};
}
/// <inheritdoc />
public Task<TenantInvoiceRecord?> FindRecordByIdAsync(
long tenantId,
long recordId,
CancellationToken cancellationToken = default)
{
return context.TenantInvoiceRecords
.Where(item => item.TenantId == tenantId && item.Id == recordId)
.FirstOrDefaultAsync(cancellationToken);
}
/// <inheritdoc />
public Task<bool> ExistsInvoiceNoAsync(
long tenantId,
string invoiceNo,
CancellationToken cancellationToken = default)
{
return context.TenantInvoiceRecords
.AsNoTracking()
.AnyAsync(
item => item.TenantId == tenantId && item.InvoiceNo == invoiceNo,
cancellationToken);
}
/// <inheritdoc />
public Task AddRecordAsync(TenantInvoiceRecord entity, CancellationToken cancellationToken = default)
{
return context.TenantInvoiceRecords.AddAsync(entity, cancellationToken).AsTask();
}
/// <inheritdoc />
public Task UpdateRecordAsync(TenantInvoiceRecord entity, CancellationToken cancellationToken = default)
{
context.TenantInvoiceRecords.Update(entity);
return Task.CompletedTask;
}
/// <inheritdoc />
public Task SaveChangesAsync(CancellationToken cancellationToken = default)
{
return context.SaveChangesAsync(cancellationToken);
}
private IQueryable<TenantInvoiceRecord> BuildRecordQuery(
long tenantId,
DateTime? startUtc,
DateTime? endUtc,
TenantInvoiceStatus? status,
TenantInvoiceType? invoiceType,
string? keyword)
{
var query = context.TenantInvoiceRecords
.AsNoTracking()
.Where(item => item.TenantId == tenantId);
if (startUtc.HasValue)
{
var normalizedStart = NormalizeUtc(startUtc.Value);
query = query.Where(item => item.AppliedAt >= normalizedStart);
}
if (endUtc.HasValue)
{
var normalizedEnd = NormalizeUtc(endUtc.Value);
query = query.Where(item => item.AppliedAt <= normalizedEnd);
}
if (status.HasValue)
{
query = query.Where(item => item.Status == status.Value);
}
if (invoiceType.HasValue)
{
query = query.Where(item => item.InvoiceType == invoiceType.Value);
}
var normalizedKeyword = (keyword ?? string.Empty).Trim();
if (!string.IsNullOrWhiteSpace(normalizedKeyword))
{
var like = $"%{normalizedKeyword}%";
query = query.Where(item =>
EF.Functions.ILike(item.InvoiceNo, like) ||
EF.Functions.ILike(item.CompanyName, like) ||
EF.Functions.ILike(item.ApplicantName, like) ||
EF.Functions.ILike(item.OrderNo, like));
}
return query;
}
private static DateTime NormalizeUtc(DateTime value)
{
return value.Kind switch
{
DateTimeKind.Utc => value,
DateTimeKind.Local => value.ToUniversalTime(),
_ => DateTime.SpecifyKind(value, DateTimeKind.Utc)
};
}
}