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;
3using System.Text;
4using System.Text.RegularExpressions;
5using System.Threading.Tasks;
6using Waher.Content;
8using Waher.Events;
13
14namespace Paiwise.Internal
15{
20 {
21 private readonly ServiceField[] fields;
22 private readonly string method;
23 private readonly string url;
24 private readonly string optionsUrl;
25 private readonly string host;
26
42 public SellEDalerPaymentService(string Id, string Name, string IconUrl, int IconWidth,
43 int IconHeight, string TemplateContractId, string Method, string Url, string OptionsUrl,
44 ServiceField[] Fields, string Host, ISellEDalerServiceProvider Provider)
46 {
47 this.method = Method;
48 this.url = Url;
49 this.optionsUrl = OptionsUrl;
50 this.fields = Fields;
51 this.host = Host;
52
53 this.SellEDalerTemplateContractId = TemplateContractId;
54 this.SellEDalerServiceProvider = Provider;
55 }
56
60 public string SellEDalerTemplateContractId { get; }
61
66
73 {
74 return !CaseInsensitiveString.IsNullOrEmpty(Currency) &&
75 Currency.Length == 3 ? Grade.Ok : Grade.NotAtAll;
76 }
77
84 public async Task<bool> CanSellEDaler(CaseInsensitiveString AccountName)
85 {
86 if (string.Compare(this.method, "POST", true) != 0)
87 return false;
88
89 string Token = await RuntimeSettings.GetAsync(PaiwisePaymentServices.TokenIdSetting, string.Empty);
90
91 return !string.IsNullOrEmpty(Token);
92 }
93
94 private static readonly Regex PaiwiseProcessorRegex = new Regex(@"Waher\.Service\.IoTBroker\.Paiwise\.PaiwiseProcessor[.+]<?(SellEDaler|SellEDaler)>?\w*([.]\w*)?",
95 RegexOptions.Compiled | RegexOptions.Singleline);
96
97 private static readonly Regex UnitTestRegex = new Regex(@"Paiwise\.Internal\.Test\.PaiwiseTests[.+]<?(Test_07_ProcessSellEDalerUsingContract|Test_08_ProcessSellEDalerUsingClientUrl)>?\w*([.]\w*)?",
98 RegexOptions.Compiled | RegexOptions.Singleline);
99
100 private static readonly object[] approvedSources = new object[]
101 {
102 PaiwiseProcessorRegex,
103 UnitTestRegex
104 };
105
122 public async Task<PaymentResult> SellEDaler(IDictionary<CaseInsensitiveString, object> ContractParameters,
123 IDictionary<CaseInsensitiveString, CaseInsensitiveString> IdentityProperties,
124 decimal Amount, string Currency, string SuccessUrl, string FailureUrl, string CancelUrl,
125 EventHandlerAsync<ClientUrlEventArgs> 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, PersonalInformation.FirstNameTag, IdentityProperties, ref First);
180 Append(sb, PersonalInformation.MiddleNamesTag, IdentityProperties, ref First);
181 Append(sb, PersonalInformation.LastNamesTag, IdentityProperties, ref First);
182
183 if (First)
184 Append(sb, PersonalInformation.PersonalNumberTag, 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, PersonalInformation.AddressTag, 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 ContentResponse Result = await InternetContent.PostAsync(new Uri(this.url), Request, Gateway.Certificate,
213 new KeyValuePair<string, string>("Authorization", "Bearer " + Token),
214 new KeyValuePair<string, string>("Accept", JsonCodec.DefaultContentType));
215
216 if (Result.HasError)
217 {
218 Log.Error("Sending paiwise request to " + this.url + ", but an error was returned:" + Result.Error.Message);
219 return new PaymentResult(Result.Error.Message);
220 }
221
222 if (!(Result.Decoded is Dictionary<string, object> Response) ||
223 !Response.TryGetValue("status", out Obj) || !(Obj is string Status))
224 {
225 return new PaymentResult("Invalid response returned from Paiwise");
226 }
227
228 if (Status == "paid")
229 {
230 if (Response.TryGetValue("amount", out Obj) && IsDecimal(Obj, out decimal Amount2))
231 Amount = Amount2;
232
233 if (Response.TryGetValue("currency", out Obj) && Obj is string Currency2)
234 Currency = Currency2;
235
236 return new PaymentResult(Amount, Currency);
237 }
238
239 if (int.TryParse(Status, out _) &&
240 Response.TryGetValue("message", out Obj) && Obj is string Message)
241 {
242 return new PaymentResult(Message);
243 }
244
245 if (Status == "pending") // TODO: Callback, if domain available.
246 {
247 if (!Response.TryGetValue("id", out Obj) || !(Obj is string TransactionId))
248 return new PaymentResult("No transaction ID returned.");
249
250 if (Response.TryGetValue("redirectUrl", out Obj) && Obj is string RedirectUrl)
251 {
252 if (ClientUrlCallback is null)
253 return new PaymentResult("No Client URL callback method defined.");
254
255 await ClientUrlCallback.Raise(this, new ClientUrlEventArgs(RedirectUrl, State));
256 }
257
258 double TimeoutMinutes = await RuntimeSettings.GetAsync(PaiwisePaymentServices.TimeoutMinutesSetting, 5.0);
259
260 StringBuilder Url = new StringBuilder();
261
262 Url.Append("https://");
263 Url.Append(this.host);
264 Url.Append("/payment/retrieve");
265
266 Request = new Dictionary<string, object>()
267 {
268 { "id", TransactionId }
269 };
270
271 DateTime Start = DateTime.Now;
272
273 while (Status == "pending" && DateTime.Now.Subtract(Start).TotalMinutes < TimeoutMinutes)
274 {
275 await Task.Delay(2000);
276
277 Result = await InternetContent.PostAsync(new Uri(Url.ToString()), Request, Gateway.Certificate,
278 new KeyValuePair<string, string>("Authorization", "Bearer " + Token),
279 new KeyValuePair<string, string>("Accept", JsonCodec.DefaultContentType));
280
281 if (Result.HasError)
282 return new PaymentResult(Result.Error.Message);
283
284 if (!(Result.Decoded is Dictionary<string, object> Response2) ||
285 !Response2.TryGetValue("status", out Obj) || !(Obj is string Status2))
286 {
287 return new PaymentResult("Invalid polling response returned from Paiwise");
288 }
289
290 if (int.TryParse(Status2, out _) &&
291 Response2.TryGetValue("message", out Obj) && Obj is string Message2)
292 {
293 return new PaymentResult(Message2);
294 }
295
296 Status = Status2;
297
298 if (Status == "paid" &&
299 Response2.TryGetValue("request", out Obj) &&
300 Obj is Dictionary<string, object> Request2)
301 {
302 if (Request2.TryGetValue("amount", out Obj) && IsDecimal(Obj, out decimal Amount2))
303 Amount = Amount2;
304
305 if (Request2.TryGetValue("currency", out Obj) && Obj is string Currency2)
306 Currency = Currency2;
307 }
308 }
309
310 if (Status == "pending")
311 {
312 await Task.Delay(2000);
313
314 Url.Clear();
315 Url.Append("https://");
316 Url.Append(this.host);
317 Url.Append("/payment/cancel");
318
319 Result = await InternetContent.PostAsync(new Uri(Url.ToString()), Request, Gateway.Certificate,
320 new KeyValuePair<string, string>("Authorization", "Bearer " + Token),
321 new KeyValuePair<string, string>("Accept", JsonCodec.DefaultContentType));
322
323 if (Result.HasError)
324 return new PaymentResult(Result.Error.Message);
325
326 if (!(Result.Decoded is Dictionary<string, object> Response2) ||
327 !Response2.TryGetValue("status", out Obj) || !(Obj is string Status2))
328 {
329 return new PaymentResult("Invalid cancel response returned from Paiwise");
330 }
331
332 if (int.TryParse(Status2, out _) &&
333 Response2.TryGetValue("message", out Obj) && Obj is string Message2)
334 {
335 return new PaymentResult(Message2);
336 }
337
338 Status = Status2;
339 }
340 }
341
342 if (Status == "paid")
343 return new PaymentResult(Amount, Currency);
344 else if (Status == "cancelled")
345 return new PaymentResult("Payment has been cancelled.");
346 else
347 return new PaymentResult(Status);
348 }
349 catch (Exception ex)
350 {
351 return new PaymentResult(ex.Message);
352 }
353 }
354
355 private static void Append(StringBuilder sb, CaseInsensitiveString Key,
356 IDictionary<CaseInsensitiveString, CaseInsensitiveString> IdentityProperties, ref bool First)
357 {
358 if (IdentityProperties.TryGetValue(Key, out CaseInsensitiveString s))
359 {
360 if (First)
361 First = false;
362 else
363 sb.Append(' ');
364
365 sb.Append(s.Value);
366 }
367 }
368
369 private static bool IsDecimal(object Obj, out decimal Result)
370 {
371 if (Obj is int i)
372 {
373 Result = i;
374 return true;
375 }
376 else if (Obj is decimal d)
377 {
378 Result = d;
379 return true;
380 }
381 else if (Obj is double d2)
382 {
383 Result = (decimal)d2;
384 return true;
385 }
386 else
387 {
388 Result = 0;
389 return false;
390 }
391 }
392
405 public async Task<IDictionary<CaseInsensitiveString, object>[]> GetPaymentOptionsForSellingEDaler(
406 IDictionary<CaseInsensitiveString, CaseInsensitiveString> IdentityProperties,
407 string SuccessUrl, string FailureUrl, string CancelUrl,
408 EventHandlerAsync<ClientUrlEventArgs> ClientUrlCallback, object State)
409 {
410 try
411 {
412 if (string.IsNullOrEmpty(this.optionsUrl))
413 return Array.Empty<IDictionary<CaseInsensitiveString, object>>();
414
415 Dictionary<string, object> Request = new Dictionary<string, object>()
416 {
417 { "webhook", string.Empty }
418 };
419
420 foreach (ServiceField Field in this.fields)
421 {
422 if (IdentityProperties.TryGetValue(Field.FieldId, out CaseInsensitiveString Obj2))
423 Request[Field.FieldId] = Obj2.Value;
424 else
425 {
426 switch (Field.FieldId.LowerCase)
427 {
428 case "returnurl":
429 Request[Field.FieldId] = SuccessUrl ?? string.Empty;
430 break;
431
432 case "returnerrorurl":
433 Request[Field.FieldId] = FailureUrl ?? string.Empty;
434 break;
435
436 case "cancelurl":
437 Request[Field.FieldId] = CancelUrl ?? string.Empty;
438 break;
439
440 case "name":
441 StringBuilder sb = new StringBuilder();
442 bool First = true;
443
444 Append(sb, PersonalInformation.FirstNameTag, IdentityProperties, ref First);
445 Append(sb, PersonalInformation.MiddleNamesTag, IdentityProperties, ref First);
446 Append(sb, PersonalInformation.LastNamesTag, IdentityProperties, ref First);
447
448 if (First)
449 Append(sb, PersonalInformation.PersonalNumberTag, IdentityProperties, ref First);
450
451 Request[Field.FieldId] = sb.ToString();
452 break;
453
454 case "address":
455 sb = new StringBuilder();
456 First = true;
457
458 Append(sb, PersonalInformation.AddressTag, IdentityProperties, ref First);
459
460 Request[Field.FieldId] = sb.ToString();
461 break;
462
463 default:
464 Request[Field.FieldId] = null;
465 break;
466 }
467 }
468 }
469
470 string Token = await RuntimeSettings.GetAsync(PaiwisePaymentServices.TokenIdSetting, string.Empty);
471 if (string.IsNullOrEmpty(Token))
472 return Array.Empty<IDictionary<CaseInsensitiveString, object>>();
473
474 ContentResponse Result = await InternetContent.PostAsync(new Uri(this.optionsUrl), Request, Gateway.Certificate,
475 new KeyValuePair<string, string>("Authorization", "Bearer " + Token),
476 new KeyValuePair<string, string>("Accept", JsonCodec.DefaultContentType));
477
478 if (Result.HasError)
479 {
480 Log.Error("Requesting payment options for paiwise request to " + this.optionsUrl + ", but an error was returned:" + Result.Error.Message);
481 return Array.Empty<IDictionary<CaseInsensitiveString, object>>();
482 }
483
484 if (!(Result.Decoded is Array A))
485 throw new Exception("Unexpected response type returned: " + Result.GetType().FullName);
486
487 List<IDictionary<CaseInsensitiveString, object>> Options = new List<IDictionary<CaseInsensitiveString, object>>();
488
489 foreach (object Item in A)
490 {
491 if (Item is Dictionary<string, object> Option)
492 {
493 Dictionary<CaseInsensitiveString, object> Properties = new Dictionary<CaseInsensitiveString, object>();
494
495 foreach (KeyValuePair<string, object> P in Option)
496 Properties[P.Key] = P.Value;
497
498 Options.Add(Properties);
499 }
500 }
501
502 return Options.ToArray();
503 }
504 catch (Exception ex)
505 {
506 Log.Exception(ex,
507 new KeyValuePair<string, object>("ServiceProvider", this.SellEDalerServiceProvider.GetType().FullName),
508 new KeyValuePair<string, object>("ServiceId", this.Id),
509 new KeyValuePair<string, object>("URL", this.optionsUrl));
510
511 return Array.Empty<IDictionary<CaseInsensitiveString, object>>();
512 }
513 }
514
515 }
516}
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, EventHandlerAsync< ClientUrlEventArgs > ClientUrlCallback, object State)
Processes payment for selling eDaler.
async Task< IDictionary< CaseInsensitiveString, object >[]> GetPaymentOptionsForSellingEDaler(IDictionary< CaseInsensitiveString, CaseInsensitiveString > IdentityProperties, string SuccessUrl, string FailureUrl, string CancelUrl, EventHandlerAsync< ClientUrlEventArgs > ClientUrlCallback, object State)
Gets available payment options for selling eDaler.
Grade Supports(CaseInsensitiveString Currency)
Checks if the service provider supports a given currency.
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 personal information found in a legal identity.
const string MiddleNamesTag
MIDDLE
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.
Contains information about a response to a content request.
bool HasError
If an error occurred.
object Decoded
Decoded object.
Exception Error
Error response.
Static class managing encoding and decoding of internet content.
static Task< ContentResponse > PostAsync(Uri Uri, object Data, params KeyValuePair< string, string >[] Headers)
Posts to a resource, using a Uniform Resource Identifier (or Locator).
const string DefaultContentType
application/json
Definition: JsonCodec.cs:20
Static class managing the application event log. Applications and services log events on this static ...
Definition: Log.cs:14
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:1657
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:692
Static class managing the runtime environment of the IoT Gateway.
Definition: Gateway.cs:147
static X509Certificate2 Certificate
Domain certificate.
Definition: Gateway.cs:3082
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.
Definition: ImplTypes.g.cs:58
Grade
Grade enumeration
Definition: Grade.cs:7