using System; using System.Collections.Generic; using System.Collections.Concurrent; using System.IO; using System.Threading; using System.Threading.Tasks; using Newtonsoft.Json; namespace Creditcall.ChipDna.Client { // This is the original start/wait mechanism shared by the XML and NDJSON adapters. internal static class TransactionExecution { internal static Dictionary Run(Action start, Func>> completion, Action suppress, TimeSpan timeout) { try { start(); var task = completion(); // Task.Wait intentionally retains its existing exception behavior. if (!task.Wait(timeout)) { suppress(); return new Dictionary { { ParameterKeys.TransactionResult, "TIMEOUT" }, { ParameterKeys.Errors, "Transaction timed out waiting for device response" } }; } try { return task.Result; } catch (AggregateException agg) { var first = agg.Flatten().InnerExceptions[0]; return new Dictionary { { ParameterKeys.TransactionResult, "ERROR" }, { ParameterKeys.Errors, $"Transaction task faulted: {first.Message}" } }; } } catch { suppress(); throw; } } } internal sealed class TransactionProgressTap { private readonly object gate = new object(); private readonly HashSet references = new HashSet(StringComparer.Ordinal); private Action listener; private string reference; private bool suppressed; internal Dictionary Observe(Action callback, Func> execute) { lock (gate) { listener += callback; } try { return execute(); } finally { lock (gate) { listener -= callback; } } } internal void Begin(string value) { lock (gate) { if (suppressed) return; // The existing reference generation can repeat within one second. // A repeated reference cannot prove callback ownership. if (reference != null || string.IsNullOrEmpty(value) || !references.Add(value)) suppressed = true; reference = value; } } internal void End() { lock (gate) { reference = null; } } internal void Suppress() { lock (gate) { suppressed = true; reference = null; } } internal void SuppressIfActive() { lock (gate) { if (reference == null) return; suppressed = true; reference = null; } } internal void StartFailed(string errors) { lock (gate) { if (suppressed) return; // SDK 3.17 StartCommand returns this exact error before SendRequest. // No other error category proves that dispatch never happened. if (errors == ClientErrorCodes.ClientNotConnectedToServer.ToString()) { if (reference != null) references.Remove(reference); reference = null; return; } suppressed = true; reference = null; } } internal void Report(string source, EventParameters parameters) { lock (gate) { if (suppressed || reference == null || parameters == null) return; string eventReference; parameters.GetValue(ParameterKeys.Reference, out eventReference); bool hasReference = !string.IsNullOrEmpty(eventReference); if (hasReference && eventReference != reference) return; string value; switch (source) { case "UPDATE": if (!parameters.GetValue(ParameterKeys.Update, out value)) return; break; case "CARD_STATUS": if (!parameters.GetValue(ParameterKeys.Notification, out value)) return; break; case "SIGNATURE": value = "Requested"; break; case "PAUSE": value = "Paused"; break; default: return; } if (!TransactionStreamFields.IsProgress(source, value)) return; // Normal SDK 3.17 progress has no reference. The serial active window // provides best-effort UI ownership, not a financial decision: a queued // callback, including removal, may briefly show stale progress in a later window. // The listener only attempts a bounded, nonblocking enqueue. listener?.Invoke(source, value); } } } internal sealed class TransactionStreamWriter { private readonly object gate = new object(); private readonly ConcurrentQueue progress = new ConcurrentQueue(); private readonly AutoResetEvent available = new AutoResetEvent(false); private readonly Stream output; private string terminal; private bool completed; private bool disconnected; private bool delivering; internal TransactionStreamWriter(Stream output) { this.output = output; } internal void Progress(string source, string value) { if (!TransactionStreamFields.IsProgress(source, value)) return; lock (gate) { if (completed || disconnected || progress.Count >= 128) return; progress.Enqueue(JsonConvert.SerializeObject(new { type = "status", source, value })); available.Set(); } } internal void Complete(object frame) { lock (gate) { if (completed || disconnected) return; completed = true; terminal = JsonConvert.SerializeObject(frame); available.Set(); } } internal void Deliver() { lock (gate) { if (delivering) return; delivering = true; } try { // One writer owns all progress and terminal writes. using (var writer = new StreamWriter(output, new System.Text.UTF8Encoding(false), 1024, true)) { while (true) { string line; bool isTerminal = false; lock (gate) { if (!progress.TryDequeue(out line) && completed) { line = terminal; isTerminal = true; } } if (line == null) { available.WaitOne(); continue; } writer.WriteLine(line); writer.Flush(); if (isTerminal) return; } } } catch (Exception ex) when (ex is IOException || ex is System.Net.HttpListenerException || ex is ObjectDisposedException) { lock (gate) { disconnected = true; } } finally { lock (gate) { disconnected = true; available.Dispose(); } } } } internal static class TransactionStreamFields { internal static bool IsProgress(string source, string value) { switch (source) { case "UPDATE": switch (value) { case "CardRequested": case "ProvideCard": case "InsertOrSwipeCard": case "InsertOrContactless": case "InsertCard": case "SwipeCard": case "CardRemovalRequested": case "CardRemovalEnforced": case "PinEntryStarted": case "PinEntryPrompt": case "PinEntryInProgress": case "PinEntryFailed": case "PinEntrySuccessful": case "TransactionStarted": case "OnlineAuthRequested": case "OnlineAuthorizeRequest": case "OnlineAuthCompleted": return true; } return false; case "CARD_STATUS": return value == "Inserted" || value == "Tapped" || value == "Swiped"; case "SIGNATURE": return value == "Requested"; case "PAUSE": return value == "Paused"; default: return false; } } internal static Dictionary Required(Dictionary fields) { var result = new Dictionary(); // Receipts remain local to hardlink; no raw parameter bag reaches the kiosk. foreach (var key in new[] { "TRANSACTION_RESULT", "ERRORS", "ERROR_DESCRIPTION", "REFERENCE", "PAN_MASKED", "CARD_SCHEME_ID", "EXPIRY_DATE", "CARD_HASH", "CARD_REFERENCE", "RECEIPT_DATA_CARDHOLDER" }) { string value; if (fields.TryGetValue(key, out value)) result[key] = key == "PAN_MASKED" ? MaskedCardNumber(value) : value; } return result; } internal static string MaskedCardNumber(string value) { int digits = 0, masks = 0; foreach (char ch in value ?? "") { if (ch >= '0' && ch <= '9') digits++; else if (ch == '*' || ch == 'x' || ch == 'X') masks++; else if (ch != ' ' && ch != '-') return ""; } return masks >= 4 && digits <= 10 ? value : ""; } } }