From b4dcc35a8baaf9d3c6fbf003c97e84a08e4d2bcc Mon Sep 17 00:00:00 2001 From: yurii Date: Sat, 30 Aug 2025 19:42:07 +0100 Subject: [PATCH] added functionality to commit transactions --- Client.cs | 41 ++++------ ClientApp.cs | 207 ++++++++++++++++++++++++++++++++++++++++++--------- 2 files changed, 183 insertions(+), 65 deletions(-) diff --git a/Client.cs b/Client.cs index bcc3773..b6682e6 100644 --- a/Client.cs +++ b/Client.cs @@ -246,11 +246,11 @@ namespace Creditcall.ChipDna.Client if (response.GetValue(ParameterKeys.Errors, out errors) && !string.IsNullOrEmpty(errors)) { var result = new Dictionary - { - { "ERROR", errors }, - { "REFERENCE", reference }, - { "TRANSACTION_RESULT", "ERROR" } - }; + { + { ParameterKeys.Errors, errors }, + { ParameterKeys.Reference, reference }, + { ParameterKeys.TransactionResult, "ERROR" } + }; if (TransactionCompletionSource != null && !TransactionCompletionSource.Task.IsCompleted) { @@ -318,7 +318,7 @@ namespace Creditcall.ChipDna.Client PerformStartTransaction("250", ""); break; case "C": - PerformConfirmTransaction(); + PerformConfirmTransaction("", out TransactionConfirmation confirmation); break; case "V": PerformVoidTransaction(); @@ -635,37 +635,19 @@ namespace Creditcall.ChipDna.Client } } - public void PerformConfirmTransaction() + public void PerformConfirmTransaction(string reference, out TransactionConfirmation confirmation) { - var reference = GetReference(true, ""); - var amount = GetAmount(false); - var gratuity = GetGratuity(false); - var closeTransaction = GetCloseTransaction(false); + confirmation = new TransactionConfirmation(); var parameters = new ParameterSet(); - if (!string.IsNullOrEmpty(reference)) - { - parameters.Add(ParameterKeys.Reference, reference); - } - if (!String.IsNullOrEmpty(amount)) - { - parameters.Add(ParameterKeys.Amount, amount); - } - if (!String.IsNullOrEmpty(gratuity)) - { - parameters.Add(ParameterKeys.Gratuity, gratuity); - } - if (!String.IsNullOrEmpty(closeTransaction)) - { - parameters.Add(ParameterKeys.CloseTransaction, closeTransaction); - } - + parameters.Add(ParameterKeys.Reference, reference); parameters.Add(GetExtraParams("ConfirmTransaction")); var response = chipDnaClientLib.ConfirmTransaction(parameters); string receipt, result; if (response.GetValue(ParameterKeys.TransactionResult, out result)) { Console.WriteLine("Confirmed Transaction Result: {0}", result); + confirmation.Result = result; } if (response.GetValue(ParameterKeys.ReceiptDataMerchant, out receipt)) { @@ -674,17 +656,20 @@ namespace Creditcall.ChipDna.Client if (response.GetValue(ParameterKeys.ReceiptDataCardholder, out receipt)) { PrintReceiptData(ReceiptData.GetReceiptDataFromXml(receipt)); + confirmation.ReceiptDataCardholder = receipt; } string errors; if (response.GetValue(ParameterKeys.Errors, out errors)) { Console.WriteLine(ErrorsString("ConfirmTransaction Errors", errors)); + confirmation.Errors = errors; } string errorDescription; if (response.GetValue(ParameterKeys.ErrorDescription, out errorDescription)) { Console.WriteLine("ErrorDescription: {0}", errorDescription); + confirmation.ErrorDescription = errorDescription; } } diff --git a/ClientApp.cs b/ClientApp.cs index d7de866..99f5cc5 100644 --- a/ClientApp.cs +++ b/ClientApp.cs @@ -5,7 +5,9 @@ using System.Net; using System.Reflection; using System.Text; using System.Threading.Tasks; +using System.Xml; using System.Xml.Serialization; +using System.Threading; namespace Creditcall.ChipDna.Client { @@ -72,56 +74,154 @@ namespace Creditcall.ChipDna.Client listener.Prefixes.Add("http://127.0.0.1:18181/start-transaction/"); listener.Start(); - Console.WriteLine("Listening on http://127.0.0.1:18181/start-transaction/"); + Console.WriteLine("Listening on:"); + Console.WriteLine(" http://127.0.0.1:18181/start-transaction/"); while (true) { HttpListenerContext context = listener.GetContext(); HttpListenerRequest request = context.Request; + string rawUrl = request.Url.AbsolutePath; - if (request.HttpMethod == "POST") + try { + if (request.HttpMethod != "POST") + { + context.Response.StatusCode = 405; + context.Response.Close(); + continue; + } + + if (rawUrl.StartsWith("/start-transaction", StringComparison.OrdinalIgnoreCase)) + { + HandleStartTransaction(context); + } + else + { + context.Response.StatusCode = 404; + context.Response.Close(); + } + } + catch (Exception ex) + { + Console.WriteLine("Unhandled server error: " + ex.Message); try { - var serializer = new XmlSerializer(typeof(TransactionPayload)); - TransactionPayload payload = (TransactionPayload)serializer.Deserialize(request.InputStream); - - Console.WriteLine($"Received transaction request: Amount={payload.amount}, Type={payload.transactionType}"); - - client.TransactionCompletionSource = new TaskCompletionSource>(); - - client.PerformStartTransaction(payload.amount, payload.transactionType); - - // Wait synchronously for result - Dictionary transactionResult = client.TransactionCompletionSource.Task.Result; - - var responseSerializer = new XmlSerializer(typeof(SerializableKeyValueList)); - context.Response.ContentType = "application/xml"; - - var wrappedResult = new SerializableKeyValueList(transactionResult); - responseSerializer.Serialize(context.Response.OutputStream, wrappedResult); - } - catch (Exception ex) - { - Console.WriteLine("Error handling request: " + ex.Message); context.Response.StatusCode = 500; + context.Response.Close(); } - finally - { - // Remove this line if you're already writing to the stream above - // Otherwise keep it if no other usage of OutputStream occurs - context.Response.OutputStream.Close(); - } - - } - else - { - context.Response.StatusCode = 405; - context.Response.Close(); + catch { } } } } + private static void HandleStartTransaction(HttpListenerContext context) + { + const int transactionTimeoutSeconds = 300; + + try + { + var serializer = new XmlSerializer(typeof(TransactionPayload)); + TransactionPayload payload = (TransactionPayload)serializer.Deserialize(context.Request.InputStream); + + Console.WriteLine($"Received start transaction: Amount={payload.amount}, Type={payload.transactionType}"); + + client.TransactionCompletionSource = new TaskCompletionSource>(); + + // start transaction (non-blocking) + client.PerformStartTransaction(payload.amount, payload.transactionType); + + // Wait for completion with timeout + var task = client.TransactionCompletionSource.Task; + if (!task.Wait(TimeSpan.FromSeconds(transactionTimeoutSeconds))) + { + // timeout + var errorMap = new Dictionary + { + { ParameterKeys.TransactionResult, "TIMEOUT" }, + { ParameterKeys.Errors, "Transaction timed out waiting for device response" } + }; + + var wrappedTimeout = new SerializableKeyValueList(errorMap); + context.Response.ContentType = "application/xml"; + new XmlSerializer(typeof(SerializableKeyValueList)).Serialize(context.Response.OutputStream, wrappedTimeout); + return; + } + + // Task completed — handle possible exception + Dictionary transactionResult; + try + { + transactionResult = task.Result; // safe now, Wait returned + } + catch (AggregateException agg) + { + var first = agg.Flatten().InnerExceptions[0]; + var errMap = new Dictionary + { + { ParameterKeys.TransactionResult, "ERROR" }, + { ParameterKeys.Errors, $"Transaction task faulted: {first.Message}" } + }; + var wrapped = new SerializableKeyValueList(errMap); + context.Response.ContentType = "application/xml"; + new XmlSerializer(typeof(SerializableKeyValueList)).Serialize(context.Response.OutputStream, wrapped); + return; + } + + // If it's a SALE and approved, call ConfirmTransaction and attach its XML + transactionResult.TryGetValue(ParameterKeys.TransactionType, out string txType); + transactionResult.TryGetValue(ParameterKeys.TransactionResult, out string txResult); + transactionResult.TryGetValue(ParameterKeys.Reference, out string txReference); + + if (!string.IsNullOrEmpty(txType) && txType.Equals("SALE", StringComparison.OrdinalIgnoreCase) && + !string.IsNullOrEmpty(txResult) && txResult.Equals("APPROVED", StringComparison.OrdinalIgnoreCase) && + !string.IsNullOrEmpty(txReference)) + { + try + { + TransactionConfirmation confirmation; + client.PerformConfirmTransaction(txReference, out confirmation); + + // Keep original fields, but also add confirmation-prefixed fields + transactionResult["CONFIRM_RESULT"] = confirmation?.Result ?? ""; + transactionResult["CONFIRM_ERRORS"] = confirmation?.Errors ?? ""; + if (!string.IsNullOrEmpty(confirmation.ErrorDescription)) + { + transactionResult["CONFIRM_ERRORS"] = $"{confirmation.Errors} {confirmation.ErrorDescription}"; + } + if (!string.IsNullOrEmpty(confirmation?.ReceiptDataCardholder)) + { + transactionResult[ParameterKeys.ReceiptDataCardholder] = confirmation.ReceiptDataCardholder; + } + + // XML (omit declaration so it can be embedded) + var xml = SerializeToXml(confirmation, omitXmlDeclaration: true); + transactionResult["TRANSACTION_CONFIRMATION"] = xml; + } + catch (Exception ex) + { + // preserve original transaction result but add an error key + transactionResult["CONFIRM_RESULT"] = "ERROR"; + transactionResult["CONFIRM_ERRORS"] = $"ConfirmTransaction failed: {ex.Message}"; + } + } + + // Return the final key/value list as before + var responseSerializer = new XmlSerializer(typeof(SerializableKeyValueList)); + context.Response.ContentType = "application/xml"; + var wrappedResult = new SerializableKeyValueList(transactionResult); + responseSerializer.Serialize(context.Response.OutputStream, wrappedResult); + } + catch (Exception ex) + { + Console.WriteLine("Error in start-transaction: " + ex.Message); + context.Response.StatusCode = 500; + } + finally + { + context.Response.OutputStream.Close(); + } + } private static string GetAbsolutePath(string fileName) { @@ -154,8 +254,29 @@ namespace Creditcall.ChipDna.Client return sb.ToString(); } - } + // helper to serialize an object to an XML string (no namespaces) + private static string SerializeToXml(T obj, bool omitXmlDeclaration = true) + { + if (obj == null) return string.Empty; + var xs = new XmlSerializer(typeof(T)); + var ns = new XmlSerializerNamespaces(); + ns.Add(string.Empty, string.Empty); // remove xsi/xsd namespaces + var settings = new XmlWriterSettings + { + OmitXmlDeclaration = omitXmlDeclaration, + Indent = false, + Encoding = Encoding.UTF8 + }; + + using (var sw = new StringWriter()) + using (var xw = XmlWriter.Create(sw, settings)) + { + xs.Serialize(xw, obj, ns); + return sw.ToString(); + } + } + } [XmlRoot("TransactionResult")] public class SerializableKeyValueList @@ -184,11 +305,23 @@ namespace Creditcall.ChipDna.Client public string Value { get; set; } } - [XmlRoot("TransactionPayload")] public class TransactionPayload { public string amount { get; set; } public string transactionType { get; set; } + + // For confirm transaction + public string transactionReference { get; set; } + } + + [XmlRoot("TransactionConfirmation")] + public class TransactionConfirmation + { + public string Result { get; set; } + public string Errors { get; set; } + public string ErrorDescription { get; set; } + public string ReceiptDataCardholder { get; set; } + } }