using System; using System.IO; using System.Linq; using System.Text; using AwesomeAssertions; using Microsoft.Extensions.DependencyInjection; // GetRequiredService using NUnit.Framework; using Soneta.Business; // Context using Soneta.Business.UI; // IReportService, ReportResult, ReportFormats using Soneta.Place; // ListaPlac, DefinicjaListyPlac, NaliczanieWypłat, Wyplata, TypNaliczenia using Soneta.Types; // Date, FromTo, YearMonth using Prac = Soneta.Kadry.Pracownik; namespace Soneta.Skills.Test.KadryPlace.Pracownik; /// /// Rozdział I — „Listy płac, przelewy, wydruki” (receptury I1, I2, I3). /// /// Testy są wykonywalną dokumentacją publicznego kontraktu list płac i ich wydruków. /// /// /// I1a — ręczne utworzenie pustej listy płac (new ListaPlac() + Place.ListyPlac.AddRow), /// ustawienie pól w wymaganej kolejności (Definicja → Data → DataWyplaty → MiesiacZUS → Okres). /// I1b — naliczenie wypłaty workerem NaliczanieSeryjne.Pracownika z jawną /// DefinicjaListy (sprawdzona ścieżka z sekcji H): worker tworzy listę płac wg tej definicji i WIĄŻE /// z nią wypłatę. Asercja: wypłata naliczona, powiązanie dwukierunkowe (w.ListaPlac niepuste, jego /// Definicja == def; w.Pracownik == pracownik). /// Rozbieżność dokumentacji: niskopoziomowy worker Soneta.Place.NaliczanieWypłat uruchomiony /// tylko z ListaPłac+Pracownik (snippet I1 w pracownik.md) w bazie Demo nie napełnia listy /// (zwraca pustą WszystkieWypłaty); działającą ścieżką naliczania jest NaliczanieSeryjne. /// I2 — PDF kwitka (paska) wypłaty przez IReportService.GenerateReport /// (wzorzec PasekWyplaty.repx, DataType = typeof(Wyplata)). /// I3 — PDF pełnej listy płac (PelnaListaPlac.repx, DataType = typeof(ListaPlac)). /// /// /// Wydruki (I2/I3): serwis (warstwa Soneta.Business.UI) jest /// w bieżącym zestawie referencji Skills.Test OSIĄGALNY (transytywnie, tak jak w wydrukach handlowych — /// rozdz. 12 dokumentów handlowych). Faktyczne wyrenderowanie PDF wymaga jednak zarejestrowanego wzorca /// *.repx (z assembly Soneta.KadryPlace.Reports) oraz silnika renderującego (DevExpress) — /// czego testowa baza Demo nie gwarantuje, a samo ładowanie DevExpress bywa niestabilne w hoście testowym. /// Dlatego generowanie owijamy w try/catch i przy braku wzorca/silnika robimy Assert.Ignore /// (suita pozostaje zielona, a kod dokumentuje publiczne API). Asercję na sygnaturze "%PDF" /// wykonujemy tylko wtedy, gdy strumień faktycznie powstał. /// /// /// Wszystko działa na bazie Demo (GoldStandard) z automatycznym rollbackiem po teście. Operujemy wyłącznie /// na publicznym kontrakcie platformy Soneta (jak dodatek programisty zewnętrznego). /// /// [TestFixture] public class RozdzialI_ListyWydrukiTest : PracownikTestBase { /// Sygnatura nagłówka pliku PDF (pierwsze 4 bajty/znaki strumienia). private const string PdfMagic = "%PDF"; /// Wzorzec wydruku paska (kwitka) wypłaty — wg tabeli I2 (DataType = Wyplata). private const string WzorzecPasek = "PasekWyplaty.repx"; /// Wzorzec wydruku pełnej listy płac — wg tabeli I3 (DataType = ListaPlac). private const string WzorzecPelnaLista = "PelnaListaPlac.repx"; /// Serwis raportowy ze scope’u bieżącej sesji (jak w wydrukach handlowych). private IReportService Raporty => Session.GetRequiredService(); // === Pomocniki lokalne === /// /// Wybiera dowolną dostępną definicję listy płac z bazy Demo (słownik konfiguracyjny /// Place.DefListPlac). Nazwy/symbole definicji zależą od wdrożenia, więc zamiast /// twardego symbolu („ETAT”) pobieramy pierwszą dostępną definicję — deterministycznie, /// bez zakładania konkretnej konfiguracji. /// private DefinicjaListyPlac DowolnaDefinicjaListy() => Place.DefListPlac.Cast().FirstOrDefault(); /// /// Dobiera okres/daty listy w obrębie aktywnego etatu pracownika: bierzemy miesiąc rozpoczęcia /// etatu (dla pracowników Demo etat zwykle zaczyna się wstecz i jest otwarty), aby naliczanie /// trafiło w okres zatrudnienia. Zwraca (okresMiesiąca, dataWyplaty = koniec miesiąca). /// private static (FromTo Okres, Date DataWyplaty) OkresWEtacie(Prac pracownik) { var from = pracownik.Last.Etat.Okres.From; var poczatek = new Date(from.Year, from.Month, 1); var koniec = poczatek.AddMonths(1).AddDays(-1); // koniec miesiąca (28–31) return (new FromTo(poczatek, koniec), koniec); } /// /// Demonstruje ręczne utworzenie pustej listy płac z wybraną definicją i polami ustawionymi /// w wymaganej kolejności (Definicja → Data → DataWyplaty → MiesiacZUS → Okres), zwraca utworzoną /// listę. Sama lista jest tworzona poprawnie; napełnienie jej wypłatami realizuje worker /// naliczający (patrz ), a nie ustawienie pól listy. /// private ListaPlac UtworzPustaListe(Prac pracownik, DefinicjaListyPlac def) { var (okres, dataWyplaty) = OkresWEtacie(pracownik); var lp = new ListaPlac(); Place.ListyPlac.AddRow(lp); lp.Definicja = def; // wzorzec listy — ustaw PIERWSZE po AddRow // Wydzial/Seria ustawiamy WARUNKOWO — tylko gdy wymaga ich definicja. if (def.Wydzial) lp.Wydzial = Kadry.Wydzialy.Firma; lp.Data = dataWyplaty; // data naliczania listy lp.DataWyplaty = dataWyplaty; // data przekazania środków (wyznacza mies./rok) lp.MiesiacZUS = new YearMonth(dataWyplaty); // miesiąc rozliczenia ZUS lp.Okres = okres; // okres listy — PO DataWyplaty return lp; } /// /// Nalicza wypłatę etatową pracownika workerem NaliczanieSeryjne.Pracownika (sprawdzona /// ścieżka z sekcji H). Worker sam dobiera/tworzy listę płac dla naliczanych wypłat i WIĄŻE je /// z nią (Wyplata.ListaPlac). /// /// Nalicz() sam otwiera i commituje transakcję w sesji — NIE owijamy go w InTransaction. /// Pola Naliczanie nie ustawiamy (domyślne; setter rzuca bez licencji „PL Złoty”). /// DefinicjaListy także NIE wymuszamy — dowolna definicja może nie pasować do typu wypłaty /// (np. lista umów ≠ etat) i wtedy nic się nie naliczy; worker dobiera definicję sam. /// Zwraca pierwszą naliczoną wypłatę albo null, gdy nic się nie naliczyło. /// /// private Wyplata NaliczWyplate(Prac pracownik) { var (okres, dataWyplaty) = OkresWEtacie(pracownik); var pars = new NaliczanieSeryjne.PracownikParams(Context) { DataWypłaty = dataWyplaty, // ustawia Okres i MiesiącDeklaracji automatycznie DataListy = dataWyplaty, TypWypłaty = TypWyplaty.Etat, // tylko wypłaty etatowe }; var naliczanie = new NaliczanieSeryjne.Pracownika(pars) { Pracownik = pracownik }; var wynik = naliczanie.Nalicz(); // self-commit w sesji return wynik.WszystkieWypłaty.Cast().FirstOrDefault(); } // =================================================================================== // I1 — Tworzenie i naliczanie listy płac // =================================================================================== [Test] [Description("I1 (część A): ręcznie tworzymy pustą listę płac — new ListaPlac() + Place.ListyPlac.AddRow + " + "pola w wymaganej kolejności (Definicja → Data → DataWyplaty → MiesiacZUS → Okres). " + "Asercja: lista istnieje, ma przypisaną definicję i jest pusta (Wyplaty napełnia dopiero worker).")] public void I1a_PustaListaPlac_TworzenieRecznePolaWKolejnosci() { var pracownik = Pracownik(Pracownik_.Andrzejewski); pracownik.Should().NotBeNull(); var def = DowolnaDefinicjaListy(); def.Should().NotBeNull("baza Demo zawiera co najmniej jedną definicję listy płac (Place.DefListPlac)"); // Tworzenie danych operacyjnych MUSI być w trybie edycji (InTransaction), inaczej AddRow // rzuca CannotEditException. ListaPlac lp = null; InTransaction(() => lp = UtworzPustaListe(pracownik, def)); lp.Should().NotBeNull(); lp.Definicja.Should().Be(def, "ustawiliśmy Definicja po AddRow"); lp.Wyplaty.Cast().Should().BeEmpty("nowo utworzona lista jest pusta — wypłaty dolicza worker"); SaveDispose(); // utrwalenie w bazie (rollback po teście i tak wycofa) } [Test] [Description("I1 (część B): naliczamy wypłatę etatową workerem NaliczanieSeryjne.Pracownika (sprawdzona " + "ścieżka z sekcji H). Worker sam dobiera/tworzy listę płac i WIĄŻE z nią wypłatę. " + "Asercja: wypłata naliczona, powiązana dwukierunkowo z listą płac (w.ListaPlac niepuste, " + "ma definicję) i z pracownikiem (w.Pracownik == pracownik). " + "Uwaga: niskopoziomowy worker Soneta.Place.NaliczanieWypłat (samo ListaPłac+Pracownik z " + "dokumentacji) w bazie Demo nie napełnia listy — sprawdzoną ścieżką jest NaliczanieSeryjne.")] public void I1b_ListaPlac_NaliczanieWyplatyPowiazanaZLista() { var pracownik = Pracownik(Pracownik_.Andrzejewski); pracownik.Should().NotBeNull(); // NaliczanieSeryjne.Nalicz() sam otwiera i commituje transakcję — NIE owijamy w InTransaction. var w = NaliczWyplate(pracownik); w.Should().NotBeNull( "naliczanie etatu dla pracownika Demo w okresie etatu powinno dać wypłatę powiązaną z listą"); // Powiązanie dwukierunkowe: wypłata wskazuje wstecz listę płac i pracownika. var lista = (ListaPlac)w.ListaPlac; lista.Should().NotBeNull("Wyplata.ListaPlac wskazuje listę, na której została naliczona"); lista.Definicja.Should().NotBeNull("lista płac utworzona przez worker ma przypisaną definicję"); w.Pracownik.Guid.Should().Be(pracownik.Guid, "Wyplata.Pracownik to pracownik, dla którego naliczono"); SaveDispose(); } // =================================================================================== // I2 — Drukowanie/PDF kwitka (paska) wypłaty // =================================================================================== [Test] [Description("I2: pasek (kwitek) wypłaty do PDF przez IReportService.GenerateReport " + "(TemplateFileName = PasekWyplaty.repx, DataType = typeof(Wyplata), OutputFormat = PDF, " + "Context.Set(wyplata)). Strumień zaczyna się od sygnatury „%PDF”. " + "Brak wzorca/silnika renderującego → Assert.Ignore (suita zielona).")] public void I2_PasekWyplaty_DoPdf_ZaczynaSieOdPdf() { var pracownik = Pracownik(Pracownik_.Bednarek); pracownik.Should().NotBeNull(); // Arrange: naliczona wypłata (wraz z listą) jako źródło danych wydruku. // NaliczanieSeryjne self-commituje — wypłata jest dostępna w bieżącej sesji. var wyplata = NaliczWyplate(pracownik); if (wyplata == null) Assert.Ignore("Worker nie naliczył wypłaty dla pracownika Demo — brak danych do wydruku paska."); // Kontekst wydruku: pojedyncza Wyplata (jak w snippetcie I2). var context = Login.CreateEmptyContext().Clone(Session); context.Set(wyplata); var rr = new ReportResult { TemplateFileName = WzorzecPasek, // tryb automatyczny (bez UI) DataType = typeof(Wyplata), // pojedyncza wypłata Context = context, OutputFormat = ReportFormats.PDF, AskForParameters = false // tryb wsadowy — nie pytaj o parametry }; // Act: generowanie do strumienia. Brak wzorca/silnika → Assert.Ignore zamiast błędu. byte[] naglowek; try { using var pdf = Raporty.GenerateReport(rr); pdf.Should().NotBeNull("GenerateReport dla formatu binarnego zwraca Stream"); naglowek = new byte[4]; int przeczytane = pdf.Read(naglowek, 0, naglowek.Length); przeczytane.Should().Be(4, "PDF ma co najmniej 4-bajtowy nagłówek"); } catch (Exception ex) { Assert.Ignore("Pominięto I2: wygenerowanie PDF paska wymaga zarejestrowanego wzorca '" + WzorzecPasek + "' (assembly Soneta.KadryPlace.Reports) oraz silnika renderującego " + "(DevExpress), których testowa baza Demo nie gwarantuje. Test dokumentuje publiczne API " + "IReportService.GenerateReport. Szczegóły: " + ex.GetType().Name + " — " + ex.Message); return; } Encoding.ASCII.GetString(naglowek).Should().StartWith(PdfMagic, "poprawny strumień PDF zaczyna się od „%PDF”."); } // =================================================================================== // I3 — Drukowanie/PDF całej listy płac // =================================================================================== [Test] [Description("I3: pełna lista płac do PDF przez IReportService.GenerateReport " + "(TemplateFileName = PelnaListaPlac.repx, DataType = typeof(ListaPlac), OutputFormat = PDF, " + "Context.Set(listaPlac)). Strumień zaczyna się od sygnatury „%PDF”. " + "Brak wzorca/silnika renderującego → Assert.Ignore (suita zielona).")] public void I3_PelnaListaPlac_DoPdf_ZaczynaSieOdPdf() { var pracownik = Pracownik(Pracownik_.Bujak); pracownik.Should().NotBeNull(); // Arrange: naliczona wypłata daje listę płac (Wyplata.ListaPlac) jako źródło danych wydruku. // NaliczanieSeryjne self-commituje — lista jest dostępna w bieżącej sesji. var wyplata = NaliczWyplate(pracownik); if (wyplata == null) Assert.Ignore("Worker nie naliczył wypłaty dla pracownika Demo — brak listy płac do wydruku."); var lp = (ListaPlac)wyplata.ListaPlac; lp.Should().NotBeNull(); var context = Login.CreateEmptyContext().Clone(Session); context.Set(lp); // ListaPlac var rr = new ReportResult { TemplateFileName = WzorzecPelnaLista, DataType = typeof(ListaPlac), Context = context, OutputFormat = ReportFormats.PDF, AskForParameters = false }; // Act: skopiowanie strumienia do pamięci (jak wzorzec integracyjny — bajty → załącznik/REST). byte[] pdfBytes; try { using Stream src = Raporty.GenerateReport(rr); using var ms = new MemoryStream(); src.CopyTo(ms); pdfBytes = ms.ToArray(); } catch (Exception ex) { Assert.Ignore("Pominięto I3: wygenerowanie PDF pełnej listy płac wymaga zarejestrowanego wzorca '" + WzorzecPelnaLista + "' (assembly Soneta.KadryPlace.Reports) oraz silnika renderującego " + "(DevExpress), których testowa baza Demo nie gwarantuje. Test dokumentuje publiczne API " + "IReportService.GenerateReport. Szczegóły: " + ex.GetType().Name + " — " + ex.Message); return; } pdfBytes.Should().NotBeNullOrEmpty("wydruk listy płac zwraca niepusty bufor bajtów"); pdfBytes.Length.Should().BeGreaterThan(4); Encoding.ASCII.GetString(pdfBytes, 0, 4).Should().StartWith(PdfMagic, "bufor bajtów to plik PDF (sygnatura „%PDF”)."); } }