Neuron®
The Neuron® is the basis for the creation of open and secure federated networks for smart societies.
Loading...
Searching...
No Matches
BuyEDalerPaymentService.cs
1using System;
3using System.Text;
4using System.Threading.Tasks;
5using Waher.Content;
7using Waher.Events;
12
14{
19 {
20 private readonly ServiceField[] fields;
21 private readonly string method;
22 private readonly string url;
23 private readonly string optionsUrl;
24 private readonly string host;
25
41 public BuyEDalerPaymentService(string Id, string Name, string IconUrl, int IconWidth,
42 int IconHeight, string TemplateContractId, string Method, string Url, string OptionsUrl,
43 ServiceField[] Fields, string Host, PaiwisePaymentServices Provider)
45 {
46 this.method = Method;
47 this.url = Url;
48 this.optionsUrl = OptionsUrl;
49 this.fields = Fields;
50 this.host = Host;
51
52 this.BuyEDalerTemplateContractId = TemplateContractId;
53 this.BuyEDalerServiceProvider = Provider;
54 this.PaymentServiceProvider = Provider;
55 }
56
60 public string BuyEDalerTemplateContractId { get; }
61
66
73 {
74 return !CaseInsensitiveString.IsNullOrEmpty(Currency) &&
75 Currency.Length == 3 ? Grade.Ok : Grade.NotAtAll;
76 }
77
84 public async Task<bool> CanBuyEDaler(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
110 public async Task<PaymentResult> BuyEDaler(IDictionary<CaseInsensitiveString, object> ContractParameters,
111 IDictionary<CaseInsensitiveString, CaseInsensitiveString> IdentityProperties,
112 decimal Amount, string Currency, string SuccessUrl, string FailureUrl, string CancelUrl,
113 EventHandlerAsync<ClientUrlEventArgs> ClientUrlCallback, object State)
114 {
115 try
116 {
117 if (Amount <= 0)
118 return new PaymentResult("Amount must be positive.");
119
120 if (string.IsNullOrEmpty(Currency) ||
121 Currency.Length != 3 ||
122 Currency.ToUpper() != Currency)
123 {
124 return new PaymentResult("Invalid currency.");
125 }
126
127 Dictionary<string, object> Request = new Dictionary<string, object>()
128 {
129 { "webhook", string.Empty }
130 };
131
132 foreach (ServiceField Field in this.fields)
133 {
134 if (ContractParameters.TryGetValue(Field.FieldId, out object Obj))
135 Request[Field.FieldId] = Obj;
136 else if (IdentityProperties.TryGetValue(Field.FieldId, out CaseInsensitiveString Obj2))
137 Request[Field.FieldId] = Obj2.Value;
138 else
139 {
140 switch (Field.FieldId.LowerCase)
141 {
142 case "returnurl":
143 Request[Field.FieldId] = SuccessUrl ?? string.Empty;
144 break;
145
146 case "returnerrorurl":
147 Request[Field.FieldId] = FailureUrl ?? string.Empty;
148 break;
149
150 case "cancelurl":
151 Request[Field.FieldId] = CancelUrl ?? string.Empty;
152 break;
153
154 case "amount":
155 Request[Field.FieldId] = Amount;
156 break;
157
158 case "currency":
159 Request[Field.FieldId] = Currency;
160 break;
161
162 default:
163 if (Field.Required)
164 return new PaymentResult("Missing Contract or ID property: " + Field.FieldId);
165 break;
166 }
167 }
168 }
169
170 return await this.ProcessRequest(Request, Amount, Currency, ClientUrlCallback, State);
171 }
172 catch (Exception ex)
173 {
174 return new PaymentResult(ex.Message);
175 }
176 }
177
178 private async Task<PaymentResult> ProcessRequest(Dictionary<string, object> Request, decimal Amount, string Currency,
179 EventHandlerAsync<ClientUrlEventArgs> ClientUrlCallback, object State)
180 {
181 // TODO: Proper callback
182
183 string Token = await RuntimeSettings.GetAsync(PaiwisePaymentServices.TokenIdSetting, string.Empty);
184 if (string.IsNullOrEmpty(Token))
185 return new PaymentResult("Paiwise token not configured");
186
187 ContentResponse Result = await InternetContent.PostAsync(new Uri(this.url), Request, Gateway.Certificate,
188 new KeyValuePair<string, string>("Authorization", "Bearer " + Token),
189 new KeyValuePair<string, string>("Accept", JsonCodec.DefaultContentType)); ;
190
191 if (Result.HasError)
192 {
193 Log.Error("Sending paiwise request to " + this.url + ", but an error was returned:" + Result.Error.Message);
194
195 return new PaymentResult(Result.Error.Message);
196 }
197
198 if (!(Result.Decoded is Dictionary<string, object> Response) ||
199 !Response.TryGetValue("status", out object Obj) || !(Obj is string Status))
200 {
201 return new PaymentResult("Invalid response returned from Paiwise");
202 }
203
204 if (Status == "paid")
205 {
206 if (Response.TryGetValue("amount", out Obj) && IsDecimal(Obj, out decimal Amount2))
207 Amount = Amount2;
208
209 if (Response.TryGetValue("currency", out Obj) && Obj is string Currency2)
210 Currency = Currency2;
211
212 return new PaymentResult(Amount, Currency);
213 }
214
215 if (int.TryParse(Status, out _) &&
216 Response.TryGetValue("message", out Obj) && Obj is string Message)
217 {
218 return new PaymentResult(Message);
219 }
220
221 if (Status == "pending") // TODO: Callback, if domain available.
222 {
223 if (!Response.TryGetValue("id", out Obj) || !(Obj is string TransactionId))
224 return new PaymentResult("No transaction ID returned.");
225
226 if (Response.TryGetValue("redirectUrl", out Obj) && Obj is string RedirectUrl)
227 {
228 if (ClientUrlCallback is null)
229 return new PaymentResult("No Client URL callback method defined.");
230
231 await ClientUrlCallback.Raise(this, new ClientUrlEventArgs(RedirectUrl, State));
232 }
233
234 double TimeoutMinutes = await RuntimeSettings.GetAsync(PaiwisePaymentServices.TimeoutMinutesSetting, 5.0);
235
236 StringBuilder Url = new StringBuilder();
237
238 Url.Append("https://");
239 Url.Append(this.host);
240 Url.Append("/payment/retrieve");
241
242 Request = new Dictionary<string, object>()
243 {
244 { "id", TransactionId }
245 };
246
247 DateTime Start = DateTime.Now;
248
249 while (Status == "pending" && DateTime.Now.Subtract(Start).TotalMinutes < TimeoutMinutes)
250 {
251 await Task.Delay(2000);
252
253 Result = await InternetContent.PostAsync(new Uri(Url.ToString()), Request, Gateway.Certificate,
254 new KeyValuePair<string, string>("Authorization", "Bearer " + Token),
255 new KeyValuePair<string, string>("Accept", JsonCodec.DefaultContentType));
256
257 if (Result.HasError)
258 return new PaymentResult(Result.Error.Message);
259
260 if (!(Result.Decoded is Dictionary<string, object> Response2) ||
261 !Response2.TryGetValue("status", out Obj) || !(Obj is string Status2))
262 {
263 return new PaymentResult("Invalid polling response returned from Paiwise");
264 }
265
266 if (int.TryParse(Status2, out _) &&
267 Response2.TryGetValue("message", out Obj) && Obj is string Message2)
268 {
269 return new PaymentResult(Message2);
270 }
271
272 Status = Status2;
273
274 if (Status == "paid" &&
275 Response2.TryGetValue("request", out Obj) &&
276 Obj is Dictionary<string, object> Request2)
277 {
278 if (Request2.TryGetValue("amount", out Obj) && IsDecimal(Obj, out decimal Amount2))
279 Amount = Amount2;
280
281 if (Request2.TryGetValue("currency", out Obj) && Obj is string Currency2)
282 Currency = Currency2;
283 }
284 }
285
286 if (Status == "pending")
287 {
288 await Task.Delay(2000);
289
290 Url.Clear();
291 Url.Append("https://");
292 Url.Append(this.host);
293 Url.Append("/payment/cancel");
294
295 Result = await InternetContent.PostAsync(new Uri(Url.ToString()), Request, Gateway.Certificate,
296 new KeyValuePair<string, string>("Authorization", "Bearer " + Token),
297 new KeyValuePair<string, string>("Accept", JsonCodec.DefaultContentType));
298
299 if (Result.HasError)
300 return new PaymentResult(Result.Error.Message);
301
302 if (!(Result.Decoded is Dictionary<string, object> Response2) ||
303 !Response2.TryGetValue("status", out Obj) || !(Obj is string Status2))
304 {
305 return new PaymentResult("Invalid cancel response returned from Paiwise");
306 }
307
308 if (int.TryParse(Status2, out _) &&
309 Response2.TryGetValue("message", out Obj) && Obj is string Message2)
310 {
311 return new PaymentResult(Message2);
312 }
313
314 Status = Status2;
315 }
316 }
317
318 if (Status == "paid")
319 return new PaymentResult(Amount, Currency);
320 else if (Status == "cancelled")
321 return new PaymentResult("Payment has been cancelled.");
322 else
323 return new PaymentResult(Status);
324 }
325
326 private static bool IsDecimal(object Obj, out decimal Result)
327 {
328 if (Obj is int i)
329 {
330 Result = i;
331 return true;
332 }
333 else if (Obj is decimal d)
334 {
335 Result = d;
336 return true;
337 }
338 else if (Obj is double d2)
339 {
340 Result = (decimal)d2;
341 return true;
342 }
343 else
344 {
345 Result = 0;
346 return false;
347 }
348 }
349
362 public async Task<IDictionary<CaseInsensitiveString, object>[]> GetPaymentOptionsForBuyingEDaler(
363 IDictionary<CaseInsensitiveString, CaseInsensitiveString> IdentityProperties,
364 string SuccessUrl, string FailureUrl, string CancelUrl,
365 EventHandlerAsync<ClientUrlEventArgs> ClientUrlCallback, object State)
366 {
367 try
368 {
369 if (string.IsNullOrEmpty(this.optionsUrl))
370 return Array.Empty<IDictionary<CaseInsensitiveString, object>>();
371
372 Dictionary<string, object> Request = new Dictionary<string, object>()
373 {
374 { "webhook", string.Empty }
375 };
376
377 foreach (ServiceField Field in this.fields)
378 {
379 if (IdentityProperties.TryGetValue(Field.FieldId, out CaseInsensitiveString Obj2))
380 Request[Field.FieldId] = Obj2.Value;
381 else
382 {
383 Request[Field.FieldId] = Field.FieldId.LowerCase switch
384 {
385 "returnurl" => SuccessUrl ?? string.Empty,
386 "returnerrorurl" => FailureUrl ?? string.Empty,
387 "cancelurl" => CancelUrl ?? string.Empty,
388 _ => null,
389 };
390 }
391 }
392
393 string Token = await RuntimeSettings.GetAsync(PaiwisePaymentServices.TokenIdSetting, string.Empty);
394 if (string.IsNullOrEmpty(Token))
395 return Array.Empty<IDictionary<CaseInsensitiveString, object>>();
396
397 ContentResponse Result = await InternetContent.PostAsync(new Uri(this.optionsUrl), Request, Gateway.Certificate,
398 new KeyValuePair<string, string>("Authorization", "Bearer " + Token),
399 new KeyValuePair<string, string>("Accept", JsonCodec.DefaultContentType)); ;
400
401 if (Result.HasError)
402 {
403 Log.Error("Requesting payment options for paiwise request to " + this.optionsUrl + ", but an error was returned:" + Result.Error.Message);
404 return Array.Empty<IDictionary<CaseInsensitiveString, object>>();
405 }
406
407 if (!(Result.Decoded is Array A))
408 throw new Exception("Unexpected response type returned: " + Result.GetType().FullName);
409
410 List<IDictionary<CaseInsensitiveString, object>> Options = new List<IDictionary<CaseInsensitiveString, object>>();
411
412 foreach (object Item in A)
413 {
414 if (Item is Dictionary<string, object> Option)
415 {
416 Dictionary<CaseInsensitiveString, object> Properties = new Dictionary<CaseInsensitiveString, object>();
417
418 foreach (KeyValuePair<string, object> P in Option)
419 Properties[P.Key] = P.Value;
420
421 Options.Add(Properties);
422 }
423 }
424
425 return Options.ToArray();
426 }
427 catch (Exception ex)
428 {
429 Log.Exception(ex,
430 new KeyValuePair<string, object>("ServiceProvider", this.BuyEDalerServiceProvider.GetType().FullName),
431 new KeyValuePair<string, object>("ServiceId", this.Id),
432 new KeyValuePair<string, object>("URL", this.optionsUrl));
433
434 return Array.Empty<IDictionary<CaseInsensitiveString, object>>();
435 }
436 }
437
442 {
443 get
444 {
445 if (!string.IsNullOrEmpty(this.BuyEDalerTemplateContractId))
446 return false;
447
448 foreach (ServiceField Field in this.fields)
449 {
450 switch (Field.FieldId.LowerCase)
451 {
452 case "returnurl":
453 case "returnerrorurl":
454 case "cancelurl":
455 case "amount":
456 case "currency":
457 break;
458
459 default:
460 if (Field.Required)
461 return false;
462 break;
463 }
464 }
465
466 return true;
467 }
468 }
469
474
487 public async Task<PaymentResult> Pay(decimal Amount, string Currency, string Description, string SuccessUrl, string FailureUrl, string CancelUrl,
488 EventHandlerAsync<ClientUrlEventArgs> ClientUrlCallback, object State)
489 {
490 try
491 {
492 if (Amount <= 0)
493 return new PaymentResult("Amount must be positive.");
494
495 if (string.IsNullOrEmpty(Currency) ||
496 Currency.Length != 3 ||
497 Currency.ToUpper() != Currency)
498 {
499 return new PaymentResult("Invalid currency.");
500 }
501
502 Dictionary<string, object> Request = new Dictionary<string, object>()
503 {
504 { "webhook", string.Empty }
505 };
506
507 foreach (ServiceField Field in this.fields)
508 {
509 switch (Field.FieldId.LowerCase)
510 {
511 case "returnurl":
512 Request[Field.FieldId] = SuccessUrl ?? string.Empty;
513 break;
514
515 case "returnerrorurl":
516 Request[Field.FieldId] = FailureUrl ?? string.Empty;
517 break;
518
519 case "cancelurl":
520 Request[Field.FieldId] = CancelUrl ?? string.Empty;
521 break;
522
523 case "amount":
524 Request[Field.FieldId] = Amount;
525 break;
526
527 case "currency":
528 Request[Field.FieldId] = Currency;
529 break;
530
531 default:
532 if (Field.Required)
533 return new PaymentResult("Missing Contract or ID property: " + Field.FieldId);
534 break;
535 }
536 }
537
538 return await this.ProcessRequest(Request, Amount, Currency, ClientUrlCallback, State);
539 }
540 catch (Exception ex)
541 {
542 return new PaymentResult(ex.Message);
543 }
544 }
545 }
546}
Contains information about a service provider that users can use to buy eDaler.
Reference to a Paiwise payment service.
string BuyEDalerTemplateContractId
Optional Contract ID of Template, for buying e-Daler
async Task< PaymentResult > Pay(decimal Amount, string Currency, string Description, string SuccessUrl, string FailureUrl, string CancelUrl, EventHandlerAsync< ClientUrlEventArgs > ClientUrlCallback, object State)
Processes a payment.
Grade Supports(CaseInsensitiveString Currency)
Checks if the service provider supports a given currency.
async Task< PaymentResult > BuyEDaler(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 buying eDaler.
IPaymentServiceProvider PaymentServiceProvider
Reference to service provider.
async Task< bool > CanBuyEDaler(CaseInsensitiveString AccountName)
If the service provider can be used to process a request to buy eDaler of a certain amount,...
bool CanBeUsedForPayment
If service can be used for payments.
async Task< IDictionary< CaseInsensitiveString, object >[]> GetPaymentOptionsForBuyingEDaler(IDictionary< CaseInsensitiveString, CaseInsensitiveString > IdentityProperties, string SuccessUrl, string FailureUrl, string CancelUrl, EventHandlerAsync< ClientUrlEventArgs > ClientUrlCallback, object State)
Gets available payment options for buying eDaler.
BuyEDalerPaymentService(string Id, string Name, string IconUrl, int IconWidth, int IconHeight, string TemplateContractId, string Method, string Url, string OptionsUrl, ServiceField[] Fields, string Host, PaiwisePaymentServices Provider)
Reference to a Paiwise payment service.
Payment services made available by Paiwise.
const string TokenIdSetting
Settings key for Paiwise token.
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.
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 buy eDaler.
Interface for information about a service provider that users can use to buy eDaler.
Interface for information about a service provider that users can use to pay for services.
Interface for information about a service provider that users can use to pay for services.
Definition: ImplTypes.g.cs:58
Grade
Grade enumeration
Definition: Grade.cs:7