80a16d8b20
TaxBaik CI/CD / build-and-deploy (push) Successful in 1m5s
**API Controllers Complete:** - ClientController (GET /api/client paged, POST/PUT/DELETE) - TaxFilingController (GET upcoming, GET by client, POST/PUT/DELETE) - FaqController (GET active/all, GET by id, POST/PUT/DELETE) - AnnouncementController (GET active/all, GET by id, POST/PUT/DELETE) **Browser Clients Complete:** - IClientBrowserClient + ClientBrowserClient - ITaxFilingBrowserClient + TaxFilingBrowserClient - IFaqBrowserClient + FaqBrowserClient - IAnnouncementBrowserClient + AnnouncementBrowserClient **All Registered in Program.cs:** - BaseAddress: http://localhost:5001/taxbaik/api/ - TokenRefreshHandler attached to all clients - DI container: AddHttpClient<IXxxClient, XxxClient> **Blazor Refactored (Partial):** - ClientList.razor: ✅ IClientBrowserClient (service → API) - ClientEdit.razor: ✅ IClientBrowserClient (service → API) - TaxFilings Blazor: ⏳ Pending refactor - Faqs Blazor: ⏳ Pending refactor - Announcements Blazor: ⏳ Pending refactor **Phase 7 Status:** - API-First Foundation: ✅ 100% (all controllers + clients ready) - Blazor Refactoring: 🟡 30% (Clients done, others pending) - Phase 6 SignalR: ⏳ Deferred (ready for real-time on API-first pages) **SOLID Applied Throughout:** ✓ Single Responsibility: Each client handles one domain ✓ Open/Closed: Extend via interface, not modification ✓ Dependency Inversion: Blazor → Interfaces, not services ✓ Interface Segregation: Specialized clients per operation ✓ Liskov Substitution: All clients follow same contract **Build:** ✅ Success (0 errors, 2 warnings in Dashboard) **Pattern:** Established & repeatable for remaining Blazor pages Next: Blazor page migrations (TaxFilings, Faqs, Announcements) Then: Phase 6 SignalR for real-time notifications Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
111 lines
3.7 KiB
C#
111 lines
3.7 KiB
C#
namespace TaxBaik.Web.Services;
|
|
|
|
using System.Net.Http.Json;
|
|
using TaxBaik.Application.DTOs;
|
|
using TaxBaik.Domain.Entities;
|
|
|
|
public interface IAnnouncementBrowserClient
|
|
{
|
|
Task<IEnumerable<Announcement>> GetAllAsync(CancellationToken ct = default);
|
|
Task<Announcement?> GetByIdAsync(int id, CancellationToken ct = default);
|
|
Task<Announcement?> CreateAsync(AnnouncementDto dto, CancellationToken ct = default);
|
|
Task<Announcement?> UpdateAsync(int id, AnnouncementDto dto, CancellationToken ct = default);
|
|
Task<bool> DeleteAsync(int id, CancellationToken ct = default);
|
|
}
|
|
|
|
public class AnnouncementBrowserClient : IAnnouncementBrowserClient
|
|
{
|
|
private readonly HttpClient _http;
|
|
private readonly ILogger<AnnouncementBrowserClient> _logger;
|
|
|
|
public AnnouncementBrowserClient(HttpClient http, ILogger<AnnouncementBrowserClient> logger)
|
|
{
|
|
_http = http;
|
|
_logger = logger;
|
|
}
|
|
|
|
public async Task<IEnumerable<Announcement>> GetAllAsync(CancellationToken ct = default)
|
|
{
|
|
try
|
|
{
|
|
var result = await _http.GetFromJsonAsync<AnnouncementListResponse>("announcement", cancellationToken: ct);
|
|
return result?.Data ?? [];
|
|
}
|
|
catch (HttpRequestException ex)
|
|
{
|
|
_logger.LogError(ex, "Failed to fetch announcements");
|
|
throw;
|
|
}
|
|
}
|
|
|
|
public async Task<Announcement?> GetByIdAsync(int id, CancellationToken ct = default)
|
|
{
|
|
try
|
|
{
|
|
return await _http.GetFromJsonAsync<Announcement>($"announcement/{id}", cancellationToken: ct);
|
|
}
|
|
catch (HttpRequestException ex)
|
|
{
|
|
_logger.LogError(ex, "Failed to fetch announcement {AnnouncementId}", id);
|
|
throw;
|
|
}
|
|
}
|
|
|
|
public async Task<Announcement?> CreateAsync(AnnouncementDto dto, CancellationToken ct = default)
|
|
{
|
|
try
|
|
{
|
|
var response = await _http.PostAsJsonAsync("announcement", dto, cancellationToken: ct);
|
|
if (!response.IsSuccessStatusCode) return null;
|
|
|
|
var content = await response.Content.ReadAsStringAsync(ct);
|
|
return System.Text.Json.JsonSerializer.Deserialize<Announcement>(
|
|
content,
|
|
new System.Text.Json.JsonSerializerOptions { PropertyNameCaseInsensitive = true });
|
|
}
|
|
catch (HttpRequestException ex)
|
|
{
|
|
_logger.LogError(ex, "Failed to create announcement");
|
|
throw;
|
|
}
|
|
}
|
|
|
|
public async Task<Announcement?> UpdateAsync(int id, AnnouncementDto dto, CancellationToken ct = default)
|
|
{
|
|
try
|
|
{
|
|
var response = await _http.PutAsJsonAsync($"announcement/{id}", dto, cancellationToken: ct);
|
|
if (!response.IsSuccessStatusCode) return null;
|
|
|
|
var content = await response.Content.ReadAsStringAsync(ct);
|
|
return System.Text.Json.JsonSerializer.Deserialize<Announcement>(
|
|
content,
|
|
new System.Text.Json.JsonSerializerOptions { PropertyNameCaseInsensitive = true });
|
|
}
|
|
catch (HttpRequestException ex)
|
|
{
|
|
_logger.LogError(ex, "Failed to update announcement {AnnouncementId}", id);
|
|
throw;
|
|
}
|
|
}
|
|
|
|
public async Task<bool> DeleteAsync(int id, CancellationToken ct = default)
|
|
{
|
|
try
|
|
{
|
|
var response = await _http.DeleteAsync($"announcement/{id}", cancellationToken: ct);
|
|
return response.IsSuccessStatusCode;
|
|
}
|
|
catch (HttpRequestException ex)
|
|
{
|
|
_logger.LogError(ex, "Failed to delete announcement {AnnouncementId}", id);
|
|
throw;
|
|
}
|
|
}
|
|
|
|
private class AnnouncementListResponse
|
|
{
|
|
public List<Announcement> Data { get; set; } = [];
|
|
}
|
|
}
|