5053245575
**Architecture Refactor (SOLID Principles):** - Implement AdminDashboardController (REST API) - Add dashboard summary endpoint - Add upcoming filings endpoint - Add recent inquiries endpoint - Add monthly statistics endpoint **Database Layer (Repository Pattern):** - Extend IInquiryRepository with date range queries - Implement CountByDateRangeAsync - Implement CountByStatusAndDateAsync - Extend InquiryRepository with new methods **Service Layer (Single Responsibility):** - Extend AdminDashboardService with API methods - Add GetRecentInquiriesAsync - Add GetMonthlyStatsAsync with caching **Test Coverage:** - Update FakeInquiryRepository mock with new methods **SOLID Application:** ✓ Single Responsibility: Each class has one reason to change ✓ Open/Closed: Dashboard API can be extended without modifying existing code ✓ Dependency Inversion: Service depends on Repository abstraction ✓ Interface Segregation: API endpoints are focused and specific Status: ✓ Compiles successfully (0 errors, 0 warnings) Next phases: - Add remaining API controllers (Announcement, Client, FAQ, TaxFiling) - Refactor Blazor components to use API instead of services - Implement JWT token refresh mechanism - Add SignalR for change notifications Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
101 lines
4.5 KiB
C#
101 lines
4.5 KiB
C#
namespace TaxBaik.Application.Tests;
|
|
|
|
using TaxBaik.Application.Services;
|
|
using TaxBaik.Domain.Entities;
|
|
using TaxBaik.Domain.Interfaces;
|
|
using Microsoft.Extensions.Caching.Memory;
|
|
using Xunit;
|
|
|
|
public class InquiryServiceTests
|
|
{
|
|
[Fact]
|
|
public async Task UpdateStatusAsync_WhenStatusIsInvalid_ThrowsValidationException()
|
|
{
|
|
var service = new InquiryService(new FakeInquiryRepository(), new FakeInquiryNotificationService(), new MemoryCache(new MemoryCacheOptions()));
|
|
|
|
await Assert.ThrowsAsync<ValidationException>(() => service.UpdateStatusAsync(1, "invalid"));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SubmitAsync_StoresEmailAndNewStatus()
|
|
{
|
|
var repository = new FakeInquiryRepository();
|
|
var service = new InquiryService(repository, new FakeInquiryNotificationService(), new MemoryCache(new MemoryCacheOptions()));
|
|
|
|
await service.SubmitAsync("홍길동", "010-1234-5678", "기장", "문의합니다.", "user@example.com");
|
|
|
|
Assert.Equal("user@example.com", repository.Inquiries.Single().Email);
|
|
Assert.Equal("new", repository.Inquiries.Single().Status);
|
|
}
|
|
|
|
private sealed class FakeInquiryRepository : IInquiryRepository
|
|
{
|
|
public List<Inquiry> Inquiries { get; } = [];
|
|
|
|
public Task<int> CreateAsync(Inquiry inquiry, CancellationToken cancellationToken = default)
|
|
{
|
|
inquiry.Id = Inquiries.Count + 1;
|
|
Inquiries.Add(inquiry);
|
|
return Task.FromResult(inquiry.Id);
|
|
}
|
|
|
|
public Task<Inquiry?> GetByIdAsync(int id, CancellationToken cancellationToken = default) =>
|
|
Task.FromResult(Inquiries.FirstOrDefault(x => x.Id == id));
|
|
|
|
public Task<(IEnumerable<Inquiry> Items, int Total)> GetPagedAsync(
|
|
int page, int pageSize, string? status = null, CancellationToken cancellationToken = default)
|
|
{
|
|
var items = status == null ? Inquiries : Inquiries.Where(x => x.Status == status).ToList();
|
|
return Task.FromResult<(IEnumerable<Inquiry>, int)>((items, items.Count()));
|
|
}
|
|
|
|
public Task<int> CountAsync(CancellationToken cancellationToken = default)
|
|
=> Task.FromResult(Inquiries.Count);
|
|
|
|
public Task<int> CountThisMonthAsync(CancellationToken cancellationToken = default)
|
|
=> Task.FromResult(Inquiries.Count);
|
|
|
|
public Task<int> CountByStatusAsync(string status, CancellationToken cancellationToken = default)
|
|
=> Task.FromResult(Inquiries.Count(x => x.Status == status));
|
|
|
|
public Task<int> CountByDateRangeAsync(DateTime startDate, DateTime endDate, CancellationToken cancellationToken = default)
|
|
=> Task.FromResult(Inquiries.Count(x => x.CreatedAt >= startDate && x.CreatedAt <= endDate));
|
|
|
|
public Task<int> CountByStatusAndDateAsync(string status, DateTime startDate, DateTime endDate, CancellationToken cancellationToken = default)
|
|
=> Task.FromResult(Inquiries.Count(x => x.Status == status && x.CreatedAt >= startDate && x.CreatedAt <= endDate));
|
|
|
|
public Task UpdateStatusAsync(int id, string status, CancellationToken cancellationToken = default)
|
|
{
|
|
var inquiry = Inquiries.FirstOrDefault(x => x.Id == id);
|
|
if (inquiry != null)
|
|
inquiry.Status = status;
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
public Task UpdateAdminMemoAsync(int id, string? adminMemo, CancellationToken cancellationToken = default)
|
|
{
|
|
var inquiry = Inquiries.FirstOrDefault(x => x.Id == id);
|
|
if (inquiry != null)
|
|
inquiry.AdminMemo = adminMemo;
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
public Task LinkClientAsync(int inquiryId, int clientId, CancellationToken cancellationToken = default)
|
|
{
|
|
var inquiry = Inquiries.FirstOrDefault(x => x.Id == inquiryId);
|
|
if (inquiry != null)
|
|
inquiry.ClientId = clientId;
|
|
return Task.CompletedTask;
|
|
}
|
|
}
|
|
|
|
private sealed class FakeInquiryNotificationService : IInquiryNotificationService
|
|
{
|
|
public Task NotifyCreatedAsync(int inquiryId, string name, string phone, string serviceType, string message, string? ipAddress, DateTime createdAtUtc, CancellationToken ct = default)
|
|
=> Task.CompletedTask;
|
|
|
|
public Task NotifyStatusChangedAsync(int inquiryId, string name, string phone, string serviceType, string previousStatus, string newStatus, string? changedBy = null, CancellationToken ct = default)
|
|
=> Task.CompletedTask;
|
|
}
|
|
}
|