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; /// /// 租户发票仓储 EF Core 实现。 /// public sealed class EfTenantInvoiceRepository(TakeoutAppDbContext context) : ITenantInvoiceRepository { /// public Task GetSettingAsync(long tenantId, CancellationToken cancellationToken = default) { return context.TenantInvoiceSettings .Where(item => item.TenantId == tenantId) .FirstOrDefaultAsync(cancellationToken); } /// public Task AddSettingAsync(TenantInvoiceSetting entity, CancellationToken cancellationToken = default) { return context.TenantInvoiceSettings.AddAsync(entity, cancellationToken).AsTask(); } /// public Task UpdateSettingAsync(TenantInvoiceSetting entity, CancellationToken cancellationToken = default) { context.TenantInvoiceSettings.Update(entity); return Task.CompletedTask; } /// public async Task<(IReadOnlyList 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); } /// public async Task 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 }; } /// public Task FindRecordByIdAsync( long tenantId, long recordId, CancellationToken cancellationToken = default) { return context.TenantInvoiceRecords .Where(item => item.TenantId == tenantId && item.Id == recordId) .FirstOrDefaultAsync(cancellationToken); } /// public Task ExistsInvoiceNoAsync( long tenantId, string invoiceNo, CancellationToken cancellationToken = default) { return context.TenantInvoiceRecords .AsNoTracking() .AnyAsync( item => item.TenantId == tenantId && item.InvoiceNo == invoiceNo, cancellationToken); } /// public Task AddRecordAsync(TenantInvoiceRecord entity, CancellationToken cancellationToken = default) { return context.TenantInvoiceRecords.AddAsync(entity, cancellationToken).AsTask(); } /// public Task UpdateRecordAsync(TenantInvoiceRecord entity, CancellationToken cancellationToken = default) { context.TenantInvoiceRecords.Update(entity); return Task.CompletedTask; } /// public Task SaveChangesAsync(CancellationToken cancellationToken = default) { return context.SaveChangesAsync(cancellationToken); } private IQueryable 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) }; } }