Files
soneta-erp-skills/Soneta.Skills.Test/KadryPlace/Pracownik/RozdzialEFrest_PlanRcpTest.cs
T
2026-06-06 22:33:15 +02:00

415 lines
21 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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;
/// <summary>
/// Rozdział E/F (część druga) — operacje na planie pracy i RCP wykraczające poza CRUD dni:
/// <list type="bullet">
/// <item>E3 — aktualizacja kalendarza pracownika (worker seryjny, wymaga Context → <c>[Ignore]</c>),</item>
/// <item>E4 — uzgodnienie doby pracowniczej (worker dnia/grupowy, wymaga Context → <c>[Ignore]</c>),</item>
/// <item>E5 — odczyt normy i czasu przepracowanego przez <c>pracownik.Czasy : KalkulatorPracownika</c> (★ pełny odczyt),</item>
/// <item>F3 — import RCP: sam import plikowy <c>[Ignore]</c>; przeliczenie we/wy przez <c>ImportDniaWorker</c> (★),</item>
/// <item>F4 — weryfikacja/korekta RCP: <c>DzienRCP</c>/<c>StanRCP</c> (★ korekta na świeżym dniu),</item>
/// <item>F5 — praca hybrydowa: strefy dnia i podzielniki (★ odczyt).</item>
/// </list>
/// <para>
/// Operujemy wyłącznie na <b>publicznym kontrakcie</b> 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".
/// </para>
/// <para>
/// <b>Granica testowalności.</b> Operacje wymagające <see cref="Context"/> (worker E3/E4 grupowy —
/// <c>Params : ContextBase</c> z ctorem <c>(Context)</c>, karmiony zaznaczeniem listy) lub źródła
/// zewnętrznego (import RCP z pliku/czytnika) są oznaczone <c>[Ignore]</c> z uzasadnieniem — opisują
/// kontrakt, nie wykonują operacji. <c>KalkulatorPracownika</c>/<c>CzasDni</c>/<c>ZestawienieNadgodzin</c>
/// nie są wierszami ORM — to obiekty liczące (czysty odczyt bez transakcji).
/// </para>
/// </summary>
[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<Pracownik[]>();
// 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<Prac>(guidPrac);
var dp2 = p2.DniPracy[Dzien];
dp2.Should().NotBeNull("dzień ewidencji z przeliczonymi odbiciami istnieje po zapisie");
dp2.WeWy.Cast<WejscieWyjscie>().Should().HaveCount(2, "wejście i wyjście zostały zachowane");
}
// ============================== F4 — Weryfikacja / korekta RCP (★ testowalne) ==============================
[Test]
[Description("F4 (odczyt): DniRCP to DateSubTable<DzienRCP> (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<Prac>(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<StrefaPracy> — 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<Prac>(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<StrefaPracy>())
{
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<ElementRozliczeniaCzasuPracy>())
{
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();
}
}