feat(finance): 完成发票管理模块后端实现
This commit is contained in:
@@ -0,0 +1,215 @@
|
||||
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)
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user