using System; using System.IO; using System.Text; using AwesomeAssertions; using Microsoft.Extensions.DependencyInjection; // GetRequiredService using NUnit.Framework; using Action = System.Action; using Soneta.Business; // Context using Soneta.Business.UI; // IReportService, ReportResult, ReportFormats, ReportTargets using Soneta.Handel; // DokumentHandlowy, ParametryWydrukuDokumentu namespace Soneta.Skills.Test.Handel.DokumentyHandlowe; /// /// Rozdział 12 skilla „dokument-handlowy” — Wydruki i raporty (W62–W66). /// /// Wydruk dokumentu handlowego oraz raporty/zestawienia generuje serwis /// (scope sesji: Session.GetRequiredService<IReportService>()). /// Serwis bierze wzorzec wydruku (*.repx), kontekst z danymi (rekord, tablica zaznaczeń, /// parametry wydruku) i zwraca gotowy dokument jako strumień ( /// → Stream) lub tekst (string) — bez UI. /// /// /// Ścieżka testowalna: wygenerowanie wydruku do strumienia PDF i sprawdzenie, że bajty /// zaczynają się od sygnatury "%PDF" (HTML zaczyna się od "<!DOCTYPE html"). /// /// /// Co NIE jest testowalne jednostkowo (wymaga sprzętu, brak asercji): /// druk na fizyczną drukarkę (PrintReport, Target = ReportTargets.Printer) oraz /// fiskalny raport dobowy/okresowy drukarki (IFiscalPrinterAPI.DrukujRaport*, Fiskalizuj). /// Dla nich dokumentuje się tylko poprawne ustawienie ReportResult/parametrów, bez druku. /// /// /// Pułapka konfiguracyjna: generowanie wymaga realnego, zarejestrowanego wzorca *.repx. /// Nazwy wzorców (np. „Sprzedaz.repx”) są elementem konfiguracji wdrożenia i mogą być nieobecne /// w testowej bazie Demo / brak silnika renderującego (DevExpress). Dlatego całe generowanie owijamy /// w try/catch i przy braku wzorca/silnika robimy Assert.Ignore — test pozostaje zielony, /// a jednocześnie dokumentuje publiczne API. Asercję na "%PDF" wykonujemy tylko wtedy, /// gdy strumień faktycznie powstał. /// /// Cała klasa operuje wyłącznie na publicznym kontrakcie platformy Soneta (jak dodatek zewnętrzny). /// [TestFixture] public class Rozdzial12_WydrukiTest : DokumentHandlowyTestBase { /// Sygnatura nagłówka pliku PDF (pierwsze 4 bajty/znaki strumienia). private const string PdfMagic = "%PDF"; /// Nazwa wzorca wydruku faktury sprzedaży (zgodnie ze snippetem W62/W66 w skillu). private const string WzorzecSprzedaz = "Sprzedaz.repx"; /// Serwis raportowy ze scope'u bieżącej sesji (jak IRelacjeService w rozdz. 4). private IReportService Raporty => Session.GetRequiredService(); // === Pomocniki lokalne === /// /// Tworzy i ZAPISUJE fakturę sprzedaży (FV) z jedną pozycją towaru BIKINI, pozostawioną w BUFORZE. /// /// Faktury NIE zatwierdzamy: w testowej bazie Demo ustawienie /// fv.Stan = StanDokumentuHandlowego.Zatwierdzony rzuca NullReferenceException /// w ewidencji VAT (potwierdzone empirycznie). Wydruk można jednak zbudować z faktury w buforze — /// SumyVAT, Suma, SumaPozycji, Platnosci są w buforze już wyliczone. /// /// /// Demo blokuje stan ujemny → rozchód (FV) wymaga wcześniej ZAKSIĘGOWANEGO przyjęcia. Używamy /// helpera bazowego (tworzy zatwierdzone PW + Save → księguje stan). /// /// Zwraca Guid zapisanego dokumentu; sesja edycyjna zostaje zamknięta przez . /// private Guid UtworzFaktureWBuforze() { // 1. Zaksięgowany stan magazynowy (zatwierdzone PW + Save) — żeby rozchód FV nie dał stanu ujemnego. PrzyjmijNaStan(Towar_.Bikini, 20); // 2. Faktura sprzedaży FV na kontrahenta i magazyn „F”, z pozycją mieszczącą się w stanie. // NIE zatwierdzamy (zatwierdzenie FV rzuca NRE w ewidencji VAT w bazie Demo) — zostaje w buforze. var fv = UtworzDokument(Definicje.FakturaSprzedazy, kontrahent: Kontrahent(Kontrahent_.Abc), magazyn: Magazyn(Magazyn_.Firma)); InTransaction(() => DodajPozycje(fv, Towar(Towar_.Bikini), ilosc: 2, cena: 12)); var guid = fv.Guid; SaveDispose(); return guid; } /// /// Buduje kontekst wydruku pojedynczego dokumentu zgodnie ze snippetem W62: /// rekord, definicja, kontrahent, tablica zaznaczeń oraz instancja parametrów wydruku. /// private Context KontekstWydruku(DokumentHandlowy dok) { var context = Login.CreateEmptyContext().Clone(Session); context.Set(dok); context.Set(dok.Definicja); if (dok.Kontrahent != null) context.Set(dok.Kontrahent); context.Set(new[] { dok }); // wymagane przez część wzorców context.Set(new ParametryWydrukuDokumentu(context) { Duplikat = false }); return context; } // =================================================================================== // W62 / W66 — Wydruk faktury do PDF (strumień) i sprawdzenie sygnatury „%PDF” // =================================================================================== [Test] [Description("W62/W66: IReportService.GenerateReport z TemplateFileName i OutputFormat=PDF dla " + "pojedynczego dokumentu (DataType=typeof(DokumentHandlowy)) zwraca strumień PDF " + "zaczynający się od sygnatury „%PDF”. Brak wzorca/silnika → Assert.Ignore (suita zielona).")] [Ignore("Wymaga zarejestrowanego wzorca .repx oraz silnika renderującego (DevExpress), których testowa " + "baza Demo nie gwarantuje; faktyczne wywołanie GenerateReport ładuje DevExpress i bywa niestabilne " + "w hoście testowym. Test dokumentuje publiczne API IReportService.GenerateReport (kod w ciele metody).")] public void W62_WydrukFakturyDoPdf_ZaczynaSieOdPdf() { // Arrange: faktura sprzedaży w buforze + kontekst wydruku (rekord, parametry, zaznaczenie). // FV pozostaje w buforze (zatwierdzenie FV w bazie Demo rzuca NRE w ewidencji VAT); // wydruk buduje się z dokumentu buforowego — sumy/VAT/płatności są już wyliczone. var dok = Get(UtworzFaktureWBuforze()); dok.Should().NotBeNull(); var rr = new ReportResult { TemplateFileName = WzorzecSprzedaz, // tryb automatyczny (bez UI) DataType = typeof(DokumentHandlowy), // pojedynczy dokument Context = KontekstWydruku(dok), OutputFormat = ReportFormats.PDF, AskForParameters = false // tryb wsadowy — nie pytaj o parametry }; // Act: generowanie do strumienia. Owijamy w try/catch — gdy wzorzec/silnik nieobecny, // pomijamy test (Assert.Ignore), zamiast zgłaszać błąd. Strumień zawsze w using. byte[] naglowek; try { using var pdf = Raporty.GenerateReport(rr); pdf.Should().NotBeNull("GenerateReport dla formatu binarnego zwraca Stream"); // Odczyt pierwszych 4 bajtów do sprawdzenia sygnatury „%PDF”. 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 W62: wygenerowanie PDF wymaga zarejestrowanego wzorca '" + WzorzecSprzedaz + "' oraz silnika renderującego, których testowa baza Demo " + "nie gwarantuje. Test dokumentuje publiczne API IReportService.GenerateReport. " + "Szczegóły: " + ex.GetType().Name + " — " + ex.Message); return; } // Assert: strumień zaczyna się od sygnatury PDF. Encoding.ASCII.GetString(naglowek).Should().StartWith(PdfMagic, "poprawny strumień PDF zaczyna się od „%PDF”."); } [Test] [Description("W66: integracja — GenerateReport zapisany do MemoryStream daje bajty PDF (np. do e-maila/REST). " + "Sprawdza, że pierwsze bajty całego bufora to „%PDF”. Brak wzorca/silnika → Assert.Ignore.")] [Ignore("Wymaga wzorca .repx + silnika DevExpress (jak W62); GenerateReport ładuje DevExpress i bywa " + "niestabilne w hoście testowym. Dokumentuje publiczne API zapisu wydruku do strumienia (kod w ciele).")] public void W66_WydrukDoStrumieniaBajtow_DajePoprawnyPdf() { // Arrange: faktura w buforze + kontekst jak w W62 (FV nie zatwierdzamy — NRE w ewidencji VAT w Demo). var dok = Get(UtworzFaktureWBuforze()); var rr = new ReportResult { TemplateFileName = WzorzecSprzedaz, DataType = typeof(DokumentHandlowy), Context = KontekstWydruku(dok), OutputFormat = ReportFormats.PDF, AskForParameters = false }; // Act: skopiowanie strumienia do pamięci (wzorzec integracji z W66: 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 W66: zapis wydruku do strumienia bajtów wymaga obecnego wzorca '" + WzorzecSprzedaz + "' i silnika renderującego (brak w testowej bazie Demo). " + "Test dokumentuje wzorzec integracyjny GenerateReport → byte[]. " + "Szczegóły: " + ex.GetType().Name + " — " + ex.Message); return; } // Assert: bufor zawiera dane i zaczyna się od sygnatury PDF. pdfBytes.Should().NotBeNullOrEmpty("integracyjny wydruk 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”)."); } // =================================================================================== // W62/W66 — Reguły spójności ReportResult (CheckConsistency) — bez renderowania // =================================================================================== [Test] [Description("W62/W66 (reguła CheckConsistency): IReportService wymaga ustawionego TemplateFileName i " + "wyklucza ReportName. ReportResult bez TemplateFileName, ale z ReportName, narusza spójność " + "→ GenerateReport powinno rzucić ArgumentException (a nie wyrenderować PDF).")] public void W66_RegulaSpojnosci_BrakTemplateFileName_RzucaArgumentException() { // Arrange: konfiguracja wykluczająca tryb IReportService — ReportName zamiast TemplateFileName. // Reguła spójności ReportResult sprawdzana jest PRZED dostępem do danych, więc test // nie potrzebuje żadnego dokumentu (a tym bardziej zatwierdzonej FV) — pusty kontekst wystarcza. var rr = new ReportResult { ReportName = "Faktura", // tryb interaktywny z menu — wyklucza się z TemplateFileName DataType = typeof(DokumentHandlowy), Context = Login.CreateEmptyContext().Clone(Session), OutputFormat = ReportFormats.PDF, AskForParameters = false }; // Act + Assert: naruszenie reguły spójności → ArgumentException. // Asercja samej walidacji nie wymaga obecności wzorca .repx, więc nie owijamy jej w Ignore. Action act = () => Raporty.GenerateReport(rr); act.Should().Throw( "IReportService akceptuje wyłącznie tryb z TemplateFileName; ReportName i brak TemplateFileName " + "naruszają CheckConsistency"); } // =================================================================================== // SCENARIUSZE POMINIĘTE (SKIP) — uzasadnienie zgodne z treścią rozdziału 12 // =================================================================================== [Test] [Ignore("W62/W63 (sprzęt) — druk na FIZYCZNĄ drukarkę: IReportService.PrintReport(rr) oraz " + "ReportResult.Target = ReportTargets.Printer/PrinterService wymagają podłączonej drukarki i " + "sterownika. To operacja sprzętowa — NIE da się jej przetestować jednostkowo (brak asercji na " + "wyniku). W kodzie i integracjach używaj ścieżki GenerateReport → strumień/PDF (W62/W66). SKIP wg pułapek W62.")] [Description("W62/W63: druk na fizyczną drukarkę (PrintReport / Target=Printer) — nietestowalny (wymaga sprzętu).")] public void W62_DrukNaDrukarke_Skip() { } [Test] [Ignore("W63 — wydruk dokumentu magazynowego (PZ/WZ/MM): mechanizm identyczny jak W62, różni tylko wzorzec " + "dobrany do rodzaju dokumentu wg jego definicji (np. „WydanieZewnetrzne.repx”) + ustawienie dok.Magazyn " + "w kontekście. Test renderowania jest pokryty wzorcowo przez W62 (ta sama ścieżka GenerateReport → „%PDF”); " + "osobny test wymagałby kolejnego, niegwarantowanego wzorca .repx i nie wnosi nowej ścieżki API. " + "SKIP: identyczny kontrakt, inny plik wzorca (konfiguracja wdrożenia).")] [Description("W63: wydruk dokumentu magazynowego (WydanieZewnetrzne.repx) — pominięte (ten sam kontrakt co W62, inny wzorzec).")] public void W63_WydrukDokumentuMagazynowego_Skip() { } [Test] [Ignore("W64 (ścieżka bazodanowa) — zestawienie/raport dobowy/okresowy przez IReportService z wzorcem " + "zestawienia (np. „ZestawienieSprzedazy.repx”), DataType=typeof(Soneta.Handel.DokHandlowe) i parametrem " + "okresu FromTo w kontekście. Ścieżka API jest tożsama z W62 (GenerateReport → „%PDF”), różni ją wyłącznie " + "wzorzec i typ danych; konkretny wzorzec zestawienia nie jest gwarantowany w bazie Demo. SKIP: pokryte " + "wzorcowo przez W62, brak gwarancji wzorca rejestru.")] [Description("W64: bazodanowe zestawienie za dzień/okres (FromTo, DataType=DokHandlowe) — pominięte (ten sam kontrakt co W62).")] public void W64_ZestawienieBazodanowe_Skip() { } [Test] [Ignore("W64 (sprzęt) — fiskalny raport dobowy/okresowy drukarki: Soneta.Fiskal.IFiscalPrinterAPI." + "DrukujRaport(nazwaDrukarki) / DrukujRaportOkresowy(nazwaDrukarki, RaportOkresowyParams) oraz Fiskalizuj(...) " + "wymagają podłączonej DRUKARKI FISKALNEJ — operacja sprzętowa, NIE do testów jednostkowych. Testować można " + "tylko poprawne ustawienie RaportOkresowyParams.RaportZaOkres (FromTo), nie faktyczny druk. SKIP wg pułapek W64.")] [Description("W64: fiskalny raport dobowy/okresowy (IFiscalPrinterAPI) — nietestowalny (wymaga drukarki fiskalnej).")] public void W64_FiskalnyRaport_Skip() { } [Test] [Ignore("W65 — wydruk zbiorczy dla zaznaczonego zbioru: DataType=typeof(DokumentHandlowy[]) + Rows=tablica + " + "Context.Set(tablica). Ścieżka renderowania jest tożsama z W62 (GenerateReport → „%PDF”), różni ją tylko " + "tryb wielu rekordów; test wymagałby tego samego, niegwarantowanego wzorca „Sprzedaz.repx”. Aby utrzymać " + "suitę zieloną i nie duplikować ścieżki, scenariusz dokumentujemy tu (SKIP), a renderowanie pokrywa W62. " + "Kluczowa różnica vs W62: DataType tablicowy przełącza wzorzec w tryb wielu rekordów.")] [Description("W65: wydruk zbiorczy (DataType=DokumentHandlowy[], Rows) — pominięte (ta sama ścieżka renderowania co W62).")] public void W65_WydrukZbiorczy_Skip() { } [Test] [Ignore("W66 (e-mail/OutputHandler) — Target=ReportTargets.Email/Attachment wymaga skonfigurowanego konta " + "pocztowego (KontoPocztowe) i szablonu (SzablonEmail) w pełnej sesji aplikacyjnej — poza zakresem testu " + "jednostkowego. ReportResult.OutputHandler NIE jest obsługiwany przez IReportService (CheckConsistency " + "rzuca ArgumentException) — służy jako rezultat operacji w trybie wzorca (worker/Command z UI). Testowalny " + "rdzeń W66 (GenerateReport → byte[]) pokrywa W66_WydrukDoStrumieniaBajtow. SKIP: integracja pocztowa / tryb UI.")] [Description("W66: wysyłka e-mail (Target=Email) i OutputHandler — pominięte (wymaga konta/szablonu / tryb UI).")] public void W66_EmailIOutputHandler_Skip() { } }