using System; using System.Linq; using AwesomeAssertions; using NUnit.Framework; using Soneta.Business; using Soneta.Kadry; using Soneta.Types; using Prac = Soneta.Kadry.Pracownik; namespace Soneta.Skills.Test.KadryPlace.Pracownik; /// /// Rozdział A — „Pracownik: zatrudnienie i dane kartotekowe" (receptury A1, A2, A7, A9, A10, A14). /// /// Testy są wykonywalną dokumentacją publicznego kontraktu platformy Soneta dla domeny /// Kadry/Płace. Każda metoda mapuje się 1:1 do receptury z dokumentu skilla pracownik.md i /// pokazuje realny model „root + historia": Pracownik (tabela Pracownicy) trzyma tylko /// nieliczne pola niezmienne, a praktycznie wszystkie dane kadrowe siedzą w zapisach historycznych /// PracHistoria (kolekcja Pracownik.Historia), w tym złożone pole Etat. /// /// /// Wszystko działa na bazie Demo (GoldStandard) z automatycznym rollbackiem po teście, więc można /// swobodnie tworzyć i modyfikować dane. Operujemy wyłącznie na publicznym kontrakcie — tak /// jak dodatek programisty zewnętrznego bez dostępu do kodu źródłowego aplikacji. /// /// [TestFixture] public class RozdzialA_PracownikTest : PracownikTestBase { // ============================== A1 — Zatrudnienie nowego pracownika ============================== [Test] [Description("A1: dodanie nowego PracownikFirmy (AddRow) automatycznie tworzy pierwszy zapis " + "historii (Last); dane osobowe ustawiamy na Last; Save() utrwala pracownika.")] public void A1_ZatrudnienieNowego_TworzyPierwszyZapisHistorii_IZapisuje() { Guid guid = Guid.Empty; var kod = "A1_" + Guid.NewGuid().ToString("N").Substring(0, 6); InTransaction(() => { // Pracownik jest typem ABSTRAKCYJNYM — tworzymy konkretny PracownikFirmy. // AddRow zwraca typowany wiersz; w OnAdded powstaje pierwszy PracHistoria + kalendarz, // dlatego NIE tworzymy zapisu historii ręcznie — od razu mamy pracownik.Last. var pracownik = Session.AddRow(new PracownikFirmy()); pracownik.Kod = kod; // pole rootu // Mechanizm „root + historia": dane osobowe idą na ZAPIS historii (pierwszy zapis = Last), // nie na root. Last istnieje już bezpośrednio po AddRow. pracownik.Last.Should().NotBeNull("OnAdded tworzy pierwszy zapis historii (Last)"); pracownik.Last.Nazwisko = "Kowalska"; pracownik.Last.Imie = "Gabriela"; pracownik.Last.PESEL = "94010812345"; guid = pracownik.Guid; }); // Save w osobnej sesji-zamknięciu: wykrywanie konfliktów/duplikatów dzieje się w Save(). SaveDispose(); // Odczyt na świeżej sesji po Guid — potwierdza utrwalenie pracownika i jego pierwszego zapisu. var zapis = Get(guid); zapis.Should().NotBeNull("pracownik został zapisany do bazy"); zapis.Kod.Should().Be(kod); zapis.Last.Should().NotBeNull("nadal istnieje pierwszy zapis historii"); zapis.Last.Nazwisko.Should().Be("Kowalska"); zapis.Last.Imie.Should().Be("Gabriela"); // Pierwszy zapis historii ma okres otwarty (do końca czasu). zapis.Last.Aktualnosc.To.Should().Be(Date.MaxValue, "pierwszy zapis ma okres otwarty"); } [Test] [Description("A1: typ Pracownik jest abstrakcyjny — konkretnym typem zatrudnienia jest PracownikFirmy " + "(dziedziczy po Pracownik); to on jest dodawany do kartoteki.")] public void A1_PracownikFirmy_JestKonkretnymTypemPracownika() { // Dokumentujemy regułę z receptury: new Pracownik() jest niemożliwe (typ abstrakcyjny), // więc używamy PracownikFirmy. Sprawdzamy relację dziedziczenia bez instancjonowania abstrakta. typeof(Prac).IsAbstract.Should().BeTrue("Pracownik jest klasą abstrakcyjną"); typeof(Prac).IsAssignableFrom(typeof(PracownikFirmy)) .Should().BeTrue("PracownikFirmy jest konkretnym typem pracownika firmy"); } // ============================== A2 — Podstawowe dane kadrowe ============================== [Test] [Description("A2: dane ewidencyjno-identyfikacyjne (NazwiskoRodowe, ImieOjca, NIP, Urodzony, " + "Obywatelstwo, Adres) ustawiamy na zapisie historii (Last) oraz jego subrowach.")] public void A2_DaneKadrowe_UstawianeNaZapisieHistorii_ISubrowach() { Guid guid = Guid.Empty; var kod = "A2_" + Guid.NewGuid().ToString("N").Substring(0, 6); InTransaction(() => { var pracownik = Session.AddRow(new PracownikFirmy()); pracownik.Kod = kod; var ph = pracownik.Last; // bieżący (ostatni) zapis kadrowy ph.Nazwisko = "Nowak"; ph.Imie = "Anna"; ph.NazwiskoRodowe = "Wiśniewska"; ph.ImieOjca = "Jan"; ph.NIP = "1234563218"; // Urodzony, Obywatelstwo, Adres to SUBROWY (pola złożone) — modyfikujemy ich pola, // nie przypisujemy całego obiektu. ph.Urodzony.Data = new Date(1994, 1, 8); ph.Urodzony.Miejsce = "Kraków"; ph.Obywatelstwo.Nazwa = "polskie"; ph.Adres.Ulica = "Wadowicka"; ph.Adres.NrDomu = "8A"; ph.Adres.KodPocztowyS = "30-415"; // wersja stringowa kodu (z myślnikiem) ph.Adres.Miejscowosc = "Kraków"; guid = pracownik.Guid; }); SaveDispose(); var ph2 = Get(guid).Last; ph2.NazwiskoRodowe.Should().Be("Wiśniewska"); ph2.ImieOjca.Should().Be("Jan"); ph2.NIP.Should().Be("1234563218"); ph2.Urodzony.Data.Should().Be(new Date(1994, 1, 8)); ph2.Urodzony.Miejsce.Should().Be("Kraków"); ph2.Obywatelstwo.Nazwa.Should().Be("polskie"); ph2.Adres.Ulica.Should().Be("Wadowicka"); ph2.Adres.NrDomu.Should().Be("8A"); ph2.Adres.Miejscowosc.Should().Be("Kraków"); // KodPocztowyS to string z myślnikiem; KodPocztowy to int (bez myślnika). ph2.Adres.KodPocztowyS.Should().Be("30-415"); ph2.Adres.KodPocztowy.Should().Be(30415); } [Test] [Description("A2: Plec to enum PłećOsoby; przy poprawnym numerze PESEL płeć jest wyliczana " + "automatycznie przez weryfikator (parzysta cyfra przed kontrolną = kobieta).")] public void A2_Plec_WyliczanaZPESEL() { var p = PierwszyPracownik(); // Pracownik etatowy z Demo ma ustawiony PESEL — płeć powinna być jedną z wartości enuma. p.Last.Plec.Should().BeOneOf(PłećOsoby.Kobieta, PłećOsoby.Mężczyzna); } // ============================== A7 — Ubezpieczenia społeczne i zdrowotne ============================== [Test] [Description("A7: tytuł ubezpieczenia (Tyub4) to rekord słownika TytulyUbezpiecz4 pobierany " + "WgKodu[int]; daty objęcia ubezpieczeniami społecznymi ustawiamy na subrowach Spoleczne.")] public void A7_Ubezpieczenia_UstawiajaTytulIDatyObjecia() { // Tytuł ubezpieczenia jest rekordem słownika KONFIGURACYJNEGO o kluczu int (np. 110 = pracownik). // Wymaga, by w bazie Demo istniał wpis o tym kodzie — w przeciwnym razie pomijamy część tytułu. var tyub110 = Kadry.TytulyUbezpiecz4.WgKodu[110] as TytulUbezpieczenia4; Guid guid = Guid.Empty; var kod = "A7_" + Guid.NewGuid().ToString("N").Substring(0, 6); var od = new Date(2026, 1, 1); InTransaction(() => { var pracownik = Session.AddRow(new PracownikFirmy()); pracownik.Kod = kod; pracownik.Last.Nazwisko = "Ubezpieczony"; // Nazwisko jest wymagane przy Save pracownik.Last.Imie = "Tomasz"; // Cała struktura ubezpieczeń jest HISTORYCZNA — siedzi w Etat danego zapisu (Last.Etat). var ub = pracownik.Last.Etat.Ubezpieczenia; if (tyub110 != null) ub.Tyub4 = tyub110; // tytuł ubezpieczenia (słownik), klucz int // Data objęcia ubezpieczeniami społecznymi (publiczny setter na subrowie Ubezpieczenia). // UWAGA: na poszczególnych subrowach Spoleczne (Emerytalne/Rentowe) pole `Od` NIE ma // publicznego settera — jest wyliczane. Publicznie ustawiamy flagi Obowiazkowe/Dobrowolne // oraz datę ObowiazkoweOd na zbiorczym subrowie Ubezpieczenia. ub.ObowiazkoweOd = od; ub.Emerytalne.Obowiazkowe = true; ub.Rentowe.Obowiazkowe = true; // Oddział NFZ to subrow — ustawiamy jego pola (np. datę OdDnia), nie cały obiekt. pracownik.Last.OddzialNFZ.OdDnia = od; guid = pracownik.Guid; }); SaveDispose(); var ub2 = Get(guid).Last.Etat.Ubezpieczenia; ub2.ObowiazkoweOd.Should().Be(od); ub2.Emerytalne.Obowiazkowe.Should().BeTrue(); ub2.Rentowe.Obowiazkowe.Should().BeTrue(); if (tyub110 != null) ub2.Tyub4.Should().NotBeNull("ustawiliśmy tytuł ubezpieczenia ze słownika"); } [Test] [Description("A7 (odczyt): tytuł ubezpieczenia obowiązujący na dzień odczytujemy metodą " + "Ubezpieczenia.WyliczTyubNaDzień(Date) — bez modyfikacji danych.")] public void A7_WyliczTyubNaDzien_ZwracaTytulNaDzien() { // Odczyt „na dzień" dla istniejącego pracownika z Demo (zatrudniony etatowo). var p = PierwszyPracownik(); var data = Date.Today; // pracownik[data] zwraca zapis obowiązujący na datę; z jego Etat.Ubezpieczenia liczymy tytuł. var zapisNaDzis = p[data]; zapisNaDzis.Should().NotBeNull("pracownik etatowy z Demo ma zapis obowiązujący na dziś"); // Metoda zwraca rekord TytulUbezpieczenia (starszy typ tytułu); może być null, gdy // pracownik nie ma określonego tytułu na tę datę — istotne, że metoda działa bez wyjątku. System.Action odczyt = () => { TytulUbezpieczenia _ = zapisNaDzis.Etat.Ubezpieczenia.WyliczTyubNaDzień(data); }; odczyt.Should().NotThrow("WyliczTyubNaDzień(Date) to publiczny odczyt tytułu na dzień"); } // ============================== A9 — Rodzina pracownika (ZCNA) ============================== [Test] [Description("A9: członka rodziny tworzymy konstruktorem CzlonekRodziny(pracownik); zgłoszenie do " + "ubezpieczenia zdrowotnego (ZCNA) to Ubezpieczony=true + UbezpieczenieOkres + StPokrewienstwa.")] public void A9_CzlonekRodziny_ZglaszanyDoUbezpieczeniaZdrowotnego() { Guid guidPrac = Guid.Empty; var kod = "A9_" + Guid.NewGuid().ToString("N").Substring(0, 6); var od = new Date(2026, 1, 1); InTransaction(() => { var pracownik = Session.AddRow(new PracownikFirmy()); pracownik.Kod = kod; pracownik.Last.Nazwisko = "Kowalski"; pracownik.Last.Imie = "Adam"; // Konstruktor CzlonekRodziny(pracownik) wiąże rekord z pracownikiem; AddRow włącza go do sesji. var dziecko = Session.AddRow(new CzlonekRodziny(pracownik)); dziecko.Nazwisko = "Kowalska"; dziecko.Imie = "Zofia"; dziecko.PESEL = "20290512345"; dziecko.Urodzony.Data = new Date(2020, 9, 5); // Urodzony to subrow dziecko.StPokrewienstwa = KodStPokrewienstwa.Dziecko; // enum stopnia pokrewieństwa // Zgłoszenie do ubezpieczenia zdrowotnego (ZCNA): dziecko.Ubezpieczony = true; dziecko.UbezpieczenieOkres = new FromTo(od, Date.MaxValue); dziecko.NaUtrzymaniu = true; guidPrac = pracownik.Guid; }); SaveDispose(); var pracownik2 = Get(guidPrac); // CzlonekRodziny pojawia się w kolekcji Rodzina pracownika (płaski child, nie historyczny). var rodzina = pracownik2.Rodzina.Cast().ToList(); rodzina.Should().ContainSingle("dodaliśmy jednego członka rodziny"); var cr = rodzina[0]; cr.Imie.Should().Be("Zofia"); cr.StPokrewienstwa.Should().Be(KodStPokrewienstwa.Dziecko); cr.Ubezpieczony.Should().BeTrue(); cr.NaUtrzymaniu.Should().BeTrue(); cr.UbezpieczenieOkres.From.Should().Be(od); // Odczyt aktualnie ubezpieczonych członków rodziny — filtr serwerowy po kolekcji (lambda). var ubezpieczeni = pracownik2.Rodzina[(CzlonekRodziny c) => c.Ubezpieczony].Cast().ToList(); ubezpieczeni.Should().ContainSingle("jedyny członek rodziny jest zgłoszony do ubezpieczenia"); } // ============================== A10 — Poprzednie miejsca pracy ============================== [Test] [Description("A10: poprzedniego pracodawcę dodajemy konkretnym typem HistoriaZatrudnienia(pracownik) " + "do kolekcji HistoriaZatrudnienia (inna niż Historia bieżącego zatrudnienia).")] public void A10_PoprzedniPracodawca_DodawanyDoHistoriiZatrudnienia() { Guid guidPrac = Guid.Empty; var kod = "A10_" + Guid.NewGuid().ToString("N").Substring(0, 6); var okres = new FromTo(new Date(2018, 3, 1), new Date(2025, 12, 31)); InTransaction(() => { var pracownik = Session.AddRow(new PracownikFirmy()); pracownik.Kod = kod; pracownik.Last.Nazwisko = "Zieliński"; pracownik.Last.Imie = "Piotr"; // HistoriaZatrudnieniaBase ma ctor protected — tworzymy konkretny typ: // HistoriaZatrudnienia (poprzedni pracodawca; ctor ustawia Typ = Zatrudnienie). var hz = Session.AddRow(new HistoriaZatrudnienia(pracownik)); hz.Nazwa = "Poprzednia Firma Sp. z o.o."; hz.Okres = okres; hz.EfektywnyOkres = okres; // to EfektywnyOkres decyduje o wliczeniu do stażu hz.Adres1 = "ul. Główna 1, Kraków"; // Drugi typ wpisu: okres nauki (UkonczonaSzkola) — także child pracownika. var szkola = Session.AddRow(new UkonczonaSzkola(pracownik)); szkola.Nazwa = "Technikum nr 1"; szkola.Okres = new FromTo(new Date(2014, 9, 1), new Date(2018, 6, 30)); guidPrac = pracownik.Guid; }); SaveDispose(); var pracownik2 = Get(guidPrac); // HistoriaZatrudnienia to kolekcja stażu u POPRZEDNICH pracodawców (typ bazowy w kolekcji). var wpisy = pracownik2.HistoriaZatrudnienia.Cast().ToList(); wpisy.Should().HaveCount(2, "dodaliśmy wpis pracy i wpis nauki"); var praca = wpisy.OfType().Single(); praca.Nazwa.Should().Be("Poprzednia Firma Sp. z o.o."); // FromTo implementuje IEnumerable, więc porównujemy granice okresu, nie cały obiekt. praca.Okres.From.Should().Be(okres.From); praca.Okres.To.Should().Be(okres.To); praca.EfektywnyOkres.From.Should().Be(okres.From); praca.EfektywnyOkres.To.Should().Be(okres.To); // Typ jest ustawiany przez ctor konkretnej klasy (praca vs nauka) — dwa różne wpisy. wpisy.OfType().Should().ContainSingle("jeden wpis nauki"); } // ============================== A14 — Aktualizacja historyczna „od daty" vs korekta ============================== [Test] [Description("A14: zmiana warunkow 'od daty' - Historia.Update(date) klonuje zapis i skraca stary; " + "nowy klon dodajemy do tabeli PracHistorie i ustawiamy na nim zmienione warunki.")] public void A14_AktualizacjaOdDaty_TworzyNowyZapisOdDnia_ISkracaStary() { Guid guidPrac = Guid.Empty; var odDnia = new Date(2026, 7, 1); InTransaction(() => { var pracownik = Session.AddRow(new PracownikFirmy()); pracownik.Kod = "A14_" + Guid.NewGuid().ToString("N").Substring(0, 6); pracownik.Last.Nazwisko = "Aktualizowany"; pracownik.Last.Imie = "Marek"; // Stan „przed zmianą" na pierwszym zapisie (pola pewnie zapisywalne na świeżym zapisie). pracownik.Last.Etat.MiejscePracy = "Kraków"; pracownik.Last.Podatki.UlgaMnoznik = 0m; // 1) Update klonuje zapis aktualny na odDnia, skraca stary do dnia poprzedniego // i zwraca NOWY klon z okresem od odDnia. var nowy = pracownik.Historia.Update(odDnia); // 2) Update + AddRow to nierozłączna para — bez AddRow klon zostaje „odpięty". pracownik.Module.PracHistorie.AddRow(nowy); // 3) Na nowym zapisie wprowadzamy zmienione warunki (obowiązujące od odDnia). nowy.Etat.MiejscePracy = "Warszawa"; // zmiana miejsca pracy od odDnia nowy.Podatki.UlgaMnoznik = 1m; // zmiana danych podatkowych od odDnia guidPrac = pracownik.Guid; }); SaveDispose(); var pracownik2 = Get(guidPrac); // Mamy teraz dwa zapisy: stary (do odDnia-1) i nowy (od odDnia). var zapisy = pracownik2.Historia.Cast().OrderBy(h => h.Aktualnosc.From).ToList(); zapisy.Should().HaveCount(2, "Update utworzył drugi zapis historii"); var stary = zapisy[0]; var nowy2 = zapisy[1]; // Stary zapis został skrócony do dnia poprzedzającego zmianę. stary.Aktualnosc.To.Should().Be(odDnia.AddDays(-1)); nowy2.Aktualnosc.From.Should().Be(odDnia, "nowy zapis obowiązuje od wskazanego dnia"); // Warunki różnią się między okresami: inne miejsce pracy i ulga przed/od zmiany. stary.Etat.MiejscePracy.Should().Be("Kraków"); nowy2.Etat.MiejscePracy.Should().Be("Warszawa"); stary.Podatki.UlgaMnoznik.Should().Be(0m); nowy2.Podatki.UlgaMnoznik.Should().Be(1m); } [Test] [Description("A14 (odczyt na dzień): indeksator pracownik[date] zwraca zapis obowiązujący na datę " + "(Aktualnosc zawiera date), a dla daty sprzed zatrudnienia zwraca null.")] public void A14_OdczytNaDzien_ZwracaWlasciwyZapis_INullPrzedZatrudnieniem() { Guid guidPrac = Guid.Empty; var odDnia = new Date(2026, 7, 1); InTransaction(() => { var pracownik = Session.AddRow(new PracownikFirmy()); pracownik.Kod = "A14r_" + Guid.NewGuid().ToString("N").Substring(0, 6); pracownik.Last.Nazwisko = "Czytany"; pracownik.Last.Imie = "Ewa"; var nowy = pracownik.Historia.Update(odDnia); pracownik.Module.PracHistorie.AddRow(nowy); nowy.NazwiskoRodowe = "PoZmianie"; guidPrac = pracownik.Guid; }); SaveDispose(); var pracownik2 = Get(guidPrac); var pierwszy = pracownik2.Historia.GetFirst(); // najstarszy zapis (okres do odDnia-1) // Odczyt „na dzień": data wewnątrz okresu pierwszego zapisu → zwraca pierwszy zapis. var dzienWStarymOkresie = pierwszy.Aktualnosc.From; pracownik2[dzienWStarymOkresie].Should().BeSameAs(pierwszy, "pracownik[date] zwraca zapis, którego Aktualnosc zawiera date"); // Data w okresie nowego zapisu → zwraca nowy (najświeższy) zapis = Last. pracownik2[odDnia].Should().BeSameAs(pracownik2.Last, "od odDnia obowiązuje nowy zapis (Last)"); // Data sprzed zatrudnienia (przed początkiem pierwszego zapisu) → brak zapisu (null). if (pierwszy.Aktualnosc.From > Date.MinValue) { var przedZatrudnieniem = pierwszy.Aktualnosc.From.AddDays(-1); pracownik2[przedZatrudnieniem].Should().BeNull( "dla daty sprzed zatrudnienia nie ma zapisu historii"); } } [Test] [Description("A14 (korekta): modyfikacja zapisu pracownik[date] BEZ Update/AddRow zmienia dane " + "w CAŁYM okresie tego zapisu — nie tworzy nowego okresu.")] public void A14_Korekta_ZmieniaIstniejacyZapis_BezNowegoOkresu() { Guid guidPrac = Guid.Empty; InTransaction(() => { var pracownik = Session.AddRow(new PracownikFirmy()); pracownik.Kod = "A14k_" + Guid.NewGuid().ToString("N").Substring(0, 6); pracownik.Last.Nazwisko = "Korygowany"; pracownik.Last.Imie = "Jan"; guidPrac = pracownik.Guid; }); SaveDispose(); // Korekta: modyfikujemy zapis obowiązujący na wskazaną datę — bez Update, bez AddRow. InTransaction(() => { var ph = Get(guidPrac)[Date.Today]; ph.Should().NotBeNull(); ph.NazwiskoRodowe = "PoprawioneNazwisko"; // korekta w istniejącym okresie }); SaveDispose(); var pracownik2 = Get(guidPrac); // Liczba zapisów się nie zmieniła — korekta nie tworzy nowego okresu. pracownik2.Historia.Cast().Should().ContainSingle("korekta nie dzieli okresu"); pracownik2.Last.NazwiskoRodowe.Should().Be("PoprawioneNazwisko"); } }