using System;
using System.Linq;
using AwesomeAssertions;
using NUnit.Framework;
using Soneta.Business;
using Soneta.Core;
using Soneta.CRM;
using Soneta.Kadry;
using Soneta.Types;
using Prac = Soneta.Kadry.Pracownik;
namespace Soneta.Skills.Test.KadryPlace.Pracownik;
///
/// Rozdział A (część kartotekowa) — pozostałe receptury danych osobowych/kadrowych pracownika:
/// A3 (adresy), A4 (kontakt), A5 (rachunki — odczyt), A6 (PIT), A8 (ZUS/NFZ), A11 (wykształcenie/
/// języki/wojsko), A12 (GUS/kod zawodu), A13 (PFRON), A15 (odczyt „na dzień"), A16 (powiązanie
/// z kontrahentem), A17 (archiwum — workery), A18 (zwolnienie), A19 (przerejestrowanie — zmiana Tyub4).
///
/// Testy są wykonywalną dokumentacją publicznego kontraktu Soneta dla domeny Kadry/Płace.
/// Operujemy wyłącznie na publicznym API — jak dodatek zewnętrzny bez dostępu do kodu źródłowego.
/// Wszystko działa na bazie Demo (GoldStandard) z rollbackiem po teście. Wartości enumów i klucze
/// słowników pobieramy/weryfikujemy dynamicznie, nie zgadujemy.
///
///
[TestFixture]
public class RozdzialArest_KartotekaTest : PracownikTestBase
{
// Helper: świeży pracownik z danymi osobowymi (Last istnieje od razu po AddRow).
private Prac NowyPracownik(string prefix, out Guid guid)
{
var pracownik = Session.AddRow(new PracownikFirmy());
pracownik.Kod = prefix + "_" + Guid.NewGuid().ToString("N").Substring(0, 6);
pracownik.Last.Nazwisko = "Testowy"; // Nazwisko wymagane przy Save
pracownik.Last.Imie = "Jan";
guid = pracownik.Guid;
return pracownik;
}
// Helper: pracownik z USTAWIONYM etatem. Cały subrow Etat jest tylko-do-odczytu, dopóki nie
// ustawi się Etat.Okres (bramka, patrz B1). Po jego ustawieniu pracownik staje się etatowy, więc
// Save wymaga Etat.Wydzial ORAZ Etat.Stanowisko — ustawiamy oba.
private Prac NowyPracownikEtatowy(string prefix, out Guid guid)
{
var pracownik = NowyPracownik(prefix, out guid);
var etat = pracownik.Last.Etat;
etat.Okres = new FromTo(new Date(2026, 1, 1), Date.MaxValue); // PIERWSZE — odblokowuje Etat
etat.Wydzial = Kadry.Wydzialy.Firma; // wymagane dla etatu
etat.Stanowisko = "Specjalista"; // wymagane dla etatu
return pracownik;
}
// ============================== A3 — Adresy ==============================
[Test]
[Description("A3: adresy (zameldowania/zamieszkania/korespondencyjny) to subrowy Soneta.Core.Adres " +
"na zapisie historii (Last) — modyfikujemy ich pola, nie przypisujemy całego obiektu.")]
public void A3_Adresy_SaSubrowamiNaZapisieHistorii()
{
var g = Guid.Empty;
InTransaction(() =>
{
var ph = NowyPracownik("A3", out g).Last;
ph.AdresZamieszkania.Miejscowosc = "Kraków";
ph.AdresZamieszkania.Ulica = "Wadowicka";
ph.AdresZamieszkania.NrDomu = "8A";
ph.AdresZamieszkania.NrLokalu = "12";
ph.AdresZamieszkania.KodPocztowyS = "30-415";
ph.AdresZameldowania.Miejscowosc = "Wieliczka";
ph.AdresDoKorespondencji.Miejscowosc = "Kraków";
});
SaveDispose();
var ph2 = Get(g).Last;
ph2.AdresZamieszkania.Ulica.Should().Be("Wadowicka");
ph2.AdresZamieszkania.NrDomu.Should().Be("8A");
ph2.AdresZamieszkania.KodPocztowyS.Should().Be("30-415");
ph2.AdresZamieszkania.KodPocztowy.Should().Be(30415); // int (bez myślnika)
ph2.AdresZameldowania.Miejscowosc.Should().Be("Wieliczka");
ph2.AdresDoKorespondencji.Miejscowosc.Should().Be("Kraków");
// Odczyt adresu na dzień:
Adres adr = Get(g)[Date.Today].AdresZamieszkania;
adr.Miejscowosc.Should().Be("Kraków");
}
// ============================== A4 — Dane kontaktowe ==============================
[Test]
[Description("A4: dane kontaktowe (EMAIL/TelefonKomorkowy/WWW) to subrow Soneta.Core.Kontakt " +
"na zapisie historii — pole nazywa się EMAIL (wielkie litery).")]
public void A4_Kontakt_EmailTelefonWWW_NaSubrowieKontakt()
{
var g = Guid.Empty;
InTransaction(() =>
{
var k = NowyPracownik("A4", out g).Last.Kontakt; // subrow Kontakt
k.EMAIL = "g.kowalska@firma.pl";
k.TelefonKomorkowy = "600100200";
k.WWW = "https://firma.pl/g.kowalska";
});
SaveDispose();
var k2 = Get(g).Last.Kontakt;
k2.EMAIL.Should().Be("g.kowalska@firma.pl");
k2.TelefonKomorkowy.Should().Be("600100200");
k2.WWW.Should().Be("https://firma.pl/g.kowalska");
}
[Test]
[Description("A4 (dostęp WWW/Pulpity): konto operatora web (IWebOperator) NIE jest zwykłym " +
"zapisywalnym polem PracHistoria — zarządza nim osobny mechanizm operatorów modułu web.")]
[Ignore("Dostęp do Pulpitów (IWebOperator) to osobny mechanizm operatorów/uprawnień web, " +
"nie pole kartoteki kadrowej — poza publicznym kontraktem ustawiania pól na pracowniku.")]
public void A4_DostepWWW_PulpityToOsobnyMechanizm()
{
}
// ============================== A5 — Rachunki bankowe (ODCZYT) ==============================
[Test]
[Description("A5 (odczyt): rachunki pracownika to kolekcja Pracownik.Rachunki " +
"(SubTable); rachunek główny zwraca Pracownik.DomyslnyRachunek.")]
public void A5_Rachunki_OdczytKolekcjiIRachunkuGlownego()
{
// Czysty odczyt na pracowniku z Demo — bez tworzenia rachunku (ctor numeru rachunku to typ
// biznesowy z walidacją IBAN/NRB, poza prostym kontraktem ustawiania pól).
var p = PierwszyPracownik();
// API odczytu istnieje i nie rzuca — kolekcja i property domyślnego rachunku.
System.Action odczyt = () =>
{
var glowny = p.DomyslnyRachunek; // może być null gdy brak rachunku
if (glowny != null)
{
_ = glowny.Domyslne;
_ = glowny.Rachunek; // subrow rachunku
}
foreach (var r in p.Rachunki)
{
_ = r.Domyslne;
_ = r.Priorytet;
}
};
odczyt.Should().NotThrow("Rachunki/DomyslnyRachunek to publiczny odczyt kontraktu A5");
}
// ============================== A6 — Dane podatkowe (PIT) ==============================
[Test]
[Description("A6: dane PIT to subrow PracHistoria.Podatki — KosztyRodzaj/TypProgow/UlgaCzesc to ENUMY, " +
"UlgaMnoznik to decimal (PIT-2). Wartości enumów pobieramy z realnych nazw składowych.")]
public void A6_DanePodatkowe_NaSubrowiePodatki()
{
var g = Guid.Empty;
InTransaction(() =>
{
var pdt = NowyPracownik("A6", out g).Last.Podatki;
pdt.KosztyRodzaj = RodzajKosztowUzyskania.JedenStosPracy; // enum (jeden stosunek pracy)
pdt.UlgaMnoznik = 1m; // pełna kwota zmniejszająca (PIT-2)
pdt.UlgaCzesc = UlgaPodatkowaCzesc.Ulga112; // podział PIT-2 (1/1)
pdt.TypProgow = TypProgowPodatkowych.Standardowe; // enum
});
SaveDispose();
var pdt2 = Get(g).Last.Podatki;
pdt2.KosztyRodzaj.Should().Be(RodzajKosztowUzyskania.JedenStosPracy);
pdt2.UlgaMnoznik.Should().Be(1m);
pdt2.UlgaCzesc.Should().Be(UlgaPodatkowaCzesc.Ulga112);
pdt2.TypProgow.Should().Be(TypProgowPodatkowych.Standardowe);
}
// ============================== A8 — ZUS / NFZ ==============================
[Test]
[Description("A8: oddział NFZ to subrow PracHistoria.OddzialNFZ (OdDnia: Date) — zapisywalny. " +
"DodSwiadczeniaZUS na świeżym zapisie jest tylko-do-odczytu (cały subrow zablokowany).")]
public void A8_DodatkoweSwiadczeniaZUS_IOddzialNFZ()
{
var g = Guid.Empty;
InTransaction(() =>
{
var ph = NowyPracownik("A8", out g).Last;
// ROZBIEŻNOŚĆ z dokumentem: na świeżym zapisie CAŁY subrow DodSwiadczeniaZUS jest
// tylko-do-odczytu (ColReadOnlyException nawet dla Numer) — staje się edytowalny dopiero
// gdy świadczenie zostanie zainicjowane (np. przez UI/kreator). Tu ustawiamy NFZ.
ph.OddzialNFZ.OdDnia = new Date(2026, 1, 1);
});
SaveDispose();
var ph2 = Get(g).Last;
ph2.OddzialNFZ.OdDnia.Should().Be(new Date(2026, 1, 1));
// Odczyt dodatkowych świadczeń ZUS — publiczny i nie rzuca (Rodzaj/Okres do odczytu).
System.Action odczyt = () =>
{
_ = ph2.DodSwiadczeniaZUS.Rodzaj;
_ = ph2.DodSwiadczeniaZUS.Okres;
};
odczyt.Should().NotThrow("dane dodatkowych świadczeń ZUS są dostępne do odczytu");
}
// ============================== A11 — Wykształcenie / języki / wojsko ==============================
[Test]
[Description("A11: wykształcenie i wojsko to subrowy PracHistoria (Kod/Stosunek/KategoriaZdrowia = " +
"ENUMY); języki obce to kolekcja na rootcie Pracownik.JęzykiObce.")]
public void A11_WyksztalcenieWojsko_NaHistorii_JezykiNaRootcie()
{
var g = Guid.Empty;
InTransaction(() =>
{
var ph = NowyPracownik("A11", out g).Last;
ph.Wyksztalcenie.Kod = KodWyksztalcenia.Wyzsze; // enum
ph.Wyksztalcenie.TytulNaukowy = "mgr inż.";
ph.Wojsko.Stosunek = KodStosDoSluzbyWojskowej.Rezerwa; // enum (uregulowany = rezerwa)
ph.Wojsko.KategoriaZdrowia = KategoriaZdrowia.A; // enum
ph.Wojsko.NrKsiazeczki = "AB123456";
});
SaveDispose();
var ph2 = Get(g).Last;
ph2.Wyksztalcenie.Kod.Should().Be(KodWyksztalcenia.Wyzsze);
ph2.Wyksztalcenie.TytulNaukowy.Should().Be("mgr inż.");
ph2.Wojsko.Stosunek.Should().Be(KodStosDoSluzbyWojskowej.Rezerwa);
ph2.Wojsko.KategoriaZdrowia.Should().Be(KategoriaZdrowia.A);
ph2.Wojsko.NrKsiazeczki.Should().Be("AB123456");
// Odczyt kolekcji języków obcych (na rootcie) — nie rzuca; może być pusta.
System.Action czytajJezyki = () =>
{
foreach (var j in Get(g).JęzykiObce) { _ = j.Jezyk; }
};
czytajJezyki.Should().NotThrow("JęzykiObce to publiczna kolekcja na rootcie pracownika");
}
// ============================== A12 — GUS / kod zawodu ==============================
[Test]
[Description("A12: dane statystyczne GUS to subrow PracHistoria.GUS (KodWyksztalcenia = enum " +
"KodWykształceniaGUS, INNE niż A11); kod zawodu to Etat.KodWykonywanegoZawodu (int).")]
public void A12_DaneGUS_IKodZawodu()
{
var g = Guid.Empty;
InTransaction(() =>
{
// Etat.KodWykonywanegoZawodu jest tylko-do-odczytu, dopóki nie ustawi się Etat.Okres
// (bramka subrowa Etat, patrz B1) — używamy więc pracownika z ustawionym etatem.
var ph = NowyPracownikEtatowy("A12", out g).Last;
ph.GUS.KodWyksztalcenia = KodWykształceniaGUS.Wyższe; // enum GUS (z diakrytykiem)
ph.GUS.GlowneMiejscePracy = true;
ph.GUS.PierwszaPraca = false;
ph.Etat.KodWykonywanegoZawodu = 251401; // kod zawodu GUS (int)
});
SaveDispose();
var ph2 = Get(g).Last;
ph2.GUS.KodWyksztalcenia.Should().Be(KodWykształceniaGUS.Wyższe);
ph2.GUS.GlowneMiejscePracy.Should().BeTrue();
ph2.Etat.KodWykonywanegoZawodu.Should().Be(251401);
}
// ============================== A13 — PFRON ==============================
[Test]
[Description("A13: dane PFRON/niepełnosprawność to subrow PracHistoria.PFRON — Stopien = enum " +
"StNiepełnosprawności, Okres = FromTo, daty = Soneta.Types.Date.")]
public void A13_PFRON_StopienOkresIDaty()
{
var g = Guid.Empty;
var okres = new FromTo(new Date(2026, 1, 1), new Date(2028, 12, 31));
InTransaction(() =>
{
var pfron = NowyPracownik("A13", out g).Last.PFRON;
pfron.Stopien = StNiepełnosprawności.Umiarkowany; // enum
pfron.Okres = okres;
pfron.DataOrzeczenia = new Date(2025, 12, 1);
pfron.DataDostarczenia = new Date(2025, 12, 15);
});
SaveDispose();
var pfron2 = Get(g).Last.PFRON;
pfron2.Stopien.Should().Be(StNiepełnosprawności.Umiarkowany);
pfron2.Okres.From.Should().Be(okres.From);
pfron2.Okres.To.Should().Be(okres.To);
pfron2.DataOrzeczenia.Should().Be(new Date(2025, 12, 1));
pfron2.DataDostarczenia.Should().Be(new Date(2025, 12, 15));
}
// ============================== A15 — Odczyt „na dzień" ==============================
[Test]
[Description("A15 (odczyt): indeksator pracownik[date] zwraca zapis obowiązujący na datę (Aktualnosc " +
"zawiera date), null dla daty sprzed zatrudnienia; GetFirst()/Last to skrajne zapisy.")]
public void A15_OdczytNaDzien_ZwracaZapisLubNull()
{
var p = PierwszyPracownik(); // zatrudniony etatowo pracownik z Demo
// 1) Zapis na dziś — istnieje dla zatrudnionego pracownika.
var phDzis = p[Date.Today];
phDzis.Should().NotBeNull("pracownik etatowy z Demo ma zapis obowiązujący na dziś");
// 2) Indeksator == kolekcja Historia[date].
p[Date.Today].Should().BeSameAs(p.Historia[Date.Today]);
// 3) Skrajne zapisy.
var pierwszy = p.Historia.GetFirst();
var ostatni = p.Last;
pierwszy.Should().NotBeNull();
ostatni.Should().NotBeNull();
p[Date.Today].Should().BeSameAs(p.Historia.GetLast(), "Last == Historia.GetLast()");
// 4) Data sprzed zatrudnienia → brak zapisu (null).
if (pierwszy.Aktualnosc.From > Date.MinValue)
{
var przed = pierwszy.Aktualnosc.From.AddDays(-1);
p[przed].Should().BeNull("dla daty sprzed pierwszego zapisu nie ma zapisu historii");
}
}
// ============================== A16 — Powiązanie z kontrahentem ==============================
[Test]
[Description("A16: powiązanie pracownika z istniejącym kontrahentem to zapisywalne pole rootu " +
"Pracownik.PowiazanyKontrahent (referencja, ta sama sesja); null = brak powiązania.")]
public void A16_PowiazanyKontrahent_UstawianyNaRootcie()
{
// Istniejący kontrahent z Demo (z tej samej sesji co pracownik).
var kontrahent = Session.GetCRM().Kontrahenci.WgKodu.Cast().First();
var g = Guid.Empty;
InTransaction(() =>
{
var pracownik = NowyPracownik("A16", out g);
pracownik.PowiazanyKontrahent = kontrahent; // relacja na rootcie
});
SaveDispose();
var p2 = Get(g);
p2.PowiazanyKontrahent.Should().NotBeNull("ustawiliśmy powiązanie z istniejącym kontrahentem");
p2.PowiazanyKontrahent.Guid.Should().Be(kontrahent.Guid);
}
// ============================== A17 — Archiwum (workery) ==============================
[Test]
[Description("A17 (odczyt): manager Pracownik.Archiwum udostępnia tylko-do-odczytu status archiwizacji " +
"(Status: enum InformacjeOArchiwum) i flagę Anonimizowany; pracownik aktywny = NieDotyczy.")]
public void A17_Archiwum_ManagerUdostepniaStatusDoOdczytu()
{
// Aktywny pracownik z Demo — nie jest w archiwum. Manager Archiwum to read-only API:
// Przenieś/Przywróć dostępne są WYŁĄCZNIE przez workery (patrz test poniżej).
var p = PierwszyPracownik();
p.Archiwum.Status.Should().Be(InformacjeOArchiwum.NieDotyczy,
"aktywny pracownik nie jest w archiwum (status = NieDotyczy)");
p.Archiwum.Anonimizowany.Should().BeFalse("aktywny pracownik nie jest zanonimizowany");
}
[Test]
[Description("A17 (zmiana stanu): przeniesienie/przywrócenie z archiwum jest dostępne WYŁĄCZNIE przez " +
"workery Pracownik.PrzenieśDoArchiwumWorker / PrzywróćZArchiwumWorker (CommitUI). Kod w ciele.")]
[Ignore("Worker PrzenieśDoArchiwum rzuca NullReferenceException w hoście testowym headless " +
"(Pracownik.ArchiwumManager) — archiwizacja zależy od stanu operatora/kontekstu UI nieobecnego " +
"w bazie Demo. Test dokumentuje jedyną publiczną drogę zmiany stanu archiwum (workery).")]
public void A17_Archiwum_PrzeniesienieIPrzywroceniePrzezWorkery()
{
var g = Guid.Empty;
InTransaction(() => NowyPracownik("A17", out g));
SaveDispose();
// Przeniesienie do archiwum — worker pojedynczego pracownika (CommitUI: worker „jak z UI").
InUITransaction(() =>
{
var worker = new Prac.PrzenieśDoArchiwumWorker { Pracownik = Get(g) };
worker.PrzenieśDoArchiwum();
});
SaveDispose();
// Odczyt stanu archiwizacji (read-only API managera).
Get(g).Archiwum.Status.Should().Be(InformacjeOArchiwum.WArchiwum);
// Przywrócenie z archiwum — drugi worker.
InUITransaction(() =>
{
var worker = new Prac.PrzywróćZArchiwumWorker { Pracownik = Get(g) };
worker.PrzywróćZArchiwum();
});
SaveDispose();
Get(g).Archiwum.Status.Should().NotBe(InformacjeOArchiwum.WArchiwum);
}
// ============================== A18 — Zwolnienie / wyrejestrowanie ==============================
[Test]
[Description("A18: zamknięcie zatrudnienia — Etat.Okres.To (ostatni dzień) + subrow Etat.RozwiazanieUmowy " +
"(Inicjatywa/PodstawaPrawna = enumy; wartości pobierane z realnych nazw składowych).")]
public void A18_Zwolnienie_EtatOkresIRozwiazanieUmowy()
{
var g = Guid.Empty;
var dataRozwiazania = new Date(2026, 6, 30);
// Podstawa prawna: enum o stałych „kodowych" (_400.._550, NieDotyczy) — bierzemy pierwszą realną
// wartość różną od NieDotyczy, zamiast zgadywać nazwę.
var podstawa = Enum.GetValues(typeof(KodPodstawyPrawnejZwolnienia))
.Cast()
.First(v => v != KodPodstawyPrawnejZwolnienia.NieDotyczy);
InTransaction(() =>
{
// Etat ustawiony (Okres+Wydzial+Stanowisko) — inaczej Save rzuca weryfikatorem wymagań etatu.
var etat = NowyPracownikEtatowy("A18", out g).Last.Etat;
// Zamknięcie okresu zatrudnienia ostatnim dniem pracy:
etat.Okres = new FromTo(etat.Okres.From, dataRozwiazania);
// Tryb rozwiązania (subrow RozwiazanieUmowy):
etat.RozwiazanieUmowy.Inicjatywa = KodInicjatywyZwolnienia.Pracownik; // enum
etat.RozwiazanieUmowy.PodstawaPrawna = podstawa; // enum (dynamicznie)
// Opcjonalnie okres wypowiedzenia:
etat.OkresWypowiedzenia.DataZlozenia = new Date(2026, 5, 31);
etat.OkresWypowiedzenia.Miesiace = 1;
});
SaveDispose();
var etat2 = Get(g).Last.Etat;
etat2.Okres.To.Should().Be(dataRozwiazania, "okres zatrudnienia zamknięty ostatnim dniem pracy");
etat2.RozwiazanieUmowy.Inicjatywa.Should().Be(KodInicjatywyZwolnienia.Pracownik);
etat2.RozwiazanieUmowy.PodstawaPrawna.Should().Be(podstawa);
etat2.OkresWypowiedzenia.Miesiace.Should().Be(1);
}
[Test]
[Description("A18 (ZWUA): wyrejestrowanie z ZUS przez WyrejestrujPracownikaWorker wymaga Params(Context) " +
"oraz środowiska deklaracji ZUS — poza prostym kontraktem ustawiania pól etatu.")]
[Ignore("Wyrejestrowanie ZUS (ZWUA) wymaga WyrejestrujPracownikaParams(Context) i kontekstu deklaracji/" +
"KEDU; samo ustawienie Etat.Okres/RozwiazanieUmowy (test A18) nie tworzy dokumentu ZWUA.")]
public void A18_WyrejestrowanieZUS_WymagaContextIKedu()
{
}
// ============================== A19 — Przerejestrowanie (zmiana Tyub4) ==============================
[Test]
[Description("A19: przerejestrowanie = nowy zapis historii od daty (A14: Update + AddRow) ze zmianą " +
"Etat.Ubezpieczenia.Tyub4 (słownik TytulyUbezpiecz4, klucz int); deklaracje ZUS — osobny worker UI.")]
public void A19_Przerejestrowanie_ZmianaTyub4OdDaty()
{
// Tyub4 to słownik o kluczu int — bierzemy dwie różne realne wartości z bazy Demo (nie hardkodujemy).
var tytuly = Kadry.TytulyUbezpiecz4.Cast().Take(2).ToList();
if (tytuly.Count < 1)
{
Assert.Ignore("Brak słownika tytułów ubezpieczenia (TytulyUbezpiecz4) w bazie Demo.");
return;
}
var nowyTyub = tytuly.Last();
var g = Guid.Empty;
var odDnia = new Date(2026, 7, 1);
InTransaction(() =>
{
var pracownik = NowyPracownik("A19", out g);
// Nowy zapis historii „od daty" (A14): Update klonuje + skraca poprzedni, AddRow dopina klon.
var nowy = pracownik.Historia.Update(odDnia);
pracownik.Module.PracHistorie.AddRow(nowy);
// Zmiana kodu tytułu ubezpieczenia (przerejestrowanie ubezpieczeniowe) na nowym zapisie:
nowy.Etat.Ubezpieczenia.Tyub4 = nowyTyub;
});
SaveDispose();
var pracownik2 = Get(g);
var zapisy = pracownik2.Historia.Cast().OrderBy(h => h.Aktualnosc.From).ToList();
zapisy.Should().HaveCount(2, "Update utworzył drugi zapis historii (przerejestrowanie od daty)");
zapisy[0].Aktualnosc.To.Should().Be(odDnia.AddDays(-1), "stary zapis skrócony do dnia poprzedzającego");
zapisy[1].Aktualnosc.From.Should().Be(odDnia, "nowy zapis obowiązuje od dnia przerejestrowania");
zapisy[1].Etat.Ubezpieczenia.Tyub4.Should().NotBeNull("nowy tytuł ubezpieczenia ustawiony od daty");
zapisy[1].Etat.Ubezpieczenia.Tyub4.Guid.Should().Be(nowyTyub.Guid);
}
}