using System; using System.Collections.Generic; using System.IO; 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 { class ClientApp { private const int ErrorExit = 1; private const int SuccessExit = 0; private const string ConfigFilename = "client.config.xml"; private const int DefaultPort = 1869; private const string DefaultIpAddress = "127.0.0.1"; private const bool DefaultSaveReceipt = false; private static Client client; private static int Main() { try { var settings = new ConfigFileParser(GetAbsolutePath(ConfigFilename)); string tid = settings.TerminalId; string apiKey = settings.ApiKey; string posId = settings.PosId; string address = settings.ConnectAddress ?? DefaultIpAddress; string sslHost = settings.SslHostName; bool saveReceipt = settings.SaveReceipt ?? DefaultSaveReceipt; int port = DefaultPort; if (!string.IsNullOrEmpty(settings.ConnectionPort)) { if (!int.TryParse(settings.ConnectionPort, out port)) { Console.WriteLine("Invalid port number in config."); return ErrorExit; } } string identifier = !string.IsNullOrEmpty(tid) ? tid : !string.IsNullOrEmpty(posId) ? posId : null; if (identifier == null || (string.IsNullOrEmpty(apiKey) && string.IsNullOrEmpty(tid))) { Console.WriteLine("Missing valid credentials."); return ErrorExit; } Console.WriteLine($"Starting ChipDNAClient with ID={identifier}, Address={address}:{port}"); client = new Client(identifier, address, port, sslHost, saveReceipt, settings); StartHttpServer(); return SuccessExit; } catch (Exception ex) { Console.WriteLine("Startup failed: " + ex.Message); return ErrorExit; } } private static void StartHttpServer() { HttpListener listener = new HttpListener(); listener.Prefixes.Add("http://127.0.0.1:18181/start-transaction/"); listener.Start(); 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; 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 { context.Response.StatusCode = 500; 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) { var path = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); return Path.Combine(path ?? "", fileName); } private static string EscapeForJson(string input) { return input?.Replace("\\", "\\\\") .Replace("\"", "\\\"") .Replace("\n", "\\n") .Replace("\r", "\\r"); } private static string SerializeToJson(Dictionary dict) { var sb = new StringBuilder(); sb.Append("{"); foreach (var kv in dict) { sb.Append($"\"{EscapeForJson(kv.Key)}\":\"{EscapeForJson(kv.Value)}\","); } if (sb.Length > 1) sb.Length--; // remove last comma sb.Append("}"); 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 { [XmlElement("Entry")] public List Items { get; set; } public SerializableKeyValueList() { } public SerializableKeyValueList(Dictionary dict) { Items = new List(); foreach (var kv in dict) { Items.Add(new Entry { Key = kv.Key, Value = kv.Value }); } } } public class Entry { [XmlElement("Key")] public string Key { get; set; } [XmlElement("Value")] 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; } } }