using System.Text; using System.Xml; using Books.Api.Saft.Models; namespace Books.Api.Saft.Services; /// /// Builds SAF-T DK compliant XML documents. /// Based on OECD SAF-T 2.0 with Danish customizations. /// public class SaftXmlBuilder { private const string SaftNamespace = "urn:StandardAuditFile-Taxation-Financial:DK"; /// /// Builds a SAF-T XML document from the provided data. /// public string Build(SaftDocument document) { var settings = new XmlWriterSettings { Indent = true, IndentChars = " ", Encoding = new UTF8Encoding(false), // UTF-8 without BOM OmitXmlDeclaration = false }; using var stream = new MemoryStream(); using (var writer = XmlWriter.Create(stream, settings)) { writer.WriteStartDocument(); writer.WriteStartElement("AuditFile", SaftNamespace); WriteHeader(writer, document.Header); WriteMasterFiles(writer, document.MasterFiles); WriteGeneralLedgerEntries(writer, document.Entries); writer.WriteEndElement(); // AuditFile writer.WriteEndDocument(); } return Encoding.UTF8.GetString(stream.ToArray()); } private static void WriteHeader(XmlWriter writer, SaftHeader header) { writer.WriteStartElement("Header"); writer.WriteElementString("AuditFileVersion", header.AuditFileVersion); writer.WriteElementString("AuditFileCountry", "DK"); writer.WriteElementString("AuditFileDateCreated", header.AuditFileDateCreated); writer.WriteElementString("SoftwareCompanyName", header.SoftwareCompanyName); writer.WriteElementString("SoftwareID", header.SoftwareID); writer.WriteElementString("SoftwareVersion", header.SoftwareVersion); WriteCompany(writer, header.Company); WriteSelectionCriteria(writer, header.SelectionCriteria); writer.WriteEndElement(); // Header } private static void WriteCompany(XmlWriter writer, SaftCompany company) { writer.WriteStartElement("Company"); writer.WriteElementString("RegistrationNumber", company.RegistrationNumber); writer.WriteElementString("Name", company.Name); WriteAddress(writer, company.Address); if (company.Contact != null) { WriteContact(writer, company.Contact); } // SAF-T DK requires TaxRegistrationNumber with "DK" prefix + CVR if (!string.IsNullOrEmpty(company.RegistrationNumber)) { writer.WriteElementString("TaxRegistrationNumber", "DK" + company.RegistrationNumber); } writer.WriteEndElement(); // Company } private static void WriteAddress(XmlWriter writer, SaftAddress address) { writer.WriteStartElement("Address"); if (!string.IsNullOrEmpty(address.StreetName)) writer.WriteElementString("StreetName", address.StreetName); if (!string.IsNullOrEmpty(address.City)) writer.WriteElementString("City", address.City); if (!string.IsNullOrEmpty(address.PostalCode)) writer.WriteElementString("PostalCode", address.PostalCode); writer.WriteElementString("Country", address.Country); writer.WriteEndElement(); // Address } private static void WriteContact(XmlWriter writer, SaftContact contact) { writer.WriteStartElement("Contact"); if (!string.IsNullOrEmpty(contact.Telephone)) writer.WriteElementString("Telephone", contact.Telephone); if (!string.IsNullOrEmpty(contact.Email)) writer.WriteElementString("Email", contact.Email); if (!string.IsNullOrEmpty(contact.Website)) writer.WriteElementString("Website", contact.Website); writer.WriteEndElement(); // Contact } private static void WriteSelectionCriteria(XmlWriter writer, SaftSelectionCriteria criteria) { writer.WriteStartElement("SelectionCriteria"); writer.WriteElementString("PeriodStart", criteria.PeriodStart); writer.WriteElementString("PeriodEnd", criteria.PeriodEnd); writer.WriteEndElement(); // SelectionCriteria } private static void WriteMasterFiles(XmlWriter writer, SaftMasterFiles masterFiles) { writer.WriteStartElement("MasterFiles"); WriteGeneralLedgerAccounts(writer, masterFiles.GeneralLedgerAccounts); WriteCustomers(writer, masterFiles.Customers); WriteSuppliers(writer, masterFiles.Suppliers); writer.WriteEndElement(); // MasterFiles } private static void WriteGeneralLedgerAccounts(XmlWriter writer, List accounts) { if (accounts.Count == 0) return; writer.WriteStartElement("GeneralLedgerAccounts"); // SAF-T DK 2027 requirement: StandardAccount metadata // Ref: BEK nr. 97 af 26/01/2023, Erhvervsstyrelsen writer.WriteElementString("NameOfStandardAccount", "Standardkontoplan"); writer.WriteElementString("VersionOfStandardAccount", "20230131"); foreach (var account in accounts) { writer.WriteStartElement("Account"); writer.WriteElementString("AccountID", account.AccountID); writer.WriteElementString("AccountDescription", account.AccountDescription); if (!string.IsNullOrEmpty(account.StandardAccountID)) writer.WriteElementString("StandardAccountID", account.StandardAccountID); writer.WriteElementString("AccountType", account.AccountType); // Opening balances if (account.OpeningDebitBalance != 0) writer.WriteElementString("OpeningDebitBalance", FormatDecimal(account.OpeningDebitBalance)); if (account.OpeningCreditBalance != 0) writer.WriteElementString("OpeningCreditBalance", FormatDecimal(account.OpeningCreditBalance)); // Closing balances if (account.ClosingDebitBalance != 0) writer.WriteElementString("ClosingDebitBalance", FormatDecimal(account.ClosingDebitBalance)); if (account.ClosingCreditBalance != 0) writer.WriteElementString("ClosingCreditBalance", FormatDecimal(account.ClosingCreditBalance)); writer.WriteEndElement(); // Account } writer.WriteEndElement(); // GeneralLedgerAccounts } private static void WriteCustomers(XmlWriter writer, List customers) { if (customers.Count == 0) return; writer.WriteStartElement("Customers"); foreach (var customer in customers) { writer.WriteStartElement("Customer"); writer.WriteElementString("CustomerID", customer.CustomerID); if (!string.IsNullOrEmpty(customer.AccountID)) writer.WriteElementString("AccountID", customer.AccountID); writer.WriteElementString("Name", customer.Name); if (!string.IsNullOrEmpty(customer.RegistrationNumber)) writer.WriteElementString("RegistrationNumber", customer.RegistrationNumber); if (customer.Address != null) WriteAddress(writer, customer.Address); if (customer.Contact != null) WriteContact(writer, customer.Contact); writer.WriteEndElement(); // Customer } writer.WriteEndElement(); // Customers } private static void WriteSuppliers(XmlWriter writer, List suppliers) { if (suppliers.Count == 0) return; writer.WriteStartElement("Suppliers"); foreach (var supplier in suppliers) { writer.WriteStartElement("Supplier"); writer.WriteElementString("SupplierID", supplier.SupplierID); if (!string.IsNullOrEmpty(supplier.AccountID)) writer.WriteElementString("AccountID", supplier.AccountID); writer.WriteElementString("Name", supplier.Name); if (!string.IsNullOrEmpty(supplier.RegistrationNumber)) writer.WriteElementString("RegistrationNumber", supplier.RegistrationNumber); if (supplier.Address != null) WriteAddress(writer, supplier.Address); if (supplier.Contact != null) WriteContact(writer, supplier.Contact); writer.WriteEndElement(); // Supplier } writer.WriteEndElement(); // Suppliers } private static void WriteGeneralLedgerEntries(XmlWriter writer, SaftGeneralLedgerEntries entries) { writer.WriteStartElement("GeneralLedgerEntries"); writer.WriteElementString("NumberOfEntries", entries.NumberOfEntries.ToString()); writer.WriteElementString("TotalDebit", FormatDecimal(entries.TotalDebit)); writer.WriteElementString("TotalCredit", FormatDecimal(entries.TotalCredit)); foreach (var journal in entries.Journals) { WriteJournal(writer, journal); } writer.WriteEndElement(); // GeneralLedgerEntries } private static void WriteJournal(XmlWriter writer, SaftJournal journal) { writer.WriteStartElement("Journal"); writer.WriteElementString("JournalID", journal.JournalID); writer.WriteElementString("Description", journal.Description); writer.WriteElementString("Type", journal.Type); foreach (var transaction in journal.Transactions) { WriteTransaction(writer, transaction); } writer.WriteEndElement(); // Journal } private static void WriteTransaction(XmlWriter writer, SaftTransaction transaction) { writer.WriteStartElement("Transaction"); writer.WriteElementString("TransactionID", transaction.TransactionID); writer.WriteElementString("Period", transaction.Period); writer.WriteElementString("TransactionDate", transaction.TransactionDate); writer.WriteElementString("Description", transaction.Description); writer.WriteElementString("SystemEntryDate", transaction.SystemEntryDate); writer.WriteElementString("GLPostingDate", transaction.GLPostingDate); foreach (var line in transaction.Lines) { WriteTransactionLine(writer, line); } writer.WriteEndElement(); // Transaction } private static void WriteTransactionLine(XmlWriter writer, SaftTransactionLine line) { writer.WriteStartElement("Line"); writer.WriteElementString("RecordID", line.RecordID); writer.WriteElementString("AccountID", line.AccountID); if (!string.IsNullOrEmpty(line.Description)) writer.WriteElementString("Description", line.Description); if (line.DebitAmount.HasValue && line.DebitAmount.Value != 0) writer.WriteElementString("DebitAmount", FormatDecimal(line.DebitAmount.Value)); if (line.CreditAmount.HasValue && line.CreditAmount.Value != 0) writer.WriteElementString("CreditAmount", FormatDecimal(line.CreditAmount.Value)); if (!string.IsNullOrEmpty(line.CustomerID)) writer.WriteElementString("CustomerID", line.CustomerID); if (!string.IsNullOrEmpty(line.SupplierID)) writer.WriteElementString("SupplierID", line.SupplierID); if (line.TaxInfo != null) { WriteTaxInformation(writer, line.TaxInfo); } writer.WriteEndElement(); // Line } private static void WriteTaxInformation(XmlWriter writer, SaftTaxInformation taxInfo) { writer.WriteStartElement("TaxInformation"); writer.WriteElementString("TaxCode", taxInfo.TaxCode); if (taxInfo.TaxPercentage.HasValue) writer.WriteElementString("TaxPercentage", FormatDecimal(taxInfo.TaxPercentage.Value)); if (taxInfo.TaxBase.HasValue) writer.WriteElementString("TaxBase", FormatDecimal(taxInfo.TaxBase.Value)); writer.WriteElementString("TaxAmount", FormatDecimal(taxInfo.TaxAmount)); writer.WriteEndElement(); // TaxInformation } private static string FormatDecimal(decimal value) { // SAF-T requires decimal format with dot as decimal separator return value.ToString("0.00", System.Globalization.CultureInfo.InvariantCulture); } }