Neuron®
The Neuron® is the basis for the creation of open and secure federated networks for smart societies.
Loading...
Searching...
No Matches
KycProcessParser.cs
1using System;
3using System.Collections.ObjectModel;
4using System.Globalization;
5using System.Linq;
6using System.Threading.Tasks;
7using System.Xml.Linq;
14
16{
20 public static class KycProcessParser
21 {
25 public static Task<KycProcess> LoadProcessAsync(string Xml, string? Lang = null)
26 {
27 //TODO: Refactor parsing to handle namespaces properly (currently works around by stripping them, in order to no break existing logic)
28 KycProcess Process = new();
29 string XmlNormalized = StripNamespaces(Xml);
30 XDocument Doc = XDocument.Parse(XmlNormalized);
31
32 // Optional process-level name (localized)
33 if (Doc.Root is not null)
34 {
35 Process.Name = ParseLocalizedText(Doc.Root.Element("Name"));
36 }
37
38 foreach (XElement PageEl in Doc.Root?.Elements("Page") ?? Enumerable.Empty<XElement>())
39 {
40 KycPage Page = new KycPage
41 {
42 Id = (string?)PageEl.Attribute("id") ?? string.Empty,
43 Title = ParseLocalizedText(PageEl.Element("Title")),
44 Description = ParseLocalizedText(PageEl.Element("Description")),
45 Condition = ParseCondition(PageEl.Element("Condition"))
46 };
47
48 // Fields (direct under Page) - owner process passed early to allow metadata logic to use it
49 foreach (XElement FieldEl in PageEl.Elements("Field"))
50 {
51 ObservableKycField Field = ParseField(FieldEl, Lang, Process);
52 Page.AllFields.Add(Field);
53 }
54
55 // Sections
56 foreach (XElement SectionEl in PageEl.Elements("Section"))
57 {
59 {
60 Label = ParseLocalizedText(SectionEl.Element("Label")),
61 Description = ParseLocalizedText(SectionEl.Element("Description"))
62 };
63
64 foreach (XElement FieldEl in SectionEl.Elements("Field"))
65 {
66 ObservableKycField Field = ParseField(FieldEl, Lang, Process);
67 Section.AllFields.Add(Field);
68 }
69
70 Page.AllSections.Add(Section);
71 }
72
73 Process.Pages.Add(Page);
74 }
75
76 Process.Initialize();
77
78 return Task.FromResult(Process);
79 }
80
85 private static string StripNamespaces(string Xml)
86 {
87 try
88 {
89 System.Text.RegularExpressions.Regex R = new System.Text.RegularExpressions.Regex("\\sxmlns(:[A-Za-z0-9_\\-\\.]+)?=\"[^\"]*\"", System.Text.RegularExpressions.RegexOptions.IgnoreCase);
90 return R.Replace(Xml, string.Empty);
91 }
92 catch
93 {
94 return Xml;
95 }
96 }
97
98 private static ObservableKycField ParseField(XElement El, string? Lang, KycProcess Owner)
99 {
100 // Determine field type
101 string TypeAttr = (string?)El.Attribute("type") ?? "text";
102 FieldType FieldType = Enum.TryParse<FieldType>(TypeAttr, true, out FieldType Ft) ? Ft : FieldType.Text;
103
104 ObservableKycField Field = FieldType switch
105 {
106 FieldType.Boolean => new ObservableBooleanField(),
107 FieldType.Date => new ObservableDateField(),
108 FieldType.Integer => new ObservableIntegerField(),
109 FieldType.Decimal => new ObservableDecimalField(),
110 FieldType.Picker => new ObservablePickerField(),
111 FieldType.Gender => new ObservablePickerField(),
112 FieldType.Radio => new ObservableRadioField(),
113 FieldType.Country => new ObservablePickerField(),
114 FieldType.Checkbox => new ObservableCheckboxField(),
115 FieldType.File => new ObservableFileField(),
116 FieldType.Image => new ObservableImageField(),
117 FieldType.Label => new ObservableLabelField(),
118 FieldType.Info => new ObservableInfoField(),
119 _ => new ObservableGenericField(),
120 };
121
122 // Basic attributes
123 Field.Id = (string?)El.Attribute("id") ?? string.Empty;
124 Field.FieldType = FieldType;
125 Field.Required = (bool?)El.Attribute("required") ?? false;
126 Field.Label = ParseLocalizedText(El.Element("Label"));
127 Field.Placeholder = ParseLocalizedText(El.Element("Placeholder"));
128 Field.Hint = ParseLocalizedText(El.Element("Hint"));
129 Field.Description = ParseLocalizedText(El.Element("Description"));
130 Field.SpecialType = (string?)El.Attribute("specialType");
131 Field.Condition = ParseCondition(El.Element("Condition"));
132
133 // Metadata
134 if (El.Element("Metadata") is XElement MetadataEl)
135 {
136 foreach (XElement MetaChild in MetadataEl.Elements())
137 {
138 string Key = MetaChild.Name.LocalName;
139 string Value = MetaChild.Value?.Trim() ?? string.Empty;
140
141 // Attempt to parse to int, double, bool; otherwise keep string
142 if (int.TryParse(Value, out int IntVal))
143 Field.Metadata[Key] = IntVal;
144 else if (double.TryParse(Value, NumberStyles.Any, CultureInfo.InvariantCulture, out double DoubleVal))
145 Field.Metadata[Key] = DoubleVal;
146 else if (bool.TryParse(Value, out bool BoolVal))
147 Field.Metadata[Key] = BoolVal;
148 else if (Key.Equals("AllowedFileTypes", StringComparison.OrdinalIgnoreCase))
149 Field.Metadata[Key] = Value.Split(',', ';', ' ').Where(s => !string.IsNullOrWhiteSpace(s)).ToArray();
150 else
151 Field.Metadata[Key] = Value;
152
153 // Handle special cases
154 // For livenessCheck image fields, disable manual upload
155 if (Field is ObservableImageField ImageField && Key.Equals("AllowUpload", StringComparison.OrdinalIgnoreCase) && bool.TryParse(Value, out bool ParsedBool))
156 ImageField.AllowUpload = ParsedBool;
157
158 // Special cases for PNR placeholders
159 if (Key.Equals("Placeholder", StringComparison.OrdinalIgnoreCase) && Value.Equals("pnr", StringComparison.OrdinalIgnoreCase))
160 {
161 string CountryCode = Owner.Values.TryGetValue("country", out string? Cc) && !string.IsNullOrEmpty(Cc) ? Cc : string.Empty;
162 if (string.IsNullOrEmpty(CountryCode))
163 {
164 try
165 {
166 CountryCode = ServiceRef.TagProfile.LegalIdentity?.GetPersonalInformation().Country ?? string.Empty;
167 }
168 catch (Exception)
169 {
170 CountryCode = string.Empty;
171 }
172 }
173 string? PnrExample = PersonalNumberSchemes.DisplayStringForCountry(CountryCode);
174 if (!string.IsNullOrEmpty(CountryCode) && !string.IsNullOrEmpty(PnrExample))
175 {
176 Field.Placeholder = new KycLocalizedText();
177 Field.Placeholder.Add(CountryCode, PnrExample);
178 }
179 }
180 }
181 }
182
183 // Set owner after metadata logic has executed (so placeholder override persists)
184 Field.SetOwnerProcess(Owner);
185
186 // Validation rules (<ValidationRules>)
187 void TryAddLengthRules(XElement RuleEl)
188 {
189 int? Min = null, Max = null;
190 if (int.TryParse((string?)RuleEl.Attribute("min"), out int MinVal)) Min = MinVal;
191 if (int.TryParse((string?)RuleEl.Attribute("max"), out int MaxVal)) Max = MaxVal;
192 if (Min.HasValue || Max.HasValue)
193 {
194 string? Msg = ParseLocalizedText(RuleEl.Element("Message"))?.Get(Lang);
195 Field.AddRule(new LengthRule(Min, Max, Msg));
196 }
197 }
198
199 void TryAddRegexRule(XElement RuleEl)
200 {
201 string? Pattern = RuleEl.Element("Regex")?.Value?.Trim();
202 if (!string.IsNullOrEmpty(Pattern))
203 {
204 string? Msg = ParseLocalizedText(RuleEl.Element("Message"))?.Get(Lang);
205 // Defensive: attempt to compile early to surface invalid patterns gracefully
206 try
207 {
208 System.Text.RegularExpressions.Regex.Match(string.Empty, Pattern); // minimal validation
209 Field.AddRule(new RegexRule(Pattern, Msg));
210 }
211 catch (Exception)
212 {
213 // Ignore invalid regex so a single bad pattern does not block entire process
214 }
215 }
216 }
217
218 void TryAddDateRangeRule(XElement RuleEl)
219 {
220 DateTime? DMin = null, DMax = null;
221 if (DateTime.TryParse((string?)RuleEl.Attribute("min"), out DateTime DMinVal)) DMin = DMinVal;
222 if (DateTime.TryParse((string?)RuleEl.Attribute("max"), out DateTime DMaxVal)) DMax = DMaxVal;
223 if (DMin.HasValue || DMax.HasValue)
224 {
225 string? Msg = ParseLocalizedText(RuleEl.Element("Message"))?.Get(Lang);
226 Field.AddRule(new DateRangeRule(DMin, DMax, Msg));
227 }
228 }
229
230 void TryAddPnrRule(XElement RuleEl)
231 {
232 string? FieldRef = (string?)RuleEl.Element("CountryFieldReference");
233 string? Msg = ParseLocalizedText(RuleEl.Element("Message"))?.Get(Lang);
234 Field.AddRule(new PersonalNumberRule(FieldRef, Msg));
235 }
236
237 XElement? ValidationEl = El.Element("ValidationRules");
238 if (ValidationEl is not null)
239 {
240 foreach (XElement Vr in ValidationEl.Elements())
241 {
242 switch (Vr.Name.ToString())
243 {
244 case "DateRangeRule":
245 TryAddDateRangeRule(Vr); break;
246 case "RegexRule":
247 TryAddRegexRule(Vr); break;
248 case "LengthRule":
249 TryAddLengthRules(Vr); break;
250 case "PersonalNumberRule":
251 TryAddPnrRule(Vr); break;
252 default:
253 break;
254 }
255 }
256 }
257
258 // Mappings
259 foreach (XElement Map in El.Elements("Mapping"))
260 {
261 KycMapping Mapping = new KycMapping
262 {
263 Key = (string?)Map.Attribute("key") ?? string.Empty
264 };
265
266 // New multi-transform syntax only
267 foreach (XElement Tx in Map.Elements("Transform"))
268 {
269 string? NameAttr = (string?)Tx.Attribute("name");
270 if (!string.IsNullOrWhiteSpace(NameAttr))
271 {
272 string Clean = NameAttr.Trim();
273 if (!Mapping.TransformNames.Any(t => string.Equals(t, Clean, StringComparison.OrdinalIgnoreCase)))
274 {
275 Mapping.TransformNames.Add(Clean);
276 }
277 }
278 }
279
280 Field.Mappings.Add(Mapping);
281 }
282
283 // Ensure personal number mappings include normalization transforms
284 bool HasPersonalNumberMapping = false;
285 foreach (KycMapping Mapping in Field.Mappings)
286 {
287 if (!string.Equals(Mapping.Key, Constants.XmppProperties.PersonalNumber, StringComparison.OrdinalIgnoreCase))
288 continue;
289 HasPersonalNumberMapping = true;
290 bool HasNormalize = Mapping.TransformNames.Any(name => string.Equals(name, "personalNumberNormalize", StringComparison.OrdinalIgnoreCase));
291 if (!HasNormalize)
292 Mapping.TransformNames.Add("personalNumberNormalize");
293 }
294
295 if (!HasPersonalNumberMapping && string.Equals(Field.Id, "personalNumber", StringComparison.OrdinalIgnoreCase))
296 {
297 KycMapping NormalizedMapping = new KycMapping { Key = Constants.XmppProperties.PersonalNumber };
298 NormalizedMapping.TransformNames.Add("personalNumberNormalize");
299 Field.Mappings.Add(NormalizedMapping);
300 }
301
302 // Options for pickers/checkboxes/radio/country
303 if (El.Element("Options") is XElement Opts)
304 {
305 foreach (XElement Opt in Opts.Elements("Option"))
306 {
307 string Val = (string?)Opt.Attribute("value") ?? string.Empty;
308 KycLocalizedText Lbl = ParseLocalizedText(Opt) ?? new KycLocalizedText();
309 Field.Options.Add(new KycOption(Val, Lbl));
310 }
311 }
312
313 // For country fields
314 if (Field is ObservablePickerField CountryField && Field.FieldType == FieldType.Country && Field.Options.Count == 0)
315 {
316 string DefaultCountry;
317 // Set default to current country
318 try
319 {
320 DefaultCountry = ServiceRef.TagProfile.LegalIdentity?.GetPersonalInformation().Country;
321 }
322 catch (Exception)
323 {
324 DefaultCountry = string.Empty;
325 }
326
327 // if no options defined, load all countries
328 if (CountryField.Options.Count == 0)
329 {
330 foreach (ISO_3166_Country Country in ISO_3166_1.Countries)
331 {
332 KycLocalizedText LocalizedText = new KycLocalizedText();
333 LocalizedText.Add(CultureInfo.CurrentCulture.TwoLetterISOLanguageName, Country.FlagAndName);
334
335 if (Country.Alpha2.Equals(DefaultCountry, StringComparison.OrdinalIgnoreCase))
336 {
337 Field.Options.Insert(0, new KycOption(Country.Alpha2, LocalizedText));
338 }
339 else
340 {
341 Field.Options.Add(new KycOption(Country.Alpha2, LocalizedText));
342 }
343 }
344 }
345
346 if (string.Equals(Field.Id, "country", StringComparison.OrdinalIgnoreCase))
347 {
348 string ExistingCountry = Owner.Values.TryGetValue(Field.Id, out string? StoredCountry) && !string.IsNullOrEmpty(StoredCountry)
349 ? StoredCountry
350 : string.Empty;
351
352 if (string.IsNullOrEmpty(ExistingCountry))
353 {
354 string SeedCountry = string.Empty;
355 try
356 {
357 string? SelectedCountry = ServiceRef.TagProfile.SelectedCountry;
358 if (!string.IsNullOrEmpty(SelectedCountry))
359 SeedCountry = SelectedCountry;
360 else
361 {
362 string? IdentityCountry = ServiceRef.TagProfile.LegalIdentity?.GetPersonalInformation().Country;
363 SeedCountry = string.IsNullOrEmpty(IdentityCountry) ? string.Empty : IdentityCountry;
364 }
365 }
366 catch (Exception)
367 {
368 SeedCountry = string.Empty;
369 }
370
371 if (!string.IsNullOrEmpty(SeedCountry))
372 {
373 Owner.Values[Field.Id] = SeedCountry;
374 Field.StringValue = SeedCountry;
375 }
376 }
377 }
378 }
379
380 // For gender fields, if no options defined, load default options
381 if (Field is ObservablePickerField && Field.FieldType == FieldType.Gender && Field.Options.Count == 0)
382 {
383 foreach (ISO_5218_Gender Gender in ISO_5218.Genders)
384 {
385 KycLocalizedText LocalizedText = new KycLocalizedText();
386 LocalizedText.Add(CultureInfo.CurrentCulture.TwoLetterISOLanguageName, new string(Gender.Unicode, 1) + "\t" + ServiceRef.Localizer[Gender.LocalizedNameId]);
387
388 Field.Options.Add(new KycOption(Gender.Letter, LocalizedText));
389 }
390 }
391
392 // Default value
393 string ? Def = El.Element("Default")?.Value?.Trim();
394 if (!string.IsNullOrEmpty(Def))
395 {
396 switch (Field)
397 {
398 case ObservableDateField DateField:
399 if (Def.Equals("now()", StringComparison.OrdinalIgnoreCase))
400 DateField.DateValue = DateTime.Today;
401 else if (DateTime.TryParse(Def, CultureInfo.InvariantCulture, DateTimeStyles.None, out DateTime Dt))
402 DateField.DateValue = Dt;
403 break;
404 case ObservableBooleanField BoolField:
405 if (bool.TryParse(Def, out bool Bv))
406 BoolField.BoolValue = Bv;
407 break;
408 case ObservableIntegerField IntField:
409 if (int.TryParse(Def, NumberStyles.Integer, CultureInfo.InvariantCulture, out int Iv))
410 IntField.IntValue = Iv;
411 break;
412 case ObservableDecimalField DecField:
413 if (decimal.TryParse(Def, NumberStyles.Number, CultureInfo.InvariantCulture, out decimal Dv))
414 DecField.DecimalValue = Dv;
415 break;
416 default:
417 Field.StringValue = Def;
418 break;
419 }
420 }
421
422 return Field;
423 }
424
425 private static KycCondition? ParseCondition(XElement? Cond)
426 {
427 if (Cond is null) return null;
428 return new KycCondition
429 {
430 FieldRef = Cond.Element("FieldRef")?.Value ?? string.Empty,
431 Equals = Cond.Element("Equals")?.Value
432 };
433 }
434
435 private static KycLocalizedText? ParseLocalizedText(XElement? Parent)
436 {
437 if (Parent is null) return null;
439 foreach (XElement Txt in Parent.Elements("Text"))
440 {
441 string Lang = (string?)Txt.Attribute("lang") ?? "en";
442 string? Val = Txt.Value?.Trim();
443 if (!string.IsNullOrEmpty(Val)) Loc.Add(Lang, Val);
444 }
445 if (!Loc.HasAny && !string.IsNullOrEmpty(Parent.Value?.Trim()))
446 Loc.Add("en", Parent.Value.Trim());
447 return Loc.HasAny ? Loc : null;
448 }
449 }
450}
const string PersonalNumber
Personal number
Definition: Constants.cs:394
A set of never changing property constants and helpful values.
Definition: Constants.cs:24
Conversion between Country Names and ISO-3166-1 country codes.
Definition: ISO_3166_1.cs:10
static ISO_3166_Country[] Countries
This collection built from Wikipedia entry on ISO3166-1 on 9th Feb 2016
Definition: ISO_3166_1.cs:37
Static class containing ISO 5218 gender codes
Definition: ISO_5218.cs:9
static readonly ISO_5218_Gender[] Genders
Available gender codes
Definition: ISO_5218.cs:58
Personal Number Schemes available in different countries.
static ? string DisplayStringForCountry(string CountryCode)
Gets the expected personal number format for the given country.
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.
Validates if date input is within a range.
Definition: KycRules.cs:142
Represents a set of localized strings, addressable by language code (e.g. "en", "sv").
string? Get(string? Lang)
Gets a localized value for a specific language.
void Add(string Lang, string Value)
Adds or replaces a localized text value.
bool HasAny
Gets a value indicating whether any localized strings have been added.
Mapping from a field value to an identity property, including optional transform pipeline.
Definition: KycMapping.cs:9
List< string > TransformNames
Ordered list of transform identifiers to apply. Empty means no transforms.
Definition: KycMapping.cs:18
string Key
Target identity property key.
Definition: KycMapping.cs:13
Represents a page in a KYC process containing fields and sections.
Definition: KycPage.cs:20
Represents a parsed KYC process with pages, fields, and current values.
Definition: KycProcess.cs:11
IDictionary< string, string?> Values
Gets a modifiable dictionary of field values keyed by field identifier.
Definition: KycProcess.cs:21
Validates text length for string fields.
Definition: KycRules.cs:87
Validates string input against a regex.
Definition: KycRules.cs:118
The base KYC field model. Use as-is for generic fields, or subclass for custom logic.
FieldType FieldType
The field type (input, picker, info, etc).
Base class that references services in the app.
Definition: ServiceRef.cs:43
static ITagProfile TagProfile
TAG Profile service.
Definition: ServiceRef.cs:202
static IReportingStringLocalizer Localizer
Localization service
Definition: ServiceRef.cs:370
Definition: ImplTypes.g.cs:58
class ISO_3166_Country(string Name, string Alpha2, string Alpha3, int NumericCode, string DialCode, EmojiInfo? EmojiInfo=null)
Representation of an ISO3166-1 Country
class ISO_5218_Gender(string Gender, int Code, string Letter, string LocalizedNameId, char Unicode)
Contains one record of the ISO 5218 data set.
FieldType
Enumeration of all supported field types in the KYC process.
Gender
Gender classification in a legal ID.
Definition: Gender.cs:7