diff --git a/scu-it/src/fiskaltrust.Middleware.SCU.IT.EpsonRTPrinter/EpsonRTPrinterSCU.cs b/scu-it/src/fiskaltrust.Middleware.SCU.IT.EpsonRTPrinter/EpsonRTPrinterSCU.cs index 92dd95629..695218cd2 100644 --- a/scu-it/src/fiskaltrust.Middleware.SCU.IT.EpsonRTPrinter/EpsonRTPrinterSCU.cs +++ b/scu-it/src/fiskaltrust.Middleware.SCU.IT.EpsonRTPrinter/EpsonRTPrinterSCU.cs @@ -22,6 +22,7 @@ public sealed class EpsonRTPrinterSCU : LegacySCU private readonly EpsonRTPrinterSCUConfiguration _configuration; private readonly ErrorInfoFactory _errorCodeFactory = new(); private string? _serialnr; + private (long ZNumber, long DocNumber)? _lastSuccessfulDoc; public EpsonRTPrinterSCU(ILogger logger, EpsonRTPrinterSCUConfiguration configuration, IEpsonFpMateClient epsonCloudHttpClient) { @@ -199,6 +200,7 @@ private async Task SetReceiptResponse(PrinterResponse? re public async Task PerformProtocolReceiptAsync(ReceiptRequest receiptRequest, ReceiptResponse receiptResponse) { + string? data = null; try { var content = EpsonCommandFactory.CreateInvoiceRequestContent(_configuration, receiptRequest); @@ -229,7 +231,7 @@ public async Task PerformProtocolReceiptAsync(ReceiptRequest re }); } - var data = SoapSerializer.Serialize(content); + data = SoapSerializer.Serialize(content); _logger.LogDebug("Request content ({receiptreference}): {content}", receiptRequest.cbReceiptReference, SoapSerializer.Serialize(data)); var response = await _httpClient.SendCommandAsync(data); @@ -254,9 +256,10 @@ public async Task PerformProtocolReceiptAsync(ReceiptRequest re RTDocMoment = fiscalReceiptResponse.ReceiptDateTime, RTDocType = "POSRECEIPT", RTCodiceLotteria = "", - RTCustomerID = "", // Todo dread customerid from data + RTCustomerID = "", // Todo dread customerid from data }; receiptResponse.ftSignatures = SignatureFactory.CreateDocumentoCommercialeSignatures(posReceiptSignatur).ToArray(); + _lastSuccessfulDoc = (fiscalReceiptResponse.ZRepNumber, fiscalReceiptResponse.ReceiptNumber); if (result?.Receipt?.PrinterStatus != null && !result.Receipt.PrinterStatus.StartsWith("0")) { @@ -265,6 +268,11 @@ public async Task PerformProtocolReceiptAsync(ReceiptRequest re return receiptResponse; } + catch (Exception e) when ((e is TaskCanceledException || e is HttpRequestException) && data != null) + { + _logger.LogWarning("({receiptreference}) Network error — checking if the printer has already printed...", receiptRequest.cbReceiptReference); + return await TryRecoverFromNetworkErrorAsync(receiptRequest, receiptResponse, data); + } catch (Exception e) { var response = Helpers.ExceptionInfo(e); @@ -275,12 +283,14 @@ public async Task PerformProtocolReceiptAsync(ReceiptRequest re public async Task PerformClassicReceiptAsync(ReceiptRequest receiptRequest, ReceiptResponse receiptResponse) { + string? data = null; try { var content = EpsonCommandFactory.CreateInvoiceRequestContent(_configuration, receiptRequest); - var data = SoapSerializer.Serialize(content); + data = SoapSerializer.Serialize(content); _logger.LogDebug("Request content ({receiptreference}): {content}", receiptRequest.cbReceiptReference, data); var response = await _httpClient.SendCommandAsync(data); + using var responseContent = await response.Content.ReadAsStreamAsync(); var result = SoapSerializer.DeserializeToSoapEnvelope(responseContent); if (result != null) @@ -302,15 +312,21 @@ public async Task PerformClassicReceiptAsync(ReceiptRequest rec RTDocMoment = fiscalReceiptResponse.ReceiptDateTime, RTDocType = "POSRECEIPT", RTCodiceLotteria = "", - RTCustomerID = "", // Todo dread customerid from data + RTCustomerID = "", // Todo dread customerid from data }; receiptResponse.ftSignatures = SignatureFactory.CreateDocumentoCommercialeSignatures(posReceiptSignatur).ToArray(); + _lastSuccessfulDoc = (fiscalReceiptResponse.ZRepNumber, fiscalReceiptResponse.ReceiptNumber); if (result?.Receipt?.PrinterStatus != null && !result.Receipt.PrinterStatus.StartsWith("0")) { receiptResponse.AddWarningSignatureItem(Helpers.GetPrinterStatus(result?.Receipt?.PrinterStatus) ?? ""); } return receiptResponse; } + catch (Exception e) when ((e is TaskCanceledException || e is HttpRequestException) && data != null) + { + _logger.LogWarning("({receiptreference}) Network error — checking if the printer has already printed...", receiptRequest.cbReceiptReference); + return await TryRecoverFromNetworkErrorAsync(receiptRequest, receiptResponse, data); + } catch (Exception e) { var response = Helpers.ExceptionInfo(e); @@ -320,6 +336,133 @@ public async Task PerformClassicReceiptAsync(ReceiptRequest rec } } + private async Task TryRecoverFromNetworkErrorAsync(ReceiptRequest receiptRequest, ReceiptResponse receiptResponse, string xmlData) + { + _logger.LogInformation("({receiptreference}) Querying last emitted document from printer...", receiptRequest.cbReceiptReference); + var docBeforeRetry = await ReadLastEmittedDocStatusAsync(receiptRequest.cbReceiptReference); + var expectedAmountCents = (long) Math.Round((receiptRequest.cbReceiptAmount ?? 0) * 100); + _logger.LogDebug("({receiptreference}) Last emitted doc: Z#{z} Doc#{doc} amount={amount}cents, expected={expected}cents", + receiptRequest.cbReceiptReference, docBeforeRetry?.ZNumber, docBeforeRetry?.DocNumber, docBeforeRetry?.TotalDocAmountCents, expectedAmountCents); + + var hasProgressed = _lastSuccessfulDoc.HasValue && IsDocAdvanced(docBeforeRetry, _lastSuccessfulDoc.Value); + var matchesByAmount = !_lastSuccessfulDoc.HasValue && docBeforeRetry != null + && docBeforeRetry.IsFiscalDocument && docBeforeRetry.TotalDocAmountCents == expectedAmountCents; + + if (hasProgressed || matchesByAmount) + { + var reason = hasProgressed ? "progression detected" : "amount matches, no cache"; + _logger.LogInformation("({receiptreference}) Document found: Z#{zNum} Doc#{docNum} ({reason}) — printer already printed, skipping retry.", + receiptRequest.cbReceiptReference, docBeforeRetry!.ZNumber, docBeforeRetry.DocNumber, reason); + return ApplyRecoveredDoc(receiptResponse, docBeforeRetry); + } + + return await RetryReceiptWithRecoveryAsync(receiptRequest, receiptResponse, xmlData, docBeforeRetry); + } + + private async Task RetryReceiptWithRecoveryAsync(ReceiptRequest receiptRequest, ReceiptResponse receiptResponse, string xmlData, LastEmittedDocStatus? docBeforeRetry) + { + for (var attempt = 0; attempt < _configuration.MaxNetworkRetries; attempt++) + { + await Task.Delay(1000); + try + { + _logger.LogWarning("({receiptreference}) Document not found — retrying receipt (attempt {attempt}/{max})...", receiptRequest.cbReceiptReference, attempt + 1, _configuration.MaxNetworkRetries); + var retryResponse = await _httpClient.SendCommandAsync(xmlData); + using var retryContent = await retryResponse.Content.ReadAsStreamAsync(); + var retryResult = SoapSerializer.DeserializeToSoapEnvelope(retryContent); + var retryFiscalResponse = await SetReceiptResponse(retryResult); + if (retryFiscalResponse.Success) + { + _logger.LogInformation("({receiptreference}) Retry succeeded: Z#{zNum} Doc#{docNum}.", receiptRequest.cbReceiptReference, retryFiscalResponse.ZRepNumber, retryFiscalResponse.ReceiptNumber); + _lastSuccessfulDoc = (retryFiscalResponse.ZRepNumber, retryFiscalResponse.ReceiptNumber); + receiptResponse.ftSignatures = SignatureFactory.CreateDocumentoCommercialeSignatures(new POSReceiptSignatureData + { + RTSerialNumber = retryResult?.Receipt?.SerialNumber ?? "", + RTZNumber = retryFiscalResponse.ZRepNumber, + RTDocNumber = retryFiscalResponse.ReceiptNumber, + RTDocMoment = retryFiscalResponse.ReceiptDateTime, + RTDocType = "POSRECEIPT", + RTCodiceLotteria = "", + RTCustomerID = "", + }).ToArray(); + return receiptResponse; + } + + _logger.LogError("({receiptreference}) Retry attempt {attempt}/{max} failed with printer error: {error}", receiptRequest.cbReceiptReference, attempt + 1, _configuration.MaxNetworkRetries, retryFiscalResponse.SSCDErrorInfo?.Info); + receiptResponse.SetReceiptResponseErrored(retryFiscalResponse.SSCDErrorInfo?.Info ?? ""); + return receiptResponse; + } + catch (Exception e) when (e is TaskCanceledException || e is HttpRequestException) + { + _logger.LogWarning("({receiptreference}) Network error on retry attempt {attempt}/{max} — checking if printer has printed...", receiptRequest.cbReceiptReference, attempt + 1, _configuration.MaxNetworkRetries); + var lastDoc = await ReadLastEmittedDocStatusAsync(receiptRequest.cbReceiptReference); + _logger.LogDebug("({receiptreference}) Current: Z#{z} Doc#{doc}, cache: Z#{bz} Doc#{bd}", receiptRequest.cbReceiptReference, lastDoc?.ZNumber, lastDoc?.DocNumber, _lastSuccessfulDoc?.ZNumber, _lastSuccessfulDoc?.DocNumber); + + var baseline = _lastSuccessfulDoc ?? + (docBeforeRetry != null ? ((long ZNumber, long DocNumber)?)(docBeforeRetry.ZNumber, docBeforeRetry.DocNumber) : null); + + if (baseline.HasValue && IsDocAdvanced(lastDoc, baseline.Value)) + { + _logger.LogInformation("({receiptreference}) Document found: Z#{zNum} Doc#{docNum} — printer already printed, skipping retry.", receiptRequest.cbReceiptReference, lastDoc!.ZNumber, lastDoc.DocNumber); + return ApplyRecoveredDoc(receiptResponse, lastDoc); + } + } + catch (Exception queryEx) + { + _logger.LogError(queryEx, "({receiptreference}) Unexpected error on attempt {attempt}/{max}.", receiptRequest.cbReceiptReference, attempt + 1, _configuration.MaxNetworkRetries); + break; + } + } + + _logger.LogError("({receiptreference}) All recovery attempts failed — unable to determine printer state.", receiptRequest.cbReceiptReference); + receiptResponse.SetReceiptResponseErrored("epson-printer-network-error"); + return receiptResponse; + } + + private static bool IsDocAdvanced(LastEmittedDocStatus? doc, (long ZNumber, long DocNumber) baseline) + { + return doc != null && doc.IsFiscalDocument && + (doc.ZNumber > baseline.ZNumber || + (doc.ZNumber == baseline.ZNumber && doc.DocNumber > baseline.DocNumber)); + } + + private ReceiptResponse ApplyRecoveredDoc(ReceiptResponse receiptResponse, LastEmittedDocStatus doc) + { + receiptResponse.ftSignatures = SignatureFactory.CreateDocumentoCommercialeSignatures(new POSReceiptSignatureData + { + RTSerialNumber = doc.PrinterSN ?? "", + RTZNumber = doc.ZNumber, + RTDocNumber = doc.DocNumber, + RTDocMoment = doc.DocumentDateTime, + RTDocType = "POSRECEIPT", + RTCodiceLotteria = "", + RTCustomerID = "", + }).ToArray(); + _lastSuccessfulDoc = (doc.ZNumber, doc.DocNumber); + return receiptResponse; + } + + private async Task ReadLastEmittedDocStatusAsync(string receiptReference) + { + try + { + var command = new PrinterCommand() { DirectIO = DirectIO.GetLastEmittedDocStatusCommand() }; + var content = SoapSerializer.Serialize(command); + var response = await _httpClient.SendCommandAsync(content); + using var responseContent = await response.Content.ReadAsStreamAsync(); + var result = SoapSerializer.DeserializeToSoapEnvelope(responseContent); + var rawData = result?.CommandResponse?.ResponseData; + _logger.LogDebug("Last emitted document query response: success={success}, printerStatus={status}, rawData={raw}", + result?.Success, result?.CommandResponse?.PrinterStatus, rawData); + return LastEmittedDocStatus.Parse(rawData); + } + catch (Exception e) when (e is TaskCanceledException || e is HttpRequestException) + { + _logger.LogWarning(e, "({receiptreference}) Could not query printer status — proceeding to retry loop.", receiptReference); + return null; + } + } + private async Task ProcessUnspecifiedProtocolReceipt(ProcessRequest request) { try diff --git a/scu-it/src/fiskaltrust.Middleware.SCU.IT.EpsonRTPrinter/EpsonRTPrinterSCUConfiguration.cs b/scu-it/src/fiskaltrust.Middleware.SCU.IT.EpsonRTPrinter/EpsonRTPrinterSCUConfiguration.cs index 62ee667fc..5257a1696 100644 --- a/scu-it/src/fiskaltrust.Middleware.SCU.IT.EpsonRTPrinter/EpsonRTPrinterSCUConfiguration.cs +++ b/scu-it/src/fiskaltrust.Middleware.SCU.IT.EpsonRTPrinter/EpsonRTPrinterSCUConfiguration.cs @@ -19,6 +19,11 @@ public class EpsonRTPrinterSCUConfiguration /// public int ServerTimeoutMs { get; set; } = 10000; + /// + /// The maximum number of retries when a network error occurs during receipt printing + /// + public int MaxNetworkRetries { get; set; } = 3; + public string? Password { get; set; } public string? AdditionalTrailerLines { get; set;} diff --git a/scu-it/src/fiskaltrust.Middleware.SCU.IT.EpsonRTPrinter/LocalEpsonFpMateClient.cs b/scu-it/src/fiskaltrust.Middleware.SCU.IT.EpsonRTPrinter/LocalEpsonFpMateClient.cs index d02916e23..110836f88 100644 --- a/scu-it/src/fiskaltrust.Middleware.SCU.IT.EpsonRTPrinter/LocalEpsonFpMateClient.cs +++ b/scu-it/src/fiskaltrust.Middleware.SCU.IT.EpsonRTPrinter/LocalEpsonFpMateClient.cs @@ -19,7 +19,7 @@ public LocalEpsonFpMateClient(EpsonRTPrinterSCUConfiguration configuration) _httpClient = new HttpClient { BaseAddress = new Uri(configuration.DeviceUrl), - + Timeout = TimeSpan.FromMilliseconds(configuration.ClientTimeoutMs) }; _commandUrl = $"cgi-bin/fpmate.cgi?timeout={configuration.ServerTimeoutMs}"; } diff --git a/scu-it/src/fiskaltrust.Middleware.SCU.IT.EpsonRTPrinter/Models/LastEmittedDocStatus.cs b/scu-it/src/fiskaltrust.Middleware.SCU.IT.EpsonRTPrinter/Models/LastEmittedDocStatus.cs new file mode 100644 index 000000000..24dc6f3b4 --- /dev/null +++ b/scu-it/src/fiskaltrust.Middleware.SCU.IT.EpsonRTPrinter/Models/LastEmittedDocStatus.cs @@ -0,0 +1,58 @@ +using System; +using System.Globalization; + +namespace fiskaltrust.Middleware.SCU.IT.EpsonRTPrinter.Models; + +public class LastEmittedDocStatus +{ + public long TotalDocAmountCents { get; set; } + public long TotalVatAmountCents { get; set; } + public DateTime DocumentDateTime { get; set; } + public long ZNumber { get; set; } + public long DocNumber { get; set; } + public string? PrinterSN { get; set; } + public bool IsFiscalDocument { get; set; } + + // Response includes OP (2 bytes) at the start, then fields per Communication Protocol v8.1, pag. 144 + // OP: 2 bytes [0-1] + // TOTAL DOC AMOUNT: 9 bytes [2-10] + // TOTAL VAT AMOUNT: 9 bytes [11-19] + // DATE (DDMMYY): 6 bytes [20-25] + // TIME (HHMMSS): 6 bytes [26-31] + // Z NUM: 4 bytes [32-35] + // DOC NUM: 4 bytes [36-39] + // PRINTER SN: 11 bytes [40-50] + // INST LOTT: 1 byte [51] + // LOTT CODE: 8 bytes [52-59] + // FISC/NON FISC: 1 byte [60] + // UUID: 40 bytes [61-100] (E-Receipt only) + // PDF NAME: 60 bytes [101-160] (E-Receipt only) + // SPARE: 32 bytes [161-192] + public static LastEmittedDocStatus? Parse(string? responseData) + { + if (string.IsNullOrEmpty(responseData) || responseData.Length < 61) + { + return null; + } + + try + { + var docDate = DateTime.ParseExact(responseData.Substring(20, 6) + responseData.Substring(26, 6), "ddMMyyHHmmss", CultureInfo.InvariantCulture); + + return new LastEmittedDocStatus + { + TotalDocAmountCents = long.TryParse(responseData.Substring(2, 9).Trim(), out var amount) ? amount : 0, + TotalVatAmountCents = long.TryParse(responseData.Substring(11, 9).Trim(), out var vat) ? vat : 0, + DocumentDateTime = docDate, + ZNumber = long.TryParse(responseData.Substring(32, 4).Trim(), out var zNum) ? zNum : 0, + DocNumber = long.TryParse(responseData.Substring(36, 4).Trim(), out var docNum) ? docNum : 0, + PrinterSN = responseData.Substring(40, 11).Trim(), + IsFiscalDocument = responseData[60] == '1' + }; + } + catch + { + return null; + } + } +} diff --git a/scu-it/src/fiskaltrust.Middleware.SCU.IT.EpsonRTPrinter/Models/PrinterCommand.cs b/scu-it/src/fiskaltrust.Middleware.SCU.IT.EpsonRTPrinter/Models/PrinterCommand.cs index bd85374f2..a8edcd250 100644 --- a/scu-it/src/fiskaltrust.Middleware.SCU.IT.EpsonRTPrinter/Models/PrinterCommand.cs +++ b/scu-it/src/fiskaltrust.Middleware.SCU.IT.EpsonRTPrinter/Models/PrinterCommand.cs @@ -29,6 +29,7 @@ public class DirectIO public string? Data { get; set; } public static DirectIO GetSerialNrCommand() => new() { Command = "3217", Data = "00" }; + public static DirectIO GetLastEmittedDocStatusCommand() => new() { Command = "1387", Data = "01" }; } [XmlType("response")]