using System; using Centron.BusinessLogic; using Centron.BusinessLogic.WebServices.Sales.Receipts.MasterDataLists; using Centron.Tests.EndToEnd.Infrastructure; using Xunit.Abstractions; namespace Centron.Tests.EndToEnd.Tests.MasterDataList { /// /// Regression tests for ticket 163981 — removing or replacing the main device's /// serial number on a Stammblatt and propagating the change to its click counters. /// public class MainDeviceSerialNumberTests : EndToEndTest { public MainDeviceSerialNumberTests(ITestOutputHelper testOutputHelper) : base(testOutputHelper) { } public override void Execute() { this.RemoveMainDeviceSerialNumber_NoCounters(); this.RemoveMainDeviceSerialNumber_WithCounters_Fails(); this.SwapMainDeviceSerialNumber_WithCounters_UpdatesAll(); } private void RemoveMainDeviceSerialNumber_NoCounters() { // Find a Stammblatt with a serial number but no active click counters. // Use the MasterDataList view so the test sees the same SerialNumberI3D the BL does // (the view derives it from Barcode.Status = 9, not from GeraeteKopf.SeriennummerI3D). var masterDataListI3D = this.Sql(@" SELECT TOP 1 mdl.I3D FROM MasterDataList mdl WHERE mdl.SerialNumberI3D > 0 AND NOT EXISTS ( SELECT 1 FROM GeraeteClickZaehler gz WHERE gz.GeraeteKopfI3D = mdl.I3D AND gz.SeriennummerI3D = mdl.SerialNumberI3D AND gz.Status = 1 ) ORDER BY mdl.I3D"); if (masterDataListI3D <= 0) throw new InvalidOperationException("Precondition failed: no master data list with a serial number and without active click counters found."); var oldSerialNumber = this.Sql($"SELECT SerialNumber FROM MasterDataList WHERE I3D = {masterDataListI3D}"); var historyCutoffI3D = this.GetCurrentMaxHistoryI3D(); using var bl = new BLSession(); var loggedInUser = this.GetLoggedInUser(); var result = bl.GetBL().RemoveMainDeviceSerialNumber(masterDataListI3D, loggedInUser); this.Verifier.Verify("RemoveMainDeviceSerialNumber_NoCounters_Result", result); this.Verifier.VerifySql( "RemoveMainDeviceSerialNumber_NoCounters_Db", $"SELECT I3D, SeriennummerI3D, Seriennummer FROM GeraeteKopf WHERE I3D = {masterDataListI3D}"); this.Verifier.VerifySql( "RemoveMainDeviceSerialNumber_NoCounters_History", BuildSerialNumberHistoryQuery( masterDataListI3D, historyCutoffI3D, loggedInUser.User.Employee.I3D, $"Seriennummer vom Hauptgerät \"{oldSerialNumber}\" wurde entfernt")); } private void RemoveMainDeviceSerialNumber_WithCounters_Fails() { // Find a Stammblatt with a serial number that still has active counters referencing it. var masterDataListI3D = this.Sql(@" SELECT TOP 1 mdl.I3D FROM MasterDataList mdl INNER JOIN GeraeteClickZaehler gz ON gz.GeraeteKopfI3D = mdl.I3D AND gz.SeriennummerI3D = mdl.SerialNumberI3D AND gz.Status = 1 WHERE mdl.SerialNumberI3D > 0 ORDER BY mdl.I3D"); if (masterDataListI3D <= 0) throw new InvalidOperationException("Precondition failed: no master data list with a serial number and active click counters found."); using var bl = new BLSession(); var loggedInUser = this.GetLoggedInUser(); var result = bl.GetBL().RemoveMainDeviceSerialNumber(masterDataListI3D, loggedInUser); this.Verifier.Verify("RemoveMainDeviceSerialNumber_WithCounters_Fails_Result", new { result.Status, result.Message }); } private void SwapMainDeviceSerialNumber_WithCounters_UpdatesAll() { // Find a Stammblatt with serial + active counters AND for which a different in-stock barcode // (state = 1) for the same article exists — both must hold for the swap to be exercisable. var masterDataListI3D = this.Sql(@" SELECT TOP 1 mdl.I3D FROM MasterDataList mdl INNER JOIN MasterDataListItems mi ON mi.HeadI3D = mdl.I3D AND mi.IsMainItem = 1 INNER JOIN GeraeteClickZaehler gz ON gz.GeraeteKopfI3D = mdl.I3D AND gz.SeriennummerI3D = mdl.SerialNumberI3D AND gz.Status = 1 WHERE mdl.SerialNumberI3D > 0 AND EXISTS ( SELECT 1 FROM Barcode b WHERE b.ArtikelI3D = mi.ArticleI3D AND b.Status = 1 AND b.I3D <> mdl.SerialNumberI3D ) ORDER BY mdl.I3D"); if (masterDataListI3D <= 0) throw new InvalidOperationException("Precondition failed: no master data list with a serial number, active click counters, and an alternative in-stock barcode for the main item's article found."); // Pick the first available in-stock barcode (state = 1) that matches the main item's article. var newSerialNumberI3D = this.Sql($@" SELECT TOP 1 b.I3D FROM Barcode b INNER JOIN MasterDataList mdl ON mdl.I3D = {masterDataListI3D} INNER JOIN MasterDataListItems mi ON mi.HeadI3D = mdl.I3D AND mi.IsMainItem = 1 WHERE b.ArtikelI3D = mi.ArticleI3D AND b.Status = 1 AND b.I3D <> mdl.SerialNumberI3D ORDER BY b.I3D"); if (newSerialNumberI3D <= 0) throw new InvalidOperationException("Precondition failed: no alternative in-stock barcode found for the main item's article."); var oldSerialNumber = this.Sql($"SELECT SerialNumber FROM MasterDataList WHERE I3D = {masterDataListI3D}"); var newSerialNumber = this.Sql($"SELECT Barcode FROM Barcode WHERE I3D = {newSerialNumberI3D}"); var historyCutoffI3D = this.GetCurrentMaxHistoryI3D(); using var bl = new BLSession(); var loggedInUser = this.GetLoggedInUser(); var result = bl.GetBL().SwapMainDeviceSerialNumber(masterDataListI3D, newSerialNumberI3D, loggedInUser); this.Verifier.Verify("SwapMainDeviceSerialNumber_WithCounters_UpdatesAll_Result", new { result.Status, result.Message }); this.Verifier.VerifySql( "SwapMainDeviceSerialNumber_WithCounters_UpdatesAll_DbHead", $"SELECT I3D, SeriennummerI3D, Seriennummer FROM GeraeteKopf WHERE I3D = {masterDataListI3D}"); this.Verifier.VerifySql( "SwapMainDeviceSerialNumber_WithCounters_UpdatesAll_DbCounters", $"SELECT I3D, GeraeteKopfI3D, SeriennummerI3D FROM GeraeteClickZaehler WHERE GeraeteKopfI3D = {masterDataListI3D} ORDER BY I3D"); this.Verifier.VerifySql( "SwapMainDeviceSerialNumber_WithCounters_UpdatesAll_History", BuildSerialNumberHistoryQuery( masterDataListI3D, historyCutoffI3D, loggedInUser.User.Employee.I3D, $"Seriennummer vom Hauptgerät wurde getauscht von \"{oldSerialNumber}\" auf \"{newSerialNumber}\"")); } /// /// Highest AnlageLog I3D before the operation under test runs, used as a cutoff so the history /// assertion only sees rows this test actually produced and not leftovers from earlier runs. /// private int GetCurrentMaxHistoryI3D() { return this.Sql("SELECT ISNULL(MAX(I3D), 0) FROM AnlageLog"); } /// /// Reads the Stammblatt history entries (AnlageLog) that the operation under test wrote. /// The cutoff limits the result to rows this run produced, so leftovers from earlier runs cannot /// satisfy the check, and an unexpected additional entry shows up as an extra row in the snapshot. /// MatchesExpected covers the exact description including old and new serial number. /// Employee and date are compared as flags because their raw values differ per run. /// private static string BuildSerialNumberHistoryQuery(int masterDataListI3D, int historyCutoffI3D, int expectedEmployeeI3D, string expectedDescription) { var expectedLiteral = expectedDescription.Replace("'", "''"); return $@" SELECT Beschreibung, CASE WHEN Beschreibung = N'{expectedLiteral}' THEN 1 ELSE 0 END AS MatchesExpected, CASE WHEN PersonalI3D = {expectedEmployeeI3D} THEN 1 ELSE 0 END AS IsExpectedEmployee, CASE WHEN Datum IS NOT NULL THEN 1 ELSE 0 END AS HasDate FROM AnlageLog WHERE AnlageI3D = {masterDataListI3D} AND I3D > {historyCutoffI3D} ORDER BY I3D"; } } }