270 lines
10 KiB
C#
270 lines
10 KiB
C#
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<string, string> Run(Action start,
|
|
Func<Task<Dictionary<string, string>>> 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<string, string>
|
|
{
|
|
{ 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<string, string>
|
|
{
|
|
{ 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<string> references = new HashSet<string>(StringComparer.Ordinal);
|
|
private Action<string, string> listener;
|
|
private string reference;
|
|
private bool suppressed;
|
|
|
|
internal Dictionary<string, string> Observe(Action<string, string> callback, Func<Dictionary<string, string>> 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<string> progress = new ConcurrentQueue<string>();
|
|
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<string, string> Required(Dictionary<string, string> fields)
|
|
{
|
|
var result = new Dictionary<string, string>();
|
|
// 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 : "";
|
|
}
|
|
}
|
|
}
|