361 lines
19 KiB
C#
361 lines
19 KiB
C#
using System.Linq;
|
||
using AwesomeAssertions;
|
||
using NUnit.Framework;
|
||
using Soneta.Business;
|
||
using Soneta.Kadry;
|
||
using Soneta.Kalend;
|
||
using Soneta.Types;
|
||
using Prac = Soneta.Kadry.Pracownik;
|
||
|
||
namespace Soneta.Skills.Test.KadryPlace.Pracownik;
|
||
|
||
/// <summary>
|
||
/// Rozdział D — „Nieobecności i czas pracy" (receptury D1, D2, D7).
|
||
/// <para>
|
||
/// Testy są <b>wykonywalną dokumentacją</b> publicznego kontraktu platformy Soneta dla obsługi
|
||
/// nieobecności pracownika oraz limitów urlopowych. Każda metoda mapuje się 1:1 do receptury
|
||
/// z dokumentu skilla <c>pracownik.md</c>:
|
||
/// <list type="bullet">
|
||
/// <item><b>D1</b> — wprowadzanie nieobecności (<c>NieobecnośćPracownika</c>, kolekcja <c>Nieobecnosci</c>);</item>
|
||
/// <item><b>D2</b> — korygowanie nieobecności (zmiana okresu/typu, rekord <c>KorektaNieobecności</c>);</item>
|
||
/// <item><b>D7</b> — analiza limitów urlopowych (naliczenie <c>NaliczanieLimitow.DodajLimit()</c> + odczyt z <c>pracownik.Limity</c>).</item>
|
||
/// </list>
|
||
/// </para>
|
||
/// <para>
|
||
/// Wszystko działa na bazie Demo (GoldStandard) z automatycznym rollbackiem po teście. Operujemy
|
||
/// wyłącznie na <b>publicznym kontrakcie</b> — tak jak dodatek programisty zewnętrznego bez dostępu
|
||
/// do kodu źródłowego aplikacji.
|
||
/// </para>
|
||
/// <para>
|
||
/// <b>Uwaga praktyczna (odkryta w trakcie testów):</b> ustawienie <c>Okres</c> na nieobecności typu
|
||
/// „urlop wypoczynkowy" wyzwala synchroniczne przeliczenie limitu i — gdy pracownik nie ma jeszcze
|
||
/// naliczonego limitu na ten dzień — rzuca <c>LimitNotFoundException</c>. Dlatego dla scenariuszy D1/D2
|
||
/// (czysta obsługa rekordu nieobecności) używamy typu nieobecności <b>niewymagającego limitu</b>
|
||
/// („Urlop bezpłatny (art 174 kp)"), a urlop wypoczynkowy testujemy dopiero po naliczeniu limitu (D7).
|
||
/// </para>
|
||
/// </summary>
|
||
[TestFixture]
|
||
public class RozdzialD_NieobecnosciTest : PracownikTestBase
|
||
{
|
||
// Typ nieobecności NIEwymagający naliczonego limitu — bezpieczny do scenariuszy obsługi rekordu.
|
||
private const string DefBezplatny = "Urlop bezpłatny (art 174 kp)";
|
||
private const string DefBezplatny2 = "Urlop bezpłatny (kod 350)";
|
||
private const string DefUrlopWyp = "Urlop wypoczynkowy";
|
||
|
||
// ============================== D1 — Wprowadzanie nieobecności ==============================
|
||
|
||
[Test]
|
||
[Description("D1: Nieobecnosc jest typem ABSTRAKCYJNYM; konkretnym typem nieobecności pracownika " +
|
||
"jest NieobecnośćPracownika (dziedziczy po Nieobecnosc) z ctorem (Pracownik).")]
|
||
public void D1_NieobecnoscPracownika_JestKonkretnymTypemNieobecnosci()
|
||
{
|
||
// Dokumentujemy regułę z receptury: new Nieobecnosc() jest niemożliwe (typ abstrakcyjny),
|
||
// więc używamy NieobecnośćPracownika. Sprawdzamy relację dziedziczenia bez instancjonowania abstrakta.
|
||
typeof(Nieobecnosc).IsAbstract.Should().BeTrue("Nieobecnosc jest klasą abstrakcyjną");
|
||
typeof(Nieobecnosc).IsAssignableFrom(typeof(NieobecnośćPracownika))
|
||
.Should().BeTrue("NieobecnośćPracownika jest konkretnym typem nieobecności pracownika");
|
||
}
|
||
|
||
[Test]
|
||
[Description("D1: nieobecność tworzymy NieobecnośćPracownika(pracownik) (ctor wiąże z pracownikiem) " +
|
||
"+ AddRow; ustawiamy Definicja (słownik DefNieobecnosci) i Okres (FromTo); zapis przez Save().")]
|
||
public void D1_WprowadzenieNieobecnosci_TworzyRekordWKolekcjiNieobecnosci()
|
||
{
|
||
var pracownik = Pracownik(Pracownik_.Andrzejewski);
|
||
pracownik.Should().NotBeNull("pracownik z Demo istnieje");
|
||
|
||
var def = Kalend.DefNieobecnosci.WgNazwy[DefBezplatny] as DefinicjaNieobecnosci;
|
||
def.Should().NotBeNull($"definicja '{DefBezplatny}' istnieje w bazie Demo");
|
||
|
||
var okres = new FromTo(new Date(2026, 7, 6), new Date(2026, 7, 10));
|
||
|
||
InTransaction(() =>
|
||
{
|
||
// Typ konkretny; ctor NieobecnośćPracownika(pracownik) wiąże nieobecność z pracownikiem.
|
||
var nieobecnosc = Session.AddRow(new NieobecnośćPracownika(pracownik));
|
||
nieobecnosc.Definicja = def; // rodzaj nieobecności (wymagany)
|
||
nieobecnosc.Okres = okres; // zakres dat „od–do"
|
||
|
||
// Relacja Pracownik jest ustawiana przez ctor i jest tylko do odczytu.
|
||
nieobecnosc.Pracownik.Should().BeSameAs(pracownik, "ctor wiąże nieobecność z pracownikiem");
|
||
});
|
||
SaveDispose();
|
||
|
||
// Odczyt: nieobecność przecinająca lipiec 2026 została zapisana w kolekcji pracownika.
|
||
var lipiec = new FromTo(new Date(2026, 7, 1), new Date(2026, 7, 31));
|
||
var pracownik2 = Pracownik(Pracownik_.Andrzejewski);
|
||
var nieobecnosci = pracownik2.Nieobecnosci.GetIntersectedRows(lipiec).Cast<Nieobecnosc>().ToList();
|
||
|
||
nieobecnosci.Should().ContainSingle("dodaliśmy jedną nieobecność w lipcu 2026")
|
||
.Which.Definicja.Nazwa.Should().Be(DefBezplatny);
|
||
var zapisana = nieobecnosci[0];
|
||
zapisana.Okres.From.Should().Be(okres.From);
|
||
zapisana.Okres.To.Should().Be(okres.To);
|
||
}
|
||
|
||
[Test]
|
||
[Description("D1 (odczyt): pracownik.Nieobecnosci.GetIntersectedRows(FromTo) zwraca nieobecności " +
|
||
"przecinające zadany przedział; poza przedziałem nieobecność nie jest zwracana.")]
|
||
public void D1_GetIntersectedRows_FiltrujePoPrzecieciuOkresu()
|
||
{
|
||
var pracownik = Pracownik(Pracownik_.Bednarek);
|
||
var def = Kalend.DefNieobecnosci.WgNazwy[DefBezplatny] as DefinicjaNieobecnosci;
|
||
|
||
InTransaction(() =>
|
||
{
|
||
var n = Session.AddRow(new NieobecnośćPracownika(pracownik));
|
||
n.Definicja = def;
|
||
n.Okres = new FromTo(new Date(2026, 8, 3), new Date(2026, 8, 7)); // sierpień
|
||
});
|
||
SaveDispose();
|
||
|
||
var pracownik2 = Pracownik(Pracownik_.Bednarek);
|
||
|
||
// Przedział przecinający się z nieobecnością → znajduje rekord.
|
||
var sierpien = new FromTo(new Date(2026, 8, 1), new Date(2026, 8, 31));
|
||
pracownik2.Nieobecnosci.GetIntersectedRows(sierpien).Cast<Nieobecnosc>()
|
||
.Should().ContainSingle("nieobecność przecina sierpień 2026");
|
||
|
||
// Przedział rozłączny (wrzesień) → brak rekordu.
|
||
var wrzesien = new FromTo(new Date(2026, 9, 1), new Date(2026, 9, 30));
|
||
pracownik2.Nieobecnosci.GetIntersectedRows(wrzesien).Cast<Nieobecnosc>()
|
||
.Should().BeEmpty("nieobecność nie przecina się z wrześniem 2026");
|
||
}
|
||
|
||
// ============================== D2 — Korygowanie nieobecności ==============================
|
||
|
||
[Test]
|
||
[Description("D2 (wariant A): okres nieobecności jest polem zapisywalnym — na istniejącym rekordzie " +
|
||
"można zmienić Okres (np. wydłużyć nieobecność) i utrwalić zmianę przez Save().")]
|
||
public void D2_ModyfikacjaOkresu_ZmianaIstniejacegoRekordu()
|
||
{
|
||
var pracownik = Pracownik(Pracownik_.Bujak);
|
||
var def = Kalend.DefNieobecnosci.WgNazwy[DefBezplatny] as DefinicjaNieobecnosci;
|
||
|
||
// Najpierw wprowadzamy nieobecność (stan „przed korektą").
|
||
var okresStary = new FromTo(new Date(2026, 3, 2), new Date(2026, 3, 6));
|
||
InTransaction(() =>
|
||
{
|
||
var n = Session.AddRow(new NieobecnośćPracownika(pracownik));
|
||
n.Definicja = def;
|
||
n.Okres = okresStary;
|
||
});
|
||
SaveDispose();
|
||
|
||
// Korekta wariant A: odszukujemy istniejący rekord i wydłużamy jego okres.
|
||
var okresNowy = new FromTo(new Date(2026, 3, 2), new Date(2026, 3, 11));
|
||
InTransaction(() =>
|
||
{
|
||
var pracownikE = Pracownik(Pracownik_.Bujak);
|
||
var nieobecnosc = (Nieobecnosc)pracownikE.Nieobecnosci.GetIntersectedRows(okresStary)[0];
|
||
nieobecnosc.Okres = okresNowy; // Okres jest polem zapisywalnym
|
||
});
|
||
SaveDispose();
|
||
|
||
// Po korekcie istnieje jeden rekord z wydłużonym okresem.
|
||
var pracownik2 = Pracownik(Pracownik_.Bujak);
|
||
var marzec = new FromTo(new Date(2026, 3, 1), new Date(2026, 3, 31));
|
||
var wynik = pracownik2.Nieobecnosci.GetIntersectedRows(marzec).Cast<Nieobecnosc>().ToList();
|
||
wynik.Should().ContainSingle("modyfikacja okresu nie tworzy nowego rekordu");
|
||
wynik[0].Okres.To.Should().Be(okresNowy.To, "okres został wydłużony do 2026-03-11");
|
||
}
|
||
|
||
[Test]
|
||
[Description("D2 (wariant A): zmiana typu nieobecności — pole Definicja jest zapisywalne, " +
|
||
"można podmienić rodzaj nieobecności na istniejącym rekordzie.")]
|
||
public void D2_ZmianaDefinicji_PodmieniaTypNieobecnosci()
|
||
{
|
||
var pracownik = Pracownik(Pracownik_.Strzelecki);
|
||
var def1 = Kalend.DefNieobecnosci.WgNazwy[DefBezplatny] as DefinicjaNieobecnosci;
|
||
var def2 = Kalend.DefNieobecnosci.WgNazwy[DefBezplatny2] as DefinicjaNieobecnosci;
|
||
def2.Should().NotBeNull($"definicja '{DefBezplatny2}' istnieje w bazie Demo");
|
||
|
||
var okres = new FromTo(new Date(2026, 4, 6), new Date(2026, 4, 10));
|
||
InTransaction(() =>
|
||
{
|
||
var n = Session.AddRow(new NieobecnośćPracownika(pracownik));
|
||
n.Definicja = def1;
|
||
n.Okres = okres;
|
||
});
|
||
SaveDispose();
|
||
|
||
// Korekta typu: podmiana definicji na inny rodzaj nieobecności bezpłatnej.
|
||
InTransaction(() =>
|
||
{
|
||
var pracownikE = Pracownik(Pracownik_.Strzelecki);
|
||
var def2e = Kalend.DefNieobecnosci.WgNazwy[DefBezplatny2] as DefinicjaNieobecnosci;
|
||
var nieobecnosc = (Nieobecnosc)pracownikE.Nieobecnosci.GetIntersectedRows(okres)[0];
|
||
nieobecnosc.Definicja = def2e;
|
||
});
|
||
SaveDispose();
|
||
|
||
var pracownik2 = Pracownik(Pracownik_.Strzelecki);
|
||
var wynik = pracownik2.Nieobecnosci.GetIntersectedRows(okres).Cast<Nieobecnosc>().Single();
|
||
wynik.Definicja.Nazwa.Should().Be(DefBezplatny2, "typ nieobecności został zmieniony");
|
||
}
|
||
|
||
[Test]
|
||
[Description("D2 (wariant C): korektę dodajemy konstruktorem KorektaNieobecności(nieobecność) — " +
|
||
"rekord korygujący o okresie ZAWARTYM w okresie korygowanym; po zapisie nieobecność " +
|
||
"pierwotna zostaje oznaczona flagą Korygowana=true.")]
|
||
public void D2_KorektaNieobecnosci_OznaczaNieobecnoscJakoKorygowana()
|
||
{
|
||
var pracownik = Pracownik(Pracownik_.Andrzejewski);
|
||
var def = Kalend.DefNieobecnosci.WgNazwy[DefBezplatny] as DefinicjaNieobecnosci;
|
||
|
||
var okresPierwotny = new FromTo(new Date(2026, 5, 4), new Date(2026, 5, 8));
|
||
// Stan „przed korektą": nieobecność nie jest korygowana.
|
||
InTransaction(() =>
|
||
{
|
||
var n = Session.AddRow(new NieobecnośćPracownika(pracownik));
|
||
n.Definicja = def;
|
||
n.Okres = okresPierwotny;
|
||
n.Korygowana.Should().BeFalse("świeża nieobecność nie jest jeszcze korygowana");
|
||
});
|
||
SaveDispose();
|
||
|
||
// Wariant C: rekord korekty dotyczy NieobecnośćPracownika (ctor przyjmuje korygowaną nieobecność).
|
||
// UWAGA: okres korekty jest OGRANICZONY do okresu nieobecności korygowanej (KorygowanyOkresException
|
||
// przy próbie wyjścia poza), dlatego okres korekty musi być PODZBIOREM okresu pierwotnego.
|
||
var okresKorekty = new FromTo(new Date(2026, 5, 4), new Date(2026, 5, 6));
|
||
InTransaction(() =>
|
||
{
|
||
var pracownikE = Pracownik(Pracownik_.Andrzejewski);
|
||
var nPrac = (NieobecnośćPracownika)pracownikE.Nieobecnosci.GetIntersectedRows(okresPierwotny)[0];
|
||
|
||
var korekta = Session.AddRow(new KorektaNieobecności(nPrac));
|
||
korekta.Definicja = nPrac.Definicja;
|
||
korekta.Okres = okresKorekty;
|
||
|
||
// KorektaNieobecności dziedziczy po Nieobecnosc.
|
||
(korekta is Nieobecnosc).Should().BeTrue("KorektaNieobecności jest rodzajem Nieobecnosc");
|
||
});
|
||
SaveDispose();
|
||
|
||
// Po korekcie nieobecność pierwotna istnieje i jest oznaczona jako korygowana.
|
||
// (Dla nieobecności bez wyliczeń płacowych — jak urlop bezpłatny — sam rekord korekty nie tworzy
|
||
// drugiego, samodzielnego wpisu w kolekcji Nieobecnosci; obserwowalnym efektem jest flaga Korygowana.)
|
||
var pracownik2 = Pracownik(Pracownik_.Andrzejewski);
|
||
var maj = new FromTo(new Date(2026, 5, 1), new Date(2026, 5, 31));
|
||
var rekordy = pracownik2.Nieobecnosci.GetIntersectedRows(maj).Cast<Nieobecnosc>().ToList();
|
||
rekordy.Should().ContainSingle("nieobecność pierwotna nadal istnieje w kolekcji")
|
||
.Which.Korygowana.Should().BeTrue("po dodaniu korekty nieobecność jest oznaczona jako korygowana");
|
||
}
|
||
|
||
[Test]
|
||
[Ignore("Worker UstalPonowniePodstawęNaliczaniaWorker (D2 wariant B) jest aktywny tylko dla zwolnień " +
|
||
"ZUS / urlopów macierzyńskich (IsEnabledPonownieUstalPodstawę), a FAKTYCZNE przeliczenie kwot " +
|
||
"zasiłku następuje dopiero przy ponownym naliczeniu wypłaty (mechanizm PodstawaZasilku). Na bazie " +
|
||
"Demo z rollbackiem, bez pełnego scenariusza naliczenia listy płac, nie da się sensownie zweryfikować " +
|
||
"efektu workera. LUKA w pracownik.md D2: dokument nie podaje minimalnego, wykonalnego scenariusza " +
|
||
"naliczenia wypłaty pozwalającego zweryfikować przeliczenie podstawy.")]
|
||
[Description("D2 (wariant B): czynność 'Ustal ponownie podstawę naliczania' przez worker — " +
|
||
"niewykonalna na samej korekcie rekordu bez naliczonej wypłaty.")]
|
||
public void D2_PonowneUstaleniePodstawy_PrzezWorker_Niewykonalne()
|
||
{
|
||
// Pozostawione jako [Ignore] — patrz uzasadnienie w atrybucie.
|
||
}
|
||
|
||
// ============================== D7 — Analiza limitów urlopowych ==============================
|
||
|
||
[Test]
|
||
[Description("D7: limit urlopowy NIE jest tworzony ręcznie — najpierw naliczamy go " +
|
||
"NaliczanieLimitow.DodajLimit(), potem odczytujemy z pracownik.Limity; arytmetyka " +
|
||
"Wykorzystane == Razem - Pozostalo jest spójna.")]
|
||
public void D7_NaliczenieLimitu_TworzyLimitDoOdczytu()
|
||
{
|
||
var pracownik = Pracownik(Pracownik_.Andrzejewski);
|
||
var defLimit = Kalend.DefinicjeLimitow.WgNazwy[DefUrlopWyp] as DefinicjaLimitu;
|
||
defLimit.Should().NotBeNull($"definicja limitu '{DefUrlopWyp}' istnieje w bazie Demo");
|
||
|
||
var rok = FromTo.Year(new Date(2026, 1, 1));
|
||
|
||
InTransaction(() =>
|
||
{
|
||
// NaliczanieLimitow: publiczny bezparametrowy ctor; Params(Context) z bieżącej sesji testu.
|
||
var naliczanie = new NaliczanieLimitow
|
||
{
|
||
Pars = new NaliczanieLimitow.Params(Context)
|
||
{
|
||
Definicja = defLimit,
|
||
Okres = rok,
|
||
KopiujKorekty = true
|
||
},
|
||
Pracownicy = new[] { pracownik }
|
||
};
|
||
naliczanie.DodajLimit(); // tworzy/aktualizuje rekordy LimitNieobecnosci
|
||
});
|
||
SaveDispose();
|
||
|
||
// Odczyt limitu — filtr serwerowy po kolekcji child pracownika TYLKO po Definicja
|
||
// (porównanie FromTo == FromTo nie jest tłumaczone na zapytanie serwerowe — okres filtrujemy w pamięci).
|
||
var pracownik2 = Pracownik(Pracownik_.Andrzejewski);
|
||
var defLimit2 = Kalend.DefinicjeLimitow.WgNazwy[DefUrlopWyp] as DefinicjaLimitu;
|
||
var lim = pracownik2.Limity[(LimitNieobecnosci l) => l.Definicja == defLimit2]
|
||
.Cast<LimitNieobecnosci>()
|
||
.FirstOrDefault(l => l.Okres.From == rok.From);
|
||
|
||
lim.Should().NotBeNull("naliczenie utworzyło limit urlopu wypoczynkowego na 2026");
|
||
// „Przysługujący" to Razem (limit kodeksowy + przeniesienia + zmiany), wykorzystany = Razem - Pozostalo.
|
||
// Uwaga: dla syntetycznych pracowników Demo Razem bywa 0 (brak danych stażu/urodzenia napędzających 20/26 dni),
|
||
// dlatego sprawdzamy spójność arytmetyki, a nie konkretną dodatnią wartość.
|
||
(lim!.Razem - lim.Pozostalo).Should().Be(lim.Wykorzystane,
|
||
"wykorzystany = przysługujący - pozostały (== pole Wykorzystane)");
|
||
lim.Razem.Should().BeGreaterThanOrEqualTo(0, "przysługujący limit nie jest ujemny");
|
||
}
|
||
|
||
[Test]
|
||
[Description("D7: wprowadzenie urlopu wypoczynkowego wymaga ISTNIEJĄCEGO limitu na ten dzień — ustawienie " +
|
||
"Okres na nieobecności urlopowej wyzwala przeliczenie limitu; po wcześniejszym naliczeniu " +
|
||
"limitu zapis przechodzi bez LimitNotFoundException, a limit jest odczytywalny.")]
|
||
public void D7_UrlopWypoczynkowy_WymagaNaliczonegoLimitu()
|
||
{
|
||
var defLimit = Kalend.DefinicjeLimitow.WgNazwy[DefUrlopWyp] as DefinicjaLimitu;
|
||
var rok = FromTo.Year(new Date(2026, 1, 1));
|
||
|
||
// 1) Najpierw nalicz limit za rok — to warunek konieczny dla urlopu wypoczynkowego.
|
||
InTransaction(() =>
|
||
{
|
||
var pracownikE = Pracownik(Pracownik_.Bednarek);
|
||
var naliczanie = new NaliczanieLimitow
|
||
{
|
||
Pars = new NaliczanieLimitow.Params(Context)
|
||
{
|
||
Definicja = defLimit,
|
||
Okres = rok,
|
||
KopiujKorekty = true
|
||
},
|
||
Pracownicy = new[] { pracownikE }
|
||
};
|
||
naliczanie.DodajLimit();
|
||
});
|
||
SaveDispose();
|
||
|
||
// 2) Dopiero teraz wprowadzenie urlopu wypoczynkowego nie rzuca LimitNotFoundException
|
||
// (definicje pobieramy ponownie w bieżącej sesji — po SaveDispose poprzednie są z innej sesji).
|
||
InTransaction(() =>
|
||
{
|
||
var pracownikE = Pracownik(Pracownik_.Bednarek);
|
||
var defUrlop = Kalend.DefNieobecnosci.WgNazwy[DefUrlopWyp] as DefinicjaNieobecnosci;
|
||
var n = Session.AddRow(new NieobecnośćPracownika(pracownikE));
|
||
n.Definicja = defUrlop;
|
||
n.Okres = new FromTo(new Date(2026, 6, 1), new Date(2026, 6, 5));
|
||
});
|
||
SaveDispose();
|
||
|
||
// 3) Odczyt: limit istnieje i jest spójny; nieobecność urlopowa została zapisana.
|
||
var pracownik2 = Pracownik(Pracownik_.Bednarek);
|
||
var defLimit2 = Kalend.DefinicjeLimitow.WgNazwy[DefUrlopWyp] as DefinicjaLimitu;
|
||
var lim = pracownik2.Limity[(LimitNieobecnosci l) => l.Definicja == defLimit2]
|
||
.Cast<LimitNieobecnosci>()
|
||
.FirstOrDefault(l => l.Okres.From == rok.From);
|
||
lim.Should().NotBeNull("limit urlopu wypoczynkowego za 2026 został naliczony");
|
||
lim!.Wykorzystane.Should().Be(lim.Razem - lim.Pozostalo,
|
||
"wykorzystany odczytany z pola jest spójny z Razem - Pozostalo");
|
||
|
||
var czerwiec = new FromTo(new Date(2026, 6, 1), new Date(2026, 6, 30));
|
||
pracownik2.Nieobecnosci.GetIntersectedRows(czerwiec).Cast<Nieobecnosc>()
|
||
.Should().ContainSingle("urlop wypoczynkowy został zapisany po naliczeniu limitu");
|
||
}
|
||
}
|