using System.Net; using System.Text; using System.Text.Json; namespace MoneyMap.Mcp; public class MoneyMapApiClient { private readonly HttpClient _http; public MoneyMapApiClient(HttpClient http) => _http = http; public async Task HealthCheckAsync() { return await GetAsync("/api/health"); } // --- Transactions --- public async Task SearchTransactionsAsync( string? query, string? startDate, string? endDate, string? category, string? merchantName, decimal? minAmount, decimal? maxAmount, int? accountId, int? cardId, string? type, bool? uncategorizedOnly, int? limit) { var qs = BuildQueryString( ("query", query), ("startDate", startDate), ("endDate", endDate), ("category", category), ("merchantName", merchantName), ("minAmount", minAmount?.ToString()), ("maxAmount", maxAmount?.ToString()), ("accountId", accountId?.ToString()), ("cardId", cardId?.ToString()), ("type", type), ("uncategorizedOnly", uncategorizedOnly?.ToString()), ("limit", limit?.ToString())); return await GetAsync($"/api/transactions{qs}"); } public async Task GetTransactionAsync(long transactionId) { return await GetAsync($"/api/transactions/{transactionId}"); } public async Task UpdateTransactionCategoryAsync(long[] transactionIds, string category, string? merchantName) { var body = new { TransactionIds = transactionIds, Category = category, MerchantName = merchantName }; return await PutAsync($"/api/transactions/{transactionIds[0]}/category", body); } public async Task BulkRecategorizeAsync(string namePattern, string toCategory, string? fromCategory, string? merchantName, bool dryRun) { var body = new { NamePattern = namePattern, ToCategory = toCategory, FromCategory = fromCategory, MerchantName = merchantName, DryRun = dryRun }; return await PostAsync("/api/transactions/bulk-recategorize", body); } public async Task GetSpendingSummaryAsync(string startDate, string endDate, int? accountId) { var qs = BuildQueryString(("startDate", startDate), ("endDate", endDate), ("accountId", accountId?.ToString())); return await GetAsync($"/api/transactions/spending-summary{qs}"); } public async Task GetIncomeSummaryAsync(string startDate, string endDate, int? accountId) { var qs = BuildQueryString(("startDate", startDate), ("endDate", endDate), ("accountId", accountId?.ToString())); return await GetAsync($"/api/transactions/income-summary{qs}"); } // --- Budgets --- public async Task GetBudgetStatusAsync(string? asOfDate) { var qs = BuildQueryString(("asOfDate", asOfDate)); return await GetAsync($"/api/budgets/status{qs}"); } public async Task CreateBudgetAsync(string? category, decimal amount, string period, string startDate) { var body = new { Category = category, Amount = amount, Period = period, StartDate = startDate }; return await PostAsync("/api/budgets", body); } public async Task UpdateBudgetAsync(int budgetId, decimal? amount, string? period, bool? isActive) { var body = new { Amount = amount, Period = period, IsActive = isActive }; return await PutAsync($"/api/budgets/{budgetId}", body); } // --- Categories --- public async Task ListCategoriesAsync() { return await GetAsync("/api/categories"); } public async Task GetCategoryMappingsAsync(string? category) { var qs = BuildQueryString(("category", category)); return await GetAsync($"/api/categories/mappings{qs}"); } public async Task AddCategoryMappingAsync(string pattern, string category, string? merchantName, int priority) { var body = new { Pattern = pattern, Category = category, MerchantName = merchantName, Priority = priority }; return await PostAsync("/api/categories/mappings", body); } // --- Receipts --- public async Task ListReceiptsAsync(long? transactionId, string? parseStatus, int? limit) { var qs = BuildQueryString(("transactionId", transactionId?.ToString()), ("parseStatus", parseStatus), ("limit", limit?.ToString())); return await GetAsync($"/api/receipts{qs}"); } public async Task GetReceiptDetailsAsync(long receiptId) { return await GetAsync($"/api/receipts/{receiptId}"); } public async Task GetReceiptImageAsync(long receiptId) { return await GetAsync($"/api/receipts/{receiptId}/image"); } public async Task GetReceiptTextAsync(long receiptId) { return await GetAsync($"/api/receipts/{receiptId}/text"); } // --- Merchants --- public async Task ListMerchantsAsync(string? query) { var qs = BuildQueryString(("query", query)); return await GetAsync($"/api/merchants{qs}"); } public async Task MergeMerchantsAsync(int sourceMerchantId, int targetMerchantId) { var body = new { SourceMerchantId = sourceMerchantId, TargetMerchantId = targetMerchantId }; return await PostAsync("/api/merchants/merge", body); } // --- Accounts --- public async Task ListAccountsAsync() { return await GetAsync("/api/accounts"); } public async Task ListCardsAsync(int? accountId) { var qs = BuildQueryString(("accountId", accountId?.ToString())); return await GetAsync($"/api/accounts/cards{qs}"); } // --- Dashboard --- public async Task GetDashboardAsync(int? topCategoriesCount, int? recentTransactionsCount) { var qs = BuildQueryString(("topCategoriesCount", topCategoriesCount?.ToString()), ("recentTransactionsCount", recentTransactionsCount?.ToString())); return await GetAsync($"/api/dashboard{qs}"); } public async Task GetMonthlyTrendAsync(int? months, string? category) { var qs = BuildQueryString(("months", months?.ToString()), ("category", category)); return await GetAsync($"/api/dashboard/monthly-trend{qs}"); } // --- HTTP Helpers --- private async Task GetAsync(string path) { try { var response = await _http.GetAsync(path); if (response.IsSuccessStatusCode) return await response.Content.ReadAsStringAsync(); if (response.StatusCode == HttpStatusCode.NotFound) { var body = await response.Content.ReadAsStringAsync(); return body.Length > 0 ? body : "Not found"; } return $"API error: {(int)response.StatusCode} - {response.ReasonPhrase}"; } catch (HttpRequestException ex) { return $"MoneyMap API is not reachable at {_http.BaseAddress}. Ensure the web app is running. Error: {ex.Message}"; } } private async Task PostAsync(string path, object body) { try { var content = new StringContent(JsonSerializer.Serialize(body), Encoding.UTF8, "application/json"); var response = await _http.PostAsync(path, content); if (response.IsSuccessStatusCode) return await response.Content.ReadAsStringAsync(); if (response.StatusCode == HttpStatusCode.NotFound) { var responseBody = await response.Content.ReadAsStringAsync(); return responseBody.Length > 0 ? responseBody : "Not found"; } return $"API error: {(int)response.StatusCode} - {response.ReasonPhrase}"; } catch (HttpRequestException ex) { return $"MoneyMap API is not reachable at {_http.BaseAddress}. Ensure the web app is running. Error: {ex.Message}"; } } private async Task PutAsync(string path, object body) { try { var content = new StringContent(JsonSerializer.Serialize(body), Encoding.UTF8, "application/json"); var response = await _http.PutAsync(path, content); if (response.IsSuccessStatusCode) return await response.Content.ReadAsStringAsync(); if (response.StatusCode == HttpStatusCode.NotFound) { var responseBody = await response.Content.ReadAsStringAsync(); return responseBody.Length > 0 ? responseBody : "Not found"; } return $"API error: {(int)response.StatusCode} - {response.ReasonPhrase}"; } catch (HttpRequestException ex) { return $"MoneyMap API is not reachable at {_http.BaseAddress}. Ensure the web app is running. Error: {ex.Message}"; } } private static string BuildQueryString(params (string key, string? value)[] parameters) { var pairs = parameters .Where(p => !string.IsNullOrWhiteSpace(p.value)) .Select(p => $"{Uri.EscapeDataString(p.key)}={Uri.EscapeDataString(p.value!)}"); var qs = string.Join("&", pairs); return qs.Length > 0 ? $"?{qs}" : ""; } }