using System; 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; /// /// Rozdział E/F (część druga) — operacje na planie pracy i RCP wykraczające poza CRUD dni: /// /// E3 — aktualizacja kalendarza pracownika (worker seryjny, wymaga Context → [Ignore]), /// E4 — uzgodnienie doby pracowniczej (worker dnia/grupowy, wymaga Context → [Ignore]), /// E5 — odczyt normy i czasu przepracowanego przez pracownik.Czasy : KalkulatorPracownika (★ pełny odczyt), /// F3 — import RCP: sam import plikowy [Ignore]; przeliczenie we/wy przez ImportDniaWorker (★), /// F4 — weryfikacja/korekta RCP: DzienRCP/StanRCP (★ korekta na świeżym dniu), /// F5 — praca hybrydowa: strefy dnia i podzielniki (★ odczyt). /// /// /// Operujemy wyłącznie na publicznym kontrakcie platformy Soneta (jak dodatek zewnętrzny), /// na bazie Demo (GoldStandard) z automatycznym rollbackiem. Daty Demo planu/pracy są nieznane, więc /// odczyty istniejących danych traktujemy defensywnie (kolekcja istnieje / indeksator nie rzuca), /// a scenariusze zapisu budujemy na własnych, jawnych datach dla pracownika „006". /// /// /// Granica testowalności. Operacje wymagające (worker E3/E4 grupowy — /// Params : ContextBase z ctorem (Context), karmiony zaznaczeniem listy) lub źródła /// zewnętrznego (import RCP z pliku/czytnika) są oznaczone [Ignore] z uzasadnieniem — opisują /// kontrakt, nie wykonują operacji. KalkulatorPracownika/CzasDni/ZestawienieNadgodzin /// nie są wierszami ORM — to obiekty liczące (czysty odczyt bez transakcji). /// /// [TestFixture] public class RozdzialEFrest_PlanRcpTest : PracownikTestBase { // Jawne daty/okresy do scenariuszy (nie Date.Today — data biznesowa Demo bywa inna). private static readonly Date Dzien = new(2026, 6, 1); private static readonly FromTo Okres = new(new Date(2026, 6, 1), new Date(2026, 6, 30)); private static readonly YearMonth Miesiac = new(2026, 6); // ============================== E3 — Aktualizacja kalendarza pracownika ============================== [Test] [Description("E3 (kontrakt, [Ignore]): AktualizujKalendarzWorker to worker seryjny z menu Czynności. " + "Pracownicy/Pars są set-only, a Params : ContextBase ma ctor (Context) — bez zaznaczenia " + "listy (Context) nie da się zbudować parametrów, więc operacji nie wykonujemy w teście.")] [Ignore("E3: AktualizujKalendarzWorker.Params : ContextBase wymaga Context (zaznaczenie listy pracowników) — brak czystego API bezkontekstowego.")] public void E3_AktualizujKalendarz_WymagaContext_Ignore() { // Świadomie nie wykonujemy — operacja seryjna sterowana zaznaczeniem UI (Context). // worker.Pracownicy = context.Get(); // worker.Pars = new AktualizujKalendarzWorker.Params(context) { Data = ..., Docelowy = ..., Zmiana = true }; // worker.Aktualizuj(); // Logout + Commit wewnątrz Assert.Fail("Test oznaczony [Ignore] — nie powinien być uruchamiany."); } [Test] [Description("E3 (odczyt konfiguracji): kalendarz docelowy/źródłowy aktualizacji to konfiguracja " + "Etat.Kalendarz oraz interpretacja Etat.InterpretacjaKalendarza — odczyt nie wymaga workera " + "ani Context i nie rzuca; pokazuje skąd worker E3 bierze stan wejściowy.")] public void E3_KalendarzIInterpretacja_OdczytKonfiguracjiEtatu_NieRzuca() { var p = Pracownik(Pracownik_.Andrzejewski); p.Should().NotBeNull("pracownik '006' istnieje w bazie Demo"); System.Action odczyt = () => { // Etat leży na bieżącym zapisie historycznym (pracownik.Last.Etat); kalendarz i interpretacja // sterują aktualizacją (E3). var etat = p.Last?.Etat; if (etat is not null) { Kalendarz kal = etat.Kalendarz; // kalendarz roboczy (źródło/cel zmiany) InterpretacjaKalendarza interpretacja = etat.InterpretacjaKalendarza; _ = interpretacja; if (kal is not null) { Time _ = kal.NormaDobowa; // norma dobowa kalendarza } } }; odczyt.Should().NotThrow("odczyt kalendarza/interpretacji z Etatu nie wymaga Context ani transakcji"); } // ============================== E4 — Uzgodnienie doby pracowniczej ============================== [Test] [Description("E4 (kontrakt, odczyt): granica doby to atrybuty KONFIGURACYJNE Etatu " + "(ConfigPoczątekDobyNiedzieledIŚwięta — read-only, NormaDobowa) — nie ma edytowalnego pola " + "początku doby na pojedynczym DzienPracy. Odczyt tych pól nie rzuca.")] public void E4_ModelDoby_OdczytKonfiguracjiEtatu_NieRzuca() { var p = Pracownik(Pracownik_.Andrzejewski); p.Should().NotBeNull("pracownik '006' istnieje w bazie Demo"); System.Action odczyt = () => { var etat = p.Last?.Etat; if (etat is not null) { Time poczatekDobySwieta = etat.ConfigPoczątekDobyNiedzieledIŚwięta; // konfiguracyjne, read-only Time normaDobowa = etat.NormaDobowa; _ = poczatekDobySwieta; _ = normaDobowa; } }; odczyt.Should().NotThrow("granica doby/normy to konfiguracja Etatu — czysty odczyt"); } [Test] [Description("E4 (kontrakt, [Ignore]): worker pojedynczego dnia DzienPracy.UzgodnijDobePracowniczaWorker " + "ma Dzień set-only i wymaga istniejącego dnia ewidencji oraz IsEnabled; worker grupowy " + "(Params : ContextBase) wymaga Context. W Demo brak deterministycznej doby nocnej do uzgodnienia, " + "więc operacji nie wykonujemy — opisujemy kontrakt (IsEnabled + Uzgodnij/Przenieś).")] [Ignore("E4: UzgodnijDobePracownicza — worker dnia wymaga deterministycznego dnia nocnego (brak w Demo); worker grupowy wymaga Context.")] public void E4_UzgodnijDobePracownicza_WymagaContextLubDanych_Ignore() { // var dzien = pracownik.DniPracy[data]; // if (DzienPracy.UzgodnijDobePracowniczaWorker.IsEnabledUzgodnijDobePracownicza(dzien)) { ... } // new DzienPracy.UzgodnijDobePracowniczaWorker { Dzień = dzien }.UzgodnijDobePracownicza(); Assert.Fail("Test oznaczony [Ignore] — nie powinien być uruchamiany."); } // ============================== E5 — Odczyt normy / czasu przepracowanego (★ testowalne) ============================== [Test] [Description("E5: pracownik.Czasy zwraca KalkulatorPracownika (NIE Row — obiekt liczący, czysty odczyt " + "bez transakcji). Kalkulator istnieje dla pracownika z bazy Demo.")] public void E5_Czasy_ZwracaKalkulatorPracownika_NieNull() { var p = Pracownik(Pracownik_.Andrzejewski); p.Should().NotBeNull("pracownik '006' istnieje w bazie Demo"); KalkulatorPracownika kalk = p.Czasy; kalk.Should().NotBeNull("pracownik.Czasy daje kalkulator czasu pracy (kontekst pracownika)"); } [Test] [Description("E5: Norma(okres) (plan) i Praca(okres) (realizacja) zwracają CzasDni (Czas : Time, Dni : int). " + "Wywołanie to czysty odczyt — nie rzuca i nie wymaga transakcji. Wartości mogą być Empty/Invalid " + "(brak danych Demo w okresie), więc sprawdzamy tylko sam kontrakt odczytu.")] public void E5_NormaIPraca_OdczytZaOkres_ZwracaCzasDni_NieRzuca() { var p = Pracownik(Pracownik_.Andrzejewski); var kalk = p.Czasy; CzasDni norma = CzasDni.Invalid; CzasDni praca = CzasDni.Invalid; System.Action odczyt = () => { norma = kalk.Norma(Okres); // params Item[] condition — wywołanie bez filtra praca = kalk.Praca(Okres); // czas przepracowany (realizacja) _ = kalk.PracaRozliczana(Okres); // czas rozliczany (do nadgodzin) }; odczyt.Should().NotThrow("odczyt Norma/Praca przez KalkulatorPracownika jest bezpieczny (bez transakcji)"); // CzasDni to obiekt wynikowy (Time + int) — pola tylko do odczytu; dostęp nie rzuca. System.Action poleCzasDni = () => { Time _ = norma.Czas; int __ = norma.Dni; Time ___ = praca.Czas; int ____ = praca.Dni; }; poleCzasDni.Should().NotThrow("CzasDni wystawia Czas/Dni jako odczyt"); } [Test] [Description("E5: NormaKodeksowa(YearMonth) zwraca normę kodeksową miesiąca (pełny etat) jako CzasDni; " + "dla czerwca 2026 (20 dni roboczych × 8h) norma kodeksowa jest dodatnia — wynik nie jest Invalid " + "i ma policzalne Dni/Czas.")] public void E5_NormaKodeksowa_DlaMiesiaca_JestDodatnia() { var p = Pracownik(Pracownik_.Andrzejewski); var kalk = p.Czasy; CzasDni norma = kalk.NormaKodeksowa(Miesiac); // Norma kodeksowa miesiąca nie zależy od danych pracownika — to kalendarz kodeksowy. norma.Should().NotBe(CzasDni.Invalid, "norma kodeksowa istnieje dla każdego pełnego miesiąca"); norma.Dni.Should().BeGreaterThan(0, "czerwiec 2026 ma dni robocze"); norma.Czas.TotalMinutes.Should().BeGreaterThan(0, "pełny etat = dodatnia norma czasu pracy"); } [Test] [Description("E5: Nadgodziny(YearMonth) zwraca ZestawienieNadgodzin (struct: N50/N100/NSW/Razem — wszystkie Time, " + "read-only). Nocne(okres) zwraca Time. Czysty odczyt — nie rzuca; przy braku danych Demo wynik = Zero.")] public void E5_NadgodzinyINocne_OdczytStatystyk_NieRzuca() { var p = Pracownik(Pracownik_.Andrzejewski); var kalk = p.Czasy; ZestawienieNadgodzin nadg = ZestawienieNadgodzin.Zero; Time nocne = new(0); System.Action odczyt = () => { nadg = kalk.Nadgodziny(Miesiac); nocne = kalk.Nocne(Okres); }; odczyt.Should().NotThrow("odczyt nadgodzin/czasu nocnego jest bezpieczny"); // Pola zestawienia to odczyt; Razem agreguje składowe (nie rzuca, może być Zero). System.Action pola = () => { Time _ = nadg.N50; Time __ = nadg.N100; Time ___ = nadg.Razem; _ = nocne; }; pola.Should().NotThrow("ZestawienieNadgodzin wystawia N50/N100/Razem jako odczyt"); } [Test] [Description("E5: DniNie(okres)/NormaNie(okres) odczytują liczbę i normę dni nieobecności za okres. " + "DniNie zwraca int (>=0), NormaNie zwraca CzasDni. Czysty odczyt — nie rzuca.")] public void E5_NieobecnosciZaOkres_OdczytLiczbyINormy_NieRzuca() { var p = Pracownik(Pracownik_.Andrzejewski); var kalk = p.Czasy; int dniNie = -1; System.Action odczyt = () => { dniNie = kalk.DniNie(Okres); // liczba dni nieobecności _ = kalk.NormaNie(Okres); // norma nieobecności (CzasDni) }; odczyt.Should().NotThrow("odczyt nieobecności za okres przez kalkulator jest bezpieczny"); dniNie.Should().BeGreaterThanOrEqualTo(0, "liczba dni nieobecności nie jest ujemna"); } // ============================== F3 — Import RCP (przeliczenie we/wy, ★) ============================== [Test] [Description("F3 ([Ignore]): import surowych odbić z pliku/czytnika RCP wymaga zewnętrznego źródła " + "(plik/serwis/format) — brak czystego API w publicznym kontrakcie. Testowalny jest jedynie " + "fragment po wczytaniu: przeliczenie już-wpisanych we/wy przez ImportDniaWorker (osobny test).")] [Ignore("F3: import z pliku/urządzenia RCP wymaga zewnętrznego źródła (I/O) — poza zakresem testu kontraktu.")] public void F3_ImportZPliku_WymagaZrodlaZewnetrznego_Ignore() { Assert.Fail("Test oznaczony [Ignore] — nie powinien być uruchamiany."); } [Test] [Description("F3 (przeliczenie, ★): po wpisaniu zdarzeń we/wy na dzień ewidencji (jak po imporcie) " + "ImportDniaWorker { DzienPracy = dzien }.Przelicz() przelicza odbicia na czas pracy — operacja " + "na obiektach sesji (bez I/O). Worker ma bezparametrowy ctor i property DzienPracy {get;set;}.")] public void F3_ImportDniaWorker_PrzeliczWeWy_NieRzuca() { Guid guidPrac = Guid.Empty; InTransaction(() => { var p = Pracownik(Pracownik_.Andrzejewski); guidPrac = p.Guid; // Dzień ewidencji (właściciel zdarzeń) — tworzymy ctorem + AddRow (sam ctor nie rejestruje). var dp = p.DniPracy[Dzien] ?? Session.AddRow(new DzienPracy(p, Dzien)); // Surowe odbicia we/wy (tabela pośrednia) — tak wyglądają dane „po imporcie", przed przeliczeniem. var we = new WejscieWyjscie(dp); Kalend.WejsciaWyjscia.AddRow(we); we.Godzina = new Time(8, 0); we.Typ = TypWejsciaWyjscia.Wejscie; var wy = new WejscieWyjscie(dp); Kalend.WejsciaWyjscia.AddRow(wy); wy.Godzina = new Time(16, 0); wy.Typ = TypWejsciaWyjscia.Wyjscie; // Przeliczenie odbić na czas pracy dnia (bez pliku/urządzenia). System.Action przelicz = () => new ImportDniaWorker { DzienPracy = dp }.Przelicz(); przelicz.Should().NotThrow("ImportDniaWorker.Przelicz() przelicza we/wy na czas pracy bez I/O"); }); SaveDispose(); // Po przeliczeniu dzień ewidencji nadal jest dostępny przez indeksator [Date]. var p2 = Get(guidPrac); var dp2 = p2.DniPracy[Dzien]; dp2.Should().NotBeNull("dzień ewidencji z przeliczonymi odbiciami istnieje po zapisie"); dp2.WeWy.Cast().Should().HaveCount(2, "wejście i wyjście zostały zachowane"); } // ============================== F4 — Weryfikacja / korekta RCP (★ testowalne) ============================== [Test] [Description("F4 (odczyt): DniRCP to DateSubTable (typowane) — indeksator [Date] zwraca DzienRCP/null " + "i nie rzuca. DzienRCP to wynik importu/weryfikacji; w Demo zwykle brak (null) dla naszej daty. " + "Odczytujemy StanRCP (enum StanWeryfikacjiRCP) i Praca.Czas defensywnie.")] public void F4_DniRCP_OdczytIndeksatoremPoDacie_NieRzuca() { var p = Pracownik(Pracownik_.Andrzejewski); p.DniRCP.Should().NotBeNull("kolekcja zweryfikowanego RCP (DniRCP) istnieje"); System.Action odczyt = () => { DzienRCP dzienRcp = p.DniRCP[Dzien]; // typowane: DzienRCP lub null if (dzienRcp is not null) { StanWeryfikacjiRCP stan = dzienRcp.StanRCP; // enum stanu weryfikacji Time czas = dzienRcp.Praca.Czas; // czas na subrowie Praca bool rcpOk = dzienRcp.RcpOK; // flaga stanu po imporcie _ = stan; _ = czas; _ = rcpOk; } }; odczyt.Should().NotThrow("indeksator [Date] na DniRCP to bezpieczny odczyt"); } [Test] [Description("F4 (korekta, ★): na świeżo utworzonym DzienRCP korygujemy godziny na subrowie Praca, " + "ustawiamy StanRCP (enum) na Poprawny i dopisujemy Uwagi (MemoText). Po zapisie DniRCP[data] " + "zwraca dzień ze zmienionym stanem i godzinami. Czas/OdGodziny na rootcie są kalkulowane (read-only).")] public void F4_KorektaDzienRCP_ZmianaStanuIGodzin_ZapisOdczyt() { Guid guidPrac = Guid.Empty; InTransaction(() => { var p = Pracownik(Pracownik_.Andrzejewski); guidPrac = p.Guid; // W Demo DzienRCP zwykle nie istnieje na naszej dacie — do scenariusza korekty // tworzymy go ctorem + AddRow (analogicznie do DzienPracy). Korekta dotyczy istniejącego rekordu. var dzienRcp = p.DniRCP[Dzien] ?? Session.AddRow(new DzienRCP(p, Dzien)); // Korekta godzin na subrowie Praca (root Czas/OdGodziny są kalkulowane). dzienRcp.Praca.OdGodziny = new Time(8, 0); dzienRcp.Praca.DoGodziny = new Time(16, 0); // Zmiana stanu weryfikacji (enum, nie string) + uwagi. dzienRcp.StanRCP = StanWeryfikacjiRCP.Poprawny; dzienRcp.Uwagi = (MemoText)"Skorygowano wyjście"; }); SaveDispose(); var p2 = Get(guidPrac); var rcp2 = p2.DniRCP[Dzien]; rcp2.Should().NotBeNull("po zapisie dzień RCP jest dostępny przez indeksator [Date]"); rcp2.StanRCP.Should().Be(StanWeryfikacjiRCP.Poprawny, "stan weryfikacji został ustawiony"); rcp2.Praca.OdGodziny.Should().Be(new Time(8, 0)); rcp2.Praca.DoGodziny.Should().Be(new Time(16, 0)); } // ============================== F5 — Praca hybrydowa / strefy / podzielniki (odczyt) ============================== [Test] [Description("F5 (odczyt): DzienPracy.Strefy to SubTable — podział dnia na strefy " + "(stacjonarna / zdalna). Każda StrefaPracy ma Definicja : DefinicjaStrefy i CzasRozliczany : Time. " + "Kolekcja istnieje (może być pusta w Demo); iteracja i odczyt pól nie rzucają.")] public void F5_StrefyDniaPracy_OdczytPodzialuNaStrefy_NieRzuca() { Guid guidPrac = Guid.Empty; // Świeży dzień pracy daje deterministyczną (pustą) kolekcję Strefy do bezpiecznego odczytu. InTransaction(() => { var p = Pracownik(Pracownik_.Andrzejewski); guidPrac = p.Guid; _ = p.DniPracy[Dzien] ?? Session.AddRow(new DzienPracy(p, Dzien)); }); SaveDispose(); var p2 = Get(guidPrac); var dzien = p2.DniPracy[Dzien]; dzien.Should().NotBeNull("dzień ewidencji istnieje"); dzien.Strefy.Should().NotBeNull("kolekcja stref pracy (Strefy) zawsze istnieje"); System.Action odczyt = () => { foreach (StrefaPracy s in dzien.Strefy.Cast()) { DefinicjaStrefy def = s.Definicja; // strefa (np. praca zdalna) Time rozliczany = s.CzasRozliczany; // czas rozliczany w strefie _ = def; _ = rozliczany; } }; odczyt.Should().NotThrow("iteracja po strefach dnia i odczyt pól są bezpieczne"); } [Test] [Description("F5 (odczyt podzielników): pracownik.RozliczeniaCzasuPracy (dokumenty) oraz " + "pracownik.ElementyRozliczeniaCzasuPracy (pozycje) to SubTable — kolekcje istnieją (mogą być puste " + "w Demo). Element ma Definicja : DefinicjaStrefy i Czas : Time; odczyt nie rzuca. Budowy dokumentu " + "rozliczenia nie testujemy — wymaga DefinicjaRozliczeniaCzasuPracy i przebiega przez extendery/UI.")] public void F5_PodzielnikiRozliczeniaCzasuPracy_OdczytKolekcji_NieRzuca() { var p = Pracownik(Pracownik_.Andrzejewski); p.RozliczeniaCzasuPracy.Should().NotBeNull("kolekcja dokumentów rozliczenia czasu pracy istnieje"); p.ElementyRozliczeniaCzasuPracy.Should().NotBeNull("kolekcja pozycji rozliczenia (podzielniki) istnieje"); System.Action odczyt = () => { foreach (ElementRozliczeniaCzasuPracy el in p.ElementyRozliczeniaCzasuPracy.Cast()) { DefinicjaStrefy def = el.Definicja; Time czas = el.Czas; _ = def; _ = czas; } }; odczyt.Should().NotThrow("iteracja po pozycjach podzielnika i odczyt pól są bezpieczne"); } [Test] [Description("F5 (kontrakt typów): DefinicjaStrefy wystawia stałe Guid Praca_Zdalna / PracaZdalnaOkazjonalna " + "(identyfikacja stref pracy zdalnej) oraz enum TypStrefy (NieWplywa/Zwieksza/Zmniejsza). " + "Stałe są niepuste — to publiczne punkty zaczepienia rozliczenia pracy hybrydowej.")] public void F5_DefinicjaStrefy_StalePracaZdalnaIEnumTypStrefy_SaDostepne() { DefinicjaStrefy.Praca_Zdalna.Should().NotBe(Guid.Empty, "stała identyfikuje strefę pracy zdalnej"); DefinicjaStrefy.PracaZdalnaOkazjonalna.Should().NotBe(Guid.Empty, "stała identyfikuje strefę pracy zdalnej okazjonalnej"); // Enum TypStrefy steruje wpływem strefy na rozliczenie czasu. System.Enum.IsDefined(typeof(TypStrefy), TypStrefy.NieWplywa).Should().BeTrue(); System.Enum.IsDefined(typeof(TypStrefy), TypStrefy.Zwieksza).Should().BeTrue(); System.Enum.IsDefined(typeof(TypStrefy), TypStrefy.Zmniejsza).Should().BeTrue(); } }