using System; using System.Collections.Generic; using System.Linq; using Centron.BusinessLogic; using Centron.BusinessLogic.Sales.CustomerAssets.TimerBilling; using Centron.BusinessLogic.WebServices.Employee; using Centron.Interfaces.BL; using Centron.Tests.EndToEnd.Infrastructure; using CentronSoftware.Centron.WebServices.Entities.Sales.CustomerAssets.TimerBilling; using Xunit.Abstractions; namespace Centron.Tests.EndToEnd.Tests.Administration.Employees; /// /// Regression tests for ticket 168634. /// Deactivating an employee used to delete the employee-article assignment (Mitarbeiterartikel). /// That assignment is the only link between a recorded time and its employee in the billing modules, /// so the deactivation silently made all already recorded times of that employee unbillable - and a /// reactivation could not restore it. /// These tests cover both halves of the fix: the assignment now survives a deactivation (only the /// articles are marked as end-of-life), and the billing search falls back to the employee who /// recorded the time when the assignment is missing. /// public class EmployeeDeactivationBillingTests : EndToEndTest { public EmployeeDeactivationBillingTests(ITestOutputHelper testOutputHelper) : base(testOutputHelper) { } public override void Execute() { var appUserI3D = this.GetAppUserWithBillableTimers(); var employeeI3D = this.Sql($"SELECT Personal FROM dbo.Sichbenu WHERE I3D = {appUserI3D}"); this.DeactivationKeepsAssignmentAndMarksArticlesEndOfLife(appUserI3D); this.ReactivationRevokesEndOfLife(appUserI3D); this.TimersStayAttributedWhenAssignmentIsMissing(appUserI3D, employeeI3D); } /// /// Finds an app-user that has employee articles which are used by open (not yet billed) timers /// recorded by that same employee. /// private int GetAppUserWithBillableTimers() { var appUserI3D = this.Sql(@" SELECT TOP 1 SB.I3D FROM dbo.hlpdsk_timer HT INNER JOIN dbo.Mitarbeiterartikel MA ON MA.ArtikelI3D = HT.ArtikelI3D INNER JOIN dbo.Sichbenu SB ON SB.I3D = MA.MitarbeiterI3D WHERE ISNULL(HT.RechPosI3D, 0) = 0 AND ISNULL(HT.LiefPosI3D, 0) = 0 AND ISNULL(HT.AufPosI3D, 0) = 0 AND HT.PersonalI3D = SB.Personal GROUP BY SB.I3D ORDER BY COUNT(*) DESC, SB.I3D"); if (appUserI3D <= 0) throw new InvalidOperationException("No app-user with employee articles and open timers found in the test database."); return appUserI3D; } /// /// Deactivating an employee must keep the employee-article assignment and only mark the /// assigned articles as end-of-life, so the articles disappear from new selections while /// the recorded times stay attributable. /// private void DeactivationKeepsAssignmentAndMarksArticlesEndOfLife(int appUserI3D) { // Reset the end-of-life flag first, so the snapshot is independent of the backup's state. this.Sql($@" UPDATE A SET A.EOL = 0 FROM dbo.ARTIK A INNER JOIN dbo.Mitarbeiterartikel MA ON MA.ArtikelI3D = A.I3D WHERE MA.MitarbeiterI3D = {appUserI3D}"); this.SetEmployeeArticlesEndOfLife(appUserI3D, true); this.VerifyEmployeeArticles("Deactivation_AssignmentKeptAndArticlesEndOfLife", appUserI3D); } /// /// Reactivating an employee must revoke the end-of-life mark that the deactivation has set. /// private void ReactivationRevokesEndOfLife(int appUserI3D) { this.SetEmployeeArticlesEndOfLife(appUserI3D, false); this.VerifyEmployeeArticles("Reactivation_ArticlesUsableAgain", appUserI3D); } /// /// Databases that were damaged by the old deactivation behaviour have no employee-article /// assignment left. The billing search must still attribute those times to the employee who /// recorded them, otherwise the times can never be billed again. /// Times that merely use one of the employee's articles but were recorded by somebody else /// move to that other employee, which is the correct attribution once the assignment is gone. /// private void TimersStayAttributedWhenAssignmentIsMissing(int appUserI3D, int employeeI3D) { var timersBefore = this.SearchTimersForEmployee(employeeI3D); if (timersBefore.Any() == false) throw new InvalidOperationException($"No open timers found for employee {employeeI3D} in the test database."); // Simulate a database that was damaged by the old deactivation behaviour. this.Sql($"DELETE FROM dbo.Mitarbeiterartikel WHERE MitarbeiterI3D = {appUserI3D}"); var timersAfter = this.SearchTimersForEmployee(employeeI3D); this.Verifier.Verify("TimerBillingSearch_WithoutEmployeeArticleAssignment", new { AssignmentsLeft = this.Sql($"SELECT COUNT(*) FROM dbo.Mitarbeiterartikel WHERE MitarbeiterI3D = {appUserI3D}"), TimersFoundBefore = timersBefore.Count, TimersRecordedByEmployeeBefore = timersBefore.Count(timer => timer.CreatorEmployeeI3D == employeeI3D), TimersFoundAfter = timersAfter.Count, TimersRecordedByEmployeeAfter = timersAfter.Count(timer => timer.CreatorEmployeeI3D == employeeI3D) }); } private void SetEmployeeArticlesEndOfLife(int appUserI3D, bool isEndOfLife) { using (var session = new BLSession()) { session.GetBL() .SetEmployeeArticlesEndOfLife(this.GetLoggedInUser(), appUserI3D, isEndOfLife) .ThrowIfError(); } } private void VerifyEmployeeArticles(string name, int appUserI3D) { this.Verifier.VerifySql(name, $@" SELECT ArticleCode = A.Artikelcode, IsEndOfLife = CAST(ISNULL(A.EOL, 0) AS bit) FROM dbo.Mitarbeiterartikel MA INNER JOIN dbo.ARTIK A ON A.I3D = MA.ArtikelI3D WHERE MA.MitarbeiterI3D = {appUserI3D} ORDER BY A.Artikelcode"); } private List SearchTimersForEmployee(int employeeI3D) { using (var session = new BLSession()) { return session.GetBL() .SearchTimers(new TimerBillingFilter { EmployeeI3Ds = new List { employeeI3D } }) .ToList(); } } }