From 398a3e3db8079c3e308b377e3d05a7100b1f44db Mon Sep 17 00:00:00 2001 From: EugeneTes Date: Mon, 20 Apr 2026 10:43:36 +0200 Subject: [PATCH 1/7] hardcoded discovery --- EpsonPrintService/PrintServerBootstrapper.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/EpsonPrintService/PrintServerBootstrapper.cs b/EpsonPrintService/PrintServerBootstrapper.cs index 043cd7d..3c1c8e8 100644 --- a/EpsonPrintService/PrintServerBootstrapper.cs +++ b/EpsonPrintService/PrintServerBootstrapper.cs @@ -54,6 +54,13 @@ public class PrintServerBootstrapper } else { + // hardcoded discovery + // quick and dirty fix for testing without discovery - just bind a preconfigured discovery service with known printer IPs + // DO NOT REMOVE !!! + //var discovery = new PreconfiguredDiscoveryService(); + //discovery.AddPrinter("192.168.1.124", "TM-T30III"); + //_kernel.Bind().ToConstant(discovery).InSingletonScope();\ + _kernel.Bind().To().InSingletonScope(); _kernel.Bind().To(); } From 91d2d18c8071c390e331ba48c6c073e23e5eb80f Mon Sep 17 00:00:00 2001 From: EugeneTes Date: Mon, 6 Jul 2026 14:51:23 +0200 Subject: [PATCH 2/7] add money return / credit note receipt type with HTML test --- EpsonTest/Program.cs | 61 ++++++- EpsonTest/TestHelpers.cs | 61 +++++++ .../Utils/MoneyReturnReceipt/Models.cs | 118 ++++++++++++++ .../MoneyReturnReceiptConverter.cs | 22 +++ .../MoneyReturnReceipt/ReceiptConverter.cs | 149 ++++++++++++++++++ .../Printers/Utils/ReceiptConverterFactory.cs | 4 +- money_return_receipt.jpg | Bin 0 -> 48513 bytes money_return_receipt_data.json | 54 +++++++ 8 files changed, 467 insertions(+), 2 deletions(-) create mode 100644 Inspectron.Epson/PrintServer/Printers/Utils/MoneyReturnReceipt/Models.cs create mode 100644 Inspectron.Epson/PrintServer/Printers/Utils/MoneyReturnReceipt/MoneyReturnReceiptConverter.cs create mode 100644 Inspectron.Epson/PrintServer/Printers/Utils/MoneyReturnReceipt/ReceiptConverter.cs create mode 100644 money_return_receipt.jpg create mode 100644 money_return_receipt_data.json diff --git a/EpsonTest/Program.cs b/EpsonTest/Program.cs index 125957b..d58c249 100644 --- a/EpsonTest/Program.cs +++ b/EpsonTest/Program.cs @@ -3,9 +3,28 @@ using Inspectron.Epson.PrintServer.Printers.Utils.BarReceipt; using Inspectron.Epson.PrintServer.Printers.Utils.FinalReceipt; using Inspectron.Epson.PrintServer.Printers.Utils.InvoiceReceipt; using Inspectron.Epson.PrintServer.Printers.Utils.KitchenReceipt; +using Inspectron.Epson.PrintServer.Printers.Utils.MoneyReturnReceipt; using Inspectron.Epson.PrintServer.Printers.Utils.OrderItems; using System.Text.Json; +// Non-interactive mode: `dotnet run -- [html|epson]` +// receiptType: kitchen | final | invoice | bar | orderitems | moneyreturn (or 0-5) +if (args.Length > 0) +{ + int rt = ParseReceiptTypeArg(args[0]); + if (rt < 0) + { + Console.WriteLine($"Unknown receipt type: {args[0]}"); + return; + } + + int pt = args.Length > 1 && args[1].Equals("epson", StringComparison.OrdinalIgnoreCase) ? 1 : 0; + Console.WriteLine($"Running {GetReceiptTypeName(rt)} on {(pt == 0 ? "HTML" : "Epson")} printer...\n"); + await RunTest(rt, pt); + Console.WriteLine("\nDone."); + return; +} + while (true) { Console.Clear(); @@ -18,10 +37,11 @@ while (true) "InvoiceReceipt", "BarReceipt", "OrderItems", + "MoneyReturnReceipt", "Exit" }); - if (receiptType == 5) break; + if (receiptType == 6) break; var printerTarget = ShowMenu("Select printer target:", new[] { @@ -96,9 +116,21 @@ static string GetReceiptTypeName(int index) => index switch 2 => "InvoiceReceipt", 3 => "BarReceipt", 4 => "OrderItems", + 5 => "MoneyReturnReceipt", _ => "Unknown" }; +static int ParseReceiptTypeArg(string arg) => arg.ToLowerInvariant() switch +{ + "0" or "kitchen" or "kitchenreceipt" => 0, + "1" or "final" or "finalreceipt" => 1, + "2" or "invoice" or "invoicereceipt" => 2, + "3" or "bar" or "barreceipt" => 3, + "4" or "orderitems" => 4, + "5" or "moneyreturn" or "moneyreturnreceipt" or "refund" or "creditnote" => 5, + _ => -1 +}; + static async Task RunTest(int receiptType, int printerTarget) { switch (receiptType) @@ -108,6 +140,7 @@ static async Task RunTest(int receiptType, int printerTarget) case 2: await RunInvoiceReceiptTest(printerTarget); break; case 3: await RunBarReceiptTest(printerTarget); break; case 4: await RunOrderItemsTest(printerTarget); break; + case 5: await RunMoneyReturnReceiptTest(printerTarget); break; } } @@ -235,3 +268,29 @@ static async Task RunOrderItemsTest(int printerTarget) await TestHelpers.PrintCommandsToEpsonPrinterAsync(printer, printCommands); } } + +static async Task RunMoneyReturnReceiptTest(int printerTarget) +{ + var receipt = TestHelpers.BuildSampleMoneyReturnReceipt(); + var serializedReceipt = JsonSerializer.Serialize(receipt, new JsonSerializerOptions { WriteIndented = true }); + + var converter = new MoneyReturnReceiptConverter(lineWidth: 48, bigFontLineWidth: 24); + var printCommands = converter.Convert(serializedReceipt); + + TestHelpers.PrintCommandsToConsole(printCommands); + + if (printerTarget == 0) + { + var printer = await TestHelpers.CreateHtmlPrinterAsync(path: @".\money_return_test.html"); + await TestHelpers.PrintCommandsToHtmlPrinterAsync( + printer, + printCommands, + logoPath: "ristorante-klinglers.ch-logo_white_bg.png"); + Console.WriteLine("Output written to: money_return_test.html"); + } + else + { + var printer = await TestHelpers.CreateEpsonPrinterAsync(); + await TestHelpers.PrintCommandsToEpsonPrinterAsync(printer, printCommands); + } +} diff --git a/EpsonTest/TestHelpers.cs b/EpsonTest/TestHelpers.cs index 310febe..80b8392 100644 --- a/EpsonTest/TestHelpers.cs +++ b/EpsonTest/TestHelpers.cs @@ -5,6 +5,7 @@ using Inspectron.Epson.PrintServer.Printers.Utils.BarReceipt; using Inspectron.Epson.PrintServer.Printers.Utils.FinalRecipe; using Inspectron.Epson.PrintServer.Printers.Utils.InvoiceReceipt; using Inspectron.Epson.PrintServer.Printers.Utils.KitchenReceipt; +using Inspectron.Epson.PrintServer.Printers.Utils.MoneyReturnReceipt; using Inspectron.Epson.PrintServer.Printers.Utils.OrderItems; namespace EpsonTest; @@ -462,6 +463,66 @@ public static class TestHelpers }; } + // Mirrors money_return_receipt_data.json plus the credit-note specific fields + // shown in money_return_receipt.jpg (original invoice reference, refund type, tip). + public static MoneyReturnReceipt BuildSampleMoneyReturnReceipt() + { + var receipt = new MoneyReturnReceipt + { + CompanyName = "Big Mac Bistro", + Address1 = "Zelena 186", + Address2 = "79000 Lviv", + Phone = "080 039 47 69", + ReceiptNumber = "25", + DateTime = new DateTime(2026, 7, 4, 11, 8, 13), + Guests = 1, + Total = 20.00m, + Currency = "CHF", + PaymentMethod = "Cash", + PaymentAmount = 13.00m, + WaiterName = "Ronald McDonald", + VatNumber = " MWST", + ThankYouMessage = "Thank you for your order!", + GoodbyeMessageLine1 = "Auf Wiedersehen.", + GoodbyeMessageLine2 = "Powered by James", + // Credit-note specific fields + OriginalInvoiceNumber = "773", + OriginalDateTime = new DateTime(2026, 7, 4, 11, 7, 0), + RefundType = "Full refund", + Tip = 7.00m + }; + + receipt.Items.Add(new MoneyReturnReceiptItem + { + Quantity = 1, + Description = "Cloudy Bay Sauvignon Blanc 2024", + UnitPrice = 13.00m, + TotalPrice = 13.00m, + TaxCategory = "D" + }); + + receipt.Items.Add(new MoneyReturnReceiptItem + { + Quantity = 1, + Description = "Trinkgeld", + UnitPrice = 7.00m, + TotalPrice = 7.00m, + TaxCategory = "D" + }); + + receipt.TaxBreakdown.Add(new MoneyReturnTaxInfo + { + Category = "D", + Rate = 0m, + Gross = 20.00m, + Net = 20.00m, + TaxAmount = 0.00m, + Currency = "CHF" + }); + + return receipt; + } + #endregion #region Print Helpers diff --git a/Inspectron.Epson/PrintServer/Printers/Utils/MoneyReturnReceipt/Models.cs b/Inspectron.Epson/PrintServer/Printers/Utils/MoneyReturnReceipt/Models.cs new file mode 100644 index 0000000..4f03313 --- /dev/null +++ b/Inspectron.Epson/PrintServer/Printers/Utils/MoneyReturnReceipt/Models.cs @@ -0,0 +1,118 @@ +namespace Inspectron.Epson.PrintServer.Printers.Utils.MoneyReturnReceipt; + +/// +/// Data model for a refund / credit note ("money return") receipt. +/// Mirrors the sale receipt payload (see money_return_receipt_data.json) and adds +/// the credit-note specific fields that reference the original invoice being refunded. +/// +public class MoneyReturnReceipt +{ + public string CompanyName { get; set; } + public string Address1 { get; set; } + public string Address2 { get; set; } + public string Phone { get; set; } + + /// Credit note number (shown as "Credit note No. {ReceiptNumber}"). + public string? ReceiptNumber { get; set; } + + /// Date/time the credit note was issued. + public DateTime DateTime { get; set; } + + public int Guests { get; set; } + public List Items { get; set; } = new List(); + + /// Refunded amount (positive). Rendered negative on the receipt. + public decimal Total { get; set; } + public string Currency { get; set; } + + public decimal? TotalInAlternateCurrency { get; set; } + public string AlternateCurrency { get; set; } + + public List SplitPayments { get; set; } = new List(); + + /// Method the original payment was made with (e.g. "Cash"). + public string PaymentMethod { get; set; } + public decimal PaymentAmount { get; set; } + + public List TaxBreakdown { get; set; } = new List(); + + public bool IsDebtor { get; set; } = false; + public string WaiterName { get; set; } + public string Terminal { get; set; } + public string TableNumber { get; set; } + public string VatNumber { get; set; } + + public string ThankYouMessage { get; set; } + public string GoodbyeMessageLine1 { get; set; } + public string GoodbyeMessageLine2 { get; set; } + + public List TerminalReceipts { get; set; } = new List(); + public MoneyReturnDiscountInfo? DiscountInfo { get; set; } + + // --- Credit-note specific fields --- + + /// Number of the original invoice this credit note refunds ("Orig. invoice No."). + public string? OriginalInvoiceNumber { get; set; } + + /// Date/time of the original invoice ("Orig. date"). + public DateTime? OriginalDateTime { get; set; } + + /// Refund type, e.g. "Full refund" or "Partial refund" ("Type"). + public string? RefundType { get; set; } + + /// Tip amount included in the refund. Shown as "incl. tip" when non-zero. + public decimal Tip { get; set; } +} + +public class MoneyReturnReceiptItem +{ + public int Quantity { get; set; } + public string Description { get; set; } + public decimal UnitPrice { get; set; } + public decimal TotalPrice { get; set; } + public string TaxCategory { get; set; } + public List SubItems { get; set; } +} + +public class MoneyReturnTaxInfo +{ + public string Category { get; set; } + public decimal Rate { get; set; } + public decimal Gross { get; set; } + public decimal Net { get; set; } + public decimal TaxAmount { get; set; } + public string Currency { get; set; } +} + +public class MoneyReturnSplitPaymentInfo +{ + public string PaymentMethod { get; set; } + public decimal Amount { get; set; } + public string Currency { get; set; } +} + +public class MoneyReturnDiscountInfo +{ + public string Description { get; set; } + public decimal Amount { get; set; } + public string Currency { get; set; } +} + +public class MoneyReturnPaymentTerminalReceipt +{ + public string ReceiptType { get; set; } + public string BookingType { get; set; } + public string PaymentSystem { get; set; } + public string TransactionNumber { get; set; } + public DateTime TransactionDateTime { get; set; } + public string TerminalId { get; set; } + public string AID { get; set; } + public string TransactionSeqCount { get; set; } + public string TransactionRefNo { get; set; } + public string AuthCode { get; set; } + public string AcquirerId { get; set; } + public decimal EftAmount { get; set; } + public decimal TipAmount { get; set; } + public decimal TotalEftAmount { get; set; } + public string Currency { get; set; } +} diff --git a/Inspectron.Epson/PrintServer/Printers/Utils/MoneyReturnReceipt/MoneyReturnReceiptConverter.cs b/Inspectron.Epson/PrintServer/Printers/Utils/MoneyReturnReceipt/MoneyReturnReceiptConverter.cs new file mode 100644 index 0000000..80e2d42 --- /dev/null +++ b/Inspectron.Epson/PrintServer/Printers/Utils/MoneyReturnReceipt/MoneyReturnReceiptConverter.cs @@ -0,0 +1,22 @@ +using System.Text.Json; + +namespace Inspectron.Epson.PrintServer.Printers.Utils.MoneyReturnReceipt; + +public class MoneyReturnReceiptConverter : IReceiptConverter +{ + private readonly int _lineWidth; + private readonly int _bigFontLineWidth; + + public MoneyReturnReceiptConverter(int lineWidth, int bigFontLineWidth) + { + _lineWidth = lineWidth; + _bigFontLineWidth = bigFontLineWidth; + } + + public List Convert(string jsonContent) + { + var receipt = JsonSerializer.Deserialize(jsonContent); + var converter = new ReceiptConverter(_lineWidth, _bigFontLineWidth); + return converter.ConvertToPrintCommands(receipt); + } +} diff --git a/Inspectron.Epson/PrintServer/Printers/Utils/MoneyReturnReceipt/ReceiptConverter.cs b/Inspectron.Epson/PrintServer/Printers/Utils/MoneyReturnReceipt/ReceiptConverter.cs new file mode 100644 index 0000000..98f2459 --- /dev/null +++ b/Inspectron.Epson/PrintServer/Printers/Utils/MoneyReturnReceipt/ReceiptConverter.cs @@ -0,0 +1,149 @@ +namespace Inspectron.Epson.PrintServer.Printers.Utils.MoneyReturnReceipt; + +/// +/// Builds the print command list for a refund / credit note receipt. +/// Layout follows money_return_receipt.jpg: company header, "Refund / Credit note" +/// title, credit note + original invoice reference block, the negative credit total, +/// the (negated) VAT breakdown and the goodbye footer. +/// +public class ReceiptConverter +{ + private readonly int _lineWidth; + private readonly int _bigFontLineWidth; + + public ReceiptConverter(int lineWidth = 48, int bigFontLineWidth = 24) + { + _lineWidth = lineWidth; + _bigFontLineWidth = bigFontLineWidth; + } + + public List ConvertToPrintCommands(MoneyReturnReceipt receipt) + { + var commands = new List(); + + // Header - Company info + commands.Add(new PrintCommand(Center(receipt.CompanyName, false))); + commands.Add(new PrintCommand(Center(receipt.Address1, false))); + commands.Add(new PrintCommand(Center(receipt.Address2, false))); + commands.Add(new PrintCommand(Center(receipt.Phone, false))); + commands.Add(new PrintCommand("")); + commands.Add(new PrintCommand("")); + + // Title between separators + commands.Add(new PrintCommand(new string('-', _lineWidth))); + commands.Add(new PrintCommand(Center("Refund / Credit note", false), isBold: true)); + commands.Add(new PrintCommand(new string('-', _lineWidth))); + commands.Add(new PrintCommand("")); + + // Credit note number + issue date + string creditNoteLabel = receipt.ReceiptNumber != null + ? $"Credit note No. {receipt.ReceiptNumber}" + : "Credit note"; + commands.Add(new PrintCommand(Justify(creditNoteLabel, $"{receipt.DateTime:HH:mm dd.MM.yyyy}"))); + commands.Add(new PrintCommand("")); + + // Original invoice reference block + commands.Add(new PrintCommand(Justify("Orig. invoice No.:", receipt.OriginalInvoiceNumber ?? ""))); + if (receipt.OriginalDateTime.HasValue) + commands.Add(new PrintCommand(Justify("Orig. date:", $"{receipt.OriginalDateTime:HH:mm dd.MM.yyyy}"))); + commands.Add(new PrintCommand(Justify("Orig. payment method:", receipt.PaymentMethod ?? ""))); + if (!string.IsNullOrEmpty(receipt.RefundType)) + commands.Add(new PrintCommand(Justify("Type:", receipt.RefundType))); + commands.Add(new PrintCommand("")); + + commands.Add(new PrintCommand(new string('-', _lineWidth))); + commands.Add(new PrintCommand("")); + + // Included tip + if (receipt.Tip != 0) + { + commands.Add(new PrintCommand($"incl. tip {receipt.Currency}: {receipt.Tip:F2}".PadLeft(_lineWidth))); + commands.Add(new PrintCommand("")); + } + + // Credit total (refund is shown as a negative amount), big + bold, centered + string creditLine = $"Credit: {-receipt.Total:F2} {receipt.Currency}"; + commands.Add(new PrintCommand(Center(creditLine, true), true, true)); + commands.Add(new PrintCommand("")); + + // Alternate currency + if (receipt.TotalInAlternateCurrency.HasValue) + { + string altCurrencyLine = $"{-receipt.TotalInAlternateCurrency:F2} {receipt.AlternateCurrency}"; + commands.Add(new PrintCommand(altCurrencyLine.PadLeft(_lineWidth))); + commands.Add(new PrintCommand("")); + } + + // Refund payment method line (e.g. "Cash: -20.00 CHF") + if (receipt.SplitPayments != null && receipt.SplitPayments.Count > 0) + { + foreach (var split in receipt.SplitPayments) + commands.Add(new PrintCommand($"{split.PaymentMethod}: {-split.Amount:F2} {split.Currency}".PadLeft(_lineWidth))); + } + else + { + commands.Add(new PrintCommand($"{receipt.PaymentMethod}: {-receipt.Total:F2} {receipt.Currency}".PadLeft(_lineWidth))); + } + commands.Add(new PrintCommand("")); + + // Tax breakdown (amounts negated for the refund) + foreach (var tax in receipt.TaxBreakdown) + { + if (receipt.TaxBreakdown.IndexOf(tax) == 0) + { + string taxHeader = "VAT %".PadRight(_lineWidth / 4) + + "Gross".PadLeft(_lineWidth / 4) + + "Net".PadLeft(_lineWidth / 4) + + "VAT".PadLeft(_lineWidth / 4); + commands.Add(new PrintCommand(taxHeader)); + } + + string taxDetail = ($"{tax.Category}:" + $"{tax.Rate}%".PadLeft(5)).PadRight(_lineWidth / 4) + + $"{-tax.Gross:F2} {tax.Currency}".PadLeft(_lineWidth / 4) + + $"{-tax.Net:F2} {tax.Currency}".PadLeft(_lineWidth / 4) + + $"{-tax.TaxAmount:F2} {tax.Currency}".PadLeft(_lineWidth / 4); + commands.Add(new PrintCommand(taxDetail)); + } + + commands.Add(new PrintCommand("")); + + if (receipt.TaxBreakdown.Count(x => x.Category != null && x.Category.ToLower() != "d") == 0) + { + commands.Add(new PrintCommand("Not subject to value added tax")); + } + + commands.Add(new PrintCommand("")); + commands.Add(new PrintCommand("")); + + // Footer / goodbye + commands.Add(new PrintCommand(Center(receipt.ThankYouMessage, false))); + commands.Add(new PrintCommand(Center(receipt.GoodbyeMessageLine1, false))); + commands.Add(new PrintCommand(Center(receipt.GoodbyeMessageLine2, false))); + + return commands; + } + + private string Justify(string left, string right) + { + left ??= ""; + right ??= ""; + + int spaces = _lineWidth - left.Length - right.Length; + if (spaces < 1) spaces = 1; + + return left + new string(' ', spaces) + right; + } + + private string Center(string text, bool isBigFont) + { + int effectiveLineWidth = isBigFont ? _bigFontLineWidth : _lineWidth; + + if (string.IsNullOrEmpty(text) || text.Length >= effectiveLineWidth) + return text; + + int totalPadding = effectiveLineWidth - text.Length; + int leftPadding = totalPadding / 2; + + return new string(' ', leftPadding) + text; + } +} diff --git a/Inspectron.Epson/PrintServer/Printers/Utils/ReceiptConverterFactory.cs b/Inspectron.Epson/PrintServer/Printers/Utils/ReceiptConverterFactory.cs index 5bde730..8c65f0d 100644 --- a/Inspectron.Epson/PrintServer/Printers/Utils/ReceiptConverterFactory.cs +++ b/Inspectron.Epson/PrintServer/Printers/Utils/ReceiptConverterFactory.cs @@ -20,6 +20,7 @@ public class ReceiptConverterFactory : IReceiptConverterFactory EReceiptType.NextCourse => new KitchenReceipt.KitchenReceiptConverterAdapter(33, 20), EReceiptType.OrdersOverview => new OrderItemsReceiptConverter(48, 24), EReceiptType.Invoice => new InvoiceReceiptConverter(48, 24), + EReceiptType.MoneyReturn => new MoneyReturnReceipt.MoneyReturnReceiptConverter(48, 24), _ => throw new ArgumentException($"Unknown receipt type: {receiptType}") }; } @@ -30,6 +31,7 @@ public class ReceiptConverterFactory : IReceiptConverterFactory WorkareaTicket, NextCourse, OrdersOverview, - Invoice + Invoice, + MoneyReturn } } diff --git a/money_return_receipt.jpg b/money_return_receipt.jpg new file mode 100644 index 0000000000000000000000000000000000000000..c07432cf4114030782ac7279ce6d30973b209d69 GIT binary patch literal 48513 zcmcG#1yo$!vM$`KnQ_bLx2$hz)`>;P{3aM0Js1!05}8~7#QGR z7ZfxE+*Lf0CK!-@YcJ@doQHq$@@Lcmo2^=5x(7qlKrv=0-EfL2ZAE& ztMsbBg?>r@!b4aCZyA8|anLn?82Ru7v=h$(Z(xYE2+hp*;Cw9J;`?_%7_laW3x=u9 zKLtdJPNISz{a2Z3R9Bk)I|zV(j6W;xA7qjHtcye~SFV4G_tzHrlfM-Mj$!`cDd7*1 z8{sz^4b3C5{s&I?--Sm?i`<3x?*xIpXM*>Z_+yO!Kx%%A2(IsuD5aS=Vqs`1;Xz%=XKSd)zm@M5AB&00>%o&T2KE|f{`&U! zc<^?;$gzO9yxvnT90URbV7p%K^+`%U5_WOqu$9268e8K#HeK=m0gJ=pARLa~=0xKr zV>MpNT3Yj-+-SS@VYEOdtgZo;j55#6V}$m=*^cb*@!a0AP(cGluxA0%L`f z9)3C_%zsb)cfCX}TY~PqB6*M|asD)mboenS`(QYiWJJ^c9E)IYt-EbVby?wOQJ9m2c#I0*V>mA2iBzdDdFNU6V1Ol z-8*;vmjayE1j#3jMha!jO<$L8@`pLxqW5KSYP!^M=!S3eNu1c6C6c&3%0wkauXY)9 zs?0_NV^bg0MaCAe?f#1iKO)DDW-_%#WZ%TC*6aL#||3Eo5enV|$f784ILg_FPP_pcQ^SL+q|6(w%OZMMjZuq425S9=;o2??zX12#fIkO}bgow%t^c81iERO#Cat*|RXFa)_9&9xUk;`Sss#%@jPK=Vz z;yEVS$Nz{${v%ZJ6Mfq`=G^Og5YPBTVzPn4Yyuplm-%6kw%PUcg_=y=?te=D(?*ZL zSfAJ_>s2S}5;w*fvOr!1!~HqK{q1jw|JC_-5&Kun{YS*UMaF&HTU27k{SHNU-nX^= zKZfgH&{qJeJ~bJLd+T?AZ!iB#a#Q_DXSJaDJ_^C@|Q@-RDuLbE#*nwHqt@SzkPAn)qO{7F`GRUtIrDLtjRv z=u{3&v`mt};Al^z#&gx&Hy;TeT(rCRSw`{WGi0S+ZGPeWLrw>G-!7W+^xVAf|H9+H zcf8~7bF?7`o=>8Oal5iVEBC;wBaP@0fqIz%*u>U=Tv> zdB+SXC7jS#oQ+~9=X2()xI+YglrR1*`4hm}~V>{VuO3EVq9A8HEng z_IAuC`pmA8Wt}YO8T9S1sD(7Qdhu*rkbl_KLg4oIeItdYQaqTqQ5X`gwqXV%Luoc! z%$caajq8#d&v^w@4!I}Bb@RSaC0Kte5>c0>rli)&KD5BIr|6z2C`&c9n_6%NE;Lp2_&^1~9T-pJ_|EPL32vlIpqA94WjRNO^!hj$;8r`-#LZDlF{Tq+-jIE_WQmB8!?GD+QlsH4Ebo8HhR*yo z8&*Yx#8^}3e9yF_!(F1%Zo)q(GC9`1zrZxVMn1wd?~d!~ooV?3Bl^gWzs7QHc~Ldb zj|2Ze^r>CB;IB3MA6$`R6Jd{*AzuO9-EAvQhs({a8cLEk;+Bie{8y%T>2)eM2Dz}B z*GT3M#Y6c;|FJLsgBMB*#s}py^*J!Y4$7voErk=Pf))SP<3M!I1}FX}Y2W`4`M=lX zdAlE?Lk56@gMmXrK|nwOAb#_KfCPsE13;sqeLzRS_{b!SOv?=7)g`85wDl!q5PG{) zf_b}t0)qm71teM(63)4#Q~z(gtQ1hf2-2Ewd={;c2EEO97heHJbv$_YrCBD|71=9U z`n3!CwKL?jnO+_u=cjLk(OQmMiI{c{TK!`m;(e?hZlX8Wm>iDr8$0;}_N5+%-2d6p zT1JU$n_cmNsj(`{;rf_hu)}lEuvbulDmr5-tFgEFjhD;*S!bbxaDs4y+%yQw$Bzx;lMmJ4 z(0tzxU87iPuRZ@KiH{i^iQ9S>?u0^tj^ppYSYD1)wV-Ty*!lHURDUunTiAn;`x<(h zsb5d>-F<7R-`^aHLbjsic*rgYk8)DIp79d6^=rIUo%1HXgaL_K4v&d=qsljLj#jv9 zUJ*9>4yqJPi9cdiX?ELQp0Am(ZpwHNy29!wxjG$&lRL^~**66(a_l;H<(egeu5y6i zIXXpNhN~iSta5`YANx4UqQLu%`8JA0()VyPsh90hGdZRYkhB zF;v95Ku-(i2zxg3_^0G!ByA7wVmWaQ-VoS!+1Y`a(Me%O+rE&!K z7JUS`;j$h(DpKKu(Jsox*oD$&@`D63PUZ%rd76p)N3&5Uv%$CnyHOEt6YCkfT@5N` z)o+SAQ^#LND7CnV<(KYN*ou)SjIxfUjnT!rQ$KP0u%7oM91!uz5u+KWa@H48pnFEl zR?Lg3wk@dXk>Ekefs2l`YgDwh>Fdr47dJ?~imXo-37<-JciaVmKx; z@y^+_w$Xzg+^{Po4HsaD7LWl8+?36;lGc|9q^{B~ZfI|!1TB*A-$XHup&mP7L$qBu zbyldj3F&oY3R}d(*Z*n*ZUTV2>n`k}-9H(`exl)x3%)6?VByPZ(fw55h~sDq)WtS4oC?l`29j zLM7JTP@R(Kj~$m*LXQqYF`t1p{4+#Awu_nNGV~2O;cDtm%Vr~Bgi0+-b`vQVJt@K( z_E;QB{&uC*ZSP3}`K$6XN-EGxiV#?j0LpP{t`wcO(;Bu)$E2#ns!1!svNsEAJ9x`r zu9nlV!`qnqd`9Y&d@?pdrjAbOxDr6c-j z#!kX$nX=opK)|~2&hzQlGW3TnvRNv_s(LvYn(($6?h(PX!1^NkrBpl|OFC12n66eQ zX9R0O9h_j{hD z`g!ST>v|4JM%e7L0>Ax#4+uS6_Ldg_X;Lu35 zU_8d|$=~+wB53r2>h!v~(clRrH|>B!|b#?E#4rF}#o%A7)_@eoS|LQZHLD;%z2K(9>fIC?j#4 z26y-fm`BRKZ6TvH0soQ=yROfUzMvIT$3_1G&HvT{g~d8vOiis2>fxIq0q&GB0I|oo zOzL}GKoXWVcr^p%JXboDj!W+3aW=8V*Atkw8>IxQ(Vn@n?W;6OqWRDF-GQl#$|_Yc zP~?){;YP!fam`5<I@d zppsLLJt(9b3txq9=boq%7Oq9g%vjGLkEg0tp4}vEDFee2X0R1jY)4AZ95Ze=qYB=f z=TlUm8anU(oiGz6ijZKP#iy8+QG7NtJg0=iIOG7xz&n>t6&ZEVm#DsVp%h+tuh8NE zj12UORs+kNLEvxFh3&c*UWMGTwa8rFrI%Kh83J`$d zpx&SelLM9kwU|nwL@cJ>tQ@8nF(jS}ZLSy{x=>E%8DIwWV?0}5ptjT(0jfW70hoQI z%4pb?_$39UOi@RxUy_2kOV)rjiCe#a9|WlVjPstHqCGdC&kHZ66n-f`@W?ue4;?=M zoH~ckuPgyj`t@-7ijxhSH1PX!KZ*(%B~Hq`3Q;K{C&VQt+4d8Av=qKy&8%v~4f;po zFiIukpP$j#G&1Zs@IuBgEhtk_l*H=jM4La9T*b(KW`G&tIdPC`LkX?LO|tyuX?b;) z&IUs=S_fNCK45iO$b4o@GHdKsiz{vU3oD(A)p%UDZ80H``9hodemC6?gl5b}wb<3P zxIf5jkG}IySm-}tSi_bVAZ9nPbhfZdWBCuuLk@pn>2EONf5JZPjMl(5l5bdDjxe7o zlZ+ag)rR~91EkZ3U79Q4E)V51-w&qCy}{VuV6Kb%{mk}%!HDb{C#aUYRnzHW3&sgk zYiD{ax87hqZ?G@_gr)9`IavdJwbFf4DNfjlgsDgs^srKR^|1+w~Kv8=HpouiG;c+uyTV)EqOj zx8N&venkNBiT5-Z);<0zSFTx<@I9gU!j?WbAiH!QVpHGFFc?{x;(vD+fIbvYqM+zh zvc}PBl!(7H0K)OlHwD@afC%DRiqNN($W+N0iytH`O019JwvGS-mc5dQ3w4G?rpx(( zfv_pHw8l!5A1KJp<@(OCgs~9ks@2ldtWPqii4VoiFXNU@vM}GvPfOR89o=J1MG@i= zSk;kc<&TW!%1kXyE8N)m6npwyK2a2ZR5zc#DoUH5JZPAZm)OoS8dkM4h9u@^MX^va z2q|XY>le9ZdkMea(Y4>e(GQHgWKHy%GGb7gdk4_jXvTOGMJZY*%2|F;Wz$KzF`KWB7J1KOr)GJFmxtR3(yw9$9kL(?pI1T z0A3d-AsvUY3Zvr(%WC)GeY77(fpkRfwsVIg!?hqI-h9N8CuRuMgAC4>3TTwA%`>k; z*dqjPJbx{YO+k_@g|=>XsmHb5JZ1O40qqOT2_f?n!ee4)&-UDGmY6k9^@h`4sS6{l z!W@eMZ>#RSm!$&f+88wyXvvC;F8x&1(j=4%djgd1x4k|{Vw@xfL9PT(P!M$~JzBLu z8khqwXb4lxj`wX3?J)F;=vp;Pvmwjt#bc=WMw#(2) zOk=u}mLPSa89VxA=00 zG{wheB$6p`i11?gYJeZZSu1$e)k~aXDw-A>jA}86&qLr8%KuOumtcdEbn^iAk#~&) zE^l8Sxx+1~Dk5K73|D3BhbrD^YU9!8Ic`xbQG=DuRB{{;Q$1PCuR49=6y$m)zd_Rh zAm&!2Y87Fv?*6J^5QPi8p_}&DfG>!?z_9y3;@y!#h;X0PRZH+CdVMY+aV{Nw@Ph-` zld&RvNCvI-1*;zQsStVcU{QOwDtW0g z@(Veh)UeaK)!H1_*jkwwf;>ngc0N!e={l`B#Bwm(>6C5{tH>lp8&Qh97cAM*X5MBW z!vFQm8MW#`=y^$zVKBbmsQLXTgHSc8vIJzNG5WT97mk-w1iUODZ00F;4vlR;aCSVA z4U!4gAF^Drs>iQaeq+73BF-Iw9>lp9OIWAW*Y~wdYu^aU4e0Q0cuxt6J>OhOT6{V$ z)xRn^DOx=JU0hE(qEgw_wq&Ntakyienq3i6@=7#6GhKDU5jD2ebES5Jg8Flm@3j@3 z;e#^XCnC(?bUcCbuNpBsgh0=mB<6^cH{f+FV5%^8(#h> zgQiFpbAh&&eeALR;!}~fr8=K>5ISf-Fma|vvLXzYY`sJ)g{GmO1>nW7YSf=ckaxl7 z6`&%GtHKL6zsGRn_=8JVSJnivUX_2GiQ(C^ z1bqZeup={!8d<9rM>cY8mn8tV#)q=lHJy0MA}2RDt%SCNt1}Q@j73THk zla<3Y1JK<_0czP?1U9?^Z0D*`WZumS+LfAeAj@9(H8Wx>&7KBZLcT8u&^>><_nI>a zRai7mPJ8UyQ_*ys?wh`R&~eq#GL$CsUQixDal0YN1ba zZAs#&lT^IXc87);n(`AaDX+?}qwjklQI(ltxH7Nl8>?jEBs+}$29YGQttj6C;|}lV zq^;!`ffAn!9%;Q3amrI>{xJVsr00?0bs?EGe5W&f7EFf44UObv38{2_lnQ>(a@Bs# zUo!k~!_TS|JMiG5^aH(h$TK%Bx~HRbT}LWFW2JIi`jn|-FCz0GPeBJ-q4QSXTFJ-u z74T30K|n#lygB8s^x$v*y}6H0PHzhQ&4qTTM6`af+12a1-RHKxfBXmj+kdZsrlCq= z8>0ZJ_BqeJ5JRGO$t@^4S}nGgQpIjtHZnwV`dcaUl7HvZ4WNY0^+4`BzoLq45`NCHISL_Fkdr z#CmP#p%Nc)PYp4yo6vaz3xdxyW=rqQ@d{LP;~wNR)IdqUA{`g(EjTO|=NBA-gMtF6 ztW)2|k3G!>wG3Bb)RYeIHEOQ}>5M)WU@o+X;bNWs}-+Osbf1tr=L-HcK>yJhI}Qpr+JNmEC`lFOpnUo42^)>2^)SCKnAae6+z zUxW$f*Y;^gZmD`~klN6BD@Ma#gbWORriLqH=qVWL7RKa42u1#O-_9q5T2 zxKrHCVL>sux)0t2x^vZE=ERpS)hb=)(86?`N3UL2Cfe-D-VuSbo}VvlQ}`Hn-v8rc zdJt_~-Sj8-G=R1E`YDj$@cHt3P1HM*bd~4hJ6nGapKE+|nu*z@N0GC=SAfoGR>k2f z;P-rE@+*Vu&OZ9*xoX6_8Sk4l5mP85U4D1<{ENq`&)&tGZa4|MS@BsSlOnMmoyy@JYS zSM(kzv+%fk*zyXP+Iq98S?59J<($j2@&92zZBfyOAsfkJ_ArtCkl|eDs=#_KL9fpt zpDfGttbuY;iuJrvpJdBBcnHNFUf<4HU34Be|9Cj>kB2Lt@nu?ks;9kwdt|{onsoJL z@E?Sn*aodQpN@w&by$5M9_lZH&v;7zZ&l^wPppsE>MgW)UI7<0#s!wdgOs{bK`rBI zy-_NBiG#`RHGCgb8btWx9i9AB*I#r9x@gmUak&>;V(8C0sf8;&zW*o>dt6MI zyTp9I#Mr%%}=;T0g|s9DTdq-}}EA}Zfhz0p=z z3%{EmMWjwv54$a<;o{imGiv1YV}2LhZ2n+dkNy zVa}CCfTx^YHuT=^qr$A4YnztjBH;r~cO#7=n@isZl((qHVZ97fcXm-zO~FQ!Z4%wH zZ85?1AZe8s;Y^@{^Kp87JUgkH6IUu1o?bRPKo`hDSzNEE1#V3VAH8eacVSPfx%bVXn_PH#SM`ds8I%?A3*n_k2^~hqpUMkLrHy1mI>U{h%c-*? zx1Q`MGW@aXT}M$uC^!B*e{FkbQFtC$obC3p1s|h5!28%SYURE7V6OHdj!wLqOt0dq zI;8J4TO#w){Wqn)C#P4y;HWLJ%dd)Z2cndUh1+{}gR7Za6{Yi5V|8?QKAZf#}R18&bvvvUjcRu{!f6;T+QPij^+6u>6_vjq0{4nLT#6u!N=$?(+q;D&aUsoW~d<2_y zm`zzL=6Of&E%~;Wz3dsW7CCKTP&8mPdL`D8Qy9>9aisppiaf0-BMtX>EE~{4aPqFk zcm>?LMRXASf}0PYL1xpjs-H}^C8n$!#s)sd9tSv_E(nQEg_;6TAv)1hLP)CL)qtF7#O+r%Vu-c<_szt*{NANGKsZ9 zS_A3~b2~p4WZi2VVyGYXlhDS>DWf*iBWZLqYiTg?DRmZP6h(>+x77i?GT>*mchsa# zz6i~Y@lH;m$kiZxaxpfyu~!0tUJrPIFP8SHZ}fg>Fm*fMKh-wU?fu$!Qq|>;{c|oj zP&UYIl@(PRBDRY~W~wEPAsWjsy|xF}NQa^)jL@UgYmapuu{Xw^t%;vS8u_hKI~foc zdSyP3jzmux9^ExT%#ambHMN<{ll^^J1}XisB! zK@BbKA{<7Ii+6{Jm2#xzpr%P21__^&=?9U;)Qbm-WYBW?@jo;o`J>T><4lGduIyVpGOFmimeIJOE-fEi!@d|t^=fEo=VsI_A3Zrms}#l0$F7)p z{$-;$snm3uoML;fmY<91m$b*8j2D)pP0tt-Xh$<{sX2|ep;iWS2cYXVXW-JoHSTfJ z+Fv$$1?;GvFkqL(Ff z7IRwB`g?9;shHlZg!vCE@oidud7ESPk4zbL5xe|L2C?K@GY>Q3#nhSyim$?r4aQHS zzYTpfn({}bDI5kNn^V-K-qKl-L)Rmw`ndzPW$KQJK@BhM>{q~O^@=G%=yuaCP)Br` zfm`eQo29dvM+aU30&m`z`s01_CGKOk#WYUur9Z~L)4uo=^k!rJ^Q#E23@a!1fPqDr zNA^K|H&%xzj!2r+G|A3ZiDj|RqhrZaipL1p4aVzNzs+=7OK-3;r0jZsz{wiP=aK z?zn*Of?)4mt%CU0KpdsR;Qsk+9T6*C;oj5m};}^?OEvW>*)j=&_ z!g*JHy#uG^C?W)+@zTE4S&nqay}|GkXQCJJTD(K8%zgOUDSML8h7v5WVBg*UV&S{m zk^td#xQ5c1yc6qsUOG-o{qpTW-b|S=8OieElKsg$hL~SWGqmz$lp+!^s3xAvgsT-n zRwZi1(p{8fmNn-X?756_gSf)k@1{p+UIAJ(+H}}2Nt1*=-q~@H25!U|26f4ec#hTO zQP>n@`HEVtftJnY+EFD38J07ry+lF18D+wy<`%?Z>Z*nbjrkda!KI3!^nK@w>F80U z&@_lszr35p<%oZYa6gE}jA^VF8|Ign<%y-UfkL|=?=IF$*h8^mlWpm5F630tKBKO=7nv#6*<11e=&G=zQ2bF5By>vGa=> zXA)MUa-*^{1ntXmA$~GV6KM^iZ()ORhMdjJGG5Lp(nGGKR`B1MJA9af#=fADp4fTR zywi|7R+(o+aYtqhKzZzJJRTvTSr45=>ma^3iqDHvso}}7V@Jon&s__h8I5+GN$uNj zAfR%)5uHaCi&uXM1um*_ZXa7pH)ukF}b=@P=?8TNB<+a}FJJmMt(nMBsUNs<_H8Aj^Cr?T(}Mp}mcCD$-++Ew2X)ulhAGeu=@%d;S zZ1!&xP7?Vu;X~~EN?hqo)t`Hrv3>hrkdinbi7j9ZhdLSm$USc$&1}f~BN)S_FV;se z10;S|`gelAljbk7lS6EE{T593mteC$idUscYGma|CwrZbBpYTBkKaT-qlGT0PK>#3 zI9?3g3SLE#3i95gOw=;B zN9D6YNbYLoDMcBf8>LT-z4j^OQzzqWTez>JJ>ki zh~T^VVSBqb+sZD8$kmi8#{ErbJWIEJa&c}lm!=L+NpS#zQ!WW`u^>V&BwZC}gH$y% zYAOU78Lydbx+t)G6hAS-q<*cOA0G9N>W7D8!VnW4eI1d+9FFE%GS_EkYHQ~0YXU^~ z^1^ae0fmHyxySy;YdI0+D&JVDFoRAvp1Y|6M3x?7g}Kd=#a~fqB7%3K!23?B@}rJY zC>CYWeLXeA>r?6{Cfq=A<8K=t^-^L2_3gAK)YQWz(I$;D3bILQEOd+(Yaf}bZuU)z zg~S}3biX#y5$qS2(2Z8+bP%5blfnv1uQM`!WPlC6?NyO@GRlskE_gZ07|p*}vuxth z%i&Cd-1_(o5Mnw(-p|StkhW>;q&>U~WA{cjM%PT5c#6|~sT=o(z00db>tX8VGK@7G zb1ph@%fUghDaD_JhV5^?;gQ54#-!Lsi==8AolDV*N2JG46jA9{Bilu^L@w;p^a!e! zm6Ot1$RR(c$Ik{U5o-kFIkJ!NPv)c6eBYiQthUB=Go5_})S4_VE{@pq)K5%rSJb03 zC<+jRg93^u>^t4A?@5b#$Cy4wFuf#+1hp_bh+n4c-F%9~wpXzA7^~+ZS3;-XxijiQ zT{Q4g=u}RF)&0sne-NZ+z4LCNBs2nDwk{=nb`&q|=Vi%v@&z`-kU-4(@qj*RF=Bed zg)3sC!Dr|e67qduhA9`@d+PMsXyZZofitKAI5>&qJdggi0GHgI%phKee zIVh8G%3jG@MAtgy5n;U8Y{zmdjJC`>uRO^eRxhmZJp-rlk4mp}1w|f_(JC?2{>&{; zDj5Qag~BH#NDZA+Ol&kCkwK+N5s?=2JQCp7z+6Ot$9bo7DZ}1Po?h7Z@;#?X7V+RNH%LK zk?K4-9fznWWmM;_+D;&#n=o(94q8&PL-o21^kJzqNH`HoN+>bm!cxOgJT`Luqk~FN zRc-z3mf~q-27{ej`h7&C7A-&4#jc}1SBl@xIigIRq!q&k6%v~E`r<#?ye$NCSaWuE zWEhkof(#q2AGY{F_pNO6Y&l6HvV7~{1&0yEy36z-pj!DWfa8HZ;}rnnlBP$hqa#^s z)uJV*Qcn8@qtyIlrTk}ELb>chEQS;LcWM*Uvooo8?G_9?cP^h!XGdrd!$1ZS#OK994U~hwI zwNu(RF;6@3VOtKUJ!ifG$_-0z5sL2-LpFaLR2YpT>5+WGM8Iqjso*`e2C% zmz;!MfC>$t$0p944?e_t21bT(e~e`Myhz73RMD5jO?){&CvC*hBR=HgH_a#Btb70Y zY8uA_cnge+%9*C>kMOg8XVE$+9zs<@#Z<498r?=!mVEQ$e3lt;09Ddx+l1(eV>~b4 zwGuKyEg3YL{tDm{j3{g6iKlDk?>NuPH?#w{g^6c>vUGjCis@KK`5GHBmX_aK?pR%>pb?+-t70wL%K-A=e!~1!?s>s2${q#w>?g16K-S zjilAcn=VY0DEjh#V6dYzp%T;8RfI2fC==V>nNesPnr?yK(;nMR&oW8!V=$|XWPW*^ zI?CiowY_`Ro`BaF<&ILGRjN`*rWYo#;qZPIQ1+Y_!-@~Ra)R!03Z=loN?`I^_gL#$ z8c$0mR~Epnu50Lv*70s!qOMpb!#483(UC2)JC$Ns2O8Pv@Hpoe#nDHjr7V3m;aKXG zRs6ENs^F&N*KlMxVU!NTnwMnh#qDG@1%CMlNcBK8EYEPK3)I1E7P)4q8E zvh8ou*E8NHybFJiY~^F!B3XwuqDE_nqGy|nGT7Rx&@Ul<*;ZYrODJcJU&Az&h^m0K zlz22bL+MEfgR0jA=Q{Up$0s4vY%Yt9(KFdr+ADKg4e{= z)|H&$4$hruBMaA_>9TD{LjHu9&s*kXhB@ipWM4eoJ}n2+Ej7et=7cut2iTY`dhVL^ zq4jFvABuYpZj*0%Am`J4#K*1IsK#JYLS-Pr#0x*eMvlLT8d9cfssP194JfMUQ<%S2O0H>Usv+Dj z9E}hQF9JM1@w#c+)Qkhke8o);b~Dn(MAiTBXY z|5&%M(~;1^fg%^pD_M`#2n4ekQ2^EiiSD|d`1kr<$aafP4VdF4rY=yb+2m_Y*7LiB zkIkgA(4B%mF^S2sDn*P>$}HA5#UPbNUrvv~2BL4eB`_MUGJttJAzY)=ZqmgmxfvAF zbR)DAcoz}X%t&zWIS`BNT@c!=m9R}a21!>He~LNhNV&VXHWNG{j&Jwh^l@(32Y!+R z(v8u#uMrSv7k|?kZrG0#-5<>i*xWnJFI%g1O&o8$R)(AGoX z&HUoZVHrPT6glXP275EpOH;mqhr3HAQ-yXS$*xyK9ON+k2zTJAwPwD4Hki7KKQD+8 zJ!u~oDnlAnyjsN{KwU(S{79ef>59mnn}KPi*_0{hI}{n&j_0T`H7gW9J?!Fqf|UJT zNAYqP;G#X(tn>fFCMrDIM~paX*{_ktjBZT6J8lqmk5>Vh{DGouk8VYVIgbhTm_{E%H$wH$= zM)LuiQE{k2@}&OHr0n8Bgl-S*S3rOQuKvk6EyRMANZSbf>=dZjU})B7Z$VIkm_$9* z0{-ITu6=~Z6VHs8TqBi91?MAO%g%>zm$OcvP9=kqF@|Baw||7%5H^kVF1P+4`gO*_az#?%&)~Y?s9&9(gy^PM8Eh@{D&1ZPdV;RWXa(4EbK! zbo(5&q}#@IFbYavYkJJhe5!AW8=u*H#>4sGtusFzXV2w`;N3IT1!2Wz?Srq>!GyBB zjB7IPh{i}F& zFYSx+PF$LwP6&lx6SH5>krZoRX||m?6%2a?2tv1CxsQs5TVvHj*OTm9Bv3i({KxY~w^+$=mkTu~kkG<=Z zSd2BH%2Ek<3xW_YRRR3DXR5npo$y|q!cY%0_d3xRwl_nN#pXTmHd5=D{bdN%Si^^p zEL<#Vp=iAr9i3P~j55f#a}jjMI@1%8^`_a8x#u*$ji}*8(h#pt?EIi&p}|`wYJ95M zWES4%Z@oPdl;uq-AVl}4zP!}^$?e*=+$tZ{c{a+e{bJex06Z_z%EeYU? zPCtL5?lONA>%6gFFQo4aC!GRoep5e=ZZ%v-IrGg30&B{P(dI8no>rv-@Kyw&UWnzW z?8{M7RwSdM6IXt3Dul#)wA-&2(4fM0gl+fA%o7RnyxHLJ{LKa{bEl=GtY6xebv=wKprU#*ADgG=jbP@sAD+fBA7YcV14ea!BeGuqCkW-TQ$B zV{>e8 znI<#y(UH03DZPP*l>*P{n{5k%_g?nHeZE2!2%X+7a)K9ZVQT(wvFVH3l;OzX6uQOa zIBL9-P-PIpQoWt4p(Mwmh7O9Ul10buFre%qcn0s8}#IPErgj6LUdyhANs0Sv-gS zL^Zs8c;%o;$g?4u2JuaWw0}AxrHiabnp6X6^jl*?h}+KxDp zdo(F3XTW!*sA}P17yqnv1(Q7Su@dqe11`%$#h^9%F01 zasB8zq_JZ_{}q6M5$jL3BQ|d>fztUAe!@>q&uvLM!|9Sd{PeKKXZc+V*} zB+%?UGjbt=#eV*`csulJQdHIhAYw=X!LeHY%9vQo`*VG#DaJU%b1C^8S?-M1`f10j z$#!Z6!MG4y)wk^a%+_9ua&wUTH)6S~;Gh=i%rwmW0@FA-i0}_Q4^Gqd)7_NH*Eu8d zY$Ez?G^cZ%(fgd{gX>%SVHJrXilP*f;#dz9Oq0-=Xmn8uUvwWdn=A^pGa;lvRzI0p zC`)EY`B=nW7+)O4EWOmhgnV&3Y8fx-BG!nVcFH#kt>gATx6$=OPfEC7>*%y+X$)3J zRyS74en^>7oawL~hC1>}bJ_nKgYKe1K}86dQ^MsU(f^hv)r~QBu{|H?|TCm@X8(+V2&D-}lG;5a{q0qCwnSvT@h39bE16n$H0%} zg_4AyKYwQAU)j|^c5XAZz5=?P&=db3&fWsLj-6Q(-DYNHW_HZ%n3-dU*-p&tn37Uj1>Hc@w0{n zRI4bz!OxHF;K2Z#VaOtyS^ zF#+i%DsoJPC{`}1qf(COKet##Ls@P;xEJW>eh_b6{p5W266#KnwJbkL(43Y|B-DBx zMRBQ6UQ(A*Qe7KI$7<2%ZoX6hvM7c@je>7tEC^JYA{H5i9;kjYseSNr{2337v0Qez zoUb0QYbBwK!y_gm-=LjJvoc*$5){I2tOrBTKg>I%#ro!cv60#Gj z%5oXC+o5HntCq@W+=_025}pK}*^%W}P2i{=;Xs2q!Y~3-yZ-QJ`;Or0xj{Ugli~?W{H5{E?9-o$}1NJldXuy z=JTSm!5MN)H6JkzE~UrZiEF_c?0E^;5f0@FL4$y~iTwECG~##mz9LiG;k0_8+xPHq z;tr8VNV!H;e^ulGdbz` zM3yDgZosK-0iA(kxwOWwVQ*+qy676BL_7@W?+F;U9Q<38^_2U&*aM^aS1S>8C>SnzU9< z12O<~e4^Y@8U$?M-cSI?Cy93$oopSI^`5!PFuZfE>%x*C$R8<*SBvuj}49O>`D z*UlZ1q%Rd-5Cq7Or&V_ujPa5@-%jt{P75p;fC=iZDN+HC#(C)VFRwlLHU46Z%8NRj zc6soDu^lg2L&JFW@}c&OFfXS?Fj8Vv_&;F0TP}WBnN{$d7$rcZ(4p)0?gPV7UZ0>D8*k5!R-!L22Ssz+0yqsOh7^fB>aUK5vxC_16 zt8rA$cMt~+RtB%W6sa$&jZtjeiX&8CZgZF$v5R8!1QKY6_R6+W4{Z8E(Zsl0m}$^u zec3PV3|mprAkRhQy>2PuPf(zYc6DcR8ra%lKpz*F4clRBwWV3HcV1l5_%!1(CcPS> z%B$07X@RdzPm>mPkg&paIH{zlFEclZ(Xk@n*-RKMutY%D3O^=MXb^h4sL;(3FCr6w zO@Vs}`u@qBRFoE3RCkb%%v?i{YLwD+n;r7poB;>b8z=-%@>b4Rn zJKfel1Vk5QsML?l4S0^vj!*`6I+gJGJEeyY;=8F$c53n&CONeBbtF4Ud`dZ0C$(;+ znx>e#QIPSq^$RRhr5}J>twvrvC=`O6>Q~}7wgL<>lyA(OdP+%YUp{eUNesVnQpu)F z0ky^is(SwbdMU^De-8{<@*>_UHKd79QcTqjsxDs0%17XUZ^hez6}&E3O^QyM1 zt~K8IS3Tu)7G|te3KDWnWnf%LV+I>FNivq1^j{L#e^(xh5WNYf_+^yHCb(@4M0&J= zm3QPpdm(RqthiJxTSoM>B^@brq$O!RRfnS01bs;imPfp~A}S19g8ICK6C>MQ2u(pK z7k}PU9=G^ukVBQIA#$xnAiB2Vu<+ERk_(NIEnn5fXY1+l(OmVi8&>vP^20z1qi0)S z-&Z}Xr@IP|fm%oY&JGBk#qaQQB9rF3=3ioZAytV zQ;`b@wb>cvC22BkjZMT*bf!U7MwC`MwZO0OOXw6vQP8sWAWt*kHOyWGuTesSrDa5P zSBdnHDZvZZxn=2{^vA7LX~ei~MWLDB*#pblJtHc9vzD=#hud7uPnPw`&b2b)YOYdR zZk)c~Ik-g?+NRXw@@3k7Zx;~ii$|Iv+6=_dNfk#}qQaTpnnTGr8Mg5;L^lPySfG_N z-p+TOB^6qQq0sg$JJaiBDdLqGm?iFT<N-g$8n38>%}hDS;TMd8U@ara~-lM^6$C1oD^xGrxA83|8O2;cIVAM`LX zTHBO5qf1}3rKEg`1MZEgOu`E}fKyC-v4-($KP*b1O*34hPuD85BhX)&*HAwzQZ6hK z#Z6dg#TX1_;;5mNGBVppPZYL^C6b&bqKq=GC5(!;LjVoxjp;TWe=Z z$e}zrBL5y?)sin9tF`c8;;aW~>yaQEwwkVq2S`p5Zh|OwL6_XKpPs?Xs#YlAze!!tK-*4imU%1g8j^%G{QuY>j^2Q45ah)xu62JDhyA zD^UA!(WWDl-e-V-QIvt{J=U+z#K1+-r+aflC07Zz;`c2+6=*W5+BDaWp4nkr_eF?j zdu4I@8rmY!=7ziD$$~s$4^>Nd=i??qP7~6Ww^9~tR_lAr=gGL?S_j}EyZk8`XNVt;6veUksmiG%lu8 zlq}21%l+J%oZ50J^r>uk;zs(pJ${M8QHaSrboy4je`vjkxmR>apK3oIJSu->a}+^kalkl{)oE=26u3EPq4V zC`Ig7$TYX<1Jx!wk8z5)M29>G{J!oT{OY1V4C9mtsK_7M-*k&+g8Y#nQomr^6j7Pj ze7qGX9ql(I4oQzV3cBD=1Mqy!!m z7z)01{`v>tk^@ueQT1!vaJtg@+XJ%hkJ*TM`*QyreTXIp{F;gO{fL}W*f}(JEn)$I zY`U*lD&gke=;Msr>Hzw0ETig}i%UofgmcpK#(6o|hoFg|2u&H>kWA(LyOMglfie|oYADB*NqL;D^`GSh*Ysq-1u`#XqNu^#5DjB<$3bqF=%LKZ`qKGWf zhj%lybVoL!eTRefQ{3k>uAAPuMKnmaA)F1iiHS)cFFS8?31s?&OBVANAHH(Uh*MQD zxpl!p(1@~R5mgDDztt^1k5+S=frgs8&3#6>E9J(40CdnbSx5ahy#W=iTQYYfj#>ir zc{uos7T)(t{goT>8^kD<1cw863w^_V32YR?V5BZ)P9&jKH1P=&WgBB&cS2Q01-e~s zg;bSUR&+@WoV^o+ob-Y(-@gsqNYAXj{$)zz3T-7}W9hW(iNTLkip^6o7>?fDb14)U zZCpq@!f_emCr-zH^R9WF8CGQ8z~tV{cl}-~8>NiPr|8@0R2HdkvVb{=ke(Pnv-RMp zs=7*aN{W)XJf0bgTc}|xEnxAj0Xsz?aQ3od{roT$>3IJj`gF1@FHfE%U)mea7n84p z`&g)V*q6B(qw8GH-q&(b8foK$vxH>k{9S@#v`*;{Af_a34@+FVfSpmcD!uv1r>2tr zC_*4F!8%9`b%^cZcD`6tJ0568&F30x9dp_$=uD)b68haevVc5Jt!^3UBfybR!i1p^ z(pTzL-`y~iTC2g4WTS*=KN6ccqIKPiY%=(ji33x`rW}0=fqiUi{}Fqxs5F7cK8&AV zH zicLl}l9oca6S7xY@%M4jd^Cj(G#aCqE3A541efa;2{<*_YzFJ38C;bh24&{fWEvv! z2p-zp(fP9KWw1qK!5oPRGR zqxF*mDdnQW9wVa}7n{-NCQv2iU3gi3^us7C&dX8O4Vgg2@^9o0Di32e3|)1~5avZC zW$tury+m7Dnw6QRe;=K6g=$GMak!9X+SUD<6{g#oqT=4hxO<-J7^x@x#2`FV;~-8)b|-ln2kG=dJ%`T;(# zzV3Kbk4!D1Yq2NNhMh5|lbA1~rmmj_YJlT2o3YLG(c8Vc*+{u@DSN1_YnZ{=L$k!n zHV$>dXRf&FwpwJf@!lf^5`DJ`W2Z)0NzLByLmQ33AnzYETKQiISnb6B0E9!@ewQDK zOkpyQc_B5=onXn^clIGgf0~}^b7d|sDM2mlQsX}g`cN7SYrZ=bn#-ERr-0qSLTq;+ zoN7BQVvDVoQ*0NXM3AJsH~vBW!(KhP6?UJk!BbRNe^G7i4RevILvxEgp-*OHUtnYQW*>Mh?y z`zb{{*L5N)(*;CKVR=GDL5UWSje74Z)FX;!oX;oaS-92*tthANHiSbnlY0@cWn=kQ2DN{+h)WsG3us4d;z5RnwDd%buv)T;i>OsCRD zvTo8hdx_byIttaI6;)A-gmfjCk7@=_6KmN%`@DmLY(yn z!1I3l27H{kjolHR7WVq*^6~$L4}6@er)of6S$V4mq(Szjvzxxirwz?V866vZtr38e z^{z~$mIqNkf`%b5@QmGd!gF_fdwc^vo*hpyk|#Kdn&FmSpHJ$ zb4kB8QrNhM*kUP5Mt*7~*Q#AIZzaRTK6_1vm^ytBOw!B=h2MiNA{3tZ0)TryL@;c; z{Fo;HUy0&1M&PP8_lYS8U|e!83ZY1c5128`+MR#Lh=YXJ2*um2J$K2;?Tg8|N#@G* zzU`DHD!6bMWtmOUNYD&Ceu4FjAX2PBS;3Bnm&vP%oSwd6_ZqA>mwnxwAQ;3oQj7|- zvop43Yr&;Py@z_%HeII?lfP`5kKJGrmTK9$Jz?(2!e*F%M?ZZ$kXPm(ASyW`Xf6T2 zhyj;i-Nw%>6?b}g3~;_Uz-ZU^>?hz7Jjb(!F6Bx7wv8jBU20osB7*y=(uB z59Ai=oB2lmc&zn6jIjqB03fcntf9hK8~lDF#%ZyS5@H90&RzBEIuj&`>M$&3n3=0@ z?A^-vni1Soh8u()5K5EmSD5T68$&b6BfjHzIodSQu%CBc?TWx6j5OXcj@h8 z-Ul?eA%f&JBd2Wn+LlS_EdKyFO!}irWnpHY9P#q~rC`}Fa~w8| zv$MggZ=GWCq~@S-cOf|h{{UX!p^vym0FRj_f$9qgit8p}$oNRG_MO7~Ba-xvLSeQ$ zQAh!4YQ&HmQ8Z!jR6H5L<5L73fF1SwqvwD;M2Z8K&NCiZ2^v`Y^xy+K``2>W)2qN2 zI5|QGpTQ`f7<69d`TLlnLIy5Mwt+Hw!X*0l8xg<~hN#NtEBC?I)?t=qO-V9I+Pw~> znJQi~V+xfRX7KOEp1${ChN-xxO$x~Mk2>8322zIO5T2){BHFjXTHTIT3$2`j=@oRD5BK|2)2NtDMtfuV`Y~uCEkseHQuLa$Z15&Z zCPiO|S(Or_t!KvXRWppbqFDnF&X5DzItwwPH3-X&ik@emP?(K_dD9!deE~n-`oXho zLCu6mrU|kFg)abH5pmCPM|cG}j?lSG7WINdR+sxECib1HgV)4gDqGuUouhsGIuL5e z1B4550Rb{vyQ{#=XRGGNyN&8d=j2n-5$lD=cFv7fyV+;#vK*V}?4Od4&@QmTb{|NM ziyzn1%#$WUPO&Zbv5NJXd}9mkF>li}eXfbur|*%|o__$o?q-?TnTqe9hWoZRwncV6 z{A;s-N)0=yF-}Phc3x@6RmrVDLYBvCgsw|e^UdT{H>EBUqm{Ru^j&~&Ph)Jus#4aT zAaP>HkEoolLe`_^e*hSoQto>~e*nfaIFGNV0)Eb*Nr#kXT2~VHF$lly+N?dIhMm=E z@8|j(lVlfjmMDjZt$JI*@c2m%wcX>gyp^5QG2iPO)I!zkh1Xil z#~%Qg^Cb;u%h{Wmf9Csr7GV1iAaQ~A7siD5u_c708HvrR&xm8ql>cpv_jCT0Ns!B= zuM-czkznaIj+_1miTf@@(?+lSwJ)gY13x|vvnk%D^s6?8z;G3dbtnN$$o_CupbnOzo1mpFV0E*919lyV?rQ8zvyw&YVF{WM0wL=JzxrlAAX1rtK;JBQ)jmTAhl6aDzT}apO z*VO)0@QkbBi%hWN-G0q@&vs_D_MU$$oR!E+VA0{6Yuz2+^%kSCIhYNujB_WsorlDP+ISsDvk+UM%ri0E z$=y^s*TQpGK zw!@6DX)5{a65KP}^(G-odl2el@MzOWu$H}-I;0a*FOLeQwfKB3pYgR>Z)f@ zMV4fF8C`POw3lel?0*1KiZGiKj5{;L2P4vuTtTpf^-FD-gWtgptr`&bhAP;jI(cZ5(mQ2R_+ftn&G(| zGwD33+*vPhl|z#Sk$pb$g>#MJkL(fLS2K>r+~@?l*L7*pN60X%<2jtFA1LnF;k-&s z-QU(I_k`vj;{g>xZ#nfUtDrtzf{|g)uT3AE(&h^bye6{TIq!N$ok+RI_GhRd1uyOE z;zcupy5&gU(HGZ6EqmYl0IdRBmHNo)Z}4%Jh)a6RkZTkhV+az8ywqUX9glH;rEFJ; zkq0SzyaC7Wc>ENbvdmuSxT_Ha664g)2m10(2^|cS*$|@ChX@iK3Bzz)>(|LN zlQ=ps%MaI4*M7H_fNptx`vbT>TSBDl?t=1JP5M*{Xv!h(#kDM!N1=sc$r95XMM$Oe zWS5>k6bC@@GJCOxEW`e?1Zwm%HAcI2d%u(PFf_qUFK1Cur@Hi2#qu5j{Q&bmsZp=8 z2a9L~bK#;L@yv1QGr0;hSvxgU^3C8-hm^4n3PzFh zv_-8@D+05^SdM=C(ko?2;U^=P>{|)YF+<#>75@0SR*$s6g% z53%2lqL%Gs^`&}?bE_Y= zIxz~9cB5{*j!45tRLD;5lt5r?>zmZ*mG;aPKRTIw{-o*C9`$$hM` z1UcAtaMIv?E#HB8UMkAz(R_Q!xaXudK9gxdP3M$D3f?pdHN*F>bZYrfT&kb8!B5au zh+pJ!_k&jfY8O{VM2kfIO6sF9X6oE~{8_%78uMivvqD;?vhU2JFwbqZf~f4jhGfn#d|M%MTppS99U1 z?ryWQyRzPU;FrAwBBD#2-aXoBS{i4l!`3P_9j83_bAo9b+5WFk`(7qH7Be_ME!?+FmmmhcWHkOD&E_g;yZDSmTovKm!{jwh@edfOTem8RmBG=g_+<3A|?#oi34i5pQclG%hdN*5~F%hp5!mcgKb{- z-i4;{jYwr&kL7vv#gXXhGc880vvWcok|2llJZ z;az*fAV`S!ukAaxo^Mk2DP@4%VqC4YX7-)1-l#TKvv0o+LI2BM$&8056WnEX*g;KX z3|KGZ4w0Ck_aYMC&bT^ww|ND3@>qQiMIWHB-_4c>?R;!StB4I9rmuB@_h}~@h6AHX z|612-l|r&n_T0Lk44LT}?|I-yIUPxa%j-9lWNgg7-xI9{Ua=CbigQd*z0@~NQ60#{ z^skN+)d@x5;!L$82nplPycXlz&z~&IH7I`zr&ToL`M*MZ_n2ykL$nD3JlsHQxqxjT z=^%Q|tni|C!k-V+rfPr5B|*!L-OEC(OdW#sPJ<6hV?t5^)21T*3_C(8t*ri!&1xy()WXi_f0WPuWr1ncHQ&<|2mewfQs;y7 zt$y-qZ1RZC-HonQIdZ)%Aqc8`gbZ({B(3q%wm)=0qiTSqDx7k{e+uZP=8&3nbK#Jn?6*Iq^j%J{CmH4idM?VaUYWrufTA;A5==d7N zWfWmIf6r5l^!^SDF5Y-_DU!92)2Y^PH{nHU+OUV*sK&kxnIUxu9gBtS>!35c?b~ZV zSiRb@V*tjK=~=3g!`DHI-A{zdaFDK5sE5BbQE@XqKHU#+AfJ$UiGKdQ^*CY=-RM>4 z8H!(nP}VZKT(^`ctelPkwyqlh?^b0DZKc5Hv=%#kdmUuBby8|83MsHG-sh4E#KLpB zhl}`48UI*Cd^l1W^$Y3ylOZvVOKlHp-VdGEeJ~7zX`eK*!NnTbGOn;wEheMn8@$8@ zz2IpPnusAt>d;;L;xntiT3d#Ex6oRBkORG;XaU<*3PhTLc?lZTL5O-??3gfo)fUic%>F-F1X# z(0|O0qCq@T;P)6ph7K0q_3LW#;7v{3S)`$BI0VC2S-Y3eZRX#*RJ8eKQ|5-$_Cr}U zSIpcp3WB(!!6Y74=u9IhNh(-aekp`XLx4n&0+LWj+!Q&BP9f$t&f5PrPxCqhBQ2u} zz|~sr^ySu*^pGjG^m&5MQ4U4SY#JlhKGf((pxYiRJ^9AaxYc+Ummow!D|~!Jqaw>R ziKN8B1aXglbWma%fZ;Gzd|X_v^Rmt8OFBI1!_S;S)RLk4+hd5uCP&|^wkTGrUYwdw z_tp&PHFCbwKQEV66-Q5eLwgJK;S_z1)Giai)jxL(Oc4h#ae3@b)7u($sZj2j&^*Q7 zCiI~hUd8gn>gOw}^V}yb!4j&vp`Jpeoq6VXepAm2KNK+BS1036yD=!mb(VbPeL+iM zjC(b$hpdhh=icx#+6(b(?a#W6_V{#nXk`M#{_1FaDuln>kbi^7pojM~^7c5+d^|h- zn@-|@xP}-q9@YM`k$c#AZM@bo+27sx%DIJ*$VCItCjz9-7IP>(p1Nk8xRLWfq&i#I(8{%)c z&jner?d4?;j~hlVAY3YV0nJNQxTyK)?}H)4u-7Sl!3-t|XtKy=pLAE(BF@b0)Orhy z5^NceNO*v?w8jsfQ(m0d>nTy*an51ynzR=xxGH=vn3uw3zNfUde-(z@eR=9mmoNYb3nf) zecs{Cdv2`PTY5VF6@Qv9@5S^RPkjFa(|Qm-b6Woa)iG>T1UF+7;((B!&uvN%-SB>! z^bjq^kJWr=&q~`vcxn#;ml&mZs4xG_{uVUSRLE?D)BJJFyGAmn20|Nzb}!7uI@sm~ z1=T8yI~Oi^j?Fa~s5s3`c{j#TH{)*{?l(!S2=S-1e9W9sJ{EvT-8p8G84wNEN8*X2 zL3i_<9Q##;2&QAwIKB`pyai`+Qhfe2c&u{Q73glWy;Ew9qtj`3BF`R|S^Sn!yt}nc zCY_OMy{s3#1<^)QgJA-bO64xwBH8Ssq|eSnf8{?eOk#IjFBS5PW1{A)yMQRI<0ur! zIOWAdVr3cWPUzAhLhEDOwXXw<9x_k$YHCU)TXlN}8O08ERmgS^FY_Ay@|c7(6)20! zkXLOKy1YHZpMRTZXi4Wf+qtg0be}@b6bcpo-C@M`y0|lptmY^2R}OS}q?4gfUo~DR zfghkTontWA1N&GyAY`>H2QN+)9ZIHX-1@3D;={vNMuZ1oxKS}AvLdw(bPV)D`tyrizJ-`#N! zR-lZQ&LVoVK}tz@Fn{XImGf%|b+n<|T4Y|}FPjws-+D#{UlVOCXDcQRm(~=goM)X0 zS-bmV=nJ%D`m<{BjG_K1u2)oWdqt<{U)0+SBUf#+st6Wii%)+5=0lx>`QF1m(|Q6V zfR@*exb!^Y(j@qet)3E!Ff`qsKY*1bgHEYewam-=45VNOc3-_Ac^5{)UpM~RJ68?h z2-PuM6cH$-)n$1Ob6vpfg`dxyh2@zsag31ge*o~rW|)LKvg9{5ShIwG01aiTZ;neF z%M#s-rVya9D)Pwq?S&lXK=XY4k?TzNjSU0GO1qmW_$is3Z_Nl@xkx zCw#@1?Dz=b8YFcxoe$p?{NDxsgQbEt^p;gwPT9Uc>2UZ;!~eZn1~7cTAm?M4;FQZ% zWCv13%B#mWe)>{2q@-P^W!8r~{~-L?RnAL@0E0C%yRWCdzddmS?OG%?rBir4t)`(S zWjK_`Ly0VcH8CT?p%56S9KED1k_<)X;f~&79?~?7auTvG1{2i`WMdEJlCLzlvv!gp z@9rv(wdFn=wWgB*6MzV{iQ~2RT>XO%ueeZF3N(WqiJ6+v?UVzyHe~7puFY zW>Tf8m8R)EkRqTKmN4=9;R6e0rH8}D{Vc6`ryUDxrz-TYugsl8826<1lsHh$x<*Y4vm^^O80-GAC013y1Y%kxc5m-C3I!ot!dj_KJ z_&HNmBYSch^E{Hks;;94^^LP1*Vzf}o6T|OJIwk;%`5X*b1fY&P}`}^HH!#i)TGwS zeSw23uEa!Fycjhue*n^x0vDr+v@Ds>Nu-*hI)fD;V2q1Z?b+W>LeOx$g7hc>I;W# zHR!U%{x;F_GnWus5~??n)=d3XCOXmOi?M_InQ?7}WVl}3eq!`2jfe_67Tz;g#G<_A zBDC_Z0h}w<_a{oNNRp4z7JC?IB=`i}uIW?&x;MqGeLKsrOGE(mgpK@*Eu-V>A^RE) z@`;*myCsBx*fwV~keHKX-id?%_&g(vLbHNce+63lO89Kw8-I}JQBBR*3;8y4u)!1O z!3P)S3*aFSr9TEbAMY%L1Z`m5!m|)|(lt~}@OLn2Mo9y7Fh;HXk7=0!<#Xc9j=s58 zh#S)D;^SYo3un4RPj-j)*DQytAA=k2w$^u@qahZib{@gPybE|eA}I~3X%*d;bcB%m z`gKGbWB_w$18q_;KIezl((vg|Z0y0_Ft(S3?@Y~U53hYUc3!*0;n7n`j!pwdS<%z> zP26jq4}A@BBE76Dz-Q}-iqk}AV3uB^hE5@}{tXnI)lVABZCwvq>-6Q6kTxLP!D)ri zRQh8ULBF-&H}Qg|8)@QnE%n>Y)YT&+HhF$`(glC_VLnWg502YPuTi4qvXWl{SMQJa6r2AkIj4v>)cA&UT z4l!}Lt9aVpftbsSDV?L)m$XwFqSb4a zlO^Lhk+6hNfMcs0aZCn}`pt1}!CVt85~tnQ(kiqV8M%WCAyjoa*Axx$P0^<*q&%EaYnli|w>^-&Jt(ZH6!CVNO_0uB zzcM_l&@2Kx;B8tH_i@}BOrZ;p#$HnTqsKo5~sv3 z>8=mA6c%hQbr>56+ug;`h~*!IM_RLRK31f*jWDBf%&*6YN_2=JZUfw*^iUd;hb?CG zPdvP6dlC5x0-m%jFIJDaPr2nfjvxdwTfpM@Jt4YBNeznCacc5ux-bX}0|PV&hq{ep zd1^`dh`1nX*uR1WX?yBR=iF!AU5YFr(c~_iboq?iBN%{f^CSjHF1ALu?jRs;(E$2NOe#su?1@C*%6HrvxHxndd z;cksxJ|LXt#)&0d=Y~@PEASHc9)&fBO`rvL?0u^Rf&Ts-H~a6%aj5_QqR{;pDYY4a zThKP}U&#L|VzLe*0tOLT|B(s8DTiXs0{}qVKO{uf0YunAMACm?0RVqBUH%_AAR;QL z!a$I?-#?xJY7yoC)FOa9>%d>){~Y5_1SAI9{<)|BXJvvbzk;FWf&XKJ0Fb1-PZ-{9 z|J+;vWP3XiPu@RE0!SQ(BK-fSSe@wbfWgoIgW&%_Mg_nE{2&2Pqez1Ru{FOjgfCPX8NdHqv_kS_|`yPc<1cmj=kFoAQ zvw{Rv`VhrNL6;1GO&B8m-yR<@0ELo(6BvjFssQ{yOZ{(oXda&l$X)gSjrOkssv!VN zy53OWzp}s_Bm!LeLATzm>J|VyIuZ3>PXM5H^a1*n#Ar#8U@86^6G5yF5Fh6E-{>F$ zX&(nU>_0L7gZ8(^Kxcw9_P5eNkJDfG;xA49b3#EB2_s*Zq7?ZjHY{r&EUF)X)$iY# zP<@cElZcVC0@Xu^|4#h33>fRx7{{zQM_=^6QU1pxS}freB1nV(m>>xg5g|eK{vQ(@ z4*>38C!jJGi2R)cZ~6P~gVO`R9sBVAUa1(5zT z^Z$|hS9DQEG!Vc%7&J6E2wVPNFYo_y42;je0s+g{_s^m3i71n@UK{=^0Ks1YBvAhV zHVwhy1UO0E{gP55%eZo`aMEky6950>ixgmZR%jM+is5&3*wAg72om5%g?J)nj%0$hs7p zg{?(~$U!iheje(~?}*red6dqUb?l#UAkuZ{uLuj!0bxy@4p=YOrhw#F zsV(ab%o~rwGb<sOINm70_2-6!KaE z!TNCeU?6tqve#ka!Hho8@@P;jDY{<2uqg`b4@p-8*Sm@(=F$Cmm{c#wK)eTX6}$M) ze3J0{B)P(*0qX)dn-d}j3$uP;(MfpNoFT>G<~U)N%%BLT;x{`uwnKc14xgY6Au?(S zsIE};dRohS0AQ|r_yKHJ1`c?g{s$sT@As$$ou@0(4z`iar1@Sl)Ad{mhX>eC%j&Qp z1qzp6oz9Wuw)|Wm?lp*^TDjN{nRKCxI&I|Lv$8o-v1Zg;e-l`I^Gwc{3WKimd^w&b zNO`>fnIeUiNA#OLl^K8KcZbQdI;y?xbEmSE@ikP993+h!Jlv9U61 zg3fVKJ&(c?K3Ie|2S})}17q$I%`1Om&mZTquFh4T{Le)@jZMMAFBSMl=x36xru&+h zV?s6tv{2mcov~HBBrGQTJnCIp;=~cZ)L9VfM{W)oj;IGwb$R0%n*Go_L4xO3p7rnl zEfu!3o2%YAyh`|B({ImfnQE#LCOj?~!2SIfN^WLsOv=GMkbcyl4aeK&X}h*#OiU-; z`g_;+x5uhR&%SQ}@R?zb)?C>aVeEd@kn3c1k+L39MAn7h3;420r?S`F%}U*_=N}fs z0aK@HEM77{+ue*|f6_HdTVisuxPYyqywvsK#C?KU#N?mbwq;Ci}#$yh~ zgDqm5Fl+^3BFc+mUg_tlOrL02qYSn97_!53l`3xcA<# zo(Z5KJ=Cfy>9vnjI92WJpVAM4x0ALWJeLx2@IzPjM<-oN7CM<`MFA+OHnBoa1F{1B z(1*{!7TK_jgVd$gcKgG4)joTPjMW|lh#Hfw!1HP&g?0iRPOBB0j{yLY-uDYabqJJD zdDIX9w_SDXsH#IrWK&y|RpUff9srW+v%>j(>pSQnIlPyvf7gccYl~k*7Z5W#DTkN| zLjoHyJWdpW8?P}87d_Y*UT%Xx>wz~j=I=rwe;I>6}d62`L%voTAQ=`yHY?(^1Y8=kKuq0oEQPbeyv zH^e-EHlADNsm^ahfOZp(-M%H-_Ur_H0MbXQrDI6(;8)isvhwdc4W5ZF6_Y955#P!3 zA;P{xc#4Dr^firicfqqr#FdP&x}l9mKii`y!G@XO3XmQtFXO{>hSX$#LxzeExGf<5 z27?6U)|2ciBJRh+(At#;7D)z)io;w5ay%`MaHx57rFq(zb19{lAXKNobLW{P?MIc{ zUFrn>*#5;HkfLz;Dg8-xt@{AvlvP)k2JQ$s6bX~(HQ9Gk=59gH9G|C%MN!C$w1I>G zF&n=RE`Z9L#N8k(hI&uY7Eu^;hb6!>&(H1PCiWQ@ot+*Z9fh!8N)4T}W4qiH^$W^G z_a*XzH%J>~EKJICXA~v`QdI?&O3ep3A=LZRpF4Av;~=ofQjjreYFO)>zIVrH4*+2D ze})>fYl_L{(6kH2{A@}vsSL=?&|1IUl-AA01pQ;=9tUA_o0UwX`|n?TZS%6J%advj zR&a_+BvFTvB}SbE)CKNdC)I!57V)%_R$z$f6wVpjzCzPt-ASX(M`YUiWlC0N#l??- zB!}ad&*IgM81%Zlc{usu`M1IRI|d8v-v;w}IX1Yrm*9;*!czAVhAvR2y9?RhqDsbv!T*qG&=o!CD~eq9&VZ%nZMlcTT@i{vd!?Z2nEg^`fD&7p~JG_ zbCjvPEDTzV*l`u|d~#3xvQOHzLey|%&;eQ1%$B}FP}bPlrw!;N6otZRvSX2A8>3?( zbYPaxi$X?b%x^}?i2#Zn+r?JbMb#P?Xj#o3;&C_)t3Wvc=H~ZcmndQD3m>%?H|$>y5=Y z0rwqyQ4i^H-cxy!7)W$3F}(G7H9IZ@#6Gq4^`k<%Et>fr8R*^7LNAwQGEeD|jC5>5 ze0cH;{Q}Bbf&eijh zs|v!@B19|8P9jLKhhFTpeuRN`1*`*?$o2hlk50E(A~x-;9Ob# zm=leXLxJ7J!Z6niIZgHXAa=s#Y2Ky=R2@0zj2XVTOe3;OV4~+vI!@Tg^(EYVSg^yB zfzP%}OufBHW0wCsm7WFs^~Oa@OYw;dpTs5zr|^peclYW%Q5*1iXpheuE9xrR47edz zzm06V`17^xM#YVI-BUj@Zeu+%vOOAR3O2A?tb|vB)}))g{c?$lpj4@GwFa9m^~?ss z?sc`C%#w+vRf#0KY){bsycE7kP^hOoeNvu%O+~GBlcMQp&WiY~iXJw?PQZtvxn0f6 zd-QN>N4AmQZslHXZ?d{amWBA@xr#23#MN6llGDY^Y~%Jh2-nD@NS@leVt%;Qo{K~M zX<;YqeSXp|XI-n>&l-YU0IR+&Mt&(wu{`>#IClUroU4bU2Smaq_d) zUZd}&O~ycdb>oQR_e|iM8L{x|CsO15Ok&Hiq2P%yY3pn_=Ld_gn36?1n>@>ovj$kr z+@AYMY-N(Tqx?^iH$%6FJPbB?5C z`b;2mGIHgA8i`VA-RD;)BZdX;V7y7WY?R>v|8gv(<;(880WHst2=$DEL!OpBV-mScBqq539Kh7rf}?w0W2!Kk-$G1yP*F` z2NM!~ncF2cEuC56t7q?;Hp!>pXYBbT^P@`t3>C@zyGlkq<;`q6u0QJCn5_L{bEso5 z$NGM%KnX1@FuW-H>~3!&e>hXZ>e~a-oUNI5eDVX*oZJrsArhd&!9*=m)%XUkwngwg z+1l=*PSk(r?H_0A`1<&@(swkwmt4>6F;SjFoGzoY#GjtH%A~yyJ!FxOk@Halx zNj(pVgH|*>xsV>uFH9A4dMYTVSgk!w>{G7543ZX!>azo~jBQ#opSp0O9rYZbJHOU% zKnj?LcGi~4t^WXo<2gH*KhOTZO3wbYE8#>KW}JgMsl*P*4!v_oeWO`)7T>TZLw`ob z3gs58I~*EPKr3x^Qp;nX%1aUNb;G{ikXE1&W+AfK6pq;u5va@KStl6OM*=#ckiJ&L zX?0!EUilY8Z-%;eQ(u&Ho$k|BQHc+J+{jj)&n4QK!X$ zLBGXpF+cfrWUIINQc#tX`9xV&`q&avAw(1+i>wj8uYrBa_FV+RNsH$vP~x=e=9hyP z)9=|p@hT-}F<)3@x8J?Qb8X&Rv=4!c=ERX=?x$ZkK)Hz5C!qCDTV!29iz-(!6D(RD zfA^|WOUUR#td}SVNVQb5hMRHHN8CaNG@z~n< zVCU8HUH`qNw$=68s1nQzS9f`EwXVnU`xr8w-|spmVa>E2!0q_>h)iP1$o%o=?s;aw z@gIyG>VM+3fq>5#yXU#V|6=SQEU@{gy}myJXW=`hbbj{D=3|)_Y^BYH@gT#K*C==BCaC0| zs9y;GyQMGw*>P?8m^kx4fF8q1>5bMO?7tUwWB(&V18td$Lw27~FgiOsLxkTmgzQOe z`wHbLR~bJmo`vYHKR*0_AaV)aMgqr&uao+$0IP7Q>4T3?P{Wdu)9<$9n%1<`e}H*! z_o}LHYoh=8?7!Cr|1E~tNsP!{#oy3;nG*n={r_O?Sk=!hJfY85-2DTf-OaxHe*}S$ ze5U(bVfwFtp3fZ@zJ18q*esrosQkYZ_1B7NMF-C=JKV@)EGb0;?)?&x+z^N&*a-H{ z$Ky+4uZd&MC2jrxmAdzmZJ44dUpxAx-|jDcbwWN^t$cO#Yx~c^bM|1h_w~JBnwkIY z>qnv%hWFDc&%y29|L+nF@E@QY#HQ+V`6r;)dRz;JGA_VA?!%%kCdFa?VH2>e`4^yk zPDIJQK(U(?mW*;Uz7-HS0;A*HZWeGUwb&KsplE`tb&zEm@Zbs_=iTQMi7al>Ay*ai|G#u15 zGP4*_5Q;o{m19tgBKYl*LG)b&aVP*ZEtmWK)`p?Q!qR%ZL1TaEJXjiqCS^xp+6B(i zdt`!eKgPj45|E85G3*@(A=Y7_{7Xb#7abJI42b`_@tt0N4lWGTeap@Cu>l0(aYG9E z^maDTe}Qnp)9{(oBmqM%W;3B($5|({{BC>*p!vWwM=QXBEFIuUR15|#zQvb+^+`1_ z8e8j)-TvV|_v%E;zM#W*?6MTwkYtoA21!PAMVI0xloS&yQYRKrz5udL8S}3!6)Z zOcd?AV$LS8#N;82P{C;`-IiOq)XW8X`tiuz=>tP(^qH4&ygnnQ`tV|ND%fdvlk z$Ku!MFk)zi4U*v{4=V9p$X5_&J2(Ak&Q1bN;+%ZVJ?Y@dNTGR^cUrSPd%pBL>G&K* zJCv6!_cBR(hHA&Cgi~w^gMcbXA_Z2;u%e4G9%9<^c^ueM&J3SfiLINKv7|JVlu7tYb47BW?a6RX}1!^Jt|>Lq20<@*$DK z7}EI@JWANgw-M775cnbcKDglL&mO-6WO%~Bil=vHt2UU$gdn)`yWaHppfAed=Ek%L zaGE?1$|B=4qL`}i^Ru=MdJ`|i8qo^I-OXj;=540b_GE-I(~AtFH&n2dM-y4oy0++* z<uEV=k)ipFarp{+T;)sFMT67#V3fWX(&xRQrNBe6~{ z4|3Zf5acFL^PQG?-3PexNX&w)Y^eAp?V0$-RC9#GHIvat)vnc5Zu3ikoN5Z+I65gV z;gS?-^!lBb6fgBjd1T&yspS6ez!cJqzS{22>ngZv&0z%cxmTX@n^f|O>sbV|qn>#NvSOVn9HHL~cX+IQ zpcY7&Dz9#_d%G0g$8W}HXZ3NvKcdsqN+GGNWP)d@unI=C5w@}|#34NgEz;PsD(tll z&SEI=IR7nC`!le@xp|rYZ_YUachVkPHS^o%X2=X){-{IQoeJtBcpa0!`XQK6TN~cx z|Nh$yPNk}XhsTpXTV6ibMT@!+yN1@6Z9*YvaBqQ}htQv6^hj%JZ_1xF-kq<4rY33s z>ifo1G7C%|S7jXk=3!c2M)Eg&yTW*PG7&K{JM%LQT3oMB`4*c_qwl?ARkRM#8*x_f z^_&Gd_Ot(A$vic^_R%v^H{d<>bh5|9%=>2@NGv$dEl{qdhcw~W8-kq;%tyx@@mIiv{#$ut%>{2r%CnD%hybf-e-d0Y|33C z#|!vb5CnVXd4-8tdfxQRj^kES;<#;zWAWD{G;Q>OprZ4R8*0IJ@Va7SeB}d=&cRn> zM2Dl_5IQ?p)W)XZ?NS6AzfkdEL%>WhjNfn|__XHaE>v~R021vu??e4c!vKfNDR!Y9 zagv>0>n+sbtALX2P?c&#wZMb45pY*^R0x;}zZapJ*@&V`?hU5GFqLlWSa$-!^?9tT zf8b{$!qC(fq6{}9iz8nRuKAiZ#gy|~Iqik#E4-VM@aO)k0DKB=N#tUxMie0Naz`E7 z?x@L8T{{S8OC9Uj!I*}f zO35o)XW^DKx`6|NHh!N@*wEVPUvq=$H7H*!R6$Wqzp5XD_cQ>%_q9b!LQy)OJs2MS z>Lzi*Ej<#D_G>&9rN=MGjyI{=^AajbK*5#zRc+u&dfS05$%H#7qGH& zBYL0A-bv&`YDAr9Q>rU5I8dOPnX#mdZp?7j{vP0-!F&G)bHd1 zzB!9Jv+hYTH|Mx(rb)Y92;}`6mi2-!X^R%LfU~7f2HDeTjZyjEwk!v}ttbjLo%SAO zzd`whi~Gyx!|zO$L`5Q#llkn;!K2TR_KP|&ov=_(YC=*r0oZgW-9Y3^$^`b0_uXE~ zDoXn}T25Wex}9s=tFN=a5h_UEXY>^x<|>vc+KO5R9FbS(EPU_m_kWyv7X)8O?GLJp zJ_}lWphUK~*Bl_2HAU8J&uF#3UMRj$ydDB-U~TqaRr2*jCab`@E@Q<}K3j)4PZQfp z0umndCIw%4AL|qJ`l_gAyUS9-=x~;rhl}jA-?nK|PQ7mmCXJn z=A9S9Ndf91@JB*=#w7=%!|fNv>~P~rcg%oN+B)1K<{@nmsP$;OaW5kwZ7byD+4jNg zWm=(<`8{A?##UyhqF)52ME?wX!11!OAnj&*!F$l~9>Cpe?d-A}T!}tOR&DGhoitD1 zh|lUZqIp~7L$eecJ2wh?Mw|Ege^{NRjL_w5P6$T1&hqf2P0IvC+u*f(4aDR!!o(*^ zVvPlGggVitF8BPgXNqLW-}f1H6KlQ35TN{aw65 z;fO`mk2v~;abtfIvW8lYKVu^rv#7n=!IJh{URMHcP4miAV(D9|No|DR@JtL^2Jnzm-e1-X|Ovzb0cOW|$M&ikT0LAPa0aZnI$3f*UGm zfg)ctl1cSE^r$hHATWkimm+0jZ?kOa*13s6fu4UI`C$A=--gPhWkNcRv z$t&)4KX7r0>u8JLy^zEqlNz5RR1W*r`p7ei9l7Eu2xtx=N=X3hN#kG?)CQ2#@BEP69W zu_q$Ht9Sb^R@5clQse-NP@g@mx-ea^j;7&|iZL@fH7%o!D6jbmW|g{@#SU%x3IOF) zO~`v7N0Uc``lWH_fWOB$#1KQC@*^3WhRn#m*lwA(w0QVQf25>mG_FklCuSIe@VS;P zF5V9ls6wU>%y&Mf@jl23$B-1s5KeJNgv86~j!P*jgao|4z9BCg*$m7hbd*{(=VYEG zMlfc66FX9KcAvzOV*SgTzsx6zBBCw*iAae1LlP%W@1%x>Op?@V3bKr51lMQ7k}r&e z)eQ;sOW zYT`CluhzV_h*eI=^L-op@*R6--$`){elt4;!;9@%4w+V5>s0LvMpnSlS3@3hp4^sV z@9Aj{Bt@rDl1Louhu`@_r-5He!r=*3sCLIdF0zP3j|VlX3zs&;n-5rTpG}`MV}f7W z8z`1HA0nWXL#lTm=cq&vVkSngknrWB1F0EaJ|thIv;p9*UPX>W4mX++VzW+Uq-iM8 zH4$xa{iv6p53p&?(9iGHSYDSXIb@Mko8V5^J{rUQ*^0F*=nC6;ys*S-K{py0p6W!O7JMtiKfH~y%U>E@OT&uR4+g5j1xB|&az|>QEmPAe zoY=@J!f2h7$LEJO>lKXB3}b`{ls*8yg@AP1{(ftBD zD#=F0v3Xiz`7_mee%87@h8eE$9tL4ssLeow(o#3{NSoG+u=r9Y5r`l_DGC}t8YuGV zXo}!yCH7b`GcleDI{0!cJy#y&n-Q`?2d;qax~ams+Zd#11+xlcIX%oFII~xyln~73 zoD8l6k{X0{&qf7{n#%SgOdQ<10>NTY2Kc{!3|!e5HAuN z8CNq$Bnv9ReZr$YrA$?_x5{#d6>}Y+bBA9@@{p_!*yOEc048iBbvdfjn@rM48DJRM;#n&=&o(R!Pv!e8;54KlGFUsh8cWvnib9rPzY8~v;0Ps~X zKI^!-$RZ`jWosA``UO_mdRo#{*2#|lIWmQ+Qtni`C=mUi3ctkqrCXUDQ7b*KO4XAL z{OIn^WcS{!jws*RiU}*!>u5D(+USG7ry+NEOP3L91UO^EqU#^s7uXy9oshf-~vvLJo)M4Hivc5^7ksIOgWJPeas}1 zb=Mn8)h`_8eSZBgjEsDIsqsz%L`gc39ONslD2(x~q90 zUv`a8!)jXc8Bu%wG8WM4pA7KA}C>OZi_rOzRWh)YMwg)KbCDrr1_Y>>eWfnIW;b>4)p zyu3tGF#N1DvMRz|HUKg&99@N#8M?Va*sFKH?=ZzuEwtYZBXS2)Ut=DxPg_A-8zk{-a z1uJ90vG%aJz6M3O&MjyF!IMv4{pUAc9?65Hzj5U%mbykF?l5lyR~#SM4drP|zEmje zLc)}EyrOe8y-QkI5pg~*_bKcVhhVNfQ#J8Mbz4e4c9B-Yb4_gXZ66_+iUsYRe(MLr zQS&XesnZVL7WZ1Muf=JL8qmAYHW?6pJ{tF?n<05H~ztgM<&{n2WhH znjARd$E3&M3_+~>!+?oMH^RJ{}6wJ$cbgA69p z<0-@DoPZIeP1(AI-qX|F$v{ob_(K^VjX-g!ZiY<#4a8kY6eZIG?Pnz+^Q_!E+Dl(o z@B7kjC-lsH$K+5G)Jm}=R-0^H^obs%eSLB=Ix^m>fi#H zlqjr=qnTkOc9P1PI@OLyzEG47H<~du^ZLC_VB?(Nc<#f9cxdu3Et7Hg(c4;79a+#! z67ye4*c2%LE{0em^6W$MmlOUf>wb~_)Nh*cjb7=d4NS^W1k#w7D(`c3M!S~W#VxN; zzfnlSbmrwVdZ!d$8YqvMQX**^G>mg4yO~963f! ztO)TKJeQ+HZ^OlzzM~AjLO!~);K@bj>IgKzl=ZQcHHsbNxCU%zw$<8UxXPAmp#v6- z%E-drcjY;|ofO-n@FHLDY=h z?1Z*zb2zSTy(pLYCCzY9%i6xs@C~JzT*34o4f6v%WIReZ+qrXYA(jVpeLqL3=lwJ( zNuNga{)Cf}V!O2&jLA1A%R7wg)}%Zf*;e|9*$6-%Y${{jAx zOVV?a&u0Jfy4*xR@6%opnY-9w%T#0(fgjOjy|Vq{21^KSGwe(7@ufA00IOA0U*CGQ zkt@-tv&Lf)Cma- zu2r%a7X1r`T_42pLg=f+1e{zcSUFf6P(+dfgu**3iRlYG|Kgxg)8ZYe{qeerG(?Hg zvOoUui9%jigJ3#z^~t$R#JzG_M)Pckw4j0H`spRP0iZmCTqKuOM%fmgp4@On}%qRqHS(HFYgmQPl+AuhP4`3 zLDvNzBm5uWxg11v517gXuaYe-32@ogHg*8lXeiYq>m{FrMuo&^RO^OHYMIn>lxp;YJ!is(73ho8kBt4%-lJyD89U z7i@Kf5Q>mWxj%^gf@38A!t^C)BsO$gOh|iWgI5wFLmS$uXJT`5@u4Y};o6(pQMgID zT?BJl1+)y8g%!-bCJ*Z5A@NYV5%V4d0ugLl%*I!-4 z98iB%4!H~`G95HjTy$LX4d}&U&K_%!Jj_$*zbTtxVWH>Lrr0RU_oD!POi0=qdo#1c zL71?~k&$JDN|@~7M|(X2($Nd_o^<(3qYutBA$RQzaig5waHWZ#m;7vuiUH$R}qk`@Sa_>pqMQyNI$QIPT7QBfooa^i#^dBuX_`|F6m)+m$H*ME+^SB zC3~<>!*2e2n{{i^P-r!fK^5D{nWU+05G^jh$Ru50k1+UjiS#26fYVvZUy|jKW0d%a z6oiRonK2aVt)SSeJ4j>tu10>zJsY)AgAot*2x`hz&10{LeK)}$22r=-1u=tqUr5G% zNJl--IBw|2qi(H>cC*2nS^oP~c-BKG>e8``XyxE6hBoM4VHHm!|J4G~-V{Z#qxUMN zPB2qR&n7$vL@x!kk8;e|icr-_Adx07XY%|#_F{SHnwbtOd&87DusqYyB^QK?$Za(f&wn)h&%t!!<&TU0bSodcA zwt%w1bu`hfYJDV|XxPBur;uV0kp=Q68Yg|>IWT@L=St6mTbKZdRi#qSzgnOV)U(_F zQ?`-rg<5|9N*jxAG{rwaLGIU#WF;E>cWaV)N{F;u<~&mi>qHZMr2R#z>yIN4XRhF1 zZ@O6XUH-Eqsk^}j>jbsn7s!}|g)UW$4a$jpDG|&x3W-EhWKHkQQ#$xS?vJHtYe5ys ziq*>xR1MZ*A$I$;yWzxcYSet;#2VsX+ol8vxhwoQUD}ibj?v`1exCMKnZ-XZ2Cwul zO;BRvsxULjl*Fck9P#Oc86s81`#qIHQdUjCGa3w0s-`_JRNH@bgfN|jBCt75^#q1v z#$V{p%up47MTioDL~IVjrM9PuUpTj)A!mYQ}z9kcz9ZuY;r+ORNt@3aiup{2iB$3IT}KeOgF z0|^UXs)z^+Za;6F`)*mBl#Mdtg0&)|89_>eq|=R=`?2p3c4dpGl$5d;C6RT4 zBTw3Tg9d=J`GJp~cIU7Ba%Aur?C<*1ZU0DNhROjX_MLAqfy`kCswtm_W~x}B{?xK8 z8YFi(Uc4{KZ0V;U3p~1Ye1r!C)nz;L3~fPXl=L6>afzUG0#bNjvDJ=ir+|)T4#f@j zt&2!UguZ+v?L)t8Z|QtJ@Q`RePm#wbLktCYFA>C=8EwZm+GaZ(gx2aVY5yD)Oze{G zjH!F`;m6izKG^O(*E1U&kuURM+pmDIN_KSnX+l61J6EgJTVP)OctYSuZe~QMY%-^E zTGodh%iS|GH@Vr$!5s+%$Ts>J1}`L8N;Ksg;_A#V3#}zts5G$HEVL;p5e;2CMBtD1 zyp&zDe8>zWZINZRc0F|Gxj@pUl?;=#h-Ly(9#reFEw<;<@>J-RMA|ModviCOUl@2B zNlPJ2aA5CKey-)6|9-m_?=3v74(FWS_;EMe(vBXh^Wh;pd#l>za16*)jRN0VB4YAC?@MdGaoJM z6+Q2tnC+Up#Ln2uOlstbi8>v+{lwofB%t>ir!it?vn5k~nn6hfzEc29N>namXd|h& z42*?{kUMv>jsaD2cK31?!@Xv#^ZXwm3uUr+815+>DK-;W5vvK-ATD4TD%c7{sbsPe zPZLU(&TFva{CIp_H!6=x`y7VmfYExK!%zLw9n;&q>n&3ITN6LvO+^rf+0VDPc=B1L zI&8c#i(7?64by3H1RpDzZ7Y{jZ7hXPhD@3K(3C?^CDa3oWLr8T&$b#`uYThyzk#@& z>6a=NdMRqG;@!nuF|d?_Z_c`>dxUmmjFC&y(F$ELPW;dW5EkUqZ{i+8a9;N^V*0v2 zvcdy5(E{^U9J{+5Nz)1_Vc;9nsx;;G{L%-_38qoWPf1@zfkU-)3}Ogc@D@NUb}q~c73*R*HrJMCPzd|naMR=BE@k0&ot`Q0<&&B>B6Nm5oA z)*zqIQS|F7V)UanxJRJkQEf#^I`WBUlX47VMGp-G;oqw7*0Z(jVtTW8o>MN%f)lJ! z_1ZZ90pj$>`q=vxt%AJ10)p;5U^4{2@_PGq6H;J zxG;_!V*lLm%#zSr&ea7ry`4^|IEG*W%>^;due8&NY_v~bW;qAJU*tZZKP3?w2hn4{ zEdpl_p|dS$OzSpOsu3141VR9ei?Z;V$KYzIV<@C1Vo-ft&W1X?4``RadBX!C(X&5R zGwydw!OEA3{UL5_r|qntcQ1o0Z(Z|qIA0<62e)1NKY$Y_Wt>EC8hw5q;W@9J&w#pO z!p+xV*Y#J>$xSDqa&{{fO}(<_=!)C(X0c|`jw0IUB-6_@!|XzK3QAa?amN^nu5=&| z8x8qTo66~F;1qnTfRJ_;_j8?~m^neKv^^5v#o9A^ac*YNuv$(-`1HX7B;WWB%kDF;%7x@(c^4KD;O`c=v^t%6*YMz49q7EbpoXO#Ja&^SCl?qXUI zrWxV~l;(X`1mOmIGlRXaVFNLHDAo%-3BfcN)i2QTOAc#APVBt!*y+`kMhQB*KfFI) zh$tPQWf(m?eF^{R9xUy)+!Ut~*Vm}oTu!2M^}pTwFZ9QyE_fCWU>!oIcln6<;PCcy ztFyTzLaL{ScDdrIjJogIu*#Y>?V2G@WJ_I>2NEY~OrtNM4ht(33<4xrof>xZb9~5b05xe$j%}~! zT!}7&ar4{Jlf2z+UFkq*gaqq%k$l_jLg>$q7pVHJa)<;zs?S;)#{|rr5J^b?d$zfs zrc8Xx*@Yp_Xm@chqOifZP>LFk;CPdym|4K^WkpQ4afkrG&b%xi+mbO3xY>=3Pp-iw}vke5r-)wt6m0$Zg-h*GY@3m=oBIxbKa9 zZ`4vI-kpRq)nmP-Ax+t#rQc!yQ*@pvHqFpoKUPlq%3aX&zM ziUAD#M*ZZ7SklWGJu81ZFkx{?q17ag2_%IFWqr!>S>xoIbFNr&$5yqCjt-mBEM-*SDucgg*a~u+M(0sF>rcL=Ulqdj7ZwT|EsD zQ^s8-yCZsK#8j~qgOxI65-$-5bcO`?CR<%QM3Srmk8fvd*bojC2SzVrc$-}gNq6Zp zsMG^-?sA!56?gJ%L~{pvtZjE8LL{yH>3H88lW{>{65h$WZ&aP=rl` zOY=$OG4+TrK350X_*!_~KkppT>zu3C8*y0zE)`*NdbCAp$B9$Jxz&K3_(!9;nb{s} zT?T*p<=FA({tFwj1R0$Jkt)Q1f8)b)q)C!93YA6{_xMaG-$}5iu{}gav!Q?GWh4(n zo9DD{Ae~MQZKNmGnGJ3{NGly=6xL%dI@Z_-M4X?Nw6HNt<+dy1n(gI14KvjiR5(7R zdJTeSG_3=z$#;wfrpd|IC|G0hdfpTR6x|3-Sjn<&oa&^THQsQmBFE<^` zOD)To-fn=W->cZAgw;z21M{V^8d! zAXlUKA!S%*jI5(a;B9&&UjB=qW&t+$gV9sJhr&5cE9^u!bzHWcT269&l(hR~OjXbAune5+TcSl+eTz-kjPp?%q(jABj)sNCq0 zqk^Lph1WjSbp)C@AMg@1CZ)pjTRhGV*;QdvNsEH2m)Rq6iKN8$nCd7#{aRX_+v#tC zzxKF1KJ^I^NmH2V)!vRiJF~cUD_Q-q9W*6SBkhO1Px2+Vy^&)ra(S)HHV`^&M9waY zw~C7D0YA#%A-NIq!{zZGnJ_1CVT$4VE5cdc{O#>J&P%NQd=gH4iE%W7>$p_;sW*CF zuig(sS}>`U)b@^^Sh<$SR&>^}$`e9;@TTc}od}#|G%#e@?nnuS!bt+Fk^1t2>wf(J zG%uz+V|q!8=={%=1V~!)WTP%RQ$+t1vAI#?Zy1dZmT_P#QQJY8*Sp=MB>P7tgH0S` zwmSTt$sTHyOr{p7+--J$o=Ax6Ue7JP6uj5iT=`c6c@i;(S;X0BHhUB)286$&1xQjV zUka4P3ERk0M&Gu5tC@M|eeGA3sL9sEW+S&0;iQ`YgLtUs7=}^pRjEXOm2AgVjQos> z(wGPCXm24TqtBm)uxkP+YoF5+G?TUoi72W!|qttV< z7}l&&8yoI?z0T%JMG~g<&Q#_%I6B26IAK4Z>V+8T$t!$$sFahWQCwIIKj`y%_&SzT zX~YY-k!@0-<=bWg`l){Nk!wK)gWNg8SW+5QH)o$FA-J+MY$nOASs9YV?vGq6Unz@Um~bS;)es4e z(Lv$6my$G56(>57%l$1DNSfd6w?p^CFFH_`9`%`)wy8AHUaS_(Vk_)tU&}`LN;H2e aoAx=bK$b?(JbfEfzx?j0;veAO(*FU+Q-kpU literal 0 HcmV?d00001 diff --git a/money_return_receipt_data.json b/money_return_receipt_data.json new file mode 100644 index 0000000..95b1620 --- /dev/null +++ b/money_return_receipt_data.json @@ -0,0 +1,54 @@ +{ + "TotalInAlternateCurrency":null, + "AlternateCurrency":"", + "SplitPayments":null, + "PaymentMethod":"Cash", + "PaymentAmount":13, + "TaxBreakdown":[ + { + "Category":"D", + "Rate":0, + "Gross":20, + "Net":20, + "TaxAmount":0, + "Currency":"CHF" + } + ], + "WaiterName":"Ronald McDonald", + "Terminal":null, + "VatNumber":" MWST", + "GoodbyeMessageLine1":"Auf Wiedersehen.", + "GoodbyeMessageLine2":"Powered by James", + "TerminalReceipts":null, + "IsDebtor":false, + "DiscountInfo":null, + "CompanyName":"Big Mac Bistro", + "Address1":"Zelena 186", + "Address2":"79000 Lviv", + "Phone":"080 039 47 69", + "ReceiptNumber":"25", + "DateTime":"2026-07-04T11:08:13.861", + "Guests":1, + "Items":[ + { + "Quantity":1, + "Description":"Cloudy Bay Sauvignon Blanc 2024", + "UnitPrice":13, + "TotalPrice":13, + "TaxCategory":"D", + "SubItems":null + }, + { + "Quantity":1, + "Description":"Trinkgeld", + "UnitPrice":7, + "TotalPrice":7, + "TaxCategory":"D", + "SubItems":null + } + ], + "Total":20, + "Currency":"CHF", + "ThankYouMessage":"Thank you for your order!", + "TableNumber":null +} \ No newline at end of file From b927f4826066e7efc404c5e468c2c912de7a30d5 Mon Sep 17 00:00:00 2001 From: EugeneTes Date: Mon, 6 Jul 2026 14:51:29 +0200 Subject: [PATCH 3/7] add print-job-arrived hook system --- EpsonPrintService/PrintServerBootstrapper.cs | 17 +++++ .../Hooks/PrintJobArrivedHandler.cs | 75 +++++++++++++++++++ Inspectron.Epson/PrintServer/PrintLoop.cs | 29 +++++-- 3 files changed, 116 insertions(+), 5 deletions(-) create mode 100644 Inspectron.Epson/PrintServer/Hooks/PrintJobArrivedHandler.cs diff --git a/EpsonPrintService/PrintServerBootstrapper.cs b/EpsonPrintService/PrintServerBootstrapper.cs index 3c1c8e8..40f5906 100644 --- a/EpsonPrintService/PrintServerBootstrapper.cs +++ b/EpsonPrintService/PrintServerBootstrapper.cs @@ -1,6 +1,7 @@ using Inspectron.Epson; using Inspectron.Epson.PrintServer; using Inspectron.Epson.PrintServer.ConfigurationSources; +using Inspectron.Epson.PrintServer.Hooks; using Inspectron.Epson.PrintServer.JobSources; using Inspectron.Epson.PrintServer.JobStatusReporters; using Inspectron.Epson.PrintServer.PrinterAssinment; @@ -17,6 +18,7 @@ public class PrintServerBootstrapper { private readonly EpsonPrintServiceConfiguration _config; private readonly CancellationTokenSource _cts; + private readonly List _arrivedHandlers = new(); private StandardKernel? _kernel; public bool IsRunning { get; private set; } @@ -27,6 +29,17 @@ public class PrintServerBootstrapper _cts = cts; } + /// + /// Register a handler that fires once per print job as it arrives from the job source, + /// before it's enqueued on the printer queue. Register all handlers before calling . + /// Handler exceptions are logged and swallowed; they never block a print. + /// + public void OnPrintJobArrived(PrintJobArrivedHandler handler) + { + if (IsRunning) throw new InvalidOperationException("Register hooks before StartAsync()."); + _arrivedHandlers.Add(handler); + } + public async Task StartAsync() { _kernel = new StandardKernel(); @@ -80,6 +93,10 @@ public class PrintServerBootstrapper _kernel.Bind().To().InSingletonScope(); _kernel.Bind().To(); _kernel.Bind().ToSelf().InSingletonScope(); + foreach (var handler in _arrivedHandlers) + { + _kernel.Bind().ToConstant(handler); + } _kernel.Bind().To().InSingletonScope(); _kernel.Bind().ToSelf().InSingletonScope(); _kernel.Bind().ToSelf().InSingletonScope(); diff --git a/Inspectron.Epson/PrintServer/Hooks/PrintJobArrivedHandler.cs b/Inspectron.Epson/PrintServer/Hooks/PrintJobArrivedHandler.cs new file mode 100644 index 0000000..e9ad144 --- /dev/null +++ b/Inspectron.Epson/PrintServer/Hooks/PrintJobArrivedHandler.cs @@ -0,0 +1,75 @@ +using System.Text.Json; +using Inspectron.Epson.PrintServer.Printers.Utils; +using Inspectron.Epson.Queue; +using FinalReceiptModel = Inspectron.Epson.PrintServer.Printers.Utils.FinalRecipe.FinalReceipt; +using InvoiceReceiptModel = Inspectron.Epson.PrintServer.Printers.Utils.InvoiceReceipt.InvoiceReceipt; +using KitchenReceiptModel = Inspectron.Epson.PrintServer.Printers.Utils.KitchenReceipt.KitchenReceipt; +using OrderItemsReceiptModel = Inspectron.Epson.PrintServer.Printers.Utils.OrderItems.OrderItemsReceipt; + +namespace Inspectron.Epson.PrintServer.Hooks; + +public delegate Task PrintJobArrivedHandler(PrintJobArrivedContext ctx, CancellationToken cancellationToken); + +public sealed class PrintJobArrivedContext +{ + private KitchenReceiptModel? _kitchen; + private FinalReceiptModel? _final; + private InvoiceReceiptModel? _invoice; + private OrderItemsReceiptModel? _orderItems; + private bool _kitchenParsed, _finalParsed, _invoiceParsed, _orderItemsParsed; + + public PrintJob Job { get; } + public int ReceiptTypeId => Job.Document.ReceiptType; + public ReceiptConverterFactory.EReceiptType ReceiptType => (ReceiptConverterFactory.EReceiptType)Job.Document.ReceiptType; + public string ContentJson => Job.Document.Content; + + public PrintJobArrivedContext(PrintJob job) + { + Job = job; + } + + public KitchenReceiptModel? AsKitchen() + { + if (_kitchenParsed) return _kitchen; + _kitchenParsed = true; + if (ReceiptType is ReceiptConverterFactory.EReceiptType.WorkareaTicket + or ReceiptConverterFactory.EReceiptType.NextCourse) + { + _kitchen = JsonSerializer.Deserialize(ContentJson); + } + return _kitchen; + } + + public FinalReceiptModel? AsFinal() + { + if (_finalParsed) return _final; + _finalParsed = true; + if (ReceiptType == ReceiptConverterFactory.EReceiptType.Receipt) + { + _final = JsonSerializer.Deserialize(ContentJson); + } + return _final; + } + + public InvoiceReceiptModel? AsInvoice() + { + if (_invoiceParsed) return _invoice; + _invoiceParsed = true; + if (ReceiptType == ReceiptConverterFactory.EReceiptType.Invoice) + { + _invoice = JsonSerializer.Deserialize(ContentJson); + } + return _invoice; + } + + public OrderItemsReceiptModel? AsOrderItems() + { + if (_orderItemsParsed) return _orderItems; + _orderItemsParsed = true; + if (ReceiptType == ReceiptConverterFactory.EReceiptType.OrdersOverview) + { + _orderItems = JsonSerializer.Deserialize(ContentJson); + } + return _orderItems; + } +} diff --git a/Inspectron.Epson/PrintServer/PrintLoop.cs b/Inspectron.Epson/PrintServer/PrintLoop.cs index 7bf5074..6c24640 100644 --- a/Inspectron.Epson/PrintServer/PrintLoop.cs +++ b/Inspectron.Epson/PrintServer/PrintLoop.cs @@ -1,4 +1,5 @@ -using Inspectron.Epson.PrintServer.PrintServices; +using Inspectron.Epson.PrintServer.Hooks; +using Inspectron.Epson.PrintServer.PrintServices; using Inspectron.Epson.Queue; using Microsoft.Extensions.Logging; @@ -9,17 +10,19 @@ public class PrintLoop private readonly global::Inspectron.Epson.Queue.PrintServer _printServer; private readonly IPrintService _printService; private readonly IPrintJobSource _jobSource; - + private readonly IReadOnlyList _arrivedHandlers; + private readonly ILogger _logger; private CancellationTokenSource? _cancellationSource; private CancellationToken _cancellationToken; - public PrintLoop(global::Inspectron.Epson.Queue.PrintServer printServer,IPrintService printService, IPrintJobSource jobSource, ILogger logger) + public PrintLoop(global::Inspectron.Epson.Queue.PrintServer printServer, IPrintService printService, IPrintJobSource jobSource, ILogger logger, IEnumerable? arrivedHandlers = null) { _printServer = printServer; _printService = printService; _jobSource = jobSource; - + _arrivedHandlers = arrivedHandlers?.ToList() ?? (IReadOnlyList)Array.Empty(); + _logger = logger; } @@ -35,7 +38,23 @@ public class PrintLoop while (!_cancellationSource!.Token.IsCancellationRequested) { var job = await _jobSource.GetNextJobAsync(_cancellationToken); - + + if (_arrivedHandlers.Count > 0) + { + var ctx = new PrintJobArrivedContext(job); + foreach (var handler in _arrivedHandlers) + { + try + { + await handler(ctx, _cancellationToken); + } + catch (Exception ex) + { + _logger.LogError(ex, "Print job arrived hook threw (job {JobId})", job.JobId); + } + } + } + _printServer.SubmitJob(job.IP, job); } } From f64d2c8f284f192763f441164d4af1692dc7725e Mon Sep 17 00:00:00 2001 From: EugeneTes Date: Mon, 6 Jul 2026 14:51:39 +0200 Subject: [PATCH 4/7] add pudu robot control client; drop global.json SDK pin --- EpsonPrintService/PuduRobotClient.cs | 118 +++++++++++++++++++++++ EpsonPrintService/robot_control.md | 138 +++++++++++++++++++++++++++ EpsonPrintService/send_robot.sh | 29 ++++++ global.json | 6 -- 4 files changed, 285 insertions(+), 6 deletions(-) create mode 100644 EpsonPrintService/PuduRobotClient.cs create mode 100644 EpsonPrintService/robot_control.md create mode 100644 EpsonPrintService/send_robot.sh delete mode 100644 global.json diff --git a/EpsonPrintService/PuduRobotClient.cs b/EpsonPrintService/PuduRobotClient.cs new file mode 100644 index 0000000..fabc2a1 --- /dev/null +++ b/EpsonPrintService/PuduRobotClient.cs @@ -0,0 +1,118 @@ +using System.Net; +using System.Net.Http.Headers; +using System.Net.Http.Json; +using System.Text.Json; + +namespace EpsonPrintService; + +public sealed class PuduRobotClient +{ + private readonly HttpClient _http; + private readonly string _baseUrl; + private readonly string _username; + private readonly string _password; + private string? _token; + + public PuduRobotClient(string baseUrl, string username, string password, HttpClient? http = null) + { + _baseUrl = baseUrl.TrimEnd('/'); + _username = username; + _password = password; + _http = http ?? new HttpClient { Timeout = TimeSpan.FromSeconds(30) }; + } + + public async Task SendToTableAsync(string robotSn, string targetPoint, CancellationToken ct = default) + { + await EnsureTokenAsync(ct); + try + { + await PostAddCommandAsync(robotSn, targetPoint, ct); + } + catch (HttpRequestException ex) when (ex.StatusCode == HttpStatusCode.Unauthorized) + { + // Token expired — refresh once and retry. + _token = null; + await EnsureTokenAsync(ct); + await PostAddCommandAsync(robotSn, targetPoint, ct); + } + } + + private async Task EnsureTokenAsync(CancellationToken ct) + { + if (_token is not null) return; + + using var resp = await _http.PostAsJsonAsync( + $"{_baseUrl}/api/Auth/login", + new { username = _username, password = _password }, + ct); + resp.EnsureSuccessStatusCode(); + + var bodyText = await resp.Content.ReadAsStringAsync(ct); + var token = ExtractToken(bodyText); + if (string.IsNullOrEmpty(token)) + throw new InvalidOperationException($"Pudu login: no token returned. Body: {bodyText}"); + + _token = token; + } + + private async Task PostAddCommandAsync(string robotSn, string targetPoint, CancellationToken ct) + { + using var req = new HttpRequestMessage(HttpMethod.Post, $"{_baseUrl}/api/Robots/add-command") + { + Content = JsonContent.Create(new { Sn = robotSn, TargetPoint = targetPoint }) + }; + req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _token); + + using var resp = await _http.SendAsync(req, ct); + resp.EnsureSuccessStatusCode(); + } + + // Recursive search so we match the token regardless of wrapping (e.g. {"data":{"token":"..."}}). + // Mirrors the behavior of `sed -n 's/.*"token":"\([^"]*\)".*/\1/p'` in send_robot.sh. + private static string? ExtractToken(string json) + { + try + { + using var doc = JsonDocument.Parse(json); + return FindTokenProperty(doc.RootElement); + } + catch (JsonException) + { + return null; + } + } + + private static string? FindTokenProperty(JsonElement element) + { + switch (element.ValueKind) + { + case JsonValueKind.Object: + foreach (var prop in element.EnumerateObject()) + { + if ((prop.Name.Equals("token", StringComparison.OrdinalIgnoreCase) + || prop.Name.Equals("accessToken", StringComparison.OrdinalIgnoreCase)) + && prop.Value.ValueKind == JsonValueKind.String) + { + return prop.Value.GetString(); + } + } + foreach (var prop in element.EnumerateObject()) + { + var nested = FindTokenProperty(prop.Value); + if (nested is not null) return nested; + } + return null; + + case JsonValueKind.Array: + foreach (var item in element.EnumerateArray()) + { + var nested = FindTokenProperty(item); + if (nested is not null) return nested; + } + return null; + + default: + return null; + } + } +} diff --git a/EpsonPrintService/robot_control.md b/EpsonPrintService/robot_control.md new file mode 100644 index 0000000..bccb909 --- /dev/null +++ b/EpsonPrintService/robot_control.md @@ -0,0 +1,138 @@ +# Robot Control — Send a Robot to a Table + +Step-by-step guide to authorize against the `PuduControl.DataSync.API` and dispatch a robot (by serial number) to a specific table (by table name). + +## Prerequisites + +- API running locally at `http://localhost:5022` (see `Properties/launchSettings.json`). +- Credentials: `admin` / `admin`. +- Robot serial number (`Sn`) and the exact table name configured as a destination point (`TargetPoint`) in the robot's shop. +- A REST client (`curl`, Postman, HTTPie, etc.). + +All endpoints return a uniform envelope: + +```json +{ "success": true, "data": { ... }, "errorMessage": null } +``` + +## Step 1 — Authorize (obtain a JWT) + +`POST /api/Auth/login` + +Request body: + +```json +{ "username": "admin", "password": "admin" } +``` + +`curl` example: + +```bash +curl -s -X POST http://localhost:5022/api/Auth/login \ + -H "Content-Type: application/json" \ + -d '{"username":"admin","password":"admin"}' +``` + +Sample response: + +```json +{ + "success": true, + "data": { + "message": "Login successful", + "username": "admin", + "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", + "expiresIn": 86400 + }, + "errorMessage": null +} +``` + +Copy `data.token` — every subsequent call must include it as: + +``` +Authorization: Bearer +``` + +The token is valid for 24 hours. + +## Step 2 — Send the robot to a table + +`POST /api/Robots/add-command` (requires `Authorization` header) + +Request body: + +| Field | Type | Description | +|---------------|--------|--------------------------------------------------| +| `Sn` | string | Robot serial number | +| `TargetPoint` | string | Table name (must exist in the robot's shop map) | + +```json +{ "Sn": "ROBOT_SERIAL_HERE", "TargetPoint": "TABLE_NAME_HERE" } +``` + +`curl` example: + +```bash +TOKEN="eyJhbGciOi..." # token from Step 1 + +curl -s -X POST http://localhost:5022/api/Robots/add-command \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $TOKEN" \ + -d '{"Sn":"ROBOT_SERIAL_HERE","TargetPoint":"TABLE_NAME_HERE"}' +``` + +Success response: + +```json +{ + "success": true, + "data": { "message": "Command added successfully." }, + "errorMessage": null +} +``` + +The command is persisted and picked up by the `DataSyncHosted` background service, which forwards it to the robot through the Pudu API. + +## One-shot script + +```bash +#!/usr/bin/env bash +set -euo pipefail + +BASE_URL="http://localhost:5022" +ROBOT_SN="$1" +TABLE_NAME="$2" + +TOKEN=$(curl -s -X POST "$BASE_URL/api/Auth/login" \ + -H "Content-Type: application/json" \ + -d '{"username":"admin","password":"admin"}' \ + | sed -n 's/.*"token":"\([^"]*\)".*/\1/p') + +curl -s -X POST "$BASE_URL/api/Robots/add-command" \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $TOKEN" \ + -d "{\"Sn\":\"$ROBOT_SN\",\"TargetPoint\":\"$TABLE_NAME\"}" +``` + +Usage: `./send_robot.sh ` + +## Troubleshooting + +- **401 Unauthorized** — token missing, expired, or malformed. Repeat Step 1. +- **`You do not have permission to control this robot.`** — the logged-in user does not own the shop this robot belongs to. Assign the shop to the user via `POST /api/Robots/assign-shop` (admin only). +- **Target point not reached / ignored** — verify `TargetPoint` matches a point name on the robot's map exactly (case-sensitive). Use `GET /api/Robots/shops/{shopId}` to list available points. +- **Listing available robots** — `GET /api/Robots/shops/{shopId}/robots` returns robots (and their `Sn`) in a shop; `GET /api/Robots/shops` lists shops the user can access. + +## Helpful companion endpoints + +| Purpose | Method & Path | +|-------------------------------|---------------------------------------------------| +| List accessible shops | `GET /api/Robots/shops` | +| List robots in a shop | `GET /api/Robots/shops/{shopId}/robots` | +| Get shop layout / points | `GET /api/Robots/shops/{shopId}` | +| Get robot status | `GET /api/Robots/shops/{shopId}/status` | +| List queued commands | `POST /api/Robots/list-commands` | +| Cancel a queued command | `POST /api/Robots/delete-command` | + +All of the above require the `Authorization: Bearer ` header. diff --git a/EpsonPrintService/send_robot.sh b/EpsonPrintService/send_robot.sh new file mode 100644 index 0000000..f96ff83 --- /dev/null +++ b/EpsonPrintService/send_robot.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash +set -euo pipefail + +if [[ $# -ne 2 ]]; then + echo "Usage: $0 " >&2 + exit 1 +fi + +BASE_URL="${BASE_URL:-https://pudu-api.tes.gd}" +USERNAME="${USERNAME:-admin}" +PASSWORD="${PASSWORD:-admin}" +ROBOT_SN="$1" +TABLE_NAME="$2" + +TOKEN=$(curl -fsS -X POST "$BASE_URL/api/Auth/login" \ + -H "Content-Type: application/json" \ + -d "{\"username\":\"$USERNAME\",\"password\":\"$PASSWORD\"}" \ + | sed -n 's/.*"token":"\([^"]*\)".*/\1/p') + +if [[ -z "$TOKEN" ]]; then + echo "Login failed: could not extract token." >&2 + exit 1 +fi + +curl -fsS -X POST "$BASE_URL/api/Robots/add-command" \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $TOKEN" \ + -d "{\"Sn\":\"$ROBOT_SN\",\"TargetPoint\":\"$TABLE_NAME\"}" +echo diff --git a/global.json b/global.json deleted file mode 100644 index 74ce97c..0000000 --- a/global.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "sdk": { - "version": "8.0.419", - "rollForward": "latestPatch" - } -} From a51e5cb6364fd9573bd957a42e2250d79c636d59 Mon Sep 17 00:00:00 2001 From: EugeneTes Date: Thu, 9 Jul 2026 08:48:26 +0200 Subject: [PATCH 5/7] adapt money return receipt to new refund payload schema --- EpsonTest/TestHelpers.cs | 90 ++++----- .../Utils/MoneyReturnReceipt/Models.cs | 173 ++++++++---------- .../MoneyReturnReceipt/ReceiptConverter.cs | 135 +++++++------- data (1).json | 57 ++++++ money_return_receipt_data.json | 96 +++++----- 5 files changed, 289 insertions(+), 262 deletions(-) create mode 100644 data (1).json diff --git a/EpsonTest/TestHelpers.cs b/EpsonTest/TestHelpers.cs index 80b8392..0209d9d 100644 --- a/EpsonTest/TestHelpers.cs +++ b/EpsonTest/TestHelpers.cs @@ -463,64 +463,46 @@ public static class TestHelpers }; } - // Mirrors money_return_receipt_data.json plus the credit-note specific fields - // shown in money_return_receipt.jpg (original invoice reference, refund type, tip). + // Mirrors money_return_receipt_data.json (restaurant refund payload with + // Unix-ms timestamps and per-rate VAT breakdown). public static MoneyReturnReceipt BuildSampleMoneyReturnReceipt() { - var receipt = new MoneyReturnReceipt + return new MoneyReturnReceipt { - CompanyName = "Big Mac Bistro", - Address1 = "Zelena 186", - Address2 = "79000 Lviv", - Phone = "080 039 47 69", - ReceiptNumber = "25", - DateTime = new DateTime(2026, 7, 4, 11, 8, 13), - Guests = 1, - Total = 20.00m, + RestaurantName = "Gaumenfreuden", + RestaurantPhoneNumber = "043 810 48 48", + RestaurantAddressLine1 = "Seestrasse 11", + RestaurantAddressLine2 = "8810 Horgen", + RestaurantWebsite = "https://gaumen-freuden.ch", + RestaurantVatNumber = null, + ThanksMessage = "Thank you for your order!", + NoVAT = false, + RefundReceiptId = null, + RefundNumber = 1, + RefundTimestamp = 1783522075649, + OriginalTransactionNumber = 186, + OriginalPaymentTimestamp = 1783521986035, + OriginalPaymentMethod = "Cash", Currency = "CHF", - PaymentMethod = "Cash", - PaymentAmount = 13.00m, - WaiterName = "Ronald McDonald", - VatNumber = " MWST", - ThankYouMessage = "Thank you for your order!", - GoodbyeMessageLine1 = "Auf Wiedersehen.", - GoodbyeMessageLine2 = "Powered by James", - // Credit-note specific fields - OriginalInvoiceNumber = "773", - OriginalDateTime = new DateTime(2026, 7, 4, 11, 7, 0), - RefundType = "Full refund", - Tip = 7.00m + RefundType = "Full", + TipAmount = 0.3m, + RefundedAmount = 11.0m, + RefundPaymentMethod = "Cash", + RefundedItems = new List + { + new() { Name = "Cappuccino", Size = "", Quantity = 1, Price = 5.8m, TaxAbbr = "A" }, + new() { Name = "Kaffee Crème", Size = "", Quantity = 1, Price = 4.9m, TaxAbbr = "A" } + }, + StandardRate = new TaxRateBreakdown + { + Rate = 8.1m, + TotalAmount = 10.7m, + TotalAmountNetto = 9.90m, + TotalAmountTax = 0.80m + }, + ReducedRate = new TaxRateBreakdown { Rate = 0m, TotalAmount = 0m, TotalAmountNetto = 0m, TotalAmountTax = 0m }, + SpecialRateForAccommodation = new TaxRateBreakdown { Rate = 0m, TotalAmount = 0m, TotalAmountNetto = 0m, TotalAmountTax = 0m } }; - - receipt.Items.Add(new MoneyReturnReceiptItem - { - Quantity = 1, - Description = "Cloudy Bay Sauvignon Blanc 2024", - UnitPrice = 13.00m, - TotalPrice = 13.00m, - TaxCategory = "D" - }); - - receipt.Items.Add(new MoneyReturnReceiptItem - { - Quantity = 1, - Description = "Trinkgeld", - UnitPrice = 7.00m, - TotalPrice = 7.00m, - TaxCategory = "D" - }); - - receipt.TaxBreakdown.Add(new MoneyReturnTaxInfo - { - Category = "D", - Rate = 0m, - Gross = 20.00m, - Net = 20.00m, - TaxAmount = 0.00m, - Currency = "CHF" - }); - - return receipt; } #endregion @@ -559,7 +541,7 @@ public static class TestHelpers public static async Task PrintCommandsToEpsonPrinterAsync( EpsonPrinter printer, IEnumerable commands, - bool useDelay = true, + bool useDelay = false, int delayMs = 200) { await printer.FeedLinesAsync(1); diff --git a/Inspectron.Epson/PrintServer/Printers/Utils/MoneyReturnReceipt/Models.cs b/Inspectron.Epson/PrintServer/Printers/Utils/MoneyReturnReceipt/Models.cs index 4f03313..43ef754 100644 --- a/Inspectron.Epson/PrintServer/Printers/Utils/MoneyReturnReceipt/Models.cs +++ b/Inspectron.Epson/PrintServer/Printers/Utils/MoneyReturnReceipt/Models.cs @@ -1,118 +1,97 @@ +using System.Text.Json.Serialization; + namespace Inspectron.Epson.PrintServer.Printers.Utils.MoneyReturnReceipt; /// /// Data model for a refund / credit note ("money return") receipt. -/// Mirrors the sale receipt payload (see money_return_receipt_data.json) and adds -/// the credit-note specific fields that reference the original invoice being refunded. +/// Matches the refund payload (see money_return_receipt_data.json / data (1).json): +/// restaurant header info, original transaction reference, refunded items and the +/// per-rate VAT breakdown. Timestamps are Unix epoch milliseconds. /// public class MoneyReturnReceipt { - public string CompanyName { get; set; } - public string Address1 { get; set; } - public string Address2 { get; set; } - public string Phone { get; set; } + /// Base64-encoded restaurant logo (optional). Rendered by the print pipeline, not the text converter. + public string? RestaurantLogo { get; set; } - /// Credit note number (shown as "Credit note No. {ReceiptNumber}"). - public string? ReceiptNumber { get; set; } + public string RestaurantName { get; set; } + public string RestaurantPhoneNumber { get; set; } + public string RestaurantAddressLine1 { get; set; } + public string RestaurantAddressLine2 { get; set; } + public string RestaurantWebsite { get; set; } - /// Date/time the credit note was issued. - public DateTime DateTime { get; set; } + /// VAT registration number. Note the misspelled JSON key ("Restauant..."). + [JsonPropertyName("RestauantVATNumber")] + public string? RestaurantVatNumber { get; set; } - public int Guests { get; set; } - public List Items { get; set; } = new List(); + public string ThanksMessage { get; set; } - /// Refunded amount (positive). Rendered negative on the receipt. - public decimal Total { get; set; } - public string Currency { get; set; } + /// When true the sale is not subject to VAT and the tax table is replaced by a note. + public bool NoVAT { get; set; } - public decimal? TotalInAlternateCurrency { get; set; } - public string AlternateCurrency { get; set; } + public long? RefundReceiptId { get; set; } - public List SplitPayments { get; set; } = new List(); + /// Credit note number (shown as "Credit note No. {RefundNumber}"). + public long RefundNumber { get; set; } + + /// When the refund was issued, as Unix epoch milliseconds. + public long RefundTimestamp { get; set; } + + /// Number of the original transaction/invoice being refunded ("Orig. invoice No."). + public long OriginalTransactionNumber { get; set; } + + /// When the original payment was made, as Unix epoch milliseconds ("Orig. date"). + public long OriginalPaymentTimestamp { get; set; } /// Method the original payment was made with (e.g. "Cash"). - public string PaymentMethod { get; set; } - public decimal PaymentAmount { get; set; } + public string OriginalPaymentMethod { get; set; } - public List TaxBreakdown { get; set; } = new List(); + public string Currency { get; set; } - public bool IsDebtor { get; set; } = false; - public string WaiterName { get; set; } - public string Terminal { get; set; } - public string TableNumber { get; set; } - public string VatNumber { get; set; } - - public string ThankYouMessage { get; set; } - public string GoodbyeMessageLine1 { get; set; } - public string GoodbyeMessageLine2 { get; set; } - - public List TerminalReceipts { get; set; } = new List(); - public MoneyReturnDiscountInfo? DiscountInfo { get; set; } - - // --- Credit-note specific fields --- - - /// Number of the original invoice this credit note refunds ("Orig. invoice No."). - public string? OriginalInvoiceNumber { get; set; } - - /// Date/time of the original invoice ("Orig. date"). - public DateTime? OriginalDateTime { get; set; } - - /// Refund type, e.g. "Full refund" or "Partial refund" ("Type"). - public string? RefundType { get; set; } + /// Refund type, e.g. "Full" or "Partial" (rendered as "Full refund"). + public string RefundType { get; set; } /// Tip amount included in the refund. Shown as "incl. tip" when non-zero. - public decimal Tip { get; set; } -} - -public class MoneyReturnReceiptItem -{ - public int Quantity { get; set; } - public string Description { get; set; } - public decimal UnitPrice { get; set; } - public decimal TotalPrice { get; set; } - public string TaxCategory { get; set; } - public List SubItems { get; set; } -} - -public class MoneyReturnTaxInfo -{ - public string Category { get; set; } - public decimal Rate { get; set; } - public decimal Gross { get; set; } - public decimal Net { get; set; } - public decimal TaxAmount { get; set; } - public string Currency { get; set; } -} - -public class MoneyReturnSplitPaymentInfo -{ - public string PaymentMethod { get; set; } - public decimal Amount { get; set; } - public string Currency { get; set; } -} - -public class MoneyReturnDiscountInfo -{ - public string Description { get; set; } - public decimal Amount { get; set; } - public string Currency { get; set; } -} - -public class MoneyReturnPaymentTerminalReceipt -{ - public string ReceiptType { get; set; } - public string BookingType { get; set; } - public string PaymentSystem { get; set; } - public string TransactionNumber { get; set; } - public DateTime TransactionDateTime { get; set; } - public string TerminalId { get; set; } - public string AID { get; set; } - public string TransactionSeqCount { get; set; } - public string TransactionRefNo { get; set; } - public string AuthCode { get; set; } - public string AcquirerId { get; set; } - public decimal EftAmount { get; set; } public decimal TipAmount { get; set; } - public decimal TotalEftAmount { get; set; } - public string Currency { get; set; } + + public List RefundedItems { get; set; } = new List(); + + /// Total refunded amount (positive). Rendered negative on the receipt. + public decimal RefundedAmount { get; set; } + + /// Standard-rate VAT bucket. Note the misspelled JSON key ("Standart..."). + [JsonPropertyName("StandartRate")] + public TaxRateBreakdown StandardRate { get; set; } + + public TaxRateBreakdown ReducedRate { get; set; } + + public TaxRateBreakdown SpecialRateForAccommodation { get; set; } + + /// Method the refund was paid out with (e.g. "Cash"). + public string RefundPaymentMethod { get; set; } + + public string TerminalReceiptData { get; set; } + public string WebPaymentReceiptData { get; set; } +} + +public class RefundedItem +{ + public string Name { get; set; } + public string Size { get; set; } + public int Quantity { get; set; } + public decimal Price { get; set; } + public string TaxAbbr { get; set; } +} + +public class TaxRateBreakdown +{ + public decimal Rate { get; set; } + + /// Gross amount at this rate. + public decimal TotalAmount { get; set; } + + /// Net amount at this rate. + public decimal TotalAmountNetto { get; set; } + + /// Tax amount at this rate. + public decimal TotalAmountTax { get; set; } } diff --git a/Inspectron.Epson/PrintServer/Printers/Utils/MoneyReturnReceipt/ReceiptConverter.cs b/Inspectron.Epson/PrintServer/Printers/Utils/MoneyReturnReceipt/ReceiptConverter.cs index 98f2459..bd21217 100644 --- a/Inspectron.Epson/PrintServer/Printers/Utils/MoneyReturnReceipt/ReceiptConverter.cs +++ b/Inspectron.Epson/PrintServer/Printers/Utils/MoneyReturnReceipt/ReceiptConverter.cs @@ -2,9 +2,9 @@ namespace Inspectron.Epson.PrintServer.Printers.Utils.MoneyReturnReceipt; /// /// Builds the print command list for a refund / credit note receipt. -/// Layout follows money_return_receipt.jpg: company header, "Refund / Credit note" -/// title, credit note + original invoice reference block, the negative credit total, -/// the (negated) VAT breakdown and the goodbye footer. +/// Layout follows money_return_receipt.jpg: restaurant header, "Refund / Credit note" +/// title, credit note + original transaction reference block, the negative credit total, +/// the (negated) per-rate VAT breakdown and the thank-you footer. /// public class ReceiptConverter { @@ -21,11 +21,13 @@ public class ReceiptConverter { var commands = new List(); - // Header - Company info - commands.Add(new PrintCommand(Center(receipt.CompanyName, false))); - commands.Add(new PrintCommand(Center(receipt.Address1, false))); - commands.Add(new PrintCommand(Center(receipt.Address2, false))); - commands.Add(new PrintCommand(Center(receipt.Phone, false))); + // Header - Restaurant info + commands.Add(new PrintCommand(Center(receipt.RestaurantName, false))); + commands.Add(new PrintCommand(Center(receipt.RestaurantAddressLine1, false))); + commands.Add(new PrintCommand(Center(receipt.RestaurantAddressLine2, false))); + commands.Add(new PrintCommand(Center(receipt.RestaurantPhoneNumber, false))); + if (!string.IsNullOrWhiteSpace(receipt.RestaurantWebsite)) + commands.Add(new PrintCommand(Center(receipt.RestaurantWebsite, false))); commands.Add(new PrintCommand("")); commands.Add(new PrintCommand("")); @@ -36,93 +38,96 @@ public class ReceiptConverter commands.Add(new PrintCommand("")); // Credit note number + issue date - string creditNoteLabel = receipt.ReceiptNumber != null - ? $"Credit note No. {receipt.ReceiptNumber}" - : "Credit note"; - commands.Add(new PrintCommand(Justify(creditNoteLabel, $"{receipt.DateTime:HH:mm dd.MM.yyyy}"))); + commands.Add(new PrintCommand(Justify( + $"Credit note No. {receipt.RefundNumber}", + FormatTimestamp(receipt.RefundTimestamp)))); commands.Add(new PrintCommand("")); - // Original invoice reference block - commands.Add(new PrintCommand(Justify("Orig. invoice No.:", receipt.OriginalInvoiceNumber ?? ""))); - if (receipt.OriginalDateTime.HasValue) - commands.Add(new PrintCommand(Justify("Orig. date:", $"{receipt.OriginalDateTime:HH:mm dd.MM.yyyy}"))); - commands.Add(new PrintCommand(Justify("Orig. payment method:", receipt.PaymentMethod ?? ""))); + // Original transaction reference block + commands.Add(new PrintCommand(Justify("Orig. invoice No.:", receipt.OriginalTransactionNumber.ToString()))); + commands.Add(new PrintCommand(Justify("Orig. date:", FormatTimestamp(receipt.OriginalPaymentTimestamp)))); + commands.Add(new PrintCommand(Justify("Orig. payment method:", receipt.OriginalPaymentMethod ?? ""))); if (!string.IsNullOrEmpty(receipt.RefundType)) - commands.Add(new PrintCommand(Justify("Type:", receipt.RefundType))); + commands.Add(new PrintCommand(Justify("Type:", $"{receipt.RefundType} refund"))); commands.Add(new PrintCommand("")); commands.Add(new PrintCommand(new string('-', _lineWidth))); commands.Add(new PrintCommand("")); // Included tip - if (receipt.Tip != 0) + if (receipt.TipAmount != 0) { - commands.Add(new PrintCommand($"incl. tip {receipt.Currency}: {receipt.Tip:F2}".PadLeft(_lineWidth))); + commands.Add(new PrintCommand($"incl. tip {receipt.Currency}: {receipt.TipAmount:F2}".PadLeft(_lineWidth))); commands.Add(new PrintCommand("")); } // Credit total (refund is shown as a negative amount), big + bold, centered - string creditLine = $"Credit: {-receipt.Total:F2} {receipt.Currency}"; + string creditLine = $"Credit: {-receipt.RefundedAmount:F2} {receipt.Currency}"; commands.Add(new PrintCommand(Center(creditLine, true), true, true)); commands.Add(new PrintCommand("")); - // Alternate currency - if (receipt.TotalInAlternateCurrency.HasValue) - { - string altCurrencyLine = $"{-receipt.TotalInAlternateCurrency:F2} {receipt.AlternateCurrency}"; - commands.Add(new PrintCommand(altCurrencyLine.PadLeft(_lineWidth))); - commands.Add(new PrintCommand("")); - } - - // Refund payment method line (e.g. "Cash: -20.00 CHF") - if (receipt.SplitPayments != null && receipt.SplitPayments.Count > 0) - { - foreach (var split in receipt.SplitPayments) - commands.Add(new PrintCommand($"{split.PaymentMethod}: {-split.Amount:F2} {split.Currency}".PadLeft(_lineWidth))); - } - else - { - commands.Add(new PrintCommand($"{receipt.PaymentMethod}: {-receipt.Total:F2} {receipt.Currency}".PadLeft(_lineWidth))); - } + // Refund payment method line (e.g. "Cash: -11.00 CHF") + commands.Add(new PrintCommand($"{receipt.RefundPaymentMethod}: {-receipt.RefundedAmount:F2} {receipt.Currency}".PadLeft(_lineWidth))); commands.Add(new PrintCommand("")); - // Tax breakdown (amounts negated for the refund) - foreach (var tax in receipt.TaxBreakdown) - { - if (receipt.TaxBreakdown.IndexOf(tax) == 0) - { - string taxHeader = "VAT %".PadRight(_lineWidth / 4) - + "Gross".PadLeft(_lineWidth / 4) - + "Net".PadLeft(_lineWidth / 4) - + "VAT".PadLeft(_lineWidth / 4); - commands.Add(new PrintCommand(taxHeader)); - } - - string taxDetail = ($"{tax.Category}:" + $"{tax.Rate}%".PadLeft(5)).PadRight(_lineWidth / 4) - + $"{-tax.Gross:F2} {tax.Currency}".PadLeft(_lineWidth / 4) - + $"{-tax.Net:F2} {tax.Currency}".PadLeft(_lineWidth / 4) - + $"{-tax.TaxAmount:F2} {tax.Currency}".PadLeft(_lineWidth / 4); - commands.Add(new PrintCommand(taxDetail)); - } - - commands.Add(new PrintCommand("")); - - if (receipt.TaxBreakdown.Count(x => x.Category != null && x.Category.ToLower() != "d") == 0) + // VAT breakdown (amounts negated for the refund) + if (receipt.NoVAT) { commands.Add(new PrintCommand("Not subject to value added tax")); } + else + { + var rates = new List<(string Category, TaxRateBreakdown Rate)>(); + AddIfPresent(rates, "A", receipt.StandardRate); + AddIfPresent(rates, "B", receipt.ReducedRate); + AddIfPresent(rates, "C", receipt.SpecialRateForAccommodation); + + if (rates.Count > 0) + { + commands.Add(new PrintCommand("VAT %".PadRight(_lineWidth / 4) + + "Gross".PadLeft(_lineWidth / 4) + + "Net".PadLeft(_lineWidth / 4) + + "VAT".PadLeft(_lineWidth / 4))); + + foreach (var (category, rate) in rates) + { + commands.Add(new PrintCommand( + ($"{category}:" + $"{rate.Rate}%".PadLeft(5)).PadRight(_lineWidth / 4) + + $"{-rate.TotalAmount:F2} {receipt.Currency}".PadLeft(_lineWidth / 4) + + $"{-rate.TotalAmountNetto:F2} {receipt.Currency}".PadLeft(_lineWidth / 4) + + $"{-rate.TotalAmountTax:F2} {receipt.Currency}".PadLeft(_lineWidth / 4))); + } + } + } + + commands.Add(new PrintCommand("")); + + // VAT registration number (if provided) + if (!string.IsNullOrWhiteSpace(receipt.RestaurantVatNumber)) + { + commands.Add(new PrintCommand(Center(receipt.RestaurantVatNumber, false))); + } commands.Add(new PrintCommand("")); commands.Add(new PrintCommand("")); - // Footer / goodbye - commands.Add(new PrintCommand(Center(receipt.ThankYouMessage, false))); - commands.Add(new PrintCommand(Center(receipt.GoodbyeMessageLine1, false))); - commands.Add(new PrintCommand(Center(receipt.GoodbyeMessageLine2, false))); + // Footer + commands.Add(new PrintCommand(Center(receipt.ThanksMessage, false))); return commands; } + private static void AddIfPresent(List<(string, TaxRateBreakdown)> rates, string category, TaxRateBreakdown? rate) + { + if (rate != null && (rate.Rate != 0 || rate.TotalAmount != 0)) + rates.Add((category, rate)); + } + + private static string FormatTimestamp(long unixMilliseconds) + { + return DateTimeOffset.FromUnixTimeMilliseconds(unixMilliseconds).LocalDateTime.ToString("HH:mm dd.MM.yyyy"); + } + private string Justify(string left, string right) { left ??= ""; diff --git a/data (1).json b/data (1).json new file mode 100644 index 0000000..19aa5b0 --- /dev/null +++ b/data (1).json @@ -0,0 +1,57 @@ +{ + "RestaurantName":"Gaumenfreuden", + "RestaurantPhoneNumber":"043 810 48 48", + "RestaurantAddressLine1":"Seestrasse 11", + "RestaurantAddressLine2":"8810 Horgen", + "RestaurantWebsite":"https://gaumen-freuden.ch", + "RestauantVATNumber":null, + "ThanksMessage":"Thank you for your order!", + "NoVAT":false, + "RefundReceiptId":null, + "RefundNumber":1, + "RefundTimestamp":1783522075649, + "OriginalTransactionNumber":186, + "OriginalPaymentTimestamp":1783521986035, + "OriginalPaymentMethod":"Cash", + "Currency":"CHF", + "RefundType":"Full", + "TipAmount":0.3, + "RefundedItems":[ + { + "Name":"Cappuccino", + "Size":"", + "Quantity":1, + "Price":5.8, + "TaxAbbr":"A" + }, + { + "Name":"Kaffee Cr\u00E8me", + "Size":"", + "Quantity":1, + "Price":4.9, + "TaxAbbr":"A" + } + ], + "RefundedAmount":11.0, + "StandartRate":{ + "Rate":8.1, + "TotalAmount":10.7, + "TotalAmountNetto":9.90, + "TotalAmountTax":0.80 + }, + "ReducedRate":{ + "Rate":0, + "TotalAmount":0, + "TotalAmountNetto":0, + "TotalAmountTax":0 + }, + "SpecialRateForAccommodation":{ + "Rate":0, + "TotalAmount":0, + "TotalAmountNetto":0, + "TotalAmountTax":0 + }, + "RefundPaymentMethod":"Cash", + "TerminalReceiptData":null, + "WebPaymentReceiptData":null +} \ No newline at end of file diff --git a/money_return_receipt_data.json b/money_return_receipt_data.json index 95b1620..a3fcac1 100644 --- a/money_return_receipt_data.json +++ b/money_return_receipt_data.json @@ -1,54 +1,58 @@ { - "TotalInAlternateCurrency":null, - "AlternateCurrency":"", - "SplitPayments":null, - "PaymentMethod":"Cash", - "PaymentAmount":13, - "TaxBreakdown":[ - { - "Category":"D", - "Rate":0, - "Gross":20, - "Net":20, - "TaxAmount":0, - "Currency":"CHF" - } - ], - "WaiterName":"Ronald McDonald", - "Terminal":null, - "VatNumber":" MWST", - "GoodbyeMessageLine1":"Auf Wiedersehen.", - "GoodbyeMessageLine2":"Powered by James", - "TerminalReceipts":null, - "IsDebtor":false, - "DiscountInfo":null, - "CompanyName":"Big Mac Bistro", - "Address1":"Zelena 186", - "Address2":"79000 Lviv", - "Phone":"080 039 47 69", - "ReceiptNumber":"25", - "DateTime":"2026-07-04T11:08:13.861", - "Guests":1, - "Items":[ + "RestaurantLogo":null, + "RestaurantName":"Gaumenfreuden", + "RestaurantPhoneNumber":"043 810 48 48", + "RestaurantAddressLine1":"Seestrasse 11", + "RestaurantAddressLine2":"8810 Horgen", + "RestaurantWebsite":"https://gaumen-freuden.ch", + "RestauantVATNumber":null, + "ThanksMessage":"Thank you for your order!", + "NoVAT":false, + "RefundReceiptId":null, + "RefundNumber":1, + "RefundTimestamp":1783522075649, + "OriginalTransactionNumber":186, + "OriginalPaymentTimestamp":1783521986035, + "OriginalPaymentMethod":"Cash", + "Currency":"CHF", + "RefundType":"Full", + "TipAmount":0.3, + "RefundedItems":[ { + "Name":"Cappuccino", + "Size":"", "Quantity":1, - "Description":"Cloudy Bay Sauvignon Blanc 2024", - "UnitPrice":13, - "TotalPrice":13, - "TaxCategory":"D", - "SubItems":null + "Price":5.8, + "TaxAbbr":"A" }, { + "Name":"Kaffee Crème", + "Size":"", "Quantity":1, - "Description":"Trinkgeld", - "UnitPrice":7, - "TotalPrice":7, - "TaxCategory":"D", - "SubItems":null + "Price":4.9, + "TaxAbbr":"A" } ], - "Total":20, - "Currency":"CHF", - "ThankYouMessage":"Thank you for your order!", - "TableNumber":null -} \ No newline at end of file + "RefundedAmount":11.0, + "StandartRate":{ + "Rate":8.1, + "TotalAmount":10.7, + "TotalAmountNetto":9.90, + "TotalAmountTax":0.80 + }, + "ReducedRate":{ + "Rate":0, + "TotalAmount":0, + "TotalAmountNetto":0, + "TotalAmountTax":0 + }, + "SpecialRateForAccommodation":{ + "Rate":0, + "TotalAmount":0, + "TotalAmountNetto":0, + "TotalAmountTax":0 + }, + "RefundPaymentMethod":"Cash", + "TerminalReceiptData":null, + "WebPaymentReceiptData":null +} From 5adb8f975cb167eb97950649dad84e56c0136929 Mon Sep 17 00:00:00 2001 From: EugeneTes Date: Thu, 9 Jul 2026 08:48:26 +0200 Subject: [PATCH 6/7] bump EpsonPrintService version to 1.0.13 --- EpsonPrintService/EpsonPrintService.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/EpsonPrintService/EpsonPrintService.csproj b/EpsonPrintService/EpsonPrintService.csproj index 505cace..a6c5b7b 100644 --- a/EpsonPrintService/EpsonPrintService.csproj +++ b/EpsonPrintService/EpsonPrintService.csproj @@ -5,7 +5,7 @@ net8.0 enable enable - 1.0.12 + 1.0.13 From db30523697a16229ec7cbc958724044cee662ff9 Mon Sep 17 00:00:00 2001 From: EugeneTes Date: Thu, 9 Jul 2026 08:54:30 +0200 Subject: [PATCH 7/7] bump EpsonPrintService version to 1.0.14 --- EpsonPrintService/EpsonPrintService.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/EpsonPrintService/EpsonPrintService.csproj b/EpsonPrintService/EpsonPrintService.csproj index a6c5b7b..c490f69 100644 --- a/EpsonPrintService/EpsonPrintService.csproj +++ b/EpsonPrintService/EpsonPrintService.csproj @@ -5,7 +5,7 @@ net8.0 enable enable - 1.0.13 + 1.0.14