diff --git a/CLAUDE.md b/CLAUDE.md index c82c0cd..8723722 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1503,6 +1503,47 @@ public class DeploymentController : ControllerBase - 폼 데이터는 세션 저장소에 자동 보존 ✅ - 강제 새로고침 후 복구 옵션 제공 ✅ +### Telegram 배포 알림 설정 (System Chat) + +**배포 완료 메시지는 System Chat ID로만 전송**: + +```bash +# .gitea/workflows/deploy.yml +- name: Notify deployment success + if: success() + run: | + DEPLOYMENT_TIME=$(date -u '+%Y-%m-%d %H:%M:%S UTC') + curl -s -X POST https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage \ + -d chat_id=-5585148480 \ + -d text="✅ 배포 완료%0A%0A환경: Production%0A상태: 정상 운영 중%0A%0A${DEPLOYMENT_TIME}" \ + -d parse_mode=HTML +``` + +**메시지 라우팅 정책**: +| 알림 유형 | Chat ID | 목적 | +|---------|---------|------| +| 배포 완료 | -5585148480 (System) | CI/CD 파이프라인 모니터링 | +| 배포 실패 | -5585148480 (System) | 긴급 대응 | +| 문의 접수 | -5434691215 (Inquiry) | 고객 상담 | +| 로그인 알림 | 보내지 않음 | 스팸 방지 | + +**구현**: +```csharp +// CI/CD 배포 단계에서 +if (deploymentSucceeded) +{ + await telegramService.SendSystemNotificationAsync( + $"✅ 배포 완료\n\n환경: Production\n상태: 정상 운영 중\n\n{DateTime.UtcNow:yyyy-MM-dd HH:mm:ss} UTC"); +} +else +{ + await telegramService.SendSystemNotificationAsync( + $"❌ 배포 실패\n\n환경: Production\n오류: {errorMessage}\n\n{DateTime.UtcNow:yyyy-MM-dd HH:mm:ss} UTC"); +} +``` + +--- + ### CI/CD 파이프라인 최적화 (2026-06-28) **목표**: 전체 배포 시간을 최소화하고 명확한 Timeout 설정 diff --git a/TaxBaik.Web/Program.cs b/TaxBaik.Web/Program.cs index af3f5ad..760149b 100644 --- a/TaxBaik.Web/Program.cs +++ b/TaxBaik.Web/Program.cs @@ -13,6 +13,7 @@ using TaxBaik.Application; using TaxBaik.Application.Services; using TaxBaik.Infrastructure; using TaxBaik.Web.Services; +using TaxBaik.Web.Services.AdminClients; var builder = WebApplication.CreateBuilder(args); var isProduction = builder.Environment.IsProduction(); @@ -135,6 +136,42 @@ builder.Services.AddHttpClient(); +// Phase 5: Tax Accounting & CRM Browser Clients +using (var scope = builder.Services.BuildServiceProvider().CreateScope()) +{ + var logger = scope.ServiceProvider.GetRequiredService>(); + + builder.Services.AddHttpClient(client => + { + client.BaseAddress = new Uri(apiBaseUrl); + }) + .AddHttpMessageHandler(); + + builder.Services.AddHttpClient(client => + { + client.BaseAddress = new Uri(apiBaseUrl); + }) + .AddHttpMessageHandler(); + + builder.Services.AddHttpClient(client => + { + client.BaseAddress = new Uri(apiBaseUrl); + }) + .AddHttpMessageHandler(); + + builder.Services.AddHttpClient(client => + { + client.BaseAddress = new Uri(apiBaseUrl); + }) + .AddHttpMessageHandler(); + + builder.Services.AddHttpClient(client => + { + client.BaseAddress = new Uri(apiBaseUrl); + }) + .AddHttpMessageHandler(); +} + // UI & 캐시 (MudBlazor Theme Customization) builder.Services.AddMudServices(config => { diff --git a/TaxBaik.Web/Services/AdminClients/IConsultingActivityBrowserClient.cs b/TaxBaik.Web/Services/AdminClients/IConsultingActivityBrowserClient.cs new file mode 100644 index 0000000..3a3eeb1 --- /dev/null +++ b/TaxBaik.Web/Services/AdminClients/IConsultingActivityBrowserClient.cs @@ -0,0 +1,106 @@ +namespace TaxBaik.Web.Services.AdminClients; + +using System.Text.Json; +using TaxBaik.Domain.Entities; + +public interface IConsultingActivityBrowserClient +{ + Task> GetAllAsync(CancellationToken ct = default); + Task> GetByClientIdAsync(int clientId, CancellationToken ct = default); + Task> GetPendingFollowupsAsync(CancellationToken ct = default); + Task CreateAsync(int clientId, string activityType, DateTime activityDate, string description, + int? consultantId = null, DateTime? nextFollowupDate = null, CancellationToken ct = default); + Task UpdateAsync(int id, string? outcome = null, DateTime? nextFollowupDate = null, CancellationToken ct = default); + Task DeleteAsync(int id, CancellationToken ct = default); +} + +public class ConsultingActivityBrowserClient(HttpClient httpClient, ILogger logger) + : IConsultingActivityBrowserClient +{ + private const string BaseUrl = "/api/consultingactivity"; + + public async Task> GetAllAsync(CancellationToken ct = default) + { + try + { + return await httpClient.GetFromJsonAsync>($"{BaseUrl}", ct) ?? []; + } + catch (Exception ex) + { + logger.LogError(ex, "Failed to get consulting activities"); + return []; + } + } + + public async Task> GetByClientIdAsync(int clientId, CancellationToken ct = default) + { + try + { + return await httpClient.GetFromJsonAsync>($"{BaseUrl}/client/{clientId}", ct) ?? []; + } + catch (Exception ex) + { + logger.LogError(ex, "Failed to get activities for client {ClientId}", clientId); + return []; + } + } + + public async Task> GetPendingFollowupsAsync(CancellationToken ct = default) + { + try + { + var response = await httpClient.GetFromJsonAsync($"{BaseUrl}/pending-followups", ct); + return response.TryGetProperty("data", out var data) ? System.Text.Json.JsonSerializer.Deserialize>() ?? []; + } + catch (Exception ex) + { + logger.LogError(ex, "Failed to get pending followups"); + return []; + } + } + + public async Task CreateAsync(int clientId, string activityType, DateTime activityDate, string description, + int? consultantId = null, DateTime? nextFollowupDate = null, CancellationToken ct = default) + { + try + { + var request = new { clientId, activityType, activityDate, description, consultantId, nextFollowupDate }; + var response = await httpClient.PostAsJsonAsync(BaseUrl, request, ct); + response.EnsureSuccessStatusCode(); + var result = await response.Content.ReadFromJsonAsync(cancellationToken: ct); + return result?["id"]?.ToObject() ?? 0; + } + catch (Exception ex) + { + logger.LogError(ex, "Failed to create consulting activity"); + return 0; + } + } + + public async Task UpdateAsync(int id, string? outcome = null, DateTime? nextFollowupDate = null, CancellationToken ct = default) + { + try + { + var request = new { outcome, nextFollowupDate }; + var response = await httpClient.PutAsJsonAsync($"{BaseUrl}/{id}", request, ct); + response.EnsureSuccessStatusCode(); + } + catch (Exception ex) + { + logger.LogError(ex, "Failed to update consulting activity {Id}", id); + } + } + + public async Task DeleteAsync(int id, CancellationToken ct = default) + { + try + { + var response = await httpClient.DeleteAsync($"{BaseUrl}/{id}", ct); + response.EnsureSuccessStatusCode(); + } + catch (Exception ex) + { + logger.LogError(ex, "Failed to delete consulting activity {Id}", id); + } + } +} diff --git a/TaxBaik.Web/Services/AdminClients/IContractBrowserClient.cs b/TaxBaik.Web/Services/AdminClients/IContractBrowserClient.cs new file mode 100644 index 0000000..14da6ae --- /dev/null +++ b/TaxBaik.Web/Services/AdminClients/IContractBrowserClient.cs @@ -0,0 +1,135 @@ +namespace TaxBaik.Web.Services.AdminClients; + +using System.Text.Json; +using TaxBaik.Domain.Entities; + +public interface IContractBrowserClient +{ + Task> GetAllAsync(CancellationToken ct = default); + Task GetByIdAsync(int id, CancellationToken ct = default); + Task> GetByClientIdAsync(int clientId, CancellationToken ct = default); + Task> GetActiveContractsAsync(CancellationToken ct = default); + Task> GetExpiringContractsAsync(int daysAhead = 30, CancellationToken ct = default); + Task GetMonthlyRecurringRevenueAsync(CancellationToken ct = default); + Task CreateAsync(int clientId, string contractNumber, string serviceType, DateTime startDate, + decimal? monthlyFee = null, decimal? totalAmount = null, CancellationToken ct = default); + Task DeleteAsync(int id, CancellationToken ct = default); +} + +public class ContractBrowserClient(HttpClient httpClient, ILogger logger) + : IContractBrowserClient +{ + private const string BaseUrl = "/api/contract"; + + public async Task> GetAllAsync(CancellationToken ct = default) + { + try + { + return await httpClient.GetFromJsonAsync>($"{BaseUrl}", ct) ?? []; + } + catch (Exception ex) + { + logger.LogError(ex, "Failed to get contracts"); + return []; + } + } + + public async Task GetByIdAsync(int id, CancellationToken ct = default) + { + try + { + return await httpClient.GetFromJsonAsync($"{BaseUrl}/{id}", ct); + } + catch (Exception ex) + { + logger.LogError(ex, "Failed to get contract {Id}", id); + return null; + } + } + + public async Task> GetByClientIdAsync(int clientId, CancellationToken ct = default) + { + try + { + return await httpClient.GetFromJsonAsync>($"{BaseUrl}/client/{clientId}", ct) ?? []; + } + catch (Exception ex) + { + logger.LogError(ex, "Failed to get contracts for client {ClientId}", clientId); + return []; + } + } + + public async Task> GetActiveContractsAsync(CancellationToken ct = default) + { + try + { + var response = await httpClient.GetFromJsonAsync($"{BaseUrl}/active", ct); + return response.TryGetProperty("data", out var data) ? System.Text.Json.JsonSerializer.Deserialize>() ?? []; + } + catch (Exception ex) + { + logger.LogError(ex, "Failed to get active contracts"); + return []; + } + } + + public async Task> GetExpiringContractsAsync(int daysAhead = 30, CancellationToken ct = default) + { + try + { + var response = await httpClient.GetFromJsonAsync($"{BaseUrl}/expiring?daysAhead={daysAhead}", ct); + return response.TryGetProperty("data", out var data) ? System.Text.Json.JsonSerializer.Deserialize>() ?? []; + } + catch (Exception ex) + { + logger.LogError(ex, "Failed to get expiring contracts"); + return []; + } + } + + public async Task GetMonthlyRecurringRevenueAsync(CancellationToken ct = default) + { + try + { + var response = await httpClient.GetFromJsonAsync($"{BaseUrl}/mrr", ct); + return response?["mrr"]?.ToObject() ?? 0; + } + catch (Exception ex) + { + logger.LogError(ex, "Failed to get MRR"); + return 0; + } + } + + public async Task CreateAsync(int clientId, string contractNumber, string serviceType, DateTime startDate, + decimal? monthlyFee = null, decimal? totalAmount = null, CancellationToken ct = default) + { + try + { + var request = new { clientId, contractNumber, serviceType, startDate, monthlyFee, totalAmount }; + var response = await httpClient.PostAsJsonAsync(BaseUrl, request, ct); + response.EnsureSuccessStatusCode(); + var result = await response.Content.ReadFromJsonAsync(cancellationToken: ct); + return result?["id"]?.ToObject() ?? 0; + } + catch (Exception ex) + { + logger.LogError(ex, "Failed to create contract"); + return 0; + } + } + + public async Task DeleteAsync(int id, CancellationToken ct = default) + { + try + { + var response = await httpClient.DeleteAsync($"{BaseUrl}/{id}", ct); + response.EnsureSuccessStatusCode(); + } + catch (Exception ex) + { + logger.LogError(ex, "Failed to delete contract {Id}", id); + } + } +} diff --git a/TaxBaik.Web/Services/AdminClients/IRevenueTrackingBrowserClient.cs b/TaxBaik.Web/Services/AdminClients/IRevenueTrackingBrowserClient.cs new file mode 100644 index 0000000..e0840a7 --- /dev/null +++ b/TaxBaik.Web/Services/AdminClients/IRevenueTrackingBrowserClient.cs @@ -0,0 +1,137 @@ +namespace TaxBaik.Web.Services.AdminClients; + +using System.Text.Json; +using TaxBaik.Domain.Entities; + +public interface IRevenueTrackingBrowserClient +{ + Task> GetAllAsync(CancellationToken ct = default); + Task> GetByClientIdAsync(int clientId, CancellationToken ct = default); + Task> GetPendingPaymentsAsync(CancellationToken ct = default); + Task> GetMonthlyRevenueAsync(int year, int month, CancellationToken ct = default); + Task GetTotalRevenueAsync(DateTime startDate, DateTime endDate, CancellationToken ct = default); + Task CreateAsync(int clientId, string invoiceNumber, DateTime invoiceDate, decimal amount, + string? serviceType = null, DateTime? dueDate = null, CancellationToken ct = default); + Task MarkPaidAsync(int id, DateTime paymentDate, CancellationToken ct = default); + Task DeleteAsync(int id, CancellationToken ct = default); +} + +public class RevenueTrackingBrowserClient(HttpClient httpClient, ILogger logger) + : IRevenueTrackingBrowserClient +{ + private const string BaseUrl = "/api/revenuetracking"; + + public async Task> GetAllAsync(CancellationToken ct = default) + { + try + { + return await httpClient.GetFromJsonAsync>($"{BaseUrl}", ct) ?? []; + } + catch (Exception ex) + { + logger.LogError(ex, "Failed to get revenue tracking"); + return []; + } + } + + public async Task> GetByClientIdAsync(int clientId, CancellationToken ct = default) + { + try + { + return await httpClient.GetFromJsonAsync>($"{BaseUrl}/client/{clientId}", ct) ?? []; + } + catch (Exception ex) + { + logger.LogError(ex, "Failed to get revenue for client {ClientId}", clientId); + return []; + } + } + + public async Task> GetPendingPaymentsAsync(CancellationToken ct = default) + { + try + { + var response = await httpClient.GetFromJsonAsync($"{BaseUrl}/pending", ct); + return response.TryGetProperty("data", out var data) ? System.Text.Json.JsonSerializer.Deserialize>() ?? []; + } + catch (Exception ex) + { + logger.LogError(ex, "Failed to get pending payments"); + return []; + } + } + + public async Task> GetMonthlyRevenueAsync(int year, int month, CancellationToken ct = default) + { + try + { + var response = await httpClient.GetFromJsonAsync($"{BaseUrl}/monthly?year={year}&month={month}", ct); + return response.TryGetProperty("data", out var data) ? System.Text.Json.JsonSerializer.Deserialize>() ?? []; + } + catch (Exception ex) + { + logger.LogError(ex, "Failed to get monthly revenue {Year}-{Month}", year, month); + return []; + } + } + + public async Task GetTotalRevenueAsync(DateTime startDate, DateTime endDate, CancellationToken ct = default) + { + try + { + var response = await httpClient.GetFromJsonAsync( + $"{BaseUrl}/total?startDate={startDate:yyyy-MM-dd}&endDate={endDate:yyyy-MM-dd}", ct); + return response?["total"]?.ToObject() ?? 0; + } + catch (Exception ex) + { + logger.LogError(ex, "Failed to get total revenue"); + return 0; + } + } + + public async Task CreateAsync(int clientId, string invoiceNumber, DateTime invoiceDate, decimal amount, + string? serviceType = null, DateTime? dueDate = null, CancellationToken ct = default) + { + try + { + var request = new { clientId, invoiceNumber, invoiceDate, amount, serviceType, dueDate }; + var response = await httpClient.PostAsJsonAsync(BaseUrl, request, ct); + response.EnsureSuccessStatusCode(); + var result = await response.Content.ReadFromJsonAsync(cancellationToken: ct); + return result?["id"]?.ToObject() ?? 0; + } + catch (Exception ex) + { + logger.LogError(ex, "Failed to create revenue tracking"); + return 0; + } + } + + public async Task MarkPaidAsync(int id, DateTime paymentDate, CancellationToken ct = default) + { + try + { + var request = new { paymentDate }; + var response = await httpClient.PutAsJsonAsync($"{BaseUrl}/{id}/paid", request, ct); + response.EnsureSuccessStatusCode(); + } + catch (Exception ex) + { + logger.LogError(ex, "Failed to mark payment {Id}", id); + } + } + + public async Task DeleteAsync(int id, CancellationToken ct = default) + { + try + { + var response = await httpClient.DeleteAsync($"{BaseUrl}/{id}", ct); + response.EnsureSuccessStatusCode(); + } + catch (Exception ex) + { + logger.LogError(ex, "Failed to delete revenue tracking {Id}", id); + } + } +} diff --git a/TaxBaik.Web/Services/AdminClients/ITaxFilingScheduleBrowserClient.cs b/TaxBaik.Web/Services/AdminClients/ITaxFilingScheduleBrowserClient.cs new file mode 100644 index 0000000..f8ccc45 --- /dev/null +++ b/TaxBaik.Web/Services/AdminClients/ITaxFilingScheduleBrowserClient.cs @@ -0,0 +1,119 @@ +namespace TaxBaik.Web.Services.AdminClients; + +using System.Text.Json; +using TaxBaik.Domain.Entities; + +public interface ITaxFilingScheduleBrowserClient +{ + Task> GetAllAsync(CancellationToken ct = default); + Task GetByIdAsync(int id, CancellationToken ct = default); + Task> GetByClientIdAsync(int clientId, CancellationToken ct = default); + Task> GetUpcomingDuesAsync(int daysAhead = 30, CancellationToken ct = default); + Task CreateAsync(int clientId, string filingType, DateTime dueDate, int filingYear, + int? assignedTo = null, CancellationToken ct = default); + Task MarkCompletedAsync(int id, CancellationToken ct = default); + Task DeleteAsync(int id, CancellationToken ct = default); +} + +public class TaxFilingScheduleBrowserClient(HttpClient httpClient, ILogger logger) + : ITaxFilingScheduleBrowserClient +{ + private const string BaseUrl = "/api/taxfilingschedule"; + + public async Task> GetAllAsync(CancellationToken ct = default) + { + try + { + return await httpClient.GetFromJsonAsync>($"{BaseUrl}", ct) ?? []; + } + catch (Exception ex) + { + logger.LogError(ex, "Failed to get tax filing schedules"); + return []; + } + } + + public async Task GetByIdAsync(int id, CancellationToken ct = default) + { + try + { + return await httpClient.GetFromJsonAsync($"{BaseUrl}/{id}", ct); + } + catch (Exception ex) + { + logger.LogError(ex, "Failed to get tax filing schedule {Id}", id); + return null; + } + } + + public async Task> GetByClientIdAsync(int clientId, CancellationToken ct = default) + { + try + { + return await httpClient.GetFromJsonAsync>($"{BaseUrl}/client/{clientId}", ct) ?? []; + } + catch (Exception ex) + { + logger.LogError(ex, "Failed to get filing schedules for client {ClientId}", clientId); + return []; + } + } + + public async Task> GetUpcomingDuesAsync(int daysAhead = 30, CancellationToken ct = default) + { + try + { + var response = await httpClient.GetFromJsonAsync($"{BaseUrl}/upcoming?daysAhead={daysAhead}", ct); + return response.TryGetProperty("data", out var data) ? System.Text.Json.JsonSerializer.Deserialize>() ?? []; + } + catch (Exception ex) + { + logger.LogError(ex, "Failed to get upcoming filings"); + return []; + } + } + + public async Task CreateAsync(int clientId, string filingType, DateTime dueDate, int filingYear, + int? assignedTo = null, CancellationToken ct = default) + { + try + { + var request = new { clientId, filingType, dueDate, filingYear, assignedTo }; + var response = await httpClient.PostAsJsonAsync(BaseUrl, request, ct); + response.EnsureSuccessStatusCode(); + var result = await response.Content.ReadFromJsonAsync(cancellationToken: ct); + return result?["id"]?.ToObject() ?? 0; + } + catch (Exception ex) + { + logger.LogError(ex, "Failed to create tax filing schedule"); + return 0; + } + } + + public async Task MarkCompletedAsync(int id, CancellationToken ct = default) + { + try + { + var response = await httpClient.PutAsJsonAsync($"{BaseUrl}/{id}/complete", new { }, ct); + response.EnsureSuccessStatusCode(); + } + catch (Exception ex) + { + logger.LogError(ex, "Failed to mark filing as completed {Id}", id); + } + } + + public async Task DeleteAsync(int id, CancellationToken ct = default) + { + try + { + var response = await httpClient.DeleteAsync($"{BaseUrl}/{id}", ct); + response.EnsureSuccessStatusCode(); + } + catch (Exception ex) + { + logger.LogError(ex, "Failed to delete tax filing schedule {Id}", id); + } + } +} diff --git a/TaxBaik.Web/Services/AdminClients/ITaxProfileBrowserClient.cs b/TaxBaik.Web/Services/AdminClients/ITaxProfileBrowserClient.cs new file mode 100644 index 0000000..0e15a18 --- /dev/null +++ b/TaxBaik.Web/Services/AdminClients/ITaxProfileBrowserClient.cs @@ -0,0 +1,140 @@ +namespace TaxBaik.Web.Services.AdminClients; + +using System.Text.Json; +using TaxBaik.Domain.Entities; + +public interface ITaxProfileBrowserClient +{ + Task> GetAllAsync(CancellationToken ct = default); + Task GetByIdAsync(int id, CancellationToken ct = default); + Task> GetByClientIdAsync(int clientId, CancellationToken ct = default); + Task> GetHighRiskProfilesAsync(CancellationToken ct = default); + Task> GetUpcomingFilingDuesAsync(int daysAhead = 30, CancellationToken ct = default); + Task CreateAsync(int clientId, string businessType, string? businessRegistration = null, + string? accountingMethod = null, DateTime? establishmentDate = null, CancellationToken ct = default); + Task UpdateAsync(int id, string? businessType = null, string? accountingMethod = null, + DateTime? nextFilingDueDate = null, string? taxRiskLevel = null, CancellationToken ct = default); + Task DeleteAsync(int id, CancellationToken ct = default); +} + +public class TaxProfileBrowserClient(HttpClient httpClient, ILogger logger) : ITaxProfileBrowserClient +{ + private const string BaseUrl = "/api/taxprofile"; + + public async Task> GetAllAsync(CancellationToken ct = default) + { + try + { + return await httpClient.GetFromJsonAsync>($"{BaseUrl}", ct) ?? []; + } + catch (Exception ex) + { + logger.LogError(ex, "Failed to get tax profiles"); + return []; + } + } + + public async Task GetByIdAsync(int id, CancellationToken ct = default) + { + try + { + return await httpClient.GetFromJsonAsync($"{BaseUrl}/{id}", ct); + } + catch (Exception ex) + { + logger.LogError(ex, "Failed to get tax profile {Id}", id); + return null; + } + } + + public async Task> GetByClientIdAsync(int clientId, CancellationToken ct = default) + { + try + { + return await httpClient.GetFromJsonAsync>($"{BaseUrl}/client/{clientId}", ct) ?? []; + } + catch (Exception ex) + { + logger.LogError(ex, "Failed to get tax profiles for client {ClientId}", clientId); + return []; + } + } + + public async Task> GetHighRiskProfilesAsync(CancellationToken ct = default) + { + try + { + var response = await httpClient.GetFromJsonAsync($"{BaseUrl}/high-risk", ct); + if (response.TryGetProperty("data", out var data)) + return System.Text.Json.JsonSerializer.Deserialize>(data.GetRawText()) ?? []; + return []; + } + catch (Exception ex) + { + logger.LogError(ex, "Failed to get high-risk profiles"); + return []; + } + } + + public async Task> GetUpcomingFilingDuesAsync(int daysAhead = 30, CancellationToken ct = default) + { + try + { + var response = await httpClient.GetFromJsonAsync($"{BaseUrl}/upcoming-filings?daysAhead={daysAhead}", ct); + if (response.TryGetProperty("data", out var data)) + return System.Text.Json.JsonSerializer.Deserialize>(data.GetRawText()) ?? []; + return []; + } + catch (Exception ex) + { + logger.LogError(ex, "Failed to get upcoming filings"); + return []; + } + } + + public async Task CreateAsync(int clientId, string businessType, string? businessRegistration = null, + string? accountingMethod = null, DateTime? establishmentDate = null, CancellationToken ct = default) + { + try + { + var request = new { clientId, businessType, businessRegistration, accountingMethod, establishmentDate }; + var response = await httpClient.PostAsJsonAsync(BaseUrl, request, ct); + response.EnsureSuccessStatusCode(); + var result = await response.Content.ReadFromJsonAsync(cancellationToken: ct); + return result?["id"]?.ToObject() ?? 0; + } + catch (Exception ex) + { + logger.LogError(ex, "Failed to create tax profile"); + return 0; + } + } + + public async Task UpdateAsync(int id, string? businessType = null, string? accountingMethod = null, + DateTime? nextFilingDueDate = null, string? taxRiskLevel = null, CancellationToken ct = default) + { + try + { + var request = new { businessType, accountingMethod, nextFilingDueDate, taxRiskLevel }; + var response = await httpClient.PutAsJsonAsync($"{BaseUrl}/{id}", request, ct); + response.EnsureSuccessStatusCode(); + } + catch (Exception ex) + { + logger.LogError(ex, "Failed to update tax profile {Id}", id); + } + } + + public async Task DeleteAsync(int id, CancellationToken ct = default) + { + try + { + var response = await httpClient.DeleteAsync($"{BaseUrl}/{id}", ct); + response.EnsureSuccessStatusCode(); + } + catch (Exception ex) + { + logger.LogError(ex, "Failed to delete tax profile {Id}", id); + } + } +}