Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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<EpsonRTPrinterSCU> logger, EpsonRTPrinterSCUConfiguration configuration, IEpsonFpMateClient epsonCloudHttpClient)
{
Expand Down Expand Up @@ -199,6 +200,7 @@ private async Task<FiscalReceiptResponse> SetReceiptResponse(PrinterResponse? re

public async Task<ReceiptResponse> PerformProtocolReceiptAsync(ReceiptRequest receiptRequest, ReceiptResponse receiptResponse)
{
string? data = null;
try
{
var content = EpsonCommandFactory.CreateInvoiceRequestContent(_configuration, receiptRequest);
Expand Down Expand Up @@ -229,7 +231,7 @@ public async Task<ReceiptResponse> 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);

Expand All @@ -254,9 +256,10 @@ public async Task<ReceiptResponse> 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"))
{
Expand All @@ -265,6 +268,11 @@ public async Task<ReceiptResponse> 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);
Expand All @@ -275,12 +283,14 @@ public async Task<ReceiptResponse> PerformProtocolReceiptAsync(ReceiptRequest re

public async Task<ReceiptResponse> 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<PrinterReceiptResponse>(responseContent);
if (result != null)
Expand All @@ -302,15 +312,21 @@ public async Task<ReceiptResponse> 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);
Expand All @@ -320,6 +336,133 @@ public async Task<ReceiptResponse> PerformClassicReceiptAsync(ReceiptRequest rec
}
}

private async Task<ReceiptResponse> 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<ReceiptResponse> 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<PrinterReceiptResponse>(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<LastEmittedDocStatus?> 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<PrinterCommandResponse>(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<ProcessResponse> ProcessUnspecifiedProtocolReceipt(ProcessRequest request)
{
try
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ public class EpsonRTPrinterSCUConfiguration
/// </summary>
public int ServerTimeoutMs { get; set; } = 10000;

/// <summary>
/// The maximum number of retries when a network error occurs during receipt printing
/// </summary>
public int MaxNetworkRetries { get; set; } = 3;

public string? Password { get; set; }

public string? AdditionalTrailerLines { get; set;}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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}";
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
}
Comment thread
accodev marked this conversation as resolved.
}
Original file line number Diff line number Diff line change
Expand Up @@ -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")]
Expand Down
Loading