books/backend/Books.Api/AiBookkeeper/ToonFormatConverter.cs
Nicolaj Hartmann 1f75c5d791 Add all backend domain, commands, repositories, and tests
This commit includes all previously untracked backend files:

Domain:
- Accounts, Attachments, BankConnections, Customers
- FiscalYears, Invoices, JournalEntryDrafts
- Orders, Products, UserAccess

Commands & Handlers:
- Full CQRS command structure for all domains

Repositories:
- PostgreSQL repositories for all read models
- Bank transaction and ledger repositories

GraphQL:
- Input types, scalars, and types for all entities
- Mutations and queries

Infrastructure:
- Banking integration (Enable Banking client)
- File storage, Invoicing, Reporting, SAF-T export
- Database migrations (003-029)

Tests:
- Integration tests for GraphQL endpoints
- Domain tests
- Invoicing and reporting tests

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-30 22:19:42 +01:00

115 lines
3.8 KiB
C#

using System.Text;
namespace Books.Api.AiBookkeeper;
/// <summary>
/// Converts ChartOfAccountsDto to .toon format for AI Bookkeeper.
/// The .toon format is a structured text format with meta and accounts sections.
/// </summary>
public static class ToonFormatConverter
{
/// <summary>
/// Convert a chart of accounts to .toon format string.
/// </summary>
public static string ConvertToToon(ChartOfAccountsDto chartOfAccounts)
{
var sb = new StringBuilder();
// Header comment
sb.AppendLine("# Chart of Accounts for AI Bookkeeper");
sb.AppendLine();
// Meta section
sb.AppendLine("meta:");
sb.AppendLine(" source: Books API");
sb.AppendLine($" organizationId: {chartOfAccounts.CompanyId}");
sb.AppendLine(" accountType: expense");
sb.AppendLine($" totalAccounts: {chartOfAccounts.Accounts.Count}");
sb.AppendLine();
// Accounts section
// Format: number,name,category,vatCode,region,vatRubric,suggestions
sb.AppendLine($"accounts[{chartOfAccounts.Accounts.Count}]{{number,name,category,vatCode,region,vatRubric,suggestions}}:");
foreach (var account in chartOfAccounts.Accounts)
{
var vatCode = MapVatCode(account.VatCodeId);
var region = DetermineRegion(account.VatCodeId);
var category = MapCategory(account.AccountType);
var suggestions = GenerateSuggestions(account.Name, account.AccountNumber);
// Format: number,name,category,vatCode,region,vatRubric,suggestions
sb.AppendLine($" {account.AccountNumber},{EscapeCommas(account.Name)},{category},{vatCode},{region},,{suggestions}");
}
return sb.ToString();
}
public static string MapVatCode(string? vatCodeId)
{
if (string.IsNullOrEmpty(vatCodeId))
return "";
return vatCodeId.ToUpperInvariant() switch
{
"I25" => "I25",
"U25" => "", // Output VAT not relevant for expense accounts
"INGEN" => "",
_ => vatCodeId
};
}
public static string DetermineRegion(string? vatCodeId)
{
if (string.IsNullOrEmpty(vatCodeId))
return "";
return vatCodeId.ToUpperInvariant() switch
{
"IEUV" => "EU", // EU goods
"IEUY" => "EU", // EU services
"IVV" => "WORLD", // World goods
"IVY" => "WORLD", // World services
_ => "" // Empty = available for all regions
};
}
public static string MapCategory(string accountType)
{
return accountType switch
{
"expense" => "Administrationsomkostninger",
"cogs" => "Variable omkostninger",
"personnel" => "Lønomkostninger",
"financial" => "Renteudgifter",
_ => "Øvrige omkostninger"
};
}
public static string GenerateSuggestions(string name, string accountNumber)
{
// Generate search keywords from account name
var suggestions = new List<string>();
// Add words from name (lowercase, no special chars)
var words = name.ToLowerInvariant()
.Replace(",", " ")
.Replace(".", " ")
.Replace("-", " ")
.Split(' ', StringSplitOptions.RemoveEmptyEntries)
.Where(w => w.Length > 2);
suggestions.AddRange(words);
// Add account number as suggestion
suggestions.Add(accountNumber);
return string.Join("|", suggestions.Distinct());
}
private static string EscapeCommas(string value)
{
// The .toon format uses commas as delimiters, so we need to handle commas in values
return value.Replace(",", " ");
}
}