Soneta.Skills.Test

This commit is contained in:
Marcin Wojas
2026-06-06 22:33:15 +02:00
parent d42ca3e825
commit fb2f2695a3
38 changed files with 10644 additions and 0 deletions
@@ -0,0 +1,307 @@
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;
/// <summary>
/// Rozdział I — „Listy płac, przelewy, wydruki” (receptury I1, I2, I3).
/// <para>
/// Testy są <b>wykonywalną dokumentacją</b> publicznego kontraktu list płac i ich wydruków.
/// </para>
/// <list type="bullet">
/// <item><b>I1a</b> — ręczne utworzenie pustej listy płac (<c>new ListaPlac()</c> + <c>Place.ListyPlac.AddRow</c>),
/// ustawienie pól w wymaganej kolejności (Definicja → Data → DataWyplaty → MiesiacZUS → Okres).</item>
/// <item><b>I1b</b> — naliczenie wypłaty workerem <c>NaliczanieSeryjne.Pracownika</c> z jawną
/// <c>DefinicjaListy</c> (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 (<c>w.ListaPlac</c> niepuste, jego
/// <c>Definicja == def</c>; <c>w.Pracownik == pracownik</c>).
/// <b>Rozbieżność dokumentacji:</b> niskopoziomowy worker <c>Soneta.Place.NaliczanieWypłat</c> uruchomiony
/// tylko z <c>ListaPłac</c>+<c>Pracownik</c> (snippet I1 w pracownik.md) w bazie Demo nie napełnia listy
/// (zwraca pustą <c>WszystkieWypłaty</c>); działającą ścieżką naliczania jest <c>NaliczanieSeryjne</c>.</item>
/// <item><b>I2</b> — PDF kwitka (paska) wypłaty przez <c>IReportService.GenerateReport</c>
/// (wzorzec <c>PasekWyplaty.repx</c>, <c>DataType = typeof(Wyplata)</c>).</item>
/// <item><b>I3</b> — PDF pełnej listy płac (<c>PelnaListaPlac.repx</c>, <c>DataType = typeof(ListaPlac)</c>).</item>
/// </list>
/// <para>
/// <b>Wydruki (I2/I3):</b> serwis <see cref="IReportService"/> (warstwa <c>Soneta.Business.UI</c>) 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
/// <c>*.repx</c> (z assembly <c>Soneta.KadryPlace.Reports</c>) 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 <c>Assert.Ignore</c>
/// (suita pozostaje zielona, a kod dokumentuje publiczne API). Asercję na sygnaturze <c>"%PDF"</c>
/// wykonujemy tylko wtedy, gdy strumień faktycznie powstał.
/// </para>
/// <para>
/// Wszystko działa na bazie Demo (GoldStandard) z automatycznym rollbackiem po teście. Operujemy wyłącznie
/// na <b>publicznym kontrakcie</b> platformy Soneta (jak dodatek programisty zewnętrznego).
/// </para>
/// </summary>
[TestFixture]
public class RozdzialI_ListyWydrukiTest : PracownikTestBase
{
/// <summary>Sygnatura nagłówka pliku PDF (pierwsze 4 bajty/znaki strumienia).</summary>
private const string PdfMagic = "%PDF";
/// <summary>Wzorzec wydruku paska (kwitka) wypłaty — wg tabeli I2 (DataType = Wyplata).</summary>
private const string WzorzecPasek = "PasekWyplaty.repx";
/// <summary>Wzorzec wydruku pełnej listy płac — wg tabeli I3 (DataType = ListaPlac).</summary>
private const string WzorzecPelnaLista = "PelnaListaPlac.repx";
/// <summary>Serwis raportowy ze scopeu bieżącej sesji (jak w wydrukach handlowych).</summary>
private IReportService Raporty => Session.GetRequiredService<IReportService>();
// === Pomocniki lokalne ===
/// <summary>
/// Wybiera dowolną dostępną definicję listy płac z bazy Demo (słownik konfiguracyjny
/// <c>Place.DefListPlac</c>). Nazwy/symbole definicji zależą od wdrożenia, więc zamiast
/// twardego symbolu („ETAT”) pobieramy pierwszą dostępną definicję — deterministycznie,
/// bez zakładania konkretnej konfiguracji.
/// </summary>
private DefinicjaListyPlac DowolnaDefinicjaListy()
=> Place.DefListPlac.Cast<DefinicjaListyPlac>().FirstOrDefault();
/// <summary>
/// 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).
/// </summary>
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 (2831)
return (new FromTo(poczatek, koniec), koniec);
}
/// <summary>
/// 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; <b>napełnienie jej wypłatami</b> realizuje worker
/// naliczający (patrz <see cref="NaliczWyplate"/>), a nie ustawienie pól listy.
/// </summary>
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;
}
/// <summary>
/// Nalicza wypłatę etatową pracownika workerem <c>NaliczanieSeryjne.Pracownika</c> (sprawdzona
/// ścieżka z sekcji H). Worker sam dobiera/tworzy listę płac dla naliczanych wypłat i WIĄŻE je
/// z nią (<c>Wyplata.ListaPlac</c>).
/// <para>
/// <c>Nalicz()</c> sam otwiera i commituje transakcję w sesji — NIE owijamy go w InTransaction.
/// Pola <c>Naliczanie</c> nie ustawiamy (domyślne; setter rzuca bez licencji „PL Złoty”).
/// <c>DefinicjaListy</c> 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 <c>null</c>, gdy nic się nie naliczyło.
/// </para>
/// </summary>
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<Wyplata>().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<Wyplata>().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”).");
}
}