Codebasis als Dateien ins Arbeitsrepo statt als Gitlink

QuellCode/CentronERP war nur als Gitlink (Submodul-Referenz auf 79c1142)
getrackt, ohne .gitmodules und ohne erreichbares Remote. Der
Untersuchungsgegenstand der Versuchsreihe war damit nicht reproduzierbar
gesichert: Ein Klon haette ein leeres Verzeichnis erhalten, und die Belege
der 3.287 Anforderungen waeren nicht ueberpruefbar gewesen.

Umstellung:
- Historie nach c:\DEV\CentronERP_git_snapshot_79c1142 ausgelagert
  (vollstaendig lesbar, enthaelt 79c1142 und Vorgaenger 89ccfd6)
- Gitlink aus dem Index entfernt
- Dateiinhalt aufgenommen: 24.557 Dateien, rund 333 MB

Die verschachtelte .gitignore der Codebasis gilt weiter, Build-Artefakte
bleiben ausgeschlossen. Details in Versuche/Versuch_01/_Codebasis-Nachweis.md
This commit is contained in:
Christoph Schwörer
2026-08-26 07:43:51 +02:00
parent 18edae75b6
commit f045b99a25
24664 changed files with 5846716 additions and 1 deletions
@@ -0,0 +1,112 @@
using System;
using System.Collections.Generic;
using System.Xml.Linq;
using Centron.Data.Entities.DataExchange.BookKeeping.Export;
using Centron.Gateway.DataExchange.BookKeeping.Abacus;
using Centron.Interfaces.DataExchange.BookKeeping;
using NSubstitute;
namespace Centron.Tests.BL.DataExchange.BookKeeping.Abacus;
public class BookKeepingExportAbacusTest
{
[Theory]
[InlineData("2024.00")]
[InlineData("2023.00")]
[InlineData("2020.00")]
public void SupplierReceiptExport_DoesNotWriteTaxTransferTypeForModernKreditorVersions(string kreditorReceiptVersion)
{
var result = CreateExport(kreditorReceiptVersion);
var document = XDocument.Parse((string)result.File);
Assert.Empty(document.Descendants("TaxTransferType"));
Assert.Equal("KRED", document.Root?.Element("Task")?.Element("Parameter")?.Element("Application")?.Value);
Assert.Equal("Document", document.Root?.Element("Task")?.Element("Parameter")?.Element("Id")?.Value);
Assert.Equal(kreditorReceiptVersion, document.Root?.Element("Task")?.Element("Parameter")?.Element("Version")?.Value);
}
[Theory]
[InlineData("2018.00")]
[InlineData("2015.00")]
public void SupplierReceiptExport_WritesTaxTransferTypeForLegacyKreditorVersions(string kreditorReceiptVersion)
{
var result = CreateExport(kreditorReceiptVersion);
var document = XDocument.Parse((string)result.File);
var taxTransferType = Assert.Single(document.Descendants("TaxTransferType"));
Assert.Equal("0", taxTransferType.Value);
Assert.Equal(kreditorReceiptVersion, document.Root?.Element("Task")?.Element("Parameter")?.Element("Version")?.Value);
}
private static IBookKeepingReceiptExportFileGeneratorResult CreateExport(string kreditorReceiptVersion)
{
var sut = new BookKeepingExportAbacus();
var result = sut.GetReceiptsBookingData(
new[] { CreateSupplierReceipt() },
CreateSettings(kreditorReceiptVersion),
CreateUserSettings(),
customInterfaceSettings: null,
customInterfaceColumns: null);
Assert.True(result.IsSuccessful, result.Message);
Assert.Empty(result.FailedList);
return result;
}
private static BookKeepingExportConfiguration CreateSettings(string kreditorReceiptVersion)
{
return new BookKeepingExportConfiguration
{
KreditorReceiptVersion = kreditorReceiptVersion,
ReceiptsExportAllPositions = true
};
}
private static BookKeepingExportUserSettings CreateUserSettings()
{
return new BookKeepingExportUserSettings
{
ExportDateFrom = new DateTime(2026, 1, 1),
ExportDateTo = new DateTime(2026, 1, 31)
};
}
private static IBookKeepingReceipt CreateSupplierReceipt()
{
var receipt = Substitute.For<IBookKeepingReceipt>();
receipt.Number.Returns(4711);
receipt.Date.Returns(new DateTime(2026, 1, 15));
receipt.ExternalReceiptDate.Returns(new DateTime(2026, 1, 14));
receipt.ExternalReceiptNumber.Returns("EXT-4711");
receipt.Type.Returns(BookKeepingReceiptKind.SupplierInvoice);
receipt.AddressBookKeepingNumber.Returns("70001");
receipt.CurrencyISOCode.Returns("CHF");
receipt.PaymentTypeNumber.Returns("30");
receipt.BankNumber.Returns(1);
receipt.NetPriceComplete.Returns(100m);
receipt.TaxPriceComplete.Returns(8.1m);
receipt.NetPriceFCComplete.Returns(100m);
receipt.TaxPriceFCComplete.Returns(8.1m);
var item = CreateReceiptItem();
receipt.Items.Returns(new List<IBookKeepingReceiptItem> { item });
return receipt;
}
private static IBookKeepingReceiptItem CreateReceiptItem()
{
var item = Substitute.For<IBookKeepingReceiptItem>();
item.Position.Returns(1);
item.ProfitAndLossAccount.Returns(4000);
item.NetPriceTotalComplete.Returns(100m);
item.TaxPriceTotalComplete.Returns(8.1m);
item.NetPriceTotalFCComplete.Returns(100m);
item.TaxPriceTotalFCComplete.Returns(8.1m);
item.TaxRate.Returns(8.1m);
item.VATCode.Returns("81");
item.Text.Returns("Test position");
return item;
}
}
@@ -0,0 +1,432 @@
using System;
using System.Collections.Generic;
using System.Reflection;
using Centron.Data.Entities.DataExchange.BookKeeping.Export;
using Centron.Gateway.DataExchange.BookKeeping.DatevXMLOnline2020;
using Centron.Interfaces.BL;
using Centron.Interfaces.DataExchange.BookKeeping;
using CentronSoftware.Centron.WebServices.Entities.DataExchange.Exports;
using NSubstitute;
namespace Centron.Tests.BL.DataExchange.BookKeeping.DatevXMLOnline2020;
/// <summary>
/// Verifies that the new DATEV-Belegtransfer ApplicationSetting
/// <c>UseAccountFromDifferentInvoiceAddress</c> (Ticket 160066) is correctly honoured
/// in <see cref="BookKeepingExportDatevXmlOnline_2020.CreateReceivableLedger"/> and
/// has no effect on <see cref="BookKeepingExportDatevXmlOnline_2020.CreatePayableLedger"/>.
/// </summary>
public class BookKeepingExportDatevXmlOnline_2020Test
{
private const string CustomerAccount = "10000";
private const string AlternateInvoiceAccount = "20000";
private const string CustomerNumberAtSupplier = "ZZZ-99";
private const string CustomerName = "Customer GmbH";
private const string AlternateInvoiceName = "Alt Recipient AG";
private const string CustomerCity = "Berlin";
private const string AlternateInvoiceCity = "Hamburg";
// ---------- CreateReceivableLedger: gating of partyId ----------
[Fact]
public void Receivable_FlagTrue_AltSet_UsesAlternateAccount()
{
var sut = new BookKeepingExportDatevXmlOnline_2020();
var receipt = CreateCustomerReceipt(addressBookKeepingNumber: CustomerAccount,
altInvoiceAccount: AlternateInvoiceAccount);
var settings = CreateSettings(useAccountFromDifferentInvoiceAddress: true);
var result = sut.CreateReceivableLedger(CreateSplit(), splitCostCentre: null, receipt, settings);
Assert.True(result.IsSuccess);
Assert.Equal(AlternateInvoiceAccount, result.Data.partyId);
}
[Fact]
public void Receivable_FlagFalse_AltSet_UsesCustomerAccount()
{
var sut = new BookKeepingExportDatevXmlOnline_2020();
var receipt = CreateCustomerReceipt(addressBookKeepingNumber: CustomerAccount,
altInvoiceAccount: AlternateInvoiceAccount);
var settings = CreateSettings(useAccountFromDifferentInvoiceAddress: false);
var result = sut.CreateReceivableLedger(CreateSplit(), splitCostCentre: null, receipt, settings);
Assert.True(result.IsSuccess);
Assert.Equal(CustomerAccount, result.Data.partyId);
}
[Fact]
public void Receivable_FlagTrue_AltEmpty_FallsBackToCustomerAccount()
{
var sut = new BookKeepingExportDatevXmlOnline_2020();
var receipt = CreateCustomerReceipt(addressBookKeepingNumber: CustomerAccount,
altInvoiceAccount: string.Empty);
var settings = CreateSettings(useAccountFromDifferentInvoiceAddress: true);
var result = sut.CreateReceivableLedger(CreateSplit(), splitCostCentre: null, receipt, settings);
Assert.True(result.IsSuccess);
Assert.Equal(CustomerAccount, result.Data.partyId);
}
[Fact]
public void Receivable_FlagTrue_AltWhitespaceOnly_FallsBackToCustomerAccount()
{
var sut = new BookKeepingExportDatevXmlOnline_2020();
var receipt = CreateCustomerReceipt(addressBookKeepingNumber: CustomerAccount,
altInvoiceAccount: " ");
var settings = CreateSettings(useAccountFromDifferentInvoiceAddress: true);
var result = sut.CreateReceivableLedger(CreateSplit(), splitCostCentre: null, receipt, settings);
Assert.True(result.IsSuccess);
Assert.Equal(CustomerAccount, result.Data.partyId);
}
[Fact]
public void Receivable_FlagFalse_NoAddressBookKeepingNumber_PartyIdIsNull()
{
var sut = new BookKeepingExportDatevXmlOnline_2020();
var receipt = CreateCustomerReceipt(addressBookKeepingNumber: string.Empty,
altInvoiceAccount: null);
var settings = CreateSettings(useAccountFromDifferentInvoiceAddress: false);
var result = sut.CreateReceivableLedger(CreateSplit(), splitCostCentre: null, receipt, settings);
Assert.True(result.IsSuccess);
Assert.Null(result.Data.partyId);
}
// ---------- CreateReceivableLedger: name / city / bpAccountNo + bookingText follow the alternate recipient ----------
[Fact]
public void Receivable_FlagTrue_AltSet_UsesAlternateNameAndCityAndBpAccount()
{
var sut = new BookKeepingExportDatevXmlOnline_2020();
var receipt = CreateCustomerReceipt(addressBookKeepingNumber: CustomerAccount,
altInvoiceAccount: AlternateInvoiceAccount,
altInvoiceName: AlternateInvoiceName,
altInvoiceCity: AlternateInvoiceCity);
var settings = CreateSettings(useAccountFromDifferentInvoiceAddress: true);
var result = sut.CreateReceivableLedger(CreateSplit(), splitCostCentre: null, receipt, settings);
Assert.True(result.IsSuccess);
Assert.Equal(AlternateInvoiceName, result.Data.customerName);
Assert.Equal(AlternateInvoiceCity, result.Data.customerCity);
Assert.Equal(int.Parse(AlternateInvoiceAccount), result.Data.bpAccountNo);
}
[Fact]
public void Receivable_FlagFalse_AltSet_KeepsPrimaryNameCityBpAccount()
{
var sut = new BookKeepingExportDatevXmlOnline_2020();
var receipt = CreateCustomerReceipt(addressBookKeepingNumber: CustomerAccount,
altInvoiceAccount: AlternateInvoiceAccount,
altInvoiceName: AlternateInvoiceName,
altInvoiceCity: AlternateInvoiceCity);
var settings = CreateSettings(useAccountFromDifferentInvoiceAddress: false);
var result = sut.CreateReceivableLedger(CreateSplit(), splitCostCentre: null, receipt, settings);
Assert.True(result.IsSuccess);
Assert.Equal(CustomerName, result.Data.customerName);
Assert.Equal(CustomerCity, result.Data.customerCity);
Assert.Equal(int.Parse(CustomerAccount), result.Data.bpAccountNo);
}
[Fact]
public void Receivable_FlagTrue_AltAccountEmpty_FallsBackForAllFields()
{
var sut = new BookKeepingExportDatevXmlOnline_2020();
var receipt = CreateCustomerReceipt(addressBookKeepingNumber: CustomerAccount,
altInvoiceAccount: string.Empty,
altInvoiceName: AlternateInvoiceName,
altInvoiceCity: AlternateInvoiceCity);
var settings = CreateSettings(useAccountFromDifferentInvoiceAddress: true);
var result = sut.CreateReceivableLedger(CreateSplit(), splitCostCentre: null, receipt, settings);
Assert.True(result.IsSuccess);
Assert.Equal(CustomerName, result.Data.customerName);
Assert.Equal(CustomerCity, result.Data.customerCity);
Assert.Equal(int.Parse(CustomerAccount), result.Data.bpAccountNo);
}
[Fact]
public void Receivable_FlagTrue_AltAccountSet_AltNameAndCityEmpty_FallsBackForNameAndCity()
{
var sut = new BookKeepingExportDatevXmlOnline_2020();
var receipt = CreateCustomerReceipt(addressBookKeepingNumber: CustomerAccount,
altInvoiceAccount: AlternateInvoiceAccount,
altInvoiceName: null,
altInvoiceCity: "");
var settings = CreateSettings(useAccountFromDifferentInvoiceAddress: true);
var result = sut.CreateReceivableLedger(CreateSplit(), splitCostCentre: null, receipt, settings);
Assert.True(result.IsSuccess);
Assert.Equal(CustomerName, result.Data.customerName);
Assert.Equal(CustomerCity, result.Data.customerCity);
Assert.Equal(int.Parse(AlternateInvoiceAccount), result.Data.bpAccountNo);
Assert.Equal(AlternateInvoiceAccount, result.Data.partyId);
}
[Fact]
public void Receivable_FlagTrue_BookingTextUsesAlternateName()
{
var sut = new BookKeepingExportDatevXmlOnline_2020();
var receipt = CreateCustomerReceipt(addressBookKeepingNumber: CustomerAccount,
altInvoiceAccount: AlternateInvoiceAccount,
altInvoiceName: AlternateInvoiceName,
altInvoiceCity: AlternateInvoiceCity);
var settings = CreateSettings(useAccountFromDifferentInvoiceAddress: true, useIndividualBookingText: false);
var result = sut.CreateReceivableLedger(CreateSplit(), splitCostCentre: null, receipt, settings);
Assert.True(result.IsSuccess);
Assert.Contains(AlternateInvoiceName, result.Data.bookingText);
Assert.DoesNotContain(CustomerName, result.Data.bookingText);
}
[Fact]
public void Receivable_FlagFalse_BookingTextUsesPrimaryName()
{
var sut = new BookKeepingExportDatevXmlOnline_2020();
var receipt = CreateCustomerReceipt(addressBookKeepingNumber: CustomerAccount,
altInvoiceAccount: AlternateInvoiceAccount,
altInvoiceName: AlternateInvoiceName,
altInvoiceCity: AlternateInvoiceCity);
var settings = CreateSettings(useAccountFromDifferentInvoiceAddress: false, useIndividualBookingText: false);
var result = sut.CreateReceivableLedger(CreateSplit(), splitCostCentre: null, receipt, settings);
Assert.True(result.IsSuccess);
Assert.Contains(CustomerName, result.Data.bookingText);
Assert.DoesNotContain(AlternateInvoiceName, result.Data.bookingText);
}
[Theory]
[InlineData(null)]
[InlineData("")]
[InlineData(" ")]
public void Receivable_EmptyIndividualBookingText_UsesDefaultBookingText(string? bookingText)
{
var sut = new BookKeepingExportDatevXmlOnline_2020();
var receipt = CreateCustomerReceipt(addressBookKeepingNumber: CustomerAccount,
altInvoiceAccount: null);
var settings = CreateSettings(
useAccountFromDifferentInvoiceAddress: false,
useIndividualBookingText: true,
individualBookingText: bookingText);
var result = sut.CreateReceivableLedger(CreateSplit(), splitCostCentre: null, receipt, settings);
Assert.True(result.IsSuccess);
Assert.Equal(CustomerName + " - 1", result.Data.bookingText);
}
// ---------- CreatePayableLedger: regression — flag must not affect supplier path ----------
[Theory]
[InlineData(true)]
[InlineData(false)]
public void Payable_FlagToggle_DoesNotChangePartyId_AlwaysAddressBookKeepingNumber(bool flag)
{
var sut = new BookKeepingExportDatevXmlOnline_2020();
var receipt = CreateSupplierReceipt(addressBookKeepingNumber: CustomerAccount,
altInvoiceAccount: AlternateInvoiceAccount,
customerNumberAtSupplier: null);
var settings = CreateSettings(useAccountFromDifferentInvoiceAddress: flag);
var result = sut.CreatePayableLedger(CreateSplit(), splitCostCentre: null, receipt, settings);
Assert.True(result.IsSuccess);
Assert.Equal(CustomerAccount, result.Data.partyId);
}
[Fact]
public void Payable_CustomerNumberAtSupplier_OverridesAddressBookKeepingNumber()
{
var sut = new BookKeepingExportDatevXmlOnline_2020();
var receipt = CreateSupplierReceipt(addressBookKeepingNumber: CustomerAccount,
altInvoiceAccount: AlternateInvoiceAccount,
customerNumberAtSupplier: CustomerNumberAtSupplier);
var settings = CreateSettings(useAccountFromDifferentInvoiceAddress: true);
var result = sut.CreatePayableLedger(CreateSplit(), splitCostCentre: null, receipt, settings);
Assert.True(result.IsSuccess);
Assert.Equal(CustomerNumberAtSupplier, result.Data.partyId);
}
[Theory]
[InlineData(null)]
[InlineData("")]
[InlineData(" ")]
public void Payable_EmptyIndividualBookingText_UsesDefaultBookingText(string? bookingText)
{
var sut = new BookKeepingExportDatevXmlOnline_2020();
var receipt = CreateSupplierReceipt(addressBookKeepingNumber: CustomerAccount,
altInvoiceAccount: null,
customerNumberAtSupplier: null);
var settings = CreateSettings(
useAccountFromDifferentInvoiceAddress: false,
useIndividualBookingText: true,
individualBookingText: bookingText);
var result = sut.CreatePayableLedger(CreateSplit(), splitCostCentre: null, receipt, settings);
Assert.True(result.IsSuccess);
Assert.Equal("Supplier GmbH - EXT-1", result.Data.bookingText);
}
[Fact]
public void Split_UsesItemServicePeriodToAsDeliveryDate_WhenItemServicePeriodIsSingleDate()
{
var (receipt, item) = CreateReceiptForSplit();
item.ServicePeriodFrom.Returns(new DateTime(2026, 6, 2, 10, 0, 0));
item.ServicePeriodTo.Returns(new DateTime(2026, 6, 2, 13, 30, 0));
var split = Assert.Single(GetSplits(receipt));
Assert.Equal(new DateTime(2026, 6, 2), split.DeliveryDate);
}
[Fact]
public void Split_UsesReceiptServicePeriodToAsDeliveryDate_WhenItemServicePeriodIsEmpty()
{
var (receipt, item) = CreateReceiptForSplit();
item.DeliveryDate.Returns((DateTime?)null);
receipt.ServicePeriodFrom.Returns(new DateTime(2026, 6, 3, 8, 0, 0));
receipt.ServicePeriodTo.Returns(new DateTime(2026, 6, 3, 18, 0, 0));
var split = Assert.Single(GetSplits(receipt));
Assert.Equal(new DateTime(2026, 6, 3), split.DeliveryDate);
}
[Fact]
public void Split_UsesItemDeliveryDate_WhenItemServicePeriodIsRange()
{
var (receipt, item) = CreateReceiptForSplit();
item.ServicePeriodFrom.Returns(new DateTime(2026, 6, 2, 10, 0, 0));
item.ServicePeriodTo.Returns(new DateTime(2026, 6, 3, 13, 30, 0));
item.DeliveryDate.Returns(new DateTime(2026, 6, 6));
receipt.ServicePeriodFrom.Returns(new DateTime(2026, 6, 4, 8, 0, 0));
receipt.ServicePeriodTo.Returns(new DateTime(2026, 6, 4, 18, 0, 0));
var split = Assert.Single(GetSplits(receipt));
Assert.Equal(new DateTime(2026, 6, 3), split.DeliveryDate);
}
// ---------- helpers ----------
private static BookKeepingExportAccountSplitDTO CreateSplit() => new()
{
Account = 8400,
Amount = 100m,
TaxAmount = 19m,
AmountFC = 100m,
TaxAmountFC = 19m,
TaxRate = 19m,
TaxId = "9"
};
private static DatevOnlineExportSettingsDTO CreateSettings(bool useAccountFromDifferentInvoiceAddress, bool useIndividualBookingText = true, string? individualBookingText = "Test") => new()
{
UseAccountFromDifferentInvoiceAddress = useAccountFromDifferentInvoiceAddress,
ExportCaption = false,
ExportDeliveryDate = false,
// Use individual booking texts to keep CreateBookingText on the explicit-template branch — safer with mocks.
UseIndividualBookingTextForCustomerReceipts = useIndividualBookingText,
IndividualBookingTextForCustomerReceipts = individualBookingText,
UseIndividualBookingTextForSupplierReceipts = useIndividualBookingText,
IndividualBookingTextForSupplierReceipts = individualBookingText
};
private static IList<BookKeepingExportAccountSplitDTO> GetSplits(IBookKeepingReceipt receipt)
{
var helperType = typeof(BookKeepingExportDatevXmlOnline_2020).Assembly.GetType("Centron.Gateway.DataExchange.BookKeeping.BookKeepingExportHelper");
Assert.NotNull(helperType);
var helper = helperType.GetProperty("Instance", BindingFlags.Public | BindingFlags.Static)?.GetValue(null);
Assert.NotNull(helper);
var method = helperType.GetMethod("GetPositionsThroughSplit", BindingFlags.Public | BindingFlags.Instance);
Assert.NotNull(method);
var result = (Result<IList<BookKeepingExportAccountSplitDTO>>)method.Invoke(helper, new object[]
{
receipt,
new BookKeepingExportConfiguration { ExportPerformanceDate = true },
false,
false
})!;
Assert.True(result.IsSuccess, result.Message);
return result.Data;
}
private static (IBookKeepingReceipt Receipt, IBookKeepingReceiptItem Item) CreateReceiptForSplit()
{
var receipt = CreateCustomerReceipt(addressBookKeepingNumber: CustomerAccount, altInvoiceAccount: null);
receipt.NetPriceComplete.Returns(100m);
receipt.TaxPriceComplete.Returns(19m);
receipt.NetPriceFCComplete.Returns(100m);
receipt.TaxPriceFCComplete.Returns(19m);
var item = Substitute.For<IBookKeepingReceiptItem>();
item.ProfitAndLossAccount.Returns(8400);
item.TaxRate.Returns(19m);
item.VATCode.Returns("9");
item.DeliveryDate.Returns(new DateTime(2026, 6, 4));
item.NetPriceTotalComplete.Returns(100m);
item.TaxPriceTotalComplete.Returns(19m);
item.NetPriceTotalFCComplete.Returns(100m);
item.TaxPriceTotalFCComplete.Returns(19m);
receipt.Items.Returns(new List<IBookKeepingReceiptItem> { item });
return (receipt, item);
}
private static IBookKeepingReceipt CreateCustomerReceipt(string addressBookKeepingNumber, string? altInvoiceAccount,
string? altInvoiceName = null, string? altInvoiceCity = null,
string addressCity = CustomerCity)
{
var r = Substitute.For<IBookKeepingReceipt>();
r.Number.Returns(1);
r.Date.Returns(new DateTime(2026, 1, 15));
r.Type.Returns(BookKeepingReceiptKind.Invoice);
r.CurrencyISOCode.Returns("EUR");
r.CurrencyFactor.Returns(1.0);
r.AddressName.Returns(CustomerName);
r.AddressCity.Returns(addressCity);
r.AddressBookKeepingNumber.Returns(addressBookKeepingNumber);
r.BookKeepingNumberDifferentInvoiceAddress.Returns(altInvoiceAccount);
r.AddressNameDifferentInvoiceAddress.Returns(altInvoiceName);
r.AddressCityDifferentInvoiceAddress.Returns(altInvoiceCity);
return r;
}
private static IBookKeepingReceipt CreateSupplierReceipt(string addressBookKeepingNumber, string? altInvoiceAccount, string? customerNumberAtSupplier)
{
var r = Substitute.For<IBookKeepingReceipt>();
r.Number.Returns(1);
r.Date.Returns(new DateTime(2026, 1, 15));
r.ExternalReceiptDate.Returns(new DateTime(2026, 1, 14));
r.ExternalReceiptNumber.Returns("EXT-1");
r.Type.Returns(BookKeepingReceiptKind.SupplierInvoice);
r.CurrencyISOCode.Returns("EUR");
r.CurrencyFactor.Returns(1.0);
r.AddressName.Returns("Supplier GmbH");
r.AddressCity.Returns("Berlin");
r.AddressBookKeepingNumber.Returns(addressBookKeepingNumber);
r.BookKeepingNumberDifferentInvoiceAddress.Returns(altInvoiceAccount);
r.CustomerNumberAtSupplier.Returns(customerNumberAtSupplier);
return r;
}
}
@@ -0,0 +1,291 @@
using System;
using System.Collections.Generic;
using Centron.BusinessLogic.DataExchange.Connectors;
using Centron.DAO;
using Centron.DAO.Mappings.DataExchange.Connectors;
using Centron.Interfaces.BL;
using CentronSoftware.Centron.WebServices.Entities.DataExchange.Connectors;
using FluentNHibernate.Cfg;
using FluentNHibernate.Cfg.Db;
using JetBrains.Annotations;
using NHibernate;
using NHibernate.Tool.hbm2ddl;
using Xunit;
namespace Centron.Tests.BL.DataExchange.Connectors;
/// <summary>
/// Integration-style tests for <see cref="DocBeeTicketTemplateBL"/> using SQLite in-memory and
/// <see cref="DocBeeTicketTemplateMaps"/> only.
/// </summary>
[TestSubject(typeof(DocBeeTicketTemplateBL))]
public class DocBeeTicketTemplateBLTest
{
[Fact]
public void GetAllTemplates_WhenEmpty_ReturnsSuccessWithEmptyList()
{
using var session = CreateSession();
var daoSession = new DAOSession(session);
var bl = new DocBeeTicketTemplateBL(daoSession);
var result = bl.GetAllTemplates();
Assert.Equal(ResultStatus.Success, result.Status);
Assert.NotNull(result.Data);
Assert.Empty(result.Data);
}
[Fact]
public void SyncTemplates_WhenNull_ReturnsError()
{
using var session = CreateSession();
var daoSession = new DAOSession(session);
var bl = new DocBeeTicketTemplateBL(daoSession);
var result = bl.SyncTemplates(null!);
Assert.Equal(ResultStatus.Error, result.Status);
Assert.Contains("Templates list is required", result.Message, StringComparison.Ordinal);
}
[Fact]
public void SyncTemplates_WhenCaptionMissing_ReturnsError()
{
using var session = CreateSession();
var daoSession = new DAOSession(session);
var bl = new DocBeeTicketTemplateBL(daoSession);
var result = bl.SyncTemplates(new List<DocBeeTicketTemplateDTO>
{
new() { TicketTemplateID = "t1", Caption = null, Info = null }
});
Assert.Equal(ResultStatus.Error, result.Status);
Assert.Contains("t1", result.Message, StringComparison.Ordinal);
Assert.Contains("Caption is required", result.Message, StringComparison.Ordinal);
}
[Fact]
public void SyncTemplates_InsertsNewRows_AndGetReturnsOrderedByCaption()
{
using var session = CreateSession();
var daoSession = new DAOSession(session);
var bl = new DocBeeTicketTemplateBL(daoSession);
var sync = bl.SyncTemplates(new List<DocBeeTicketTemplateDTO>
{
new() { TicketTemplateID = "id-z", Caption = "Zebra", Info = "i1" },
new() { TicketTemplateID = "id-a", Caption = "Alpha", Info = "i2" }
});
Assert.Equal(ResultStatus.Success, sync.Status);
Assert.Equal(2, sync.Data);
session.Flush();
session.Clear();
var get = bl.GetAllTemplates();
Assert.Equal(ResultStatus.Success, get.Status);
Assert.NotNull(get.Data);
Assert.Equal(2, get.Data.Count);
Assert.Equal("Alpha", get.Data[0].Caption);
Assert.Equal("id-a", get.Data[0].TicketTemplateID);
Assert.Equal("Zebra", get.Data[1].Caption);
Assert.Equal("id-z", get.Data[1].TicketTemplateID);
}
[Fact]
public void SyncTemplates_UpdatesCaptionAndInfo_WhenIdExists()
{
using var session = CreateSession();
var daoSession = new DAOSession(session);
var bl = new DocBeeTicketTemplateBL(daoSession);
bl.SyncTemplates(new List<DocBeeTicketTemplateDTO>
{
new() { TicketTemplateID = "same", Caption = "OldCap", Info = "OldInfo" }
});
session.Flush();
session.Clear();
var sync2 = bl.SyncTemplates(new List<DocBeeTicketTemplateDTO>
{
new() { TicketTemplateID = "same", Caption = "NewCap", Info = "NewInfo" }
});
Assert.Equal(ResultStatus.Success, sync2.Status);
session.Flush();
session.Clear();
var get = bl.GetAllTemplates();
var row = Assert.Single(get.Data!);
Assert.Equal("same", row.TicketTemplateID);
Assert.Equal("NewCap", row.Caption);
Assert.Equal("NewInfo", row.Info);
}
[Fact]
public void SyncTemplates_RemovesRowsNotInIncomingList()
{
using var session = CreateSession();
var daoSession = new DAOSession(session);
var bl = new DocBeeTicketTemplateBL(daoSession);
bl.SyncTemplates(new List<DocBeeTicketTemplateDTO>
{
new() { TicketTemplateID = "keep", Caption = "K", Info = null },
new() { TicketTemplateID = "drop", Caption = "D", Info = null }
});
session.Flush();
session.Clear();
var sync2 = bl.SyncTemplates(new List<DocBeeTicketTemplateDTO>
{
new() { TicketTemplateID = "keep", Caption = "K", Info = null }
});
Assert.Equal(ResultStatus.Success, sync2.Status);
Assert.Equal(1, sync2.Data);
session.Flush();
session.Clear();
var get = bl.GetAllTemplates();
var row = Assert.Single(get.Data!);
Assert.Equal("keep", row.TicketTemplateID);
}
[Fact]
public void SyncTemplates_EmptyList_RemovesAllTemplates()
{
using var session = CreateSession();
var daoSession = new DAOSession(session);
var bl = new DocBeeTicketTemplateBL(daoSession);
bl.SyncTemplates(new List<DocBeeTicketTemplateDTO>
{
new() { TicketTemplateID = "a", Caption = "A", Info = null },
new() { TicketTemplateID = "b", Caption = "B", Info = null }
});
session.Flush();
session.Clear();
var sync2 = bl.SyncTemplates(new List<DocBeeTicketTemplateDTO>());
Assert.Equal(ResultStatus.Success, sync2.Status);
Assert.Equal(0, sync2.Data);
session.Flush();
session.Clear();
var get = bl.GetAllTemplates();
Assert.Empty(get.Data!);
}
[Fact]
public void SyncTemplates_ClearsReferencesInArticleAndMaterialGroups_WhenTemplateRemoved()
{
using var session = CreateSession();
var daoSession = new DAOSession(session);
var bl = new DocBeeTicketTemplateBL(daoSession);
bl.SyncTemplates(new List<DocBeeTicketTemplateDTO>
{
new() { TicketTemplateID = "keep", Caption = "Keep", Info = null },
new() { TicketTemplateID = "drop", Caption = "Drop", Info = null }
});
session.Flush();
session.Clear();
session.CreateSQLQuery("INSERT INTO ARTIK (I3D, DocBeeTicketTemplateID) VALUES (1, 'drop')").ExecuteUpdate();
session.CreateSQLQuery("INSERT INTO ARTIK (I3D, DocBeeTicketTemplateID) VALUES (2, 'keep')").ExecuteUpdate();
session.CreateSQLQuery("INSERT INTO WAREN (I3D, DocBeeTicketTemplateID) VALUES (1, 'drop')").ExecuteUpdate();
session.CreateSQLQuery("INSERT INTO UNTERWAREN (I3D, DocBeeTicketTemplateID) VALUES (1, 'drop')").ExecuteUpdate();
var sync2 = bl.SyncTemplates(new List<DocBeeTicketTemplateDTO>
{
new() { TicketTemplateID = "keep", Caption = "Keep", Info = null }
});
Assert.Equal(ResultStatus.Success, sync2.Status);
session.Flush();
session.Clear();
var artikDropRef = session.CreateSQLQuery("SELECT DocBeeTicketTemplateID FROM ARTIK WHERE I3D = 1").UniqueResult();
Assert.Null(artikDropRef);
var artikKeepRef = (string)session.CreateSQLQuery("SELECT DocBeeTicketTemplateID FROM ARTIK WHERE I3D = 2").UniqueResult();
Assert.Equal("keep", artikKeepRef);
var warenRef = session.CreateSQLQuery("SELECT DocBeeTicketTemplateID FROM WAREN WHERE I3D = 1").UniqueResult();
Assert.Null(warenRef);
var unterwarenRef = session.CreateSQLQuery("SELECT DocBeeTicketTemplateID FROM UNTERWAREN WHERE I3D = 1").UniqueResult();
Assert.Null(unterwarenRef);
}
[Fact]
public void SyncTemplates_SkipsNullOrWhitespaceTicketTemplateID()
{
using var session = CreateSession();
var daoSession = new DAOSession(session);
var bl = new DocBeeTicketTemplateBL(daoSession);
var sync = bl.SyncTemplates(new List<DocBeeTicketTemplateDTO>
{
new() { TicketTemplateID = "valid", Caption = "V", Info = null },
new() { TicketTemplateID = " ", Caption = "X", Info = null },
null!
});
Assert.True(
sync.Status == ResultStatus.Success,
$"Expected Success, got {sync.Status}: {sync.Message}");
Assert.Equal(1, sync.Data);
session.Flush();
session.Clear();
var get = bl.GetAllTemplates();
var row = Assert.Single(get.Data!);
Assert.Equal("valid", row.TicketTemplateID);
}
[Fact]
public void SyncTemplates_TrimsTicketTemplateIdAndCaption()
{
using var session = CreateSession();
var daoSession = new DAOSession(session);
var bl = new DocBeeTicketTemplateBL(daoSession);
var sync = bl.SyncTemplates(new List<DocBeeTicketTemplateDTO>
{
new() { TicketTemplateID = " id1 ", Caption = " cap ", Info = " info " }
});
Assert.Equal(ResultStatus.Success, sync.Status);
session.Flush();
session.Clear();
var get = bl.GetAllTemplates();
var row = Assert.Single(get.Data!);
Assert.Equal("id1", row.TicketTemplateID);
Assert.Equal("cap", row.Caption);
Assert.Equal("info", row.Info);
}
private static ISession CreateSession()
{
var fluentConfig = Fluently.Configure()
.Database(SQLiteConfiguration.Standard.InMemory)
.Mappings(m => m.FluentMappings.Add<DocBeeTicketTemplateMaps>());
var sessionFactory = fluentConfig.BuildSessionFactory();
var session = sessionFactory.OpenSession();
new SchemaExport(fluentConfig.BuildConfiguration()).Execute(true, true, false, session.Connection, Console.Out);
session.CreateSQLQuery("CREATE TABLE IF NOT EXISTS ARTIK (I3D INTEGER PRIMARY KEY AUTOINCREMENT, DocBeeTicketTemplateID TEXT)").ExecuteUpdate();
session.CreateSQLQuery("CREATE TABLE IF NOT EXISTS WAREN (I3D INTEGER PRIMARY KEY AUTOINCREMENT, DocBeeTicketTemplateID TEXT)").ExecuteUpdate();
session.CreateSQLQuery("CREATE TABLE IF NOT EXISTS UNTERWAREN (I3D INTEGER PRIMARY KEY AUTOINCREMENT, DocBeeTicketTemplateID TEXT)").ExecuteUpdate();
return session;
}
}
@@ -0,0 +1,78 @@
using System;
using System.Collections.Generic;
using Centron.BusinessLogic.WebServices.DataExchange.Connectors;
using Centron.DAO;
using Centron.DAO.Mappings.DataExchange.Connectors;
using Centron.Data.Entities.Administration.Logins;
using Centron.Interfaces.BL;
using CentronSoftware.Centron.WebServices.Entities.DataExchange.Connectors;
using FluentNHibernate.Cfg;
using FluentNHibernate.Cfg.Db;
using JetBrains.Annotations;
using NHibernate;
using NHibernate.Tool.hbm2ddl;
using Xunit;
namespace Centron.Tests.BL.DataExchange.Connectors;
/// <summary>
/// Guard tests for <see cref="DocBeeTicketTemplateWebServiceBL"/> (no full rights/DB setup for SETTINGS).
/// </summary>
[TestSubject(typeof(DocBeeTicketTemplateWebServiceBL))]
public class DocBeeTicketTemplateWebServiceBLTest
{
[Fact]
public void SyncTemplates_WhenCurrentUserNull_ReturnsError()
{
using var session = CreateSession();
var daoSession = new DAOSession(session);
var wsbl = new DocBeeTicketTemplateWebServiceBL(daoSession);
var result = wsbl.SyncTemplates(new List<DocBeeTicketTemplateDTO>(), null!);
Assert.Equal(ResultStatus.Error, result.Status);
Assert.Contains("User information is required", result.Message, StringComparison.Ordinal);
}
[Fact]
public void SyncTemplates_WhenUserNull_ReturnsError()
{
using var session = CreateSession();
var daoSession = new DAOSession(session);
var wsbl = new DocBeeTicketTemplateWebServiceBL(daoSession);
var loggedIn = new LoggedInUser(appUserI3D: null, user: null);
var result = wsbl.SyncTemplates(new List<DocBeeTicketTemplateDTO>(), loggedIn);
Assert.Equal(ResultStatus.Error, result.Status);
Assert.Contains("User does not have permission", result.Message, StringComparison.Ordinal);
}
[Fact]
public void GetAllTemplates_DelegatesToBl_EmptyDatabase()
{
using var session = CreateSession();
var daoSession = new DAOSession(session);
var wsbl = new DocBeeTicketTemplateWebServiceBL(daoSession);
var result = wsbl.GetAllTemplates();
Assert.Equal(ResultStatus.Success, result.Status);
Assert.NotNull(result.Data);
Assert.Empty(result.Data);
}
private static ISession CreateSession()
{
var fluentConfig = Fluently.Configure()
.Database(SQLiteConfiguration.Standard.InMemory)
.Mappings(m => m.FluentMappings.Add<DocBeeTicketTemplateMaps>());
var sessionFactory = fluentConfig.BuildSessionFactory();
var session = sessionFactory.OpenSession();
new SchemaExport(fluentConfig.BuildConfiguration()).Execute(true, true, false, session.Connection, Console.Out);
return session;
}
}
@@ -0,0 +1,311 @@
using System.Linq;
using Centron.BusinessLogic.DataExchange.EDI;
using Centron.BusinessLogic.DataExchange.EDI.SaleInvoices;
using Centron.Data.Entities.Sales.Receipts.Invoices;
using Centron.Interfaces.DataExchange.BookKeeping;
using JetBrains.Annotations;
namespace Centron.Tests.BL.DataExchange.EDI.SaleInvoices;
/// <summary>
/// Tests for the contract-contingent balance merge in <see cref="InvoiceZugferdBL.MergeBalanceCompensationItems"/>.
/// Service and compensation items share a <see cref="ZugferdExportPositionItem.BalanceID"/>; the merge folds the
/// compensation amount into the service item and removes the compensation line from the export.
/// On invoices the service item is positive and the compensation is negative; credit vouchers store the same
/// pair with inverted signs (Ticket 164110 — without the sign-aware merge the wrong line would be removed
/// and the position total would no longer match the receipt header).
/// </summary>
[TestSubject(typeof(InvoiceZugferdBL))]
public class InvoiceZugferdBalanceMergeTests
{
private const string BalanceA = "BAL-A";
private const string BalanceB = "BAL-B";
private static ZugferdExportPositionItem Service(string balanceId, decimal netTotal, decimal netUnit, decimal quantity)
=> new()
{
BalanceID = balanceId,
NetPriceTotalFC = netTotal,
NetPrice = netUnit,
Quantity = quantity,
TaxPriceTotalFC = 0m,
ArticleCode = "SERPELISTD",
Text = "Servicedienstleistung",
};
private static ZugferdExportPositionItem Compensation(string balanceId, decimal netTotal)
=> new()
{
BalanceID = balanceId,
NetPriceTotalFC = netTotal,
NetPrice = -netTotal,
Quantity = 0m,
TaxPriceTotalFC = 0m,
ArticleCode = "AUSGLEICH",
Text = "Ausgleichsartikel für Kontingente",
};
private static ZugferdExportItem ItemWith(params ZugferdExportPositionItem[] positions)
=> new() { Positions = positions.ToList() };
[Fact]
public void Invoice_FullyCoveredByContingent_ReducesServiceToZeroAndKeepsBasisQuantity()
{
var service = Service(BalanceA, netTotal: 150m, netUnit: 150m, quantity: 1m);
var compensation = Compensation(BalanceA, netTotal: -150m);
var export = ItemWith(service, compensation);
InvoiceZugferdBL.MergeBalanceCompensationItems(export, BookKeepingReceiptKind.Invoice);
Assert.Single(export.Positions);
Assert.Same(service, export.Positions[0]);
Assert.Equal(0m, service.NetPriceTotalFC);
Assert.Equal(0m, service.Quantity);
Assert.Equal(1m, service.BasisQuantity);
}
[Fact]
public void Invoice_PartiallyCovered_ReducesServiceProportionally()
{
var service = Service(BalanceA, netTotal: 150m, netUnit: 150m, quantity: 1m);
var compensation = Compensation(BalanceA, netTotal: -90m);
var export = ItemWith(service, compensation);
InvoiceZugferdBL.MergeBalanceCompensationItems(export, BookKeepingReceiptKind.Invoice);
Assert.Single(export.Positions);
Assert.Equal(60m, service.NetPriceTotalFC);
Assert.Equal(0.4m, service.Quantity);
Assert.Equal(1m, service.BasisQuantity);
}
[Fact]
public void CreditVoucher_FullyCoveredByContingent_ReducesServiceToZeroAndKeepsRealServiceLine()
{
// Credit voucher: signs are inverted compared to the source invoice
var service = Service(BalanceA, netTotal: -150m, netUnit: -150m, quantity: 1m);
var compensation = Compensation(BalanceA, netTotal: 150m);
var export = ItemWith(service, compensation);
InvoiceZugferdBL.MergeBalanceCompensationItems(export, BookKeepingReceiptKind.CreditVoucher);
Assert.Single(export.Positions);
Assert.Same(service, export.Positions[0]);
Assert.Equal("SERPELISTD", export.Positions[0].ArticleCode);
Assert.Equal(0m, service.NetPriceTotalFC);
Assert.Equal(0m, service.Quantity);
Assert.Equal(1m, service.BasisQuantity);
}
[Fact]
public void CreditVoucher_PartiallyCovered_ReducesServiceProportionally()
{
var service = Service(BalanceA, netTotal: -150m, netUnit: -150m, quantity: 1m);
var compensation = Compensation(BalanceA, netTotal: 90m);
var export = ItemWith(service, compensation);
InvoiceZugferdBL.MergeBalanceCompensationItems(export, BookKeepingReceiptKind.CreditVoucher);
Assert.Single(export.Positions);
Assert.Equal(-60m, service.NetPriceTotalFC);
Assert.Equal(0.4m, service.Quantity);
Assert.Equal(1m, service.BasisQuantity);
}
[Fact]
public void MultipleBalanceGroups_AreMergedIndependently()
{
var service1 = Service(BalanceA, netTotal: 150m, netUnit: 150m, quantity: 1m);
var compensation1 = Compensation(BalanceA, netTotal: -150m);
var service2 = Service(BalanceB, netTotal: 200m, netUnit: 100m, quantity: 2m);
var compensation2 = Compensation(BalanceB, netTotal: -100m);
var export = ItemWith(service1, compensation1, service2, compensation2);
InvoiceZugferdBL.MergeBalanceCompensationItems(export, BookKeepingReceiptKind.Invoice);
Assert.Equal(2, export.Positions.Count);
Assert.Equal(0m, service1.NetPriceTotalFC);
Assert.Equal(0m, service1.Quantity);
Assert.Equal(100m, service2.NetPriceTotalFC);
Assert.Equal(1m, service2.Quantity);
Assert.DoesNotContain(compensation1, export.Positions);
Assert.DoesNotContain(compensation2, export.Positions);
}
[Fact]
public void PositionsWithoutBalanceId_AreLeftUntouched()
{
var standalone = new ZugferdExportPositionItem
{
BalanceID = null,
NetPriceTotalFC = 99m,
NetPrice = 99m,
Quantity = 1m,
ArticleCode = "OTHER",
};
var export = ItemWith(standalone);
InvoiceZugferdBL.MergeBalanceCompensationItems(export, BookKeepingReceiptKind.Invoice);
Assert.Single(export.Positions);
Assert.Same(standalone, export.Positions[0]);
Assert.Equal(99m, standalone.NetPriceTotalFC);
Assert.Equal(1m, standalone.Quantity);
Assert.Null(standalone.BasisQuantity);
}
[Fact]
public void ResolveServicePeriod_ItemPeriodWinsBeforeReceiptHeadAndFallback()
{
var receipt = new ReceiptInvoice
{
ServicePeriodFrom = new System.DateTime(2024, 1, 1),
ServicePeriodTo = new System.DateTime(2024, 1, 31),
};
var item = new ReceiptInvoiceItem
{
I3D = 10,
ServicePeriodFrom = new System.DateTime(2024, 2, 2, 14, 0, 0),
ServicePeriodTo = new System.DateTime(2024, 2, 4, 16, 0, 0),
};
var fallbackCalled = false;
var result = InvoiceZugferdBL.ResolveServicePeriod(
receipt,
item,
BookKeepingReceiptKind.Invoice,
(_, _) =>
{
fallbackCalled = true;
return (new System.DateTime(2024, 3, 1), new System.DateTime(2024, 3, 31));
});
Assert.Equal(new System.DateTime(2024, 2, 2), result.From);
Assert.Equal(new System.DateTime(2024, 2, 4), result.To);
Assert.False(fallbackCalled);
}
[Fact]
public void ResolveServicePeriod_ReceiptHeadWinsBeforeFallback()
{
var receipt = new ReceiptInvoice
{
ServicePeriodFrom = new System.DateTime(2024, 1, 1, 9, 0, 0),
ServicePeriodTo = new System.DateTime(2024, 1, 31, 18, 0, 0),
};
var item = new ReceiptInvoiceItem { I3D = 11 };
var fallbackCalled = false;
var result = InvoiceZugferdBL.ResolveServicePeriod(
receipt,
item,
BookKeepingReceiptKind.Invoice,
(_, _) =>
{
fallbackCalled = true;
return (new System.DateTime(2024, 3, 1), new System.DateTime(2024, 3, 31));
});
Assert.Equal(new System.DateTime(2024, 1, 1), result.From);
Assert.Equal(new System.DateTime(2024, 1, 31), result.To);
Assert.False(fallbackCalled);
}
[Fact]
public void ResolveServicePeriod_UsesContractFallbackWhenItemAndHeadAreEmpty()
{
var receipt = new ReceiptInvoice();
var item = new ReceiptInvoiceItem { I3D = 12 };
var result = InvoiceZugferdBL.ResolveServicePeriod(
receipt,
item,
BookKeepingReceiptKind.Invoice,
(itemI3D, receiptKind) =>
{
Assert.Equal(12, itemI3D);
Assert.Equal(BookKeepingReceiptKind.Invoice, receiptKind);
return (new System.DateTime(2024, 3, 1), new System.DateTime(2024, 3, 31));
});
Assert.Equal(new System.DateTime(2024, 3, 1), result.From);
Assert.Equal(new System.DateTime(2024, 3, 31), result.To);
}
[Fact]
public void ApplyHeaderBillingPeriodFromPositions_UsesMinMaxAndKeepsSameDayPeriod()
{
var export = ItemWith(
new ZugferdExportPositionItem
{
BillingPeriodFrom = new System.DateTime(2024, 2, 15),
BillingPeriodTo = new System.DateTime(2024, 2, 15),
},
new ZugferdExportPositionItem
{
BillingPeriodFrom = new System.DateTime(2024, 1, 1),
BillingPeriodTo = new System.DateTime(2024, 3, 31),
});
InvoiceZugferdBL.ApplyHeaderBillingPeriodFromPositions(export);
Assert.Equal(new System.DateTime(2024, 1, 1), export.BillingPeriodFrom);
Assert.Equal(new System.DateTime(2024, 3, 31), export.BillingPeriodTo);
}
[Fact]
public void IsSingleServicePeriodDate_ReturnsTrue_WhenDatesAreEqualIgnoringTime()
{
var isSingleDate = InvoiceZugferdBL.IsSingleServicePeriodDate(
new System.DateTime(2026, 2, 6, 10, 0, 0),
new System.DateTime(2026, 2, 6, 13, 30, 0));
Assert.True(isSingleDate);
}
[Fact]
public void GetHeaderDeliveryDate_UsesServicePeriodTo_WhenHeaderPeriodIsSingleDate()
{
var exportItem = ItemWith();
exportItem.ReceiptDate = new System.DateTime(2026, 2, 1);
exportItem.DeliveryDate = new System.DateTime(2026, 2, 4);
exportItem.BillingPeriodFrom = new System.DateTime(2026, 2, 6, 10, 0, 0);
exportItem.BillingPeriodTo = new System.DateTime(2026, 2, 6, 13, 30, 0);
var deliveryDate = InvoiceZugferdBL.GetHeaderDeliveryDate(exportItem);
Assert.Equal(new System.DateTime(2026, 2, 6), deliveryDate);
}
[Fact]
public void DoCreateActualDeliverySupplyChainEvent_UsesOnlyDeliveryDateWhenPeriodDatesAreEqual()
{
var document = new System.Xml.XmlDocument();
var node = InvoiceZugferdBL.DoCreateActualDeliverySupplyChainEvent(
new System.DateTime(2026, 2, 6, 13, 30, 0),
document);
var occurrenceDateTimeNode = Assert.Single(node.ChildNodes.Cast<System.Xml.XmlNode>(), f => f.LocalName == "OccurrenceDateTime");
var dateTimeStringNode = Assert.Single(occurrenceDateTimeNode.ChildNodes.Cast<System.Xml.XmlNode>(), f => f.LocalName == "DateTimeString");
Assert.Equal("102", dateTimeStringNode.Attributes?["format"]?.Value);
Assert.Equal("20260206", dateTimeStringNode.InnerText);
}
[Fact]
public void DoCreateBillingSpecifiedPeriod_UsesStartAndEndDateWhenDatesDiffer()
{
var document = new System.Xml.XmlDocument();
var node = InvoiceZugferdBL.DoCreateBillingSpecifiedPeriod(
new System.DateTime(2026, 2, 6, 10, 0, 0),
new System.DateTime(2026, 2, 7, 13, 30, 0),
document);
var startDateTimeNode = Assert.Single(node.ChildNodes.Cast<System.Xml.XmlNode>(), f => f.LocalName == "StartDateTime");
var endDateTimeNode = Assert.Single(node.ChildNodes.Cast<System.Xml.XmlNode>(), f => f.LocalName == "EndDateTime");
Assert.Equal("20260206", startDateTimeNode.InnerText);
Assert.Equal("20260207", endDateTimeNode.InnerText);
}
}