using System; using System.Collections.Generic; using System.Linq; using AwesomeAssertions; using NUnit.Framework; using Soneta.Handel; namespace Soneta.Skills.Test.Handel.DokumentyHandlowe; /// /// Rozdział 10 — Operacje zbiorcze (batch), wzorce W53–W55. /// /// Operacje na zbiorze dokumentów wykonujemy bezpiecznie i wydajnie: filtr serwerowy /// (a nie pełny skan tabeli operacyjnej DokHandlowe), krótkie transakcje /// (paczki) oraz świadoma obsługa zapisu (Save(), gdzie wykrywane są konflikty /// optymistyczne). W testach krótka transakcja = InTransaction(...), a zamknięcie /// paczki = SaveDispose() (Save + zamknięcie okna edycji sesji). /// /// /// W bazie Demo działa StanUjemnyVerifier (blokada stanu ujemnego), więc do operacji /// zbiorczych używamy przychodów (PW) — nie podlegają tej blokadzie i nie wymagają /// wcześniejszego zapasu towaru. Magazyn księguje się dopiero po Session.Save(). /// /// Cała klasa operuje wyłącznie na publicznym kontrakcie platformy (jak dodatek zewnętrzny). /// [TestFixture] public class Rozdzial10_BatchTest : DokumentHandlowyTestBase { // === Pomocnik lokalny: kilka przyjęć (PW) w buforze, zapisanych trwale === /// /// Tworzy dokumentów przyjęcia wewnętrznego (PW) z jedną pozycją /// BIKINI, pozostawia je w buforze i zapisuje trwale. Zwraca listę Guidów (sesja zostaje /// zamknięta przez , więc dalej pracujemy przez odczyt po Guid). /// PW to przychód — bez ryzyka blokady stanu ujemnego, idealny do testów wsadowych. /// private List UtworzPwWBuforzeIZapisz(int ile, double ilosc = 10, double cena = 5) { var guidy = new List(ile); for (int i = 0; i < ile; i++) { // Każdy dokument tworzymy przez bazowy helper (AddRow -> Definicja -> Magazyn). var dok = UtworzDokument(Definicje.PrzyjecieWewnetrzne, magazyn: Magazyn(Magazyn_.Firma)); InTransaction(() => DodajPozycje(dok, Towar(Towar_.Bikini), ilosc, cena)); guidy.Add(dok.Guid); } // Jeden wspólny Save dla wszystkich utworzonych dokumentów. SaveDispose(); return guidy; } // === W54 — Hurtowe zatwierdzanie wielu dokumentów w jednej transakcji === [Test] [Description("W54: hurtowe zatwierdzanie — kilka PW w buforze zatwierdzonych pętlą po Stan w jednej transakcji; po Save wszystkie są Zatwierdzone.")] public void W54_HurtoweZatwierdzanie_WszystkieDokumentyZatwierdzone() { // 1. Przygotowanie: 3 dokumenty PW w buforze, zapisane trwale. var guidy = UtworzPwWBuforzeIZapisz(ile: 3); // Wczytujemy je na świeżej sesji i potwierdzamy stan wyjściowy = Bufor. var dokumenty = guidy.Select(g => Get(g)).ToArray(); dokumenty.Should().OnlyContain(d => d.Bufor); // 2. Hurtowe zatwierdzenie: jedna (krótka) transakcja, pętla po zbiorze i zmiana Stan. // W teście InTransaction odpowiada wzorcowi session.Logout(true) + Commit z dokumentu. InTransaction(() => { foreach (var d in dokumenty) d.Stan = StanDokumentuHandlowego.Zatwierdzony; }); SaveDispose(); // 3. Asercja: po Save wszystkie dokumenty są zatwierdzone (czytamy pola kalkulowane). foreach (var g in guidy) { var zapisany = Get(g); zapisany.Zatwierdzony.Should().BeTrue(); zapisany.Bufor.Should().BeFalse(); } } [Test] [Description("W54: hurtowe cofnięcie do bufora — kilka zatwierdzonych PW cofniętych jedną pętlą po Stan; po Save wszystkie wracają do bufora.")] public void W54_HurtoweCofniecieDoBufora_WszystkieWBuforze() { // 1. Najpierw zatwierdzamy kilka PW (stan wyjściowy do cofnięcia). var guidy = UtworzPwWBuforzeIZapisz(ile: 2); var zatwierdzone = guidy.Select(g => Get(g)).ToArray(); InTransaction(() => { foreach (var d in zatwierdzone) d.Stan = StanDokumentuHandlowego.Zatwierdzony; }); SaveDispose(); guidy.Select(g => Get(g)).Should().OnlyContain(d => d.Zatwierdzony); // 2. Hurtowe cofnięcie: zatwierdzony -> bufor (odksięgowanie przy Save) w jednej transakcji. var doCofniecia = guidy.Select(g => Get(g)).ToArray(); InTransaction(() => { foreach (var d in doCofniecia) d.Stan = StanDokumentuHandlowego.Bufor; }); SaveDispose(); // 3. Asercja: wszystkie z powrotem w buforze. guidy.Select(g => Get(g)) .Should().OnlyContain(d => d.Bufor && !d.Zatwierdzony); } // === W55 — Wydajne przetwarzanie w paczkach (krótkie transakcje, okresowy Save) === [Test] [Description("W55: przetwarzanie w paczkach — kilka dokumentów dzielonych na małe transakcje z okresowym Save; po przetworzeniu wszystkie poprawnie zatwierdzone.")] public void W55_PrzetwarzanieWPaczkach_WszystkieDokumentyPrzetworzone() { // 1. Większy (na potrzeby testu kilkuelementowy) zbiór PW w buforze. const int ileDokumentow = 5; var guidy = UtworzPwWBuforzeIZapisz(ile: ileDokumentow); // 2. Wzorzec paczkowy: małe paczki + Save po każdej paczce (krótka transakcja). // W produkcyjnym kodzie rozmiar paczki to ~200; w teście używamy 2, by faktycznie // domknąć więcej niż jedną paczkę i pokazać wzorzec "Save -> nowa sesja po Guid". // Po SaveDispose okno edycji jest zamknięte, więc kolejną paczkę edytujemy na // świeżej sesji (odczyt po Guid) — odpowiednik nowej session.Logout(true). const int rozmiarPaczki = 2; int przetworzone = 0; // Iterujemy serwerowo wyłonione dokumenty (tu: po znanych Guidach) paczkami. foreach (var paczka in guidy.Chunk(rozmiarPaczki)) { // Każda paczka = osobna krótka transakcja na świeżej sesji. var dokumentyPaczki = paczka.Select(g => Get(g)).ToArray(); InTransaction(() => { foreach (var d in dokumentyPaczki) { d.Stan = StanDokumentuHandlowego.Zatwierdzony; przetworzone++; } }); // Okresowy Save zamyka paczkę (krótka transakcja); kolejna paczka -> nowa sesja. SaveDispose(); } // 3. Asercja poprawności: liczba przetworzonych = liczba dokumentów, // a każdy dokument jest trwale zatwierdzony. przetworzone.Should().Be(ileDokumentow); foreach (var g in guidy) Get(g).Zatwierdzony.Should().BeTrue(); } [Test] [Description("W55: filtr serwerowy z zakresem czasowym — wsadowo zatwierdzamy tylko PW z dzisiejszą datą i w buforze; wzorzec SubTable[condition] zamiast pełnego skanu.")] public void W55_FiltrSerwerowyZakresCzasowy_PrzetwarzaTylkoWybranePaczki() { // 1. Tworzymy kilka PW w buforze (data = dziś, nadana domyślnie przez definicję). const int ileDokumentow = 4; var guidy = UtworzPwWBuforzeIZapisz(ile: ileDokumentow); var oczekiwane = new HashSet(guidy); // 2. Filtr SERWEROWY z zakresem czasowym na tabeli operacyjnej DokHandlowe — // NIE iterujemy całej tabeli z if-em w pamięci. Zawężamy do PW w buforze z dzisiaj. var fv = Definicja(Definicje.PrzyjecieWewnetrzne); var od = Soneta.Types.Date.Today; // Materializujemy zbiór do paczkowego przetwarzania (w produkcji iterujemy strumieniowo). var doPrzetworzenia = Handel.DokHandlowe.WgDaty[(DokumentHandlowy d) => d.Data >= od && d.Definicja == fv && d.Stan == StanDokumentuHandlowego.Bufor] .Cast() .Where(d => oczekiwane.Contains(d.Guid)) // zawężenie tylko do dokumentów tego testu .Select(d => d.Guid) .ToList(); // Filtr serwerowy odnalazł wszystkie utworzone dokumenty tego testu. doPrzetworzenia.Should().HaveCount(ileDokumentow); // 3. Przetwarzanie paczkami (krótkie transakcje) na wyłonionym zbiorze. const int rozmiarPaczki = 2; foreach (var paczka in doPrzetworzenia.Chunk(rozmiarPaczki)) { var dokumentyPaczki = paczka.Select(g => Get(g)).ToArray(); InTransaction(() => { foreach (var d in dokumentyPaczki) d.Stan = StanDokumentuHandlowego.Zatwierdzony; }); SaveDispose(); } // 4. Asercja: wszystkie wyłonione filtrem dokumenty zostały zatwierdzone. foreach (var g in doPrzetworzenia) Get(g).Zatwierdzony.Should().BeTrue(); } // === W53 — Ewidencjonowanie zbiorcze (EwidencjonowanieZbiorczeWorker) === [Test] [Description("W53: ewidencjonowanie zbiorcze (EwidencjonowanieZbiorczeWorker) — pomijane: wymaga konfiguracji księgowej/ewidencji niedostępnej wprost w bazie Demo.")] public void W53_EwidencjonowanieZbiorcze_PominietePoniewazWymagaKonfiguracjiKsiegowej() { // SKIP: pełny tor ewidencjonowania zbiorczego wymaga skonfigurowanej ewidencji // księgowej (definicja dokumentu ewidencji typu SprzedażZbiorczaEwidencja) oraz // dokumentów źródłowych z niepustym symbolem kasy/drukarki fiskalnej. W bazie Demo // nie jest to dostępne wprost, więc tworzenie zbiorczych DokEwidencji nie zadziała // w sposób powtarzalny. Opisujemy tu jedynie PUBLICZNY tor wywołania: // // var worker = new EwidencjonowanieZbiorczeWorker // { // Param = new EwidencjonowanieZbiorczeWorker.Params(context) // { // RaportDla = EwidencjonowanieZbiorczeWorker.RaportDla.Paragonów, // lub KorektParagonów // ZaOkres = new FromTo(new Date(2026, 6, 1), new Date(2026, 6, 30)), // data wystawienia // OkresDostawyZaliczki = FromTo.All, // bez filtra dostawy/zaliczki // SymbolKasy = "D1", // jedna drukarka; puste = wszystkie z symbolem kasy // Definicja = CoreModule.GetInstance(session).DefDokumentow.WgSymbolu["SPZE"], // opcjonalnie // } // }; // worker.Ewidencjonuj(); // worker SAM otwiera transakcję i robi CommitUI() w środku // session.Save(); // dopiero teraz zapis do bazy (tu wykrywane konflikty optymistyczne) // // Uwagi (pułapki): // - NIE owijaj Ewidencjonuj() we własną transakcję edycyjną (worker robi Session.Logout(true) // + CommitUI() wewnętrznie); zagnieżdżenie = podwójny commit. // - Param to property [Context] — ustaw PRZED Ewidencjonuj(), inaczej NullReferenceException. // - Worker przetwarza tylko dokumenty Zatwierdzone/Zablokowane i pomija już // zaewidencjonowane (EwidencjaZbiorcza != null). // - Definicja to rekord konfiguracyjny — pobierz istniejący (WgSymbolu/WgTypu), nie twórz "w locie". Assert.Ignore("W53: ewidencjonowanie zbiorcze wymaga konfiguracji ewidencji księgowej/kasy " + "niedostępnej wprost w bazie Demo. Publiczny tor (Ewidencjonuj() + Params) opisany w komentarzu."); } }