Neuron®
The Neuron® is the basis for the creation of open and secure federated networks for smart societies.
Loading...
Searching...
No Matches
SellEDalerPaymentService.cs
1using System;
2using System.Collections.Generic;
3using System.Text;
4using System.Text.RegularExpressions;
5using System.Threading.Tasks;
6using Waher.Content;
8using Waher.Events;
14
15namespace Paiwise.Internal
16{
21 {
22 private readonly ServiceField[] fields;
23 private readonly string method;
24 private readonly string url;
25 private readonly string optionsUrl;
26 private readonly string host;
27
43 public SellEDalerPaymentService(string Id, string Name, string IconUrl, int IconWidth,
44 int IconHeight, string TemplateContractId, string Method, string Url, string OptionsUrl,
45 ServiceField[] Fields, string Host, ISellEDalerServiceProvider Provider)
47 {
48 this.method = Method;
49 this.url = Url;
50 this.optionsUrl = OptionsUrl;
51 this.fields = Fields;
52 this.host = Host;
53
54 this.SellEDalerTemplateContractId = TemplateContractId;
55 this.SellEDalerServiceProvider = Provider;
56 }
57
61 public string SellEDalerTemplateContractId { get; }
62
67
74 {
75 return !CaseInsensitiveString.IsNullOrEmpty(Currency) &&
76 Currency.Length == 3 ? Grade.Ok : Grade.NotAtAll;
77 }
78
85 public async Task<bool> CanSellEDaler(CaseInsensitiveString AccountName)
86 {
87 if (string.Compare(this.method, "POST", true) != 0)
88 return false;
89
90 string Token = await RuntimeSettings.GetAsync(PaiwisePaymentServices.TokenIdSetting, string.Empty);
91
92 return !string.IsNullOrEmpty(Token);
93 }
94
95 private static readonly Regex PaiwiseProcessorRegex = new Regex(@"Waher\.Service\.IoTBroker\.Paiwise\.PaiwiseProcessor[.+]<(SellEDaler|SellEDaler)>\w*([.]\w*)?",
96 RegexOptions.Compiled | RegexOptions.Singleline);
97
98 private static readonly Regex UnitTestRegex = new Regex(@"Paiwise\.Internal\.Test\.PaiwiseTests[.+]<(Test_07_ProcessSellEDalerUsingContract|Test_08_ProcessSellEDalerUsingClientUrl)>\w*([.]\w*)?",
99 RegexOptions.Compiled | RegexOptions.Singleline);
100
101 private static readonly object[] approvedSources = new object[]
102 {
103 PaiwiseProcessorRegex,
104 UnitTestRegex
105 };
106
123 public async Task<PaymentResult> SellEDaler(IDictionary<CaseInsensitiveString, object> ContractParameters,
124 IDictionary<CaseInsensitiveString, CaseInsensitiveString> IdentityProperties,
125 decimal Amount, string Currency, string SuccessUrl, string FailureUrl, string CancelUrl, ClientUrlEventHandler ClientUrlCallback, object State)
126 {
127 try
128 {
129 if (Amount <= 0)
130 return new PaymentResult("Amount must be positive.");
131
132 if (string.IsNullOrEmpty(Currency) ||
133 Currency.Length != 3 ||
134 Currency.ToUpper() != Currency)
135 {
136 return new PaymentResult("Invalid currency.");
137 }
138
139 object Obj;
140 Dictionary<string, object> Request = new Dictionary<string, object>()
141 {
142 { "webhook", string.Empty }
143 };
144
145 foreach (ServiceField Field in this.fields)
146 {
147 if (ContractParameters.TryGetValue(Field.FieldId, out Obj))
148 Request[Field.FieldId] = Obj;
149 else if (IdentityProperties.TryGetValue(Field.FieldId, out CaseInsensitiveString Obj2))
150 Request[Field.FieldId] = Obj2.Value;
151 else
152 {
153 switch (Field.FieldId.LowerCase)
154 {
155 case "returnurl":
156 Request[Field.FieldId] = SuccessUrl;
157 break;
158
159 case "returnerrorurl":
160 Request[Field.FieldId] = FailureUrl;
161 break;
162
163 case "cancelurl":
164 Request[Field.FieldId] = CancelUrl;
165 break;
166
167 case "amount":
168 Request[Field.FieldId] = Amount;
169 break;
170
171 case "currency":
172 Request[Field.FieldId] = Currency;
173 break;
174
175 case "name":
176 StringBuilder sb = new StringBuilder();
177 bool First = true;
178
179 Append(sb, "FIRST", IdentityProperties, ref First);
180 Append(sb, "MIDDLE", IdentityProperties, ref First);
181 Append(sb, "LAST", IdentityProperties, ref First);
182
183 if (First)
184 Append(sb, "PNR", IdentityProperties, ref First);
185
186 Request[Field.FieldId] = sb.ToString();
187 break;
188
189 case "address":
190 sb = new StringBuilder();
191 First = true;
192
193 Append(sb, "ADDR", IdentityProperties, ref First);
194
195 Request[Field.FieldId] = sb.ToString();
196 break;
197
198 default:
199 if (Field.Required)
200 Request[Field.FieldId] = string.Empty;
201 break;
202 }
203 }
204 }
205
206 // TODO: Proper callback
207
208 string Token = await RuntimeSettings.GetAsync(PaiwisePaymentServices.TokenIdSetting, string.Empty);
209 if (string.IsNullOrEmpty(Token))
210 return new PaymentResult("Paiwise token not configured");
211
212 object Result;
213
214 try
215 {
216 Result = await InternetContent.PostAsync(new Uri(this.url), Request, Gateway.Certificate,
217 new KeyValuePair<string, string>("Authorization", "Bearer " + Token),
218 new KeyValuePair<string, string>("Accept", JsonCodec.DefaultContentType));
219 }
220 catch (Exception ex)
221 {
222 Log.Error("Sending paiwise request to " + this.url + ", but an error was returned:" + ex.Message +
223 "\r\n\r\n" + JSON.Encode(Request, true), new KeyValuePair<string, object>("Token", "Token")); // TODO: Remove
224
225 return new PaymentResult(ex.Message);
226 }
227
228 if (!(Result is Dictionary<string, object> Response) ||
229 !Response.TryGetValue("status", out Obj) || !(Obj is string Status))
230 {
231 return new PaymentResult("Invalid response returned from Paiwise");
232 }
233
234 if (Status == "paid")
235 {
236 if (Response.TryGetValue("amount", out Obj) && IsDecimal(Obj, out decimal Amount2))
237 Amount = Amount2;
238
239 if (Response.TryGetValue("currency", out Obj) && Obj is string Currency2)
240 Currency = Currency2;
241
242 return new PaymentResult(Amount, Currency);
243 }
244
245 if (int.TryParse(Status, out _) &&
246 Response.TryGetValue("message", out Obj) && Obj is string Message)
247 {
248 return new PaymentResult(Message);
249 }
250
251 if (Status == "pending") // TODO: Callback, if domain available.
252 {
253 if (!Response.TryGetValue("id", out Obj) || !(Obj is string TransactionId))
254 return new PaymentResult("No transaction ID returned.");
255
256 if (Response.TryGetValue("redirectUrl", out Obj) && Obj is string RedirectUrl)
257 {
258 if (ClientUrlCallback is null)
259 return new PaymentResult("No Client URL callback method defined.");
260
261 try
262 {
263 await ClientUrlCallback(this, new ClientUrlEventArgs(RedirectUrl, State));
264 }
265 catch (Exception ex)
266 {
267 return new PaymentResult(ex.Message);
268 }
269 }
270
271 double TimeoutMinutes = await RuntimeSettings.GetAsync(PaiwisePaymentServices.TimeoutMinutesSetting, 5.0);
272
273 StringBuilder Url = new StringBuilder();
274
275 Url.Append("https://");
276 Url.Append(this.host);
277 Url.Append("/payment/retrieve");
278
279 Request = new Dictionary<string, object>()
280 {
281 { "id", TransactionId }
282 };
283
284 DateTime Start = DateTime.Now;
285
286 while (Status == "pending" && DateTime.Now.Subtract(Start).TotalMinutes < TimeoutMinutes)
287 {
288 await Task.Delay(2000);
289
290 Obj = await InternetContent.PostAsync(new Uri(Url.ToString()), Request, Gateway.Certificate,
291 new KeyValuePair<string, string>("Authorization", "Bearer " + Token),
292 new KeyValuePair<string, string>("Accept", JsonCodec.DefaultContentType));
293
294 if (!(Obj is Dictionary<string, object> Response2) ||
295 !Response2.TryGetValue("status", out Obj) || !(Obj is string Status2))
296 {
297 return new PaymentResult("Invalid polling response returned from Paiwise");
298 }
299
300 if (int.TryParse(Status2, out _) &&
301 Response2.TryGetValue("message", out Obj) && Obj is string Message2)
302 {
303 return new PaymentResult(Message2);
304 }
305
306 Status = Status2;
307
308 if (Status == "paid" &&
309 Response2.TryGetValue("request", out Obj) &&
310 Obj is Dictionary<string, object> Request2)
311 {
312 if (Request2.TryGetValue("amount", out Obj) && IsDecimal(Obj, out decimal Amount2))
313 Amount = Amount2;
314
315 if (Request2.TryGetValue("currency", out Obj) && Obj is string Currency2)
316 Currency = Currency2;
317 }
318 }
319
320 if (Status == "pending")
321 {
322 await Task.Delay(2000);
323
324 Url.Clear();
325 Url.Append("https://");
326 Url.Append(this.host);
327 Url.Append("/payment/cancel");
328
329 Obj = await InternetContent.PostAsync(new Uri(Url.ToString()), Request, Gateway.Certificate,
330 new KeyValuePair<string, string>("Authorization", "Bearer " + Token),
331 new KeyValuePair<string, string>("Accept", JsonCodec.DefaultContentType));
332
333 if (!(Obj is Dictionary<string, object> Response2) ||
334 !Response2.TryGetValue("status", out Obj) || !(Obj is string Status2))
335 {
336 return new PaymentResult("Invalid cancel response returned from Paiwise");
337 }
338
339 if (int.TryParse(Status2, out _) &&
340 Response2.TryGetValue("message", out Obj) && Obj is string Message2)
341 {
342 return new PaymentResult(Message2);
343 }
344
345 Status = Status2;
346 }
347 }
348
349 if (Status == "paid")
350 return new PaymentResult(Amount, Currency);
351 else if (Status == "cancelled")
352 return new PaymentResult("Payment has been cancelled.");
353 else
354 return new PaymentResult(Status);
355 }
356 catch (Exception ex)
357 {
358 return new PaymentResult(ex.Message);
359 }
360 }
361
362 private static void Append(StringBuilder sb, CaseInsensitiveString Key,
363 IDictionary<CaseInsensitiveString, CaseInsensitiveString> IdentityProperties, ref bool First)
364 {
365 if (IdentityProperties.TryGetValue(Key, out CaseInsensitiveString s))
366 {
367 if (First)
368 First = false;
369 else
370 sb.Append(' ');
371
372 sb.Append(s.Value);
373 }
374 }
375
376 private static bool IsDecimal(object Obj, out decimal Result)
377 {
378 if (Obj is int i)
379 {
380 Result = i;
381 return true;
382 }
383 else if (Obj is decimal d)
384 {
385 Result = d;
386 return true;
387 }
388 else if (Obj is double d2)
389 {
390 Result = (decimal)d2;
391 return true;
392 }
393 else
394 {
395 Result = 0;
396 return false;
397 }
398 }
399
412 public async Task<IDictionary<CaseInsensitiveString, object>[]> GetPaymentOptionsForSellingEDaler(
413 IDictionary<CaseInsensitiveString, CaseInsensitiveString> IdentityProperties,
414 string SuccessUrl, string FailureUrl, string CancelUrl,
415 ClientUrlEventHandler ClientUrlCallback, object State)
416 {
417 try
418 {
419 if (string.IsNullOrEmpty(this.optionsUrl))
420 return new IDictionary<CaseInsensitiveString, object>[0];
421
422 Dictionary<string, object> Request = new Dictionary<string, object>()
423 {
424 { "webhook", string.Empty }
425 };
426
427 foreach (ServiceField Field in this.fields)
428 {
429 if (IdentityProperties.TryGetValue(Field.FieldId, out CaseInsensitiveString Obj2))
430 Request[Field.FieldId] = Obj2.Value;
431 else
432 {
433 switch (Field.FieldId.LowerCase)
434 {
435 case "returnurl":
436 Request[Field.FieldId] = SuccessUrl ?? string.Empty;
437 break;
438
439 case "returnerrorurl":
440 Request[Field.FieldId] = FailureUrl ?? string.Empty;
441 break;
442
443 case "cancelurl":
444 Request[Field.FieldId] = CancelUrl ?? string.Empty;
445 break;
446
447 case "name":
448 StringBuilder sb = new StringBuilder();
449 bool First = true;
450
451 Append(sb, "FIRST", IdentityProperties, ref First);
452 Append(sb, "MIDDLE", IdentityProperties, ref First);
453 Append(sb, "LAST", IdentityProperties, ref First);
454
455 if (First)
456 Append(sb, "PNR", IdentityProperties, ref First);
457
458 Request[Field.FieldId] = sb.ToString();
459 break;
460
461 case "address":
462 sb = new StringBuilder();
463 First = true;
464
465 Append(sb, "ADDR", IdentityProperties, ref First);
466
467 Request[Field.FieldId] = sb.ToString();
468 break;
469
470 default:
471 Request[Field.FieldId] = null;
472 break;
473 }
474 }
475 }
476
477 string Token = await RuntimeSettings.GetAsync(PaiwisePaymentServices.TokenIdSetting, string.Empty);
478 if (string.IsNullOrEmpty(Token))
479 return new IDictionary<CaseInsensitiveString, object>[0];
480
481 object Result;
482
483 try
484 {
485 Result = await InternetContent.PostAsync(new Uri(this.optionsUrl), Request, Gateway.Certificate,
486 new KeyValuePair<string, string>("Authorization", "Bearer " + Token),
487 new KeyValuePair<string, string>("Accept", JsonCodec.DefaultContentType));
488 }
489 catch (Exception ex)
490 {
491 Log.Error("Requesting payment options for paiwise request to " + this.optionsUrl + ", but an error was returned:" + ex.Message +
492 "\r\n\r\n" + JSON.Encode(Request, true), new KeyValuePair<string, object>("Token", "Token")); // TODO: Remove
493
494 return new IDictionary<CaseInsensitiveString, object>[0];
495 }
496
497 if (!(Result is Array A))
498 throw new Exception("Unexpected response type returned: " + Result.GetType().FullName);
499
500 List<IDictionary<CaseInsensitiveString, object>> Options = new List<IDictionary<CaseInsensitiveString, object>>();
501
502 foreach (object Item in A)
503 {
504 if (Item is Dictionary<string, object> Option)
505 {
506 Dictionary<CaseInsensitiveString, object> Properties = new Dictionary<CaseInsensitiveString, object>();
507
508 foreach (KeyValuePair<string, object> P in Option)
509 Properties[P.Key] = P.Value;
510
511 Options.Add(Properties);
512 }
513 }
514
515 return Options.ToArray();
516 }
517 catch (Exception ex)
518 {
519 Log.Exception(ex,
520 new KeyValuePair<string, object>("ServiceProvider", this.SellEDalerServiceProvider.GetType().FullName),
521 new KeyValuePair<string, object>("ServiceId", this.Id),
522 new KeyValuePair<string, object>("URL", this.optionsUrl));
523
524 return new IDictionary<CaseInsensitiveString, object>[0];
525 }
526 }
527
528 }
529}
Event arguments for callback methods with the aim of pushing a URL to a client.
Payment services made available by Paiwise.
const string TimeoutMinutesSetting
Settings key for Paiwise timeout, in minutes.
const string TokenIdSetting
Settings key for Paiwise token.
Reference to a Paiwise payment service.
SellEDalerPaymentService(string Id, string Name, string IconUrl, int IconWidth, int IconHeight, string TemplateContractId, string Method, string Url, string OptionsUrl, ServiceField[] Fields, string Host, ISellEDalerServiceProvider Provider)
Reference to a Paiwise payment service.
string SellEDalerTemplateContractId
Optional Contract ID of Template, for selling e-Daler
async Task< bool > CanSellEDaler(CaseInsensitiveString AccountName)
If the service provider can be used to process a request to sell eDaler of a certain amount,...
async Task< PaymentResult > SellEDaler(IDictionary< CaseInsensitiveString, object > ContractParameters, IDictionary< CaseInsensitiveString, CaseInsensitiveString > IdentityProperties, decimal Amount, string Currency, string SuccessUrl, string FailureUrl, string CancelUrl, ClientUrlEventHandler ClientUrlCallback, object State)
Processes payment for selling eDaler.
Grade Supports(CaseInsensitiveString Currency)
Checks if the service provider supports a given currency.
async Task< IDictionary< CaseInsensitiveString, object >[]> GetPaymentOptionsForSellingEDaler(IDictionary< CaseInsensitiveString, CaseInsensitiveString > IdentityProperties, string SuccessUrl, string FailureUrl, string CancelUrl, ClientUrlEventHandler ClientUrlCallback, object State)
Gets available payment options for selling eDaler.
Represents a field in a service.
Definition: ServiceField.cs:9
bool Required
If the field is required or not.
Definition: ServiceField.cs:36
CaseInsensitiveString FieldId
Field ID
Definition: ServiceField.cs:26
Result of request payment.
Definition: PaymentResult.cs:7
Contains information about a service provider that users can use to sell eDaler.
Contains information about a service provider.
string IconUrl
Optional URL to icon of service provider.
string Id
ID of service provider.
int IconHeight
Height of icon, if available.
int IconWidth
Width of icon, if available.
string Name
Displayable name of service provider.
Static class managing encoding and decoding of internet content.
static Task< object > PostAsync(Uri Uri, object Data, params KeyValuePair< string, string >[] Headers)
Posts to a resource, using a Uniform Resource Identifier (or Locator).
Helps with common JSON-related tasks.
Definition: JSON.cs:14
static string Encode(string s)
Encodes a string for inclusion in JSON.
Definition: JSON.cs:507
const string DefaultContentType
application/json
Definition: JsonCodec.cs:19
Static class managing the application event log. Applications and services log events on this static ...
Definition: Log.cs:13
static void Exception(Exception Exception, string Object, string Actor, string EventId, EventLevel Level, string Facility, string Module, params KeyValuePair< string, object >[] Tags)
Logs an exception. Event type will be determined by the severity of the exception.
Definition: Log.cs:1647
static void Error(string Message, string Object, string Actor, string EventId, EventLevel Level, string Facility, string Module, string StackTrace, params KeyValuePair< string, object >[] Tags)
Logs an error event.
Definition: Log.cs:682
Static class managing the runtime environment of the IoT Gateway.
Definition: Gateway.cs:126
static X509Certificate2 Certificate
Domain certificate.
Definition: Gateway.cs:2349
Represents a case-insensitive string.
string LowerCase
Lower-case representation of the case-insensitive string.
static bool IsNullOrEmpty(CaseInsensitiveString value)
Indicates whether the specified string is null or an CaseInsensitiveString.Empty string.
Static class managing persistent settings.
static async Task< string > GetAsync(string Key, string DefaultValue)
Gets a string-valued setting.
Interface for information about a service provider that users can use to sell eDaler.
Interface for information about a service provider that users can use to sell eDaler.
delegate Task ClientUrlEventHandler(object Sender, ClientUrlEventArgs e)
Delegat for client URL callback methods.
Grade
Grade enumeration
Definition: Grade.cs:7