Neuron®
The Neuron® is the basis for the creation of open and secure federated networks for smart societies.
Loading...
Searching...
No Matches
KycService.cs
1using System;
3using System.Collections.ObjectModel;
4using System.Linq;
5using System.IO;
6using System.Reflection;
7using System.Text;
8using System.Threading;
9using System.Threading.Tasks;
10using System.Threading.Channels;
24using SkiaSharp;
26using System.Diagnostics;
28using System.Xml;
29using Microsoft.Maui.Storage;
32
34{
39 [Singleton]
40 public class KycService : IKycService, IDisposable
41 {
42 private static readonly string backupKyc = "TestKYCNeuro.xml";
43 private const string KycTemplateNodeId = "NeuroAccessKyc";
44 private const int DefaultTemplatePageSize = 20;
45
46 private readonly System.Collections.Concurrent.ConcurrentDictionary<string, AsyncLock> referenceLocks = new();
47 private readonly Channel<string> autosaveChannel;
48 private readonly System.Collections.Concurrent.ConcurrentDictionary<string, AutosaveEntry> pendingAutosave = new();
49 private readonly CancellationTokenSource autosaveCts = new();
50 private readonly Task autosaveWorkerTask;
51
52 // Snapshot metrics (exposed for future diagnostics)
53 private long snapshotsPersisted = 0;
54 private long snapshotsSkipped = 0;
55
56 private sealed class AutosaveEntry
57 {
58 public AutosaveEntry(KycReference reference, KycReferenceSnapshot snapshot)
59 {
60 this.Reference = reference;
61 this.Snapshot = snapshot;
62 }
63 public KycReference Reference { get; }
64 public KycReferenceSnapshot Snapshot { get; }
65 }
66
67 private bool disposedValue;
68 public event EventHandler<ApplicationReviewEventArgs>? ApplicationReviewUpdated;
69
74 public KycService()
75 {
76 this.autosaveChannel = Channel.CreateUnbounded<string>(new UnboundedChannelOptions
77 {
78 SingleReader = true,
79 SingleWriter = false,
80 AllowSynchronousContinuations = false
81 });
82 this.autosaveWorkerTask = Task.Run(this.AutosaveWorkerLoop);
83 }
84
88 private async Task AutosaveWorkerLoop()
89 {
90 ChannelReader<string> Reader = this.autosaveChannel.Reader;
91 while (await Reader.WaitToReadAsync(this.autosaveCts.Token).ConfigureAwait(false))
92 {
93 while (Reader.TryRead(out string? Key))
94 {
95 if (this.autosaveCts.IsCancellationRequested)
96 return;
97 if (!this.pendingAutosave.TryRemove(Key, out AutosaveEntry? Entry))
98 continue; // Coalesced away
99 try
100 {
101 await this.SaveSnapshotAsync(Entry.Reference, Entry.Snapshot, false).ConfigureAwait(false);
102 }
103 catch (OperationCanceledException)
104 {
105 return;
106 }
107 catch (Exception Ex)
108 {
109 ServiceRef.LogService.LogException(Ex, new KeyValuePair<string, object?>("Operation", "KYC.AutosaveWorker"));
110 }
111 }
112 }
113 }
114
115 #region Loading & Persistence
116
123 public async Task<KycReference> LoadKycReferenceAsync(string? Lang = null, KycApplicationTemplate? Template = null)
124 {
125 KycReference? LoadedReference;
126 try
127 {
128 LoadedReference = await ServiceRef.StorageService.FindFirstDeleteRest<KycReference>();
129 }
130 catch (Exception FindEx)
131 {
132 ServiceRef.LogService.LogException(FindEx, this.GetClassAndMethod(MethodBase.GetCurrentMethod()));
133 LoadedReference = null;
134 }
135
136 if (LoadedReference is null || string.IsNullOrEmpty(LoadedReference.ObjectId))
137 {
138 LoadedReference = new KycReference
139 {
140 CreatedUtc = DateTime.UtcNow
141 };
142 try
143 {
144 await ServiceRef.StorageService.Insert(LoadedReference);
145 }
146 catch (Exception Ex)
147 {
148 ServiceRef.LogService.LogException(Ex, this.GetClassAndMethod(MethodBase.GetCurrentMethod()));
149 }
150 }
151
152 KycApplicationTemplate? TemplateToApply = Template;
153 if (TemplateToApply is null && string.IsNullOrEmpty(LoadedReference.KycXml))
154 {
155 try
156 {
157 KycApplicationPage TemplatePage = await this.LoadKycApplicationsPageAsync(null, null, null, 1, Lang).ConfigureAwait(false);
158 TemplateToApply = TemplatePage.Templates.FirstOrDefault();
159 }
160 catch (Exception Ex)
161 {
162 ServiceRef.LogService.LogException(Ex, this.GetClassAndMethod(MethodBase.GetCurrentMethod()));
163 }
164 }
165
166 if (TemplateToApply is not null)
167 {
168 try
169 {
170 await this.ApplyTemplateToReferenceAsync(LoadedReference, TemplateToApply, Lang).ConfigureAwait(false);
171 }
172 catch (Exception Ex)
173 {
174 ServiceRef.LogService.LogException(Ex, this.GetClassAndMethod(MethodBase.GetCurrentMethod()));
175 }
176 }
177
178 if (string.IsNullOrEmpty(LoadedReference.KycXml))
179 {
180 try
181 {
182 await this.ApplyBundledTemplateAsync(LoadedReference, Lang).ConfigureAwait(false);
183 }
184 catch (Exception Ex)
185 {
186 ServiceRef.LogService.LogException(Ex, this.GetClassAndMethod(MethodBase.GetCurrentMethod()));
187 }
188 }
189
190 // Localize friendly name if available
191 try
192 {
193 KycProcess? Process = await LoadedReference.GetProcess(Lang).ConfigureAwait(false);
194 if (Process?.Name is not null)
195 {
196 string NewName = Process.Name.Text;
197 if (!string.Equals(LoadedReference.FriendlyName, NewName, StringComparison.Ordinal))
198 {
199 LoadedReference.FriendlyName = NewName;
200 try { await ServiceRef.StorageService.Update(LoadedReference); } catch { }
201 }
202 }
203 }
204 catch (Exception Ex)
205 {
206 ServiceRef.LogService.LogException(Ex, this.GetClassAndMethod(MethodBase.GetCurrentMethod()));
207 }
208
209 return LoadedReference;
210 }
211
216 public async Task SaveKycReferenceAsync(KycReference Reference)
217 {
218 try
219 {
220 if (string.IsNullOrEmpty(Reference.ObjectId))
221 await ServiceRef.StorageService.Insert(Reference);
222 else
223 await ServiceRef.StorageService.Update(Reference);
224 }
225 catch (KeyNotFoundException)
226 {
227 await ServiceRef.StorageService.Insert(Reference);
228 }
229 }
230
236 public async Task<IReadOnlyList<KycReference>> LoadAvailableKycReferencesAsync(string? Lang = null)
237 {
238 try
239 {
240 KycApplicationPage Page = await this.LoadKycApplicationsPageAsync(null, null, null, null, Lang).ConfigureAwait(false);
241 List<KycReference> References = new List<KycReference>();
242 foreach (KycApplicationTemplate Template in Page.Templates)
243 {
244 References.Add(Template.Reference);
245 }
246 return References;
247 }
248 catch (Exception Ex)
249 {
250 ServiceRef.LogService.LogException(Ex, this.GetClassAndMethod(MethodBase.GetCurrentMethod()));
251 return Array.Empty<KycReference>();
252 }
253 }
254
255 public async Task<KycApplicationPage> LoadKycApplicationsPageAsync(string? After = null, string? Before = null, int? Index = null, int? Max = null, string? Lang = null, CancellationToken CancellationToken = default)
256 {
257 int PageSize = Max.HasValue && Max.Value > 0 ? Max.Value : DefaultTemplatePageSize;
258 string Domain = ServiceRef.TagProfile.Domain ?? string.Empty;
259 string? ServiceAddress = ServiceRef.TagProfile.PubSubJid;
260
261 bool IsInitialRequest = After is null && Before is null && (!Index.HasValue || Index.Value == 0);
262
263 try
264 {
265 PubSubPageResult? PageResult = await ServiceRef.XmppService.GetItemsPageAsync(KycTemplateNodeId, ServiceAddress, After, Before, Index, PageSize).ConfigureAwait(false);
266 if (PageResult is null && !string.IsNullOrWhiteSpace(ServiceAddress))
267 {
268 PageResult = await ServiceRef.XmppService.GetItemsPageAsync(KycTemplateNodeId, null, After, Before, Index, PageSize).ConfigureAwait(false);
269 }
270 if (PageResult is null)
271 {
272 if (IsInitialRequest)
273 {
274 IReadOnlyList<KycApplicationTemplate> FallbackTemplates = await this.LoadFallbackTemplatesAsync(Lang, CancellationToken).ConfigureAwait(false);
275 return new KycApplicationPage(FallbackTemplates, null, true);
276 }
277 // Subsequent page but could not load -> return empty page, no fallback.
278 return new KycApplicationPage(Array.Empty<KycApplicationTemplate>(), null, false);
279 }
280
281 IReadOnlyList<KycApplicationTemplate> Templates = await this.ConvertToTemplatesAsync(PageResult.Items, Domain, Lang, CancellationToken).ConfigureAwait(false);
282 if (Templates.Count == 0)
283 {
284 if (IsInitialRequest)
285 {
286 IReadOnlyList<KycApplicationTemplate> FallbackTemplates = await this.LoadFallbackTemplatesAsync(Lang, CancellationToken).ConfigureAwait(false);
287 return new KycApplicationPage(FallbackTemplates, PageResult.ResultPage, true);
288 }
289 // No more remote items -> return empty page without fallback.
290 return new KycApplicationPage(Array.Empty<KycApplicationTemplate>(), PageResult.ResultPage, false);
291 }
292
293 return new KycApplicationPage(Templates, PageResult.ResultPage, false);
294 }
295 catch (OperationCanceledException)
296 {
297 throw;
298 }
299 catch (Exception Ex)
300 {
301 ServiceRef.LogService.LogException(Ex, this.GetClassAndMethod(MethodBase.GetCurrentMethod()));
302 if (IsInitialRequest)
303 {
304 IReadOnlyList<KycApplicationTemplate> FallbackTemplates = await this.LoadFallbackTemplatesAsync(Lang, CancellationToken).ConfigureAwait(false);
305 return new KycApplicationPage(FallbackTemplates, null, true);
306 }
307 return new KycApplicationPage(Array.Empty<KycApplicationTemplate>(), null, false);
308 }
309 }
310
311 private async Task<IReadOnlyList<KycApplicationTemplate>> ConvertToTemplatesAsync(PubSubItem[]? Items, string Domain, string? Lang, CancellationToken CancellationToken)
312 {
313 List<KycApplicationTemplate> Templates = new List<KycApplicationTemplate>();
314 if (Items is null || Items.Length == 0)
315 return Templates;
316
317 foreach (PubSubItem Item in Items)
318 {
319 CancellationToken.ThrowIfCancellationRequested();
320
321 if (!KycApplicationItem.TryCreate(Item, out KycApplicationItem? Application) || Application is null)
322 continue;
323
324 try
325 {
326 KycApplicationTemplate? Template = await this.CreateTemplateAsync(Application, Application.ProcessXml, Domain, Lang, CancellationToken).ConfigureAwait(false);
327 if (Template is not null)
328 Templates.Add(Template);
329 }
330 catch (OperationCanceledException)
331 {
332 throw;
333 }
334 catch (Exception Ex)
335 {
336 ServiceRef.LogService.LogException(Ex, this.GetClassAndMethod(MethodBase.GetCurrentMethod()));
337 }
338 }
339
340 return Templates;
341 }
342
343 private async Task<KycApplicationTemplate?> CreateTemplateAsync(KycApplicationItem? Application, string Xml, string Domain, string? Lang, CancellationToken CancellationToken)
344 {
345 CancellationToken.ThrowIfCancellationRequested();
346 bool IsValid = await this.ValidateKycXmlAsync(string.IsNullOrWhiteSpace(Domain) ? "unknown" : Domain, Xml).ConfigureAwait(false);
347 if (!IsValid)
348 return null;
349
350 CancellationToken.ThrowIfCancellationRequested();
351 KycProcess Process = await KycProcessParser.LoadProcessAsync(Xml, Lang).ConfigureAwait(false);
352 string? FriendlyName = Process.Name?.PrimaryText;
353 if (string.IsNullOrWhiteSpace(FriendlyName) && Application is not null)
354 FriendlyName = Application.PrimaryDisplayName ?? Application.DisplayName;
355 if (string.IsNullOrWhiteSpace(FriendlyName) && Application is not null)
356 FriendlyName = Application.ItemId;
357 if (string.IsNullOrWhiteSpace(FriendlyName))
358 FriendlyName = "KYC Application";
359
360 KycReference Reference = KycReference.FromProcess(Process, Xml, FriendlyName);
361 return new KycApplicationTemplate(Reference, Application);
362 }
363
364 private async Task<IReadOnlyList<KycApplicationTemplate>> LoadFallbackTemplatesAsync(string? Lang, CancellationToken CancellationToken)
365 {
366 List<KycApplicationTemplate> Templates = new List<KycApplicationTemplate>();
367 try
368 {
369 string Xml = await this.LoadBundledKycXmlAsync(CancellationToken).ConfigureAwait(false);
370 KycApplicationTemplate? Template = await this.CreateTemplateAsync(null, Xml, "fallback", Lang, CancellationToken).ConfigureAwait(false);
371 if (Template is not null)
372 Templates.Add(Template);
373 }
374 catch (OperationCanceledException)
375 {
376 throw;
377 }
378 catch (Exception Ex)
379 {
380 ServiceRef.LogService.LogException(Ex, this.GetClassAndMethod(MethodBase.GetCurrentMethod()));
381 }
382
383 return Templates;
384 }
385
386 private async Task<string> LoadBundledKycXmlAsync(CancellationToken CancellationToken)
387 {
388 CancellationToken.ThrowIfCancellationRequested();
389 using Stream Stream = await FileSystem.OpenAppPackageFileAsync(backupKyc);
390 using StreamReader Reader = new(Stream);
391 string Xml = await Reader.ReadToEndAsync(CancellationToken).ConfigureAwait(false);
392 return Xml;
393 }
394
395 private async Task<bool> ValidateKycXmlAsync(string Domain, string Xml)
396 {
397 try
398 {
399 bool Valid = await ServiceRef.XmlSchemaValidationService.ValidateAsync(Constants.Schemes.NeuroAccessKycProcessUrl, Xml).ConfigureAwait(false);
400 if (Valid)
401 {
402 ServiceRef.LogService.LogDebug("KycXmlValidationPrimarySuccess", new KeyValuePair<string, object?>("Domain", Domain));
403 return true;
404 }
405
406 bool LegacyValid = await ServiceRef.XmlSchemaValidationService.ValidateAsync(Constants.Schemes.KYCProcess, Xml).ConfigureAwait(false);
407 if (LegacyValid)
408 {
409 ServiceRef.LogService.LogInformational(
410 "KycXmlValidationFallbackSuccess",
411 new KeyValuePair<string, object?>("Domain", Domain),
412 new KeyValuePair<string, object?>("LegacyKey", Constants.Schemes.KYCProcess));
413 return true;
414 }
415
416 ServiceRef.LogService.LogWarning(
417 "KycXmlValidationBothFailed",
418 new KeyValuePair<string, object?>("Domain", Domain),
419 new KeyValuePair<string, object?>("PrimaryKey", Constants.Schemes.NeuroAccessKycProcessUrl),
420 new KeyValuePair<string, object?>("LegacyKey", Constants.Schemes.KYCProcess));
421 return false;
422 }
423 catch (Exception Ex)
424 {
425 ServiceRef.LogService.LogException(Ex, this.GetClassAndMethod(MethodBase.GetCurrentMethod()));
426 return false;
427 }
428 }
429
430 private async Task ApplyTemplateToReferenceAsync(KycReference Reference, KycApplicationTemplate Template, string? Lang)
431 {
432 if (Template.Reference.KycXml is null)
433 return;
434
435 string Xml = Template.Reference.KycXml;
436 KycProcess? Process = await Template.Reference.GetProcess(Lang).ConfigureAwait(false);
437 if (Process is null)
438 Process = await KycProcessParser.LoadProcessAsync(Xml, Lang).ConfigureAwait(false);
439
440 Reference.SetProcess(Process, Xml, DateTime.UtcNow, DateTime.UtcNow);
441 string? FriendlyName = Process.Name?.PrimaryText ?? Template.Reference.FriendlyName;
442 if (!string.IsNullOrWhiteSpace(FriendlyName))
443 Reference.FriendlyName = FriendlyName;
444 }
445
446 private async Task ApplyBundledTemplateAsync(KycReference Reference, string? Lang)
447 {
448 string Xml = await this.LoadBundledKycXmlAsync(default).ConfigureAwait(false);
449 KycProcess Process = await KycProcessParser.LoadProcessAsync(Xml, Lang).ConfigureAwait(false);
450 Reference.SetProcess(Process, Xml, DateTime.UtcNow, DateTime.UtcNow);
451 string? FriendlyName = Process.Name?.Text;
452 if (!string.IsNullOrWhiteSpace(FriendlyName))
453 Reference.FriendlyName = FriendlyName;
454 }
455
456 #endregion
457
458 #region Validation
464 public async Task<bool> ValidatePageAsync(KycPage Page)
465 {
466 if (Page is null)
467 return false;
468 IEnumerable<ObservableKycField> Fields = Page.VisibleFields;
469 ReadOnlyObservableCollection<KycSection> Sections = Page.VisibleSections;
470 if (Sections is not null)
471 Fields = Fields.Concat(Sections.SelectMany(S => S.VisibleFields));
472 bool Ok = true;
473 List<Task> Tasks = new();
474 foreach (ObservableKycField Field in Fields)
475 {
477 Field.ValidationTask.Run();
478 Task T = MainThread.InvokeOnMainThreadAsync(async () =>
479 {
480 await Field.ValidationTask.WaitAllAsync();
481 if (!Field.IsValid)
482 Ok = false;
483 });
484 Tasks.Add(T);
485 }
486 await Task.WhenAll(Tasks);
487 return Ok;
488 }
489
494 public async Task<int> GetFirstInvalidVisiblePageIndexAsync(KycProcess Process)
495 {
496 if (Process is null)
497 return -1;
498 for (int i = 0; i < Process.Pages.Count; i++)
499 {
500 KycPage Page = Process.Pages[i];
501 if (!Page.IsVisible(Process.Values))
502 continue;
503 if (!await this.ValidatePageAsync(Page))
504 return i;
505 }
506 return -1;
507 }
508 #endregion
509
510 #region Data Preparation
514 public async Task<(IReadOnlyList<Property> Properties, IReadOnlyList<LegalIdentityAttachment> Attachments)> PreparePropertiesAndAttachmentsAsync(KycProcess Process, CancellationToken CancellationToken)
515 {
516 List<Property> Mapped = new();
517 List<LegalIdentityAttachment> Attachments = new();
518 foreach (KycPage Page in Process.Pages)
519 {
520 if (!Page.IsVisible(Process.Values))
521 continue;
522 foreach (ObservableKycField Field in Page.VisibleFields)
523 {
524 if (this.CheckAndHandleFile(Process, Field, Attachments))
525 continue;
526 foreach (Property P in await this.BuildPropertiesFromFieldAsync(Process, Field, CancellationToken))
527 Mapped.Add(P);
528 }
529 foreach (KycSection Section in Page.AllSections)
530 {
531 foreach (ObservableKycField Field in Section.VisibleFields)
532 {
533 if (this.CheckAndHandleFile(Process, Field, Attachments))
534 continue;
535 foreach (Property P in await this.BuildPropertiesFromFieldAsync(Process, Field, CancellationToken))
536 Mapped.Add(P);
537 }
538 }
539 }
540 KycOrderingComparer Comparer = KycOrderingComparer.Create(Process);
541 Mapped.Sort(Comparer.PropertyComparer);
542 return (Mapped, Attachments);
543 }
544
548 public Task ScheduleSnapshotAsync(KycReference Reference, KycProcess Process, KycNavigationSnapshot Navigation, double Progress, string? CurrentPageId)
549 {
550 if (Reference is null || Process is null)
551 return Task.CompletedTask;
552 AsyncLock Lock = this.GetLockFor(Reference);
553 return this.ScheduleSnapshotCoreAsync(Lock, Reference, Process, Navigation, Progress, CurrentPageId);
554 }
555
556 private async Task ScheduleSnapshotCoreAsync(AsyncLock Lock, KycReference Reference, KycProcess Process, KycNavigationSnapshot Navigation, double Progress, string? CurrentPageId)
557 {
558 await using (await Lock.LockAsync().ConfigureAwait(false))
559 {
560 KycReferenceSnapshot Snapshot = CreateSnapshot(Reference, Process, Navigation, Progress, CurrentPageId);
561 string Key = Reference.ObjectId ?? string.Empty;
562 this.pendingAutosave[Key] = new AutosaveEntry(Reference, Snapshot); // coalesce to latest
563 _ = this.autosaveChannel.Writer.TryWrite(Key);
564 }
565 }
566
570 public async Task FlushSnapshotAsync(KycReference Reference, KycProcess Process, KycNavigationSnapshot Navigation, double Progress, string? CurrentPageId)
571 {
572 if (Reference is null || Process is null)
573 return;
574 AsyncLock Lock = this.GetLockFor(Reference);
575 KycReferenceSnapshot Snapshot;
576 await using (await Lock.LockAsync().ConfigureAwait(false))
577 {
578 Snapshot = CreateSnapshot(Reference, Process, Navigation, Progress, CurrentPageId);
579 }
580 try
581 {
582 await this.SaveSnapshotAsync(Reference, Snapshot, true).ConfigureAwait(false);
583 }
584 catch (Exception Ex)
585 {
586 ServiceRef.LogService.LogException(Ex, new KeyValuePair<string, object?>("Operation", "KYC.ImmediateAutosave"));
587 }
588 string Key = Reference.ObjectId ?? string.Empty;
589 this.pendingAutosave.TryRemove(Key, out _);
590 }
591
595 public async Task ApplySubmissionAsync(KycReference Reference, LegalIdentity Identity)
596 {
597 if (Reference is null || Identity is null)
598 return;
599 AsyncLock Lock = this.GetLockFor(Reference);
600 await using (await Lock.LockAsync().ConfigureAwait(false))
601 {
602 Reference.CreatedIdentityId = Identity.Id;
603 Reference.CreatedIdentityState = Identity.State;
604 Reference.ApplicationReview = null;
605 Reference.RejectionMessage = null;
606 Reference.RejectionCode = null;
607 Reference.InvalidClaims = null;
608 Reference.InvalidPhotos = null;
609 Reference.InvalidClaimDetails = null;
610 Reference.InvalidPhotoDetails = null;
611 Reference.Version++;
612 Reference.UpdatedUtc = DateTime.UtcNow;
613 await SaveReferenceAsync(Reference);
614 }
615 }
616
622 public async Task UpdateSubmissionStateAsync(KycReference Reference, LegalIdentity Identity)
623 {
624 if (Reference is null || Identity is null)
625 return;
626
627 AsyncLock Lock = this.GetLockFor(Reference);
628 await using (await Lock.LockAsync().ConfigureAwait(false))
629 {
630 Reference.CreatedIdentityId = Identity.Id;
631 Reference.CreatedIdentityState = Identity.State;
632 Reference.Version++;
633 Reference.UpdatedUtc = DateTime.UtcNow;
634 await SaveReferenceAsync(Reference);
635 }
636 }
637
641 public async Task ClearSubmissionAsync(KycReference Reference)
642 {
643 if (Reference is null)
644 return;
645 AsyncLock Lock = this.GetLockFor(Reference);
646 await using (await Lock.LockAsync().ConfigureAwait(false))
647 {
648 Reference.CreatedIdentityId = null;
649 Reference.CreatedIdentityState = null;
650 Reference.Version++;
651 Reference.UpdatedUtc = DateTime.UtcNow;
652 await SaveReferenceAsync(Reference);
653 }
654 }
655
661 public async Task ApplyApplicationReviewAsync(KycReference Reference, ApplicationReview Review)
662 {
663 if (Reference is null || Review is null)
664 return;
665
666 ApplicationReview Clone = CloneReview(Review)!;
667 AsyncLock Lock = this.GetLockFor(Reference);
668 await using (await Lock.LockAsync().ConfigureAwait(false))
669 {
670 Reference.ApplicationReview = Clone;
671 Reference.RejectionMessage = null;
672 Reference.RejectionCode = null;
673 Reference.InvalidClaims = null;
674 Reference.InvalidPhotos = null;
675 Reference.InvalidClaimDetails = null;
676 Reference.InvalidPhotoDetails = null;
677 Reference.Version++;
678 Reference.UpdatedUtc = DateTime.UtcNow;
679 await SaveReferenceAsync(Reference).ConfigureAwait(false);
680 }
681
682 this.RaiseApplicationReviewUpdated(Reference, Clone);
683 }
684
688 public async Task PrepareReferenceForNewApplicationAsync(KycReference Reference, string? Language, IReadOnlyList<KycFieldValue>? SeedFields)
689 {
690 if (Reference is null)
691 return;
692 AsyncLock Lock = this.GetLockFor(Reference);
693 await using (await Lock.LockAsync().ConfigureAwait(false))
694 {
695 Reference.LastVisitedMode = "Form";
696 Reference.LastVisitedPageId = null;
697 Reference.ApplicationReview = null;
698 Reference.RejectionMessage = null;
699 Reference.RejectionCode = null;
700 Reference.InvalidClaims = null;
701 Reference.InvalidPhotos = null;
702 Reference.InvalidClaimDetails = null;
703 Reference.InvalidPhotoDetails = null;
704 Reference.CreatedIdentityId = null;
705 Reference.CreatedIdentityState = null;
706 if (SeedFields is not null && SeedFields.Count > 0)
707 Reference.Fields = SeedFields.Select(Field => new KycFieldValue(Field.FieldId, Field.Value)).ToArray();
708 else
709 Reference.Fields = null;
710 Reference.Version++;
711 Reference.UpdatedUtc = DateTime.UtcNow;
712 await SaveReferenceAsync(Reference);
713 }
714
715 if (SeedFields is not null && SeedFields.Count > 0 && !string.IsNullOrWhiteSpace(Language))
716 {
717 try
718 {
719 await Reference.ApplyFieldsToProcessAsync(Language);
720 }
721 catch (Exception Exception)
722 {
723 ServiceRef.LogService.LogException(Exception, this.GetClassAndMethod(MethodBase.GetCurrentMethod()));
724 }
725 }
726 }
727
731 private static KycReferenceSnapshot CreateSnapshot(KycReference Reference, KycProcess Process, KycNavigationSnapshot Navigation, double Progress, string? CurrentPageId)
732 {
733 Reference.Version++;
734 KycFieldValue[] Fields = [.. Process.Values.Select(Pair => new KycFieldValue(Pair.Key, Pair.Value))];
735 string? LastVisitedPageId = ResolveLastVisitedPageId(Reference, Process, Navigation, CurrentPageId);
736 string Mode = ResolveMode(Navigation);
737 DateTime Now = DateTime.UtcNow;
738 Reference.UpdatedUtc = Now;
739 Reference.Fields = Fields;
740 Reference.Progress = Progress;
741 Reference.LastVisitedPageId = LastVisitedPageId;
742 Reference.LastVisitedMode = Mode;
743
744 ApplicationReview? SnapshotReview = CloneReview(Reference.ApplicationReview);
746 Reference.ObjectId,
747 Reference.Version,
748 Fields,
749 Progress,
750 LastVisitedPageId,
751 Mode,
752 SnapshotReview,
753 Reference.CreatedUtc,
754 Now);
755
756 try
757 {
758 ServiceRef.LogService.LogDebug("KycSnapshotCreated",
759 new KeyValuePair<string, object?>("ReferenceId", Reference.ObjectId ?? string.Empty),
760 new KeyValuePair<string, object?>("Version", Snapshot.Version),
761 new KeyValuePair<string, object?>("FieldCount", Fields.Length));
762 }
763 catch { }
764
765 return Snapshot;
766 }
767
768 private AsyncLock GetLockFor(KycReference Reference)
769 {
770 string Key = Reference.ObjectId ?? string.Empty;
771 return this.referenceLocks.GetOrAdd(Key, _ => new AsyncLock());
772 }
773
774 private static string ResolveMode(KycNavigationSnapshot Navigation)
775 {
776 if (Navigation.State == KycFlowState.Summary || Navigation.State == KycFlowState.PendingSummary || Navigation.State == KycFlowState.RejectedSummary)
777 return "Summary";
778 return "Form";
779 }
780
781 private static string? ResolveLastVisitedPageId(KycReference Reference, KycProcess Process, KycNavigationSnapshot Navigation, string? CurrentPageId)
782 {
783 if (!string.IsNullOrWhiteSpace(CurrentPageId))
784 return CurrentPageId;
785 if (Navigation.CurrentPageIndex >= 0 && Navigation.CurrentPageIndex < Process.Pages.Count)
786 return Process.Pages[Navigation.CurrentPageIndex].Id;
787 return Reference.LastVisitedPageId;
788 }
789
793 private async Task SaveSnapshotAsync(KycReference Reference, KycReferenceSnapshot Snapshot, bool IsImmediate)
794 {
795 Stopwatch Sw = Stopwatch.StartNew();
796 AsyncLock Lock = this.GetLockFor(Reference);
797 try
798 {
799 await using (await Lock.LockAsync().ConfigureAwait(false))
800 {
801 ServiceRef.LogService.LogDebug("KycSnapshotPersistAttempt",
802 new KeyValuePair<string, object?>("ReferenceId", Reference.ObjectId ?? string.Empty),
803 new KeyValuePair<string, object?>("Version", Snapshot.Version),
804 new KeyValuePair<string, object?>("IsImmediate", IsImmediate));
805
806 // Stale check
807 if (Snapshot.Version < Reference.Version)
808 {
809 Interlocked.Increment(ref this.snapshotsSkipped);
810 ServiceRef.LogService.LogDebug("KycSnapshotStaleSkipped",
811 new KeyValuePair<string, object?>("ReferenceId", Reference.ObjectId ?? string.Empty),
812 new KeyValuePair<string, object?>("SnapshotVersion", Snapshot.Version),
813 new KeyValuePair<string, object?>("CurrentVersion", Reference.Version));
814 return;
815 }
816
817 if (Reference.Fields != Snapshot.Fields)
818 Reference.Fields = Snapshot.Fields;
819 Reference.Progress = Snapshot.Progress;
820 Reference.LastVisitedPageId = Snapshot.LastVisitedPageId;
821 Reference.LastVisitedMode = Snapshot.LastVisitedMode;
822 Reference.ApplicationReview = CloneReview(Snapshot.ApplicationReview);
823 Reference.UpdatedUtc = Snapshot.UpdatedUtc;
824 await SaveReferenceAsync(Reference).ConfigureAwait(false);
825
826 Interlocked.Increment(ref this.snapshotsPersisted);
827 string Hash = ComputeFieldsHash(Snapshot.Fields);
828 ServiceRef.LogService.LogInformational("KycSnapshotPersisted",
829 new KeyValuePair<string, object?>("ReferenceId", Reference.ObjectId ?? string.Empty),
830 new KeyValuePair<string, object?>("Version", Snapshot.Version),
831 new KeyValuePair<string, object?>("FieldCount", Snapshot.Fields?.Length ?? 0),
832 new KeyValuePair<string, object?>("DurationMs", Sw.ElapsedMilliseconds),
833 new KeyValuePair<string, object?>("Hash", Hash));
834 }
835 }
836 finally
837 {
838 Sw.Stop();
839 }
840 }
841
842 private static string ComputeFieldsHash(KycFieldValue[]? Fields)
843 {
844 if (Fields is null || Fields.Length == 0)
845 return "0";
846 try
847 {
848 using SHA256 Sha = SHA256.Create();
849 foreach (KycFieldValue F in Fields.OrderBy(f => f.FieldId, StringComparer.Ordinal))
850 {
851 string Line = F.FieldId + "=" + (F.Value ?? string.Empty) + "\n";
852 byte[] Bytes = Encoding.UTF8.GetBytes(Line);
853 Sha.TransformBlock(Bytes, 0, Bytes.Length, null, 0);
854 }
855 Sha.TransformFinalBlock(Array.Empty<byte>(), 0, 0);
856 return Convert.ToHexString(Sha.Hash);
857 }
858 catch
859 {
860 return "ERR";
861 }
862 }
863
864 private static ApplicationReview? CloneReview(ApplicationReview? Source)
865 {
866 if (Source is null)
867 return null;
868
870 {
871 Message = Source.Message,
872 Code = Source.Code,
873 ReceivedUtc = Source.ReceivedUtc,
874 InvalidClaims = Source.InvalidClaims?.ToArray() ?? Array.Empty<string>(),
875 InvalidPhotos = Source.InvalidPhotos?.ToArray() ?? Array.Empty<string>(),
876 UnvalidatedClaims = Source.UnvalidatedClaims?.ToArray() ?? Array.Empty<string>(),
877 UnvalidatedPhotos = Source.UnvalidatedPhotos?.ToArray() ?? Array.Empty<string>()
878 };
879
880 Clone.InvalidClaimDetails = Source.InvalidClaimDetails?
881 .Select(d =>
882 {
883 ApplicationReviewClaimDetail Detail = new ApplicationReviewClaimDetail(d.Claim, d.Reason, d.ReasonLanguage, d.ReasonCode, d.Service)
884 {
885 DisplayName = d.DisplayName
886 };
887 return Detail;
888 })
889 .ToArray() ?? Array.Empty<ApplicationReviewClaimDetail>();
890
891 Clone.InvalidPhotoDetails = Source.InvalidPhotoDetails?
892 .Select(d => new ApplicationReviewPhotoDetail(d.FileName, d.DisplayName, d.Reason, d.ReasonLanguage, d.ReasonCode, d.Service))
893 .ToArray() ?? Array.Empty<ApplicationReviewPhotoDetail>();
894
895 return Clone;
896 }
897
898 private void RaiseApplicationReviewUpdated(KycReference Reference, ApplicationReview? Review)
899 {
900 if (Reference is null)
901 return;
902
903 try
904 {
905 ApplicationReviewEventArgs Args = new ApplicationReviewEventArgs(Reference, Review);
906 this.ApplicationReviewUpdated?.Invoke(this, Args);
907 }
908 catch (Exception Ex)
909 {
910 ServiceRef.LogService.LogException(Ex);
911 }
912 }
913
914 private static async Task SaveReferenceAsync(KycReference Reference)
915 {
916 try
917 {
918 if (string.IsNullOrEmpty(Reference.ObjectId))
919 {
920 await ServiceRef.StorageService.Insert(Reference);
921 }
922 else
923 {
924 await ServiceRef.StorageService.Update(Reference);
925 }
926 }
927 catch (KeyNotFoundException)
928 {
929 await ServiceRef.StorageService.Insert(Reference);
930 }
931 }
932
933 private async Task<List<Property>> BuildPropertiesFromFieldAsync(KycProcess Process, ObservableKycField Field, CancellationToken Ct)
934 {
935 List<Property> Result = new();
936 if (Field.Mappings.Count == 0)
937 return Result;
938 if (Field.Condition is not null && !Field.Condition.Evaluate(Process.Values))
939 return Result;
940 string BaseValue = Field.StringValue?.Trim() ?? string.Empty;
941 if (string.IsNullOrEmpty(BaseValue))
942 return Result;
943 foreach (KycMapping Map in Field.Mappings)
944 {
945 if (string.IsNullOrEmpty(Map.Key))
946 continue;
947 string Current = BaseValue;
948 foreach (string Name in Map.TransformNames)
949 {
950 if (string.IsNullOrWhiteSpace(Name))
951 continue;
953 {
954 try { Current = await Transform.ApplyAsync(Field, Process, Current, Ct); }
955 catch (Exception Ex2) { ServiceRef.LogService.LogException(Ex2); }
956 }
957 if (string.IsNullOrEmpty(Current))
958 break;
959 }
960 if (!string.IsNullOrEmpty(Current))
961 Result.Add(new Property(Map.Key, Current));
962 }
963 return Result;
964 }
965
966 private bool CheckAndHandleFile(KycProcess Process, ObservableKycField Field, List<LegalIdentityAttachment> List)
967 {
968 if (Field.Condition is not null && (Field.Mappings.Count == 0 || !Field.Condition.Evaluate(Process.Values)))
969 return false;
970 if (!string.IsNullOrEmpty(Field.StringValue) && Field is ObservableImageField ImageField)
971 {
972 byte[]? Data = ImageField.StringValue is null ? null : this.CompressImage(this.Base64ToStream(ImageField.StringValue));
973 if (Data is not null)
974 List.Add(new LegalIdentityAttachment(ImageField.Mappings.First().Key + ".jpg", Constants.MimeTypes.Jpeg, Data));
975 return true;
976 }
977 return false;
978 }
979
980 private MemoryStream Base64ToStream(string Base64)
981 {
982 byte[] Bytes = Convert.FromBase64String(Base64);
983 return new MemoryStream(Bytes);
984 }
985
986 private byte[]? CompressImage(Stream InputStream)
987 {
988 try
989 {
990 using SKManagedStream Ms = new(InputStream);
991 using SKData ImgData = SKData.Create(Ms);
992 using SKCodec Codec = SKCodec.Create(ImgData);
993 SKBitmap Bmp = SKBitmap.Decode(ImgData);
994 Bmp = this.HandleOrientation(Bmp, Codec.EncodedOrigin);
995 bool Resize = false;
996 int H = Bmp.Height; int W = Bmp.Width;
997 if (W >= H && W > 1920) { H = (int)(H * (1920.0 / W) + 0.5); W = 1920; Resize = true; }
998 else if (H > W && H > 1920) { W = (int)(W * (1920.0 / H) + 0.5); H = 1920; Resize = true; }
999 if (Resize)
1000 {
1001 SKImageInfo Info = Bmp.Info;
1002 SKImageInfo Ni = new(W, H, Info.ColorType, Info.AlphaType, Info.ColorSpace);
1003 SKBitmap? Resized = Bmp.Resize(Ni, SKFilterQuality.High);
1004 if (Resized is not null) { Bmp.Dispose(); Bmp = Resized; }
1005 }
1006 byte[] Bytes2;
1007 using (SKData Encoded = Bmp.Encode(SKEncodedImageFormat.Jpeg, 80)) Bytes2 = Encoded.ToArray();
1008 Bmp.Dispose();
1009 return Bytes2;
1010 }
1011 catch (Exception Ex) { ServiceRef.LogService.LogException(Ex); return null; }
1012 }
1013
1014 private SKBitmap HandleOrientation(SKBitmap Bmp, SKEncodedOrigin O)
1015 {
1016 SKBitmap Rotated;
1017 switch (O)
1018 {
1019 case SKEncodedOrigin.BottomRight:
1020 Rotated = new SKBitmap(Bmp.Width, Bmp.Height);
1021 using (SKCanvas Canvas = new(Rotated)) { Canvas.RotateDegrees(180, Bmp.Width / 2, Bmp.Height / 2); Canvas.DrawBitmap(Bmp, 0, 0); }
1022 break;
1023 case SKEncodedOrigin.RightTop:
1024 Rotated = new SKBitmap(Bmp.Height, Bmp.Width);
1025 using (SKCanvas Canvas = new(Rotated)) { Canvas.Translate(Rotated.Width, 0); Canvas.RotateDegrees(90); Canvas.DrawBitmap(Bmp, 0, 0); }
1026 break;
1027 case SKEncodedOrigin.LeftBottom:
1028 Rotated = new SKBitmap(Bmp.Height, Bmp.Width);
1029 using (SKCanvas Canvas = new(Rotated)) { Canvas.Translate(0, Rotated.Height); Canvas.RotateDegrees(270); Canvas.DrawBitmap(Bmp, 0, 0); }
1030 break;
1031 default: return Bmp;
1032 }
1033 return Rotated;
1034 }
1035
1036 #endregion // Data Preparation
1037
1041 public async Task FlushAsync(CancellationToken cancellationToken = default)
1042 {
1043 foreach (KeyValuePair<string, AutosaveEntry> Pair in this.pendingAutosave.ToArray())
1044 {
1045 if (cancellationToken.IsCancellationRequested)
1046 break;
1047 if (this.pendingAutosave.TryRemove(Pair.Key, out AutosaveEntry? Entry))
1048 {
1049 try { await this.SaveSnapshotAsync(Entry.Reference, Entry.Snapshot, true).ConfigureAwait(false); }
1050 catch (Exception Ex) { ServiceRef.LogService.LogException(Ex, new KeyValuePair<string, object?>("Operation", "KYC.FlushAutosave")); }
1051 }
1052 }
1053 }
1054
1058 public async Task ShutdownAsync(CancellationToken cancellationToken = default)
1059 {
1060 try
1061 {
1062 this.autosaveCts.Cancel();
1063 this.autosaveChannel.Writer.TryComplete();
1064 await Task.WhenAny(this.autosaveWorkerTask, Task.Delay(TimeSpan.FromSeconds(2), cancellationToken)).ConfigureAwait(false);
1065 }
1066 catch (Exception Ex)
1067 {
1068 ServiceRef.LogService.LogException(Ex);
1069 }
1070 await this.FlushAsync(cancellationToken).ConfigureAwait(false);
1071 }
1072
1076 protected virtual void Dispose(bool disposing)
1077 {
1078 if (this.disposedValue)
1079 return;
1080 if (disposing)
1081 {
1082 try
1083 {
1084 this.autosaveCts.Cancel();
1085 this.autosaveChannel.Writer.TryComplete();
1086 try
1087 {
1088 this.autosaveWorkerTask.Wait(TimeSpan.FromSeconds(2));
1089 }
1090 catch (AggregateException ex)
1091 {
1092 if (ex.InnerExceptions.Any(e => e is not TaskCanceledException and not OperationCanceledException))
1093 ServiceRef.LogService.LogException(ex);
1094 }
1095 catch (OperationCanceledException)
1096 {
1097 // Expected when cancellation is observed; ignore.
1098 }
1099 }
1100 catch (Exception Ex)
1101 {
1102 ServiceRef.LogService.LogException(Ex);
1103 }
1104 }
1105 this.disposedValue = true;
1106 }
1107
1111 public void Dispose()
1112 {
1113 this.Dispose(true);
1114 GC.SuppressFinalize(this);
1115 }
1116
1120 internal (long Persisted, long Skipped) GetSnapshotMetrics() => (Interlocked.Read(ref this.snapshotsPersisted), Interlocked.Read(ref this.snapshotsSkipped));
1121 }
1122}
const string Jpeg
The JPEG MIME type.
Definition: Constants.cs:291
XML Schemes (namespaces used also as schema registration keys) + packaged file names.
Definition: Constants.cs:112
A set of never changing property constants and helpful values.
Definition: Constants.cs:24
Contains additional data about an invalid claim.
Captures the result of an application review returned from backend services.
ApplicationReviewPhotoDetail[] InvalidPhotoDetails
Optional details about invalid photos.
string[] InvalidPhotos
Invalid photo identifiers reported by the service.
string[] InvalidClaims
Invalid claim identifiers reported by the service.
DateTime ReceivedUtc
Moment when the review was received.
string? Code
Optional machine readable code identifying the review reason.
string[] UnvalidatedPhotos
Photos still pending validation.
string Message
The localized or raw message describing the review result.
string[] UnvalidatedClaims
Claims still pending validation.
ApplicationReviewClaimDetail[] InvalidClaimDetails
Optional details about invalid claims.
Contains additional data about an invalid photo.
Event arguments emitted when a KYC application review changes.
Represents a KYC application template retrieved from PubSub storage.
static bool TryCreate(PubSubItem Item, out KycApplicationItem? Application)
Attempts to create an instance from a PubSub item.
Represents a page of KYC application templates along with pagination metadata.
IReadOnlyList< KycApplicationTemplate > Templates
Gets the application templates on this page.
Represents a parsed KYC application template along with optional PubSub metadata.
KycReference Reference
Gets the parsed reference constructed from the template XML.
Parses an XML-described KYC process into view-model objects.
static Task< KycProcess > LoadProcessAsync(string Xml, string? Lang=null)
Parses the KYC pages from an XML string.
Contains a local reference to a KYC process.
Definition: KycReference.cs:22
string? LastVisitedPageId
Last visited page identifier to support resuming.
Definition: KycReference.cs:93
static KycReference FromProcess(KycProcess process, string xml, string? friendlyName=null)
Creates a KycReference from a KycProcess, serializing its field values.
ApplicationReview? ApplicationReview
Latest backend review for this application, if any.
string FriendlyName
Optional friendly name.
Definition: KycReference.cs:69
async Task ApplyFieldsToProcessAsync(string? lang=null)
Ensures the current process reflects values in Fields by applying them into the cached process instan...
async Task< KycProcess?> GetProcess(string? lang=null)
Gets a parsed KYC process, populating its fields from the reference.
KycFieldValue?[] Fields
Field values in the process.
Definition: KycReference.cs:63
int Version
Monotonically increasing version for optimistic concurrency and snapshot ordering....
Definition: KycReference.cs:57
DateTime CreatedUtc
When the reference was created.
Definition: KycReference.cs:40
void SetProcess(KycProcess process, string xml)
Stores a parsed KYC process into the reference.
string? KycXml
XML describing the KYC process.
Definition: KycReference.cs:35
Immutable snapshot of the state of a KycReference at a point in time. Used to persist ordered,...
Service providing KYC process loading, validation, snapshotting and persistence. Implements ordered,...
Definition: KycService.cs:41
async Task< bool > ValidatePageAsync(KycPage Page)
Validates all visible fields on a page (including sections) using synchronous and asynchronous valida...
Definition: KycService.cs:464
async Task< KycApplicationPage > LoadKycApplicationsPageAsync(string? After=null, string? Before=null, int? Index=null, int? Max=null, string? Lang=null, CancellationToken CancellationToken=default)
Loads a page of KYC application templates using PubSub pagination, with bundled fallback when remote ...
Definition: KycService.cs:255
async Task FlushAsync(CancellationToken cancellationToken=default)
Flushes any pending coalesced snapshots by forcing immediate persistence.
Definition: KycService.cs:1041
async Task< IReadOnlyList< KycReference > > LoadAvailableKycReferencesAsync(string? Lang=null)
Loads available KYC references from the provider; falls back to local bundled test definition....
Definition: KycService.cs:236
async Task ShutdownAsync(CancellationToken cancellationToken=default)
Performs an orderly shutdown of the service, stopping the worker and flushing snapshots.
Definition: KycService.cs:1058
async Task PrepareReferenceForNewApplicationAsync(KycReference Reference, string? Language, IReadOnlyList< KycFieldValue >? SeedFields)
Resets a reference for a new application session, optionally seeding field values.
Definition: KycService.cs:688
async Task< KycReference > LoadKycReferenceAsync(string? Lang=null, KycApplicationTemplate? Template=null)
Loads the single persisted KycReference (creating a new one if none exists) and ensures KYC XML is av...
Definition: KycService.cs:123
async Task ApplyApplicationReviewAsync(KycReference Reference, ApplicationReview Review)
Records the latest application review from the provider.
Definition: KycService.cs:661
KycService()
Creates a new instance of the KycService class. Initializes the autosave channel and background worke...
Definition: KycService.cs:74
async Task UpdateSubmissionStateAsync(KycReference Reference, LegalIdentity Identity)
Updates stored submission state without clearing any existing application review.
Definition: KycService.cs:622
async Task ApplySubmissionAsync(KycReference Reference, LegalIdentity Identity)
Records submission data (identity id + state) and persists the reference.
Definition: KycService.cs:595
async Task< int > GetFirstInvalidVisiblePageIndexAsync(KycProcess Process)
Finds the first visible page index that fails validation, or -1 if all visible pages are valid.
Definition: KycService.cs:494
async Task ClearSubmissionAsync(KycReference Reference)
Clears stored submission information from the reference.
Definition: KycService.cs:641
Task ScheduleSnapshotAsync(KycReference Reference, KycProcess Process, KycNavigationSnapshot Navigation, double Progress, string? CurrentPageId)
Captures a snapshot and schedules an asynchronous persistence operation (coalescing by reference).
Definition: KycService.cs:548
async Task FlushSnapshotAsync(KycReference Reference, KycProcess Process, KycNavigationSnapshot Navigation, double Progress, string? CurrentPageId)
Captures a snapshot and persists it immediately (bypasses the autosave queue).
Definition: KycService.cs:570
async Task<(IReadOnlyList< Property > Properties, IReadOnlyList< LegalIdentityAttachment > Attachments)> PreparePropertiesAndAttachmentsAsync(KycProcess Process, CancellationToken CancellationToken)
Builds identity properties and attachments (images) from a process, respecting visibility and transfo...
Definition: KycService.cs:514
void Dispose()
Disposes the service.
Definition: KycService.cs:1111
virtual void Dispose(bool disposing)
Releases resources used by the service.
Definition: KycService.cs:1076
async Task SaveKycReferenceAsync(KycReference Reference)
Persists an existing KycReference instance (insert or update).
Definition: KycService.cs:216
Represents a field value in a KYC process reference.
string? PrimaryText
Gets the first localized text that was added, regardless of current culture.
string Text
Gets the localized text for the current UI culture, or an appropriate fallback.
Mapping from a field value to an identity property, including optional transform pipeline.
Definition: KycMapping.cs:9
Represents a page in a KYC process containing fields and sections.
Definition: KycPage.cs:20
ReadOnlyObservableCollection< ObservableKycField > VisibleFields
Gets the read-only collection of visible fields in the page.
Definition: KycPage.cs:59
ObservableCollection< KycSection > AllSections
Gets all sections contained in the page.
Definition: KycPage.cs:64
ReadOnlyObservableCollection< KycSection > VisibleSections
Gets the read-only collection of visible sections in the page.
Definition: KycPage.cs:72
bool IsVisible(IDictionary< string, string?> Values)
Gets a value indicating whether the page is visible based on current values and its condition.
Definition: KycPage.cs:98
Represents a parsed KYC process with pages, fields, and current values.
Definition: KycProcess.cs:11
KycLocalizedText? Name
Optional process-level localized name for display.
Definition: KycProcess.cs:15
ObservableCollection< KycPage > Pages
Gets the collection of pages in the KYC process.
Definition: KycProcess.cs:26
IDictionary< string, string?> Values
Gets a modifiable dictionary of field values keyed by field identifier.
Definition: KycProcess.cs:21
Transform registry (static). Register new transforms at startup or via static ctor.
The base KYC field model. Use as-is for generic fields, or subclass for custom logic.
abstract ? string StringValue
Gets or sets the value as a string, parsing or formatting as needed for the field type.
ObservableCollection< KycMapping > Mappings
Mappings to apply output.
KycCondition? Condition
Conditional display of this field.
void ForceSynchronousValidation()
Forces immediate synchronous validation (used when advancing pages or explicit full validation is req...
Simple async-exclusive lock (lightweight). Each call to LockAsync yields a disposable releaser that m...
Definition: AsyncLock.cs:11
async ValueTask< Releaser > LockAsync(CancellationToken cancellationToken=default)
Acquires the lock asynchronously, returning a disposable releaser that must be disposed exactly once.
Definition: AsyncLock.cs:19
Base class that references services in the app.
Definition: ServiceRef.cs:43
static ILogService LogService
Log service.
Definition: ServiceRef.cs:214
static IStorageService StorageService
Storage service.
Definition: ServiceRef.cs:310
static ITagProfile TagProfile
TAG Profile service.
Definition: ServiceRef.cs:202
static IXmppService XmppService
The XMPP service for XMPP communication.
Definition: ServiceRef.cs:190
static IXmlSchemaValidationService XmlSchemaValidationService
XML schema validation service.
Definition: ServiceRef.cs:444
Represents a page of PubSub items along with associated pagination metadata.
async Task WaitAllAsync(bool waitWhilePending=false)
Asynchronously waits until all UI notifications and state updates for the current task have been proc...
void Run()
Start the configured task now (no CanExecute gating).
Represents a published item.
Definition: PubSubItem.cs:12
Service for loading KYC processes, performing validation, building identity artifacts and managing pe...
Definition: IKycService.cs:17
Contract for KYC mapping transforms. Transforms can normalize, format or derive new values.
Task< PubSubPageResult?> GetItemsPageAsync(string NodeId, string? ServiceAddress=null, string? After=null, string? Before=null, int? Index=null, int? Max=null)
Retrieves a page of items for a specified node using XEP-0059 result set management parameters.
Definition: ImplTypes.g.cs:58
KycFlowState
High-level flow states for the KYC process UI.
Definition: KycDomain.cs:7
sealed record KycNavigationSnapshot(int CurrentPageIndex, int AnchorPageIndex, KycFlowState State)
Navigation snapshot capturing minimal mutable UI navigation state.