using System.Diagnostics; using foodmarket.Pos.Core.Data; using foodmarket.Pos.Core.Logic; using foodmarket.Pos.Core.Models; using foodmarket.Pos.Core.Sync; using foodmarket.Shared.Pos.V1; namespace foodmarket.Pos.Tooling; /// CLI-smoke кассы: сценарий «открыть смену → продажи → возврат → закрыть» /// + офлайн-очередь, через логику pos.core (без UI/дисплея). Работает одинаково /// на Windows и Linux — для проверки на реальной машине. Запуск: --smoke public static class SmokeRunner { public static int Run(string[] args) { var lines = new List(); void Log(string s) { Console.WriteLine(s); lines.Add(s); } // Холодный старт: от запуска процесса до входа в логику (загрузка рантайма + JIT). double coldStartMs = 0; try { coldStartMs = (DateTime.Now - Process.GetCurrentProcess().StartTime).TotalMilliseconds; } catch { } var sw = Stopwatch.StartNew(); using var store = PosStore.InMemory(); // Каталог: 30k товаров для замера поиска + несколько «реальных». var ids = SeedCatalog(store, big: args.Contains("--big")); var seedMs = sw.ElapsedMilliseconds; // Замер: поиск по штрихкоду и по имени. var swBc = Stopwatch.StartNew(); var byBc = store.FindByBarcode("4870000000005"); swBc.Stop(); var swName = Stopwatch.StartNew(); var found = store.SearchByName("товар 1", 20); swName.Stop(); // Сценарий дня. var cart = new Cart(); var p1 = store.GetProduct(ids[0])!; var p2 = store.GetProduct(ids[1])!; cart.AddProduct(p1, 2); cart.AddProduct(p2, 1); var totals = cart.Totals(); var pay = PaymentMath.Compute(totals.Total, totals.Total + 500m, 0m); // Положить 5 продаж в очередь (офлайн). for (int i = 0; i < 5; i++) store.EnqueueSale(new PosSaleDto { ClientSaleId = Guid.NewGuid(), OccurredAt = DateTime.UtcNow, Payment = 0, PaidCash = totals.Total, PaidCard = 0, Lines = new[] { new PosSaleLineDto { ProductId = ids[0], Quantity = 1m, UnitPrice = p1.Price, Discount = 0m } }, }); var pending = store.PendingCount(); var expected = ShiftMath.ExpectedCash(5000m, 3775m, 1500m, 10000m, 3000m); var diff = ShiftMath.CashDifference(14775m, expected); var batchKey = SyncService.StableBatchKey(store.PendingSales().Select(s => s.ClientSaleId)); Log("=== POS smoke ==="); Log($"ОС: {System.Runtime.InteropServices.RuntimeInformation.OSDescription}"); Log($"Холодный старт (процесс→логика): {coldStartMs:F0} мс"); Log($"Каталог засеян: {store.ProductCount()} товаров за {seedMs} мс"); Log($"Поиск по штрихкоду: {(byBc is null ? "НЕ найден" : byBc.Name)} за {swBc.Elapsed.TotalMilliseconds:F2} мс"); Log($"Поиск по имени: {found.Count} совпадений за {swName.Elapsed.TotalMilliseconds:F2} мс"); Log($"Чек: {totals.LineCount} поз., итог {totals.Total} ₸; оплата {pay.Paid} ₸ → сдача {pay.Change} ₸ (достаточно: {pay.Sufficient})"); Log($"Очередь офлайн: {pending} чеков (ожид. 5); idempotency-ключ батча: {batchKey}"); Log($"Смена: должно {expected} ₸, факт 14775 ₸, разница {diff} ₸"); Log($"Всего инициализация+сценарий: {sw.ElapsedMilliseconds} мс"); var ok = byBc is not null && pay.Sufficient && pending == 5 && expected == 14275m && diff == 500m; Log(ok ? "РЕЗУЛЬТАТ: OK" : "РЕЗУЛЬТАТ: ОШИБКА"); // Результат в файл — на Windows exe собран как WinExe (нет консоли). var outArg = args.SkipWhile(a => a != "--out").Skip(1).FirstOrDefault(); var outPath = outArg ?? Path.Combine(Path.GetTempPath(), "pos-smoke-result.txt"); try { File.WriteAllText(outPath, string.Join(Environment.NewLine, lines)); } catch { } return ok ? 0 : 1; } private static List SeedCatalog(PosStore store, bool big) { var n = big ? 30_000 : 50; var products = new List(n); var prices = new List(n); var ids = new List(); for (int i = 0; i < n; i++) { var id = Guid.NewGuid(); ids.Add(id); var bc = (4_870_000_000_000 + i).ToString(); products.Add(new ProductSyncDto { Id = id, Name = $"Товар {i + 1}", Barcodes = new[] { bc }, UnitCode = i % 7 == 0 ? "кг" : "шт", Packaging = i % 7 == 0 ? 2 : 1, VatPercent = 12m, VatEnabled = true, IsMarked = false, IsArchived = false, UpdatedAt = DateTime.UtcNow, }); prices.Add(new PriceSyncDto { ProductId = id, PriceTypeId = Guid.Empty, PriceTypeName = "Розн", IsSystem = true, Amount = 100m + i % 900, CurrencyCode = "KZT", UpdatedAt = DateTime.UtcNow }); } store.ApplySync(new PosSyncResponse { ServerTime = DateTime.UtcNow, Products = products, Prices = prices, Stocks = Array.Empty(), Counterparties = Array.Empty(), DeletedProductIds = Array.Empty(), }); return ids; } }