From 27b7bfb38cc3223ad8da811a39102febf857e556 Mon Sep 17 00:00:00 2001 From: Raffael Herrmann Date: Tue, 2 Nov 2021 19:25:29 +0100 Subject: [PATCH 01/10] Initial version of RussianPaymentPayload Grep from @AlexandreZaytsev --- QRCoder/PayloadGenerator.cs | 188 ++++++++++++++++++++++++++++++++++++ 1 file changed, 188 insertions(+) diff --git a/QRCoder/PayloadGenerator.cs b/QRCoder/PayloadGenerator.cs index d3b83c13..06235a96 100644 --- a/QRCoder/PayloadGenerator.cs +++ b/QRCoder/PayloadGenerator.cs @@ -2386,6 +2386,194 @@ public SlovenianUpnQr(string payerName, string payerAddress, string payerPlace, _recipientSiReference = LimitLength(recipientSiReference.Trim(), 22); } + public class RussiaPaymentOrder : Payload + { + //https://docs.cntd.ru/document/1200110981 + //https://roskazna.gov.ru/upload/iblock/5fa/gost_r_56042_2014.pdf + //https://sbqr.ru/standard/files/standart.pdf + + //base + private CharacterSets characterSet; + // [Required(ErrorMessage = "Name must be a filled string max. 160 characters", AllowEmptyStrings = true)] + private readonly string Name;// { get; set; } // 1-160 char + private readonly string PersonalAcc; // 20 digit (UInt64) + private readonly string BankName; // 1-45 char + private readonly string BIC; // 9 digit (UInt32) + private readonly string CorrespAcc = "0"; // up to 20 digit (0-default) (UInt64) + //extend + private readonly string Sum; + private readonly string Purpose; + private readonly string PayeeINN; + private readonly string PayerINN; + private readonly string DrawerStatus; + private readonly string KPP; + private readonly string CBC; + private readonly string OKTMO; + private readonly string PaytReason; + private readonly string ТaxPeriod; + private readonly string DocNo; + private readonly DateTime? DocDate; + private readonly string TaxPaytKind; + //other + private readonly string LastName; + private readonly string FirstName; + private readonly string MiddleName; + private readonly string PayerAddress; + private readonly string PersonalAccount; + private readonly string DocIdx; + private readonly string PensAcc; + private readonly string Contract; + private readonly string PersAcc; + private readonly string Flat; + private readonly string Phone; + private readonly string PayerIdType; + private readonly string PayerIdNum; + private readonly string ChildFio; + private readonly DateTime? BirthDate; + private readonly string PaymTerm; + private readonly string PaymPeriod; + private readonly string Category; + private readonly string ServiceName; + private readonly string CounterId; + private readonly string CounterVal; + private readonly string QuittId; + private readonly DateTime? QuittDate; + private readonly string InstNum; + private readonly string ClassNum; + private readonly string SpecFio; + private readonly string AddAmount; + private readonly string RuleId; + private readonly string ExecId; + private readonly string RegType; + private readonly string UIN; + private readonly TechCode techCode; + + public RussiaPaymentOrder(CharacterSets characterSet, string Name, string PersonalAcc, string BankName, string BIC, string CorrespAcc = "0") + { + this.characterSet = characterSet; + this.Name = Name; + this.PersonalAcc = PersonalAcc; + this.BankName = BankName; + this.BIC = BIC; + this.CorrespAcc = CorrespAcc; + } + public RussiaPaymentOrder(CharacterSets characterSet, string Name, string PersonalAcc, string BankName, string BIC, string CorrespAcc = "0", + string PayeeINN = "", string LastName = "", string FirstName = "", string MiddleName = "", string Purpose = "", string PayerAddress = "", string Sum = "0") + { + this.characterSet = characterSet; + this.Name = Name; + this.PersonalAcc = PersonalAcc; + this.BankName = BankName; + this.BIC = BIC; + this.CorrespAcc = CorrespAcc; + + if (!string.IsNullOrEmpty(PayeeINN) && !(PayeeINN.Length >= 10 && Regex.IsMatch(PayeeINN.Replace(" ", ""), @"^[0-9]+$"))) + throw new RussiaPaymentOrderException("PayeeINN must be a filled 1-10(12) digits."); + if (!string.IsNullOrEmpty(Sum) && !(Sum.Length <= 18 && Regex.IsMatch(Sum.Replace(" ", ""), @"^[0-9]+$"))) + throw new Exception("Sum must be a filled 1-18 digits (*including Sum/100 (the last two digits) without a separator sign)"); + + this.PayeeINN = PayeeINN; + this.LastName = LastName; + this.FirstName = FirstName; + this.MiddleName = MiddleName; + this.Purpose = Purpose; + this.PayerAddress = PayerAddress; + this.Sum = Sum; + + /* + if (string.IsNullOrEmpty(Name)) + throw new RussiaPaymentOrderException("Name must be a filled string max. 160 characters."); + if (string.IsNullOrEmpty(PersonalAcc)) + throw new RussiaPaymentOrderException("PersonalAcc must be a filled string max. 20 characters."); + if (string.IsNullOrEmpty(BankName)) + throw new RussiaPaymentOrderException("BankName must be a filled string max. 45 characters."); + if (string.IsNullOrEmpty(BIC)) + throw new RussiaPaymentOrderException("BIC must be a filled string max. 9 characters."); + if (string.IsNullOrEmpty(CorrespAcc)) + throw new RussiaPaymentOrderException("CorrespAcc must be a filled string max. 20 characters."); + */ + + } + + public override string ToString() + { + if (string.IsNullOrEmpty(Name) && PersonalAcc.Length <= 160) + throw new Exception("Name must be a filled string 1-160 characters"); + if (!(!string.IsNullOrEmpty(PersonalAcc) && PersonalAcc.Length == 20 && Regex.IsMatch(PersonalAcc.Replace(" ", ""), @"^[0-9]+$"))) + throw new Exception("PersonalAcc must be a filled strong 20 digits"); + if (string.IsNullOrEmpty(BankName) && BankName.Length <= 45) + throw new Exception("BankName must be a filled string 1-45 characters"); + if (!(!string.IsNullOrEmpty(BIC) && BIC.Length == 9 && Regex.IsMatch(BIC.Replace(" ", ""), @"^[0-9]+$"))) + throw new Exception("BIC must be a filled strong 9 digits"); + if (!(!string.IsNullOrEmpty(CorrespAcc) && CorrespAcc.Length <= 20 && Regex.IsMatch(CorrespAcc.Replace(" ", ""), @"^[0-9]+$"))) + throw new Exception("CorrespAcc must be a filled 1-20 digits or 0 value if empty"); + + string ret = $"ST0001" + ((int)characterSet).ToString() + $"|Name={this.Name}" + + $"|PersonalAcc={this.PersonalAcc}" + + $"|BankName={this.BankName}" + + $"|BIC={this.BIC}" + + $"|CorrespAcc={this.CorrespAcc}" + + $"|PayeeINN={this.PayeeINN}" + + $"|LastName={this.LastName}" + + $"|FirstName={this.FirstName}" + + $"|MiddleName={this.MiddleName}" + + $"|Purpose={this.Purpose}" + + $"|PayerAddress={this.PayerAddress}" + + $"|Sum={this.Sum}" + ; + + string page = this.characterSet.ToString().Replace("_", "-"); + return Encoding.GetEncoding(page).GetString(Encoding.Convert(Encoding.Default, Encoding.GetEncoding(page), Encoding.GetEncoding(page).GetBytes(ret))); + } + + /// + /// Перечень значений технического кода платежа + /// (List of values of the technical code of the payment) + /// + private enum TechCode + { + Мобильная_связь_стационарный_телефон = 01, + Коммунальные_услуги_ЖКХAFN = 02, + ГИБДД_налоги_пошлины_бюджетные_платежи = 03, + Охранные_услуги = 04, + Услуги_оказываемые_УФМС = 05, + ПФР = 06, + Погашение_кредитов = 07, + Образовательные_учреждения = 08, + Интернет_и_ТВ = 09, + Электронные_деньги = 10, + Отдых_и_путешествия = 11, + Инвестиции_и_страхование = 12, + Спорт_и_здоровье = 13, + Благотворительные_и_общественные_организации = 14, + Прочие_услуги = 15 + } + + public enum CharacterSets + { + windows_1251 = 1, // Encoding.GetEncoding("windows-1251") + utf_8 = 2, // Encoding.UTF8 + koi8_r = 3 // Encoding.GetEncoding("koi8-r") + + } + + public class RussiaPaymentOrderException : Exception + { + public RussiaPaymentOrderException() + { + } + public RussiaPaymentOrderException(string message) + : base(message) + { + } + public RussiaPaymentOrderException(string message, Exception inner) + : base(message, inner) + { + } + } + + } + private string FormatAmount(double amount) { int _amt = (int)Math.Round(amount * 100.0); From df3eeab2959e6a4c13cd0b2f520a0bf4a543a540 Mon Sep 17 00:00:00 2001 From: Raffael Herrmann Date: Tue, 2 Nov 2021 19:33:33 +0100 Subject: [PATCH 02/10] Added NETSTANDARD1_1 switch --- QRCoder/PayloadGenerator.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/QRCoder/PayloadGenerator.cs b/QRCoder/PayloadGenerator.cs index 06235a96..d4e5023c 100644 --- a/QRCoder/PayloadGenerator.cs +++ b/QRCoder/PayloadGenerator.cs @@ -2523,7 +2523,12 @@ public override string ToString() ; string page = this.characterSet.ToString().Replace("_", "-"); +#if NETSTANDARD1_1 + var bytes = Encoding.Convert(Encoding.UTF8, Encoding.GetEncoding(page), Encoding.GetEncoding(page).GetBytes(ret)); + return Encoding.GetEncoding(page).GetString(bytes,0,bytes.Length); +#else return Encoding.GetEncoding(page).GetString(Encoding.Convert(Encoding.Default, Encoding.GetEncoding(page), Encoding.GetEncoding(page).GetBytes(ret))); +#endif } /// From 7ba0d71e2c93c2e2ec53b162e413e40e796b5f57 Mon Sep 17 00:00:00 2001 From: Raffael Herrmann Date: Tue, 2 Nov 2021 19:48:54 +0100 Subject: [PATCH 03/10] Move to correct class --- QRCoder/PayloadGenerator.cs | 391 ++++++++++++++++++------------------ 1 file changed, 197 insertions(+), 194 deletions(-) diff --git a/QRCoder/PayloadGenerator.cs b/QRCoder/PayloadGenerator.cs index d4e5023c..919889f3 100644 --- a/QRCoder/PayloadGenerator.cs +++ b/QRCoder/PayloadGenerator.cs @@ -2385,199 +2385,7 @@ public SlovenianUpnQr(string payerName, string payerAddress, string payerPlace, _recipientSiModel = LimitLength(recipientSiModel.Trim().ToUpper(), 4); _recipientSiReference = LimitLength(recipientSiReference.Trim(), 22); } - - public class RussiaPaymentOrder : Payload - { - //https://docs.cntd.ru/document/1200110981 - //https://roskazna.gov.ru/upload/iblock/5fa/gost_r_56042_2014.pdf - //https://sbqr.ru/standard/files/standart.pdf - - //base - private CharacterSets characterSet; - // [Required(ErrorMessage = "Name must be a filled string max. 160 characters", AllowEmptyStrings = true)] - private readonly string Name;// { get; set; } // 1-160 char - private readonly string PersonalAcc; // 20 digit (UInt64) - private readonly string BankName; // 1-45 char - private readonly string BIC; // 9 digit (UInt32) - private readonly string CorrespAcc = "0"; // up to 20 digit (0-default) (UInt64) - //extend - private readonly string Sum; - private readonly string Purpose; - private readonly string PayeeINN; - private readonly string PayerINN; - private readonly string DrawerStatus; - private readonly string KPP; - private readonly string CBC; - private readonly string OKTMO; - private readonly string PaytReason; - private readonly string ТaxPeriod; - private readonly string DocNo; - private readonly DateTime? DocDate; - private readonly string TaxPaytKind; - //other - private readonly string LastName; - private readonly string FirstName; - private readonly string MiddleName; - private readonly string PayerAddress; - private readonly string PersonalAccount; - private readonly string DocIdx; - private readonly string PensAcc; - private readonly string Contract; - private readonly string PersAcc; - private readonly string Flat; - private readonly string Phone; - private readonly string PayerIdType; - private readonly string PayerIdNum; - private readonly string ChildFio; - private readonly DateTime? BirthDate; - private readonly string PaymTerm; - private readonly string PaymPeriod; - private readonly string Category; - private readonly string ServiceName; - private readonly string CounterId; - private readonly string CounterVal; - private readonly string QuittId; - private readonly DateTime? QuittDate; - private readonly string InstNum; - private readonly string ClassNum; - private readonly string SpecFio; - private readonly string AddAmount; - private readonly string RuleId; - private readonly string ExecId; - private readonly string RegType; - private readonly string UIN; - private readonly TechCode techCode; - - public RussiaPaymentOrder(CharacterSets characterSet, string Name, string PersonalAcc, string BankName, string BIC, string CorrespAcc = "0") - { - this.characterSet = characterSet; - this.Name = Name; - this.PersonalAcc = PersonalAcc; - this.BankName = BankName; - this.BIC = BIC; - this.CorrespAcc = CorrespAcc; - } - public RussiaPaymentOrder(CharacterSets characterSet, string Name, string PersonalAcc, string BankName, string BIC, string CorrespAcc = "0", - string PayeeINN = "", string LastName = "", string FirstName = "", string MiddleName = "", string Purpose = "", string PayerAddress = "", string Sum = "0") - { - this.characterSet = characterSet; - this.Name = Name; - this.PersonalAcc = PersonalAcc; - this.BankName = BankName; - this.BIC = BIC; - this.CorrespAcc = CorrespAcc; - - if (!string.IsNullOrEmpty(PayeeINN) && !(PayeeINN.Length >= 10 && Regex.IsMatch(PayeeINN.Replace(" ", ""), @"^[0-9]+$"))) - throw new RussiaPaymentOrderException("PayeeINN must be a filled 1-10(12) digits."); - if (!string.IsNullOrEmpty(Sum) && !(Sum.Length <= 18 && Regex.IsMatch(Sum.Replace(" ", ""), @"^[0-9]+$"))) - throw new Exception("Sum must be a filled 1-18 digits (*including Sum/100 (the last two digits) without a separator sign)"); - - this.PayeeINN = PayeeINN; - this.LastName = LastName; - this.FirstName = FirstName; - this.MiddleName = MiddleName; - this.Purpose = Purpose; - this.PayerAddress = PayerAddress; - this.Sum = Sum; - - /* - if (string.IsNullOrEmpty(Name)) - throw new RussiaPaymentOrderException("Name must be a filled string max. 160 characters."); - if (string.IsNullOrEmpty(PersonalAcc)) - throw new RussiaPaymentOrderException("PersonalAcc must be a filled string max. 20 characters."); - if (string.IsNullOrEmpty(BankName)) - throw new RussiaPaymentOrderException("BankName must be a filled string max. 45 characters."); - if (string.IsNullOrEmpty(BIC)) - throw new RussiaPaymentOrderException("BIC must be a filled string max. 9 characters."); - if (string.IsNullOrEmpty(CorrespAcc)) - throw new RussiaPaymentOrderException("CorrespAcc must be a filled string max. 20 characters."); - */ - - } - - public override string ToString() - { - if (string.IsNullOrEmpty(Name) && PersonalAcc.Length <= 160) - throw new Exception("Name must be a filled string 1-160 characters"); - if (!(!string.IsNullOrEmpty(PersonalAcc) && PersonalAcc.Length == 20 && Regex.IsMatch(PersonalAcc.Replace(" ", ""), @"^[0-9]+$"))) - throw new Exception("PersonalAcc must be a filled strong 20 digits"); - if (string.IsNullOrEmpty(BankName) && BankName.Length <= 45) - throw new Exception("BankName must be a filled string 1-45 characters"); - if (!(!string.IsNullOrEmpty(BIC) && BIC.Length == 9 && Regex.IsMatch(BIC.Replace(" ", ""), @"^[0-9]+$"))) - throw new Exception("BIC must be a filled strong 9 digits"); - if (!(!string.IsNullOrEmpty(CorrespAcc) && CorrespAcc.Length <= 20 && Regex.IsMatch(CorrespAcc.Replace(" ", ""), @"^[0-9]+$"))) - throw new Exception("CorrespAcc must be a filled 1-20 digits or 0 value if empty"); - - string ret = $"ST0001" + ((int)characterSet).ToString() + $"|Name={this.Name}" + - $"|PersonalAcc={this.PersonalAcc}" + - $"|BankName={this.BankName}" + - $"|BIC={this.BIC}" + - $"|CorrespAcc={this.CorrespAcc}" + - $"|PayeeINN={this.PayeeINN}" + - $"|LastName={this.LastName}" + - $"|FirstName={this.FirstName}" + - $"|MiddleName={this.MiddleName}" + - $"|Purpose={this.Purpose}" + - $"|PayerAddress={this.PayerAddress}" + - $"|Sum={this.Sum}" - ; - - string page = this.characterSet.ToString().Replace("_", "-"); -#if NETSTANDARD1_1 - var bytes = Encoding.Convert(Encoding.UTF8, Encoding.GetEncoding(page), Encoding.GetEncoding(page).GetBytes(ret)); - return Encoding.GetEncoding(page).GetString(bytes,0,bytes.Length); -#else - return Encoding.GetEncoding(page).GetString(Encoding.Convert(Encoding.Default, Encoding.GetEncoding(page), Encoding.GetEncoding(page).GetBytes(ret))); -#endif - } - - /// - /// Перечень значений технического кода платежа - /// (List of values of the technical code of the payment) - /// - private enum TechCode - { - Мобильная_связь_стационарный_телефон = 01, - Коммунальные_услуги_ЖКХAFN = 02, - ГИБДД_налоги_пошлины_бюджетные_платежи = 03, - Охранные_услуги = 04, - Услуги_оказываемые_УФМС = 05, - ПФР = 06, - Погашение_кредитов = 07, - Образовательные_учреждения = 08, - Интернет_и_ТВ = 09, - Электронные_деньги = 10, - Отдых_и_путешествия = 11, - Инвестиции_и_страхование = 12, - Спорт_и_здоровье = 13, - Благотворительные_и_общественные_организации = 14, - Прочие_услуги = 15 - } - - public enum CharacterSets - { - windows_1251 = 1, // Encoding.GetEncoding("windows-1251") - utf_8 = 2, // Encoding.UTF8 - koi8_r = 3 // Encoding.GetEncoding("koi8-r") - - } - - public class RussiaPaymentOrderException : Exception - { - public RussiaPaymentOrderException() - { - } - public RussiaPaymentOrderException(string message) - : base(message) - { - } - public RussiaPaymentOrderException(string message, Exception inner) - : base(message, inner) - { - } - } - - } + private string FormatAmount(double amount) { @@ -2624,7 +2432,202 @@ public override string ToString() _sb.AppendFormat("{0:000}", CalculateChecksum()).Append('\n'); return _sb.ToString(); } - } + } + + + public class RussiaPaymentOrder : Payload + { + //https://docs.cntd.ru/document/1200110981 + //https://roskazna.gov.ru/upload/iblock/5fa/gost_r_56042_2014.pdf + //https://sbqr.ru/standard/files/standart.pdf + + //base + private CharacterSets characterSet; + // [Required(ErrorMessage = "Name must be a filled string max. 160 characters", AllowEmptyStrings = true)] + private readonly string Name;// { get; set; } // 1-160 char + private readonly string PersonalAcc; // 20 digit (UInt64) + private readonly string BankName; // 1-45 char + private readonly string BIC; // 9 digit (UInt32) + private readonly string CorrespAcc = "0"; // up to 20 digit (0-default) (UInt64) + //extend + private readonly string Sum; + private readonly string Purpose; + private readonly string PayeeINN; + private readonly string PayerINN; + private readonly string DrawerStatus; + private readonly string KPP; + private readonly string CBC; + private readonly string OKTMO; + private readonly string PaytReason; + private readonly string ТaxPeriod; + private readonly string DocNo; + private readonly DateTime? DocDate; + private readonly string TaxPaytKind; + //other + private readonly string LastName; + private readonly string FirstName; + private readonly string MiddleName; + private readonly string PayerAddress; + private readonly string PersonalAccount; + private readonly string DocIdx; + private readonly string PensAcc; + private readonly string Contract; + private readonly string PersAcc; + private readonly string Flat; + private readonly string Phone; + private readonly string PayerIdType; + private readonly string PayerIdNum; + private readonly string ChildFio; + private readonly DateTime? BirthDate; + private readonly string PaymTerm; + private readonly string PaymPeriod; + private readonly string Category; + private readonly string ServiceName; + private readonly string CounterId; + private readonly string CounterVal; + private readonly string QuittId; + private readonly DateTime? QuittDate; + private readonly string InstNum; + private readonly string ClassNum; + private readonly string SpecFio; + private readonly string AddAmount; + private readonly string RuleId; + private readonly string ExecId; + private readonly string RegType; + private readonly string UIN; + private readonly TechCode techCode; + + public RussiaPaymentOrder(CharacterSets characterSet, string Name, string PersonalAcc, string BankName, string BIC, string CorrespAcc = "0") + { + this.characterSet = characterSet; + this.Name = Name; + this.PersonalAcc = PersonalAcc; + this.BankName = BankName; + this.BIC = BIC; + this.CorrespAcc = CorrespAcc; + } + public RussiaPaymentOrder(CharacterSets characterSet, string Name, string PersonalAcc, string BankName, string BIC, string CorrespAcc = "0", + string PayeeINN = "", string LastName = "", string FirstName = "", string MiddleName = "", string Purpose = "", string PayerAddress = "", string Sum = "0") + { + this.characterSet = characterSet; + this.Name = Name; + this.PersonalAcc = PersonalAcc; + this.BankName = BankName; + this.BIC = BIC; + this.CorrespAcc = CorrespAcc; + + if (!string.IsNullOrEmpty(PayeeINN) && !(PayeeINN.Length >= 10 && Regex.IsMatch(PayeeINN.Replace(" ", ""), @"^[0-9]+$"))) + throw new RussiaPaymentOrderException("PayeeINN must be a filled 1-10(12) digits."); + if (!string.IsNullOrEmpty(Sum) && !(Sum.Length <= 18 && Regex.IsMatch(Sum.Replace(" ", ""), @"^[0-9]+$"))) + throw new Exception("Sum must be a filled 1-18 digits (*including Sum/100 (the last two digits) without a separator sign)"); + + this.PayeeINN = PayeeINN; + this.LastName = LastName; + this.FirstName = FirstName; + this.MiddleName = MiddleName; + this.Purpose = Purpose; + this.PayerAddress = PayerAddress; + this.Sum = Sum; + + /* + if (string.IsNullOrEmpty(Name)) + throw new RussiaPaymentOrderException("Name must be a filled string max. 160 characters."); + if (string.IsNullOrEmpty(PersonalAcc)) + throw new RussiaPaymentOrderException("PersonalAcc must be a filled string max. 20 characters."); + if (string.IsNullOrEmpty(BankName)) + throw new RussiaPaymentOrderException("BankName must be a filled string max. 45 characters."); + if (string.IsNullOrEmpty(BIC)) + throw new RussiaPaymentOrderException("BIC must be a filled string max. 9 characters."); + if (string.IsNullOrEmpty(CorrespAcc)) + throw new RussiaPaymentOrderException("CorrespAcc must be a filled string max. 20 characters."); +*/ + + } + + public override string ToString() + { + if (string.IsNullOrEmpty(Name) && PersonalAcc.Length <= 160) + throw new Exception("Name must be a filled string 1-160 characters"); + if (!(!string.IsNullOrEmpty(PersonalAcc) && PersonalAcc.Length == 20 && Regex.IsMatch(PersonalAcc.Replace(" ", ""), @"^[0-9]+$"))) + throw new Exception("PersonalAcc must be a filled strong 20 digits"); + if (string.IsNullOrEmpty(BankName) && BankName.Length <= 45) + throw new Exception("BankName must be a filled string 1-45 characters"); + if (!(!string.IsNullOrEmpty(BIC) && BIC.Length == 9 && Regex.IsMatch(BIC.Replace(" ", ""), @"^[0-9]+$"))) + throw new Exception("BIC must be a filled strong 9 digits"); + if (!(!string.IsNullOrEmpty(CorrespAcc) && CorrespAcc.Length <= 20 && Regex.IsMatch(CorrespAcc.Replace(" ", ""), @"^[0-9]+$"))) + throw new Exception("CorrespAcc must be a filled 1-20 digits or 0 value if empty"); + + string ret = $"ST0001" + ((int)characterSet).ToString() + $"|Name={this.Name}" + + $"|PersonalAcc={this.PersonalAcc}" + + $"|BankName={this.BankName}" + + $"|BIC={this.BIC}" + + $"|CorrespAcc={this.CorrespAcc}" + + $"|PayeeINN={this.PayeeINN}" + + $"|LastName={this.LastName}" + + $"|FirstName={this.FirstName}" + + $"|MiddleName={this.MiddleName}" + + $"|Purpose={this.Purpose}" + + $"|PayerAddress={this.PayerAddress}" + + $"|Sum={this.Sum}" + ; + + string page = this.characterSet.ToString().Replace("_", "-"); +#if NETSTANDARD1_1 + var bytes = Encoding.Convert(Encoding.UTF8, Encoding.GetEncoding(page), Encoding.GetEncoding(page).GetBytes(ret)); + return Encoding.GetEncoding(page).GetString(bytes,0,bytes.Length); +#else + return Encoding.GetEncoding(page).GetString(Encoding.Convert(Encoding.Default, Encoding.GetEncoding(page), Encoding.GetEncoding(page).GetBytes(ret))); +#endif + } + + /// + /// Перечень значений технического кода платежа + /// (List of values of the technical code of the payment) + /// + private enum TechCode + { + Мобильная_связь_стационарный_телефон = 01, + Коммунальные_услуги_ЖКХAFN = 02, + ГИБДД_налоги_пошлины_бюджетные_платежи = 03, + Охранные_услуги = 04, + Услуги_оказываемые_УФМС = 05, + ПФР = 06, + Погашение_кредитов = 07, + Образовательные_учреждения = 08, + Интернет_и_ТВ = 09, + Электронные_деньги = 10, + Отдых_и_путешествия = 11, + Инвестиции_и_страхование = 12, + Спорт_и_здоровье = 13, + Благотворительные_и_общественные_организации = 14, + Прочие_услуги = 15 + } + + public enum CharacterSets + { + windows_1251 = 1, // Encoding.GetEncoding("windows-1251") + utf_8 = 2, // Encoding.UTF8 + koi8_r = 3 // Encoding.GetEncoding("koi8-r") + + } + + public class RussiaPaymentOrderException : Exception + { + public RussiaPaymentOrderException() + { + } + public RussiaPaymentOrderException(string message) + : base(message) + { + } + public RussiaPaymentOrderException(string message, Exception inner) + : base(message, inner) + { + } + } + + } + private static bool IsValidIban(string iban) { From e03302f76135c5c42b45a96846c4ea1570b6d156 Mon Sep 17 00:00:00 2001 From: Raffael Herrmann Date: Tue, 2 Nov 2021 20:28:08 +0100 Subject: [PATCH 04/10] Added first test cases for RussiaPayment --- QRCoderTests/PayloadGeneratorTests.cs | 52 +++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/QRCoderTests/PayloadGeneratorTests.cs b/QRCoderTests/PayloadGeneratorTests.cs index 64717574..e4236bab 100644 --- a/QRCoderTests/PayloadGeneratorTests.cs +++ b/QRCoderTests/PayloadGeneratorTests.cs @@ -3054,6 +3054,58 @@ public void monero_generator_should_throw_no_address_exception() Assert.IsType(exception); exception.Message.ShouldBe("The address is mandatory and has to be set."); } + + + [Fact] + [Category("PayloadGenerator/RussiaPaymentOrder")] + public void russiapayment_generator_can_generate_payload_minimal() + { + var account = "40702810138250123017"; + var bic = "044525225"; + var bankName = "=ОАО \"БАНК\""; + var name = "ООО «Три кита»"; + var generator = new PayloadGenerator.RussiaPaymentOrder(PayloadGenerator.RussiaPaymentOrder.CharacterSets.utf_8, name, account, bankName, bic); + + generator + .ToString() + .ShouldBe($"ST00012|Name={name}|PersonalAcc={account}|BankName={bankName}|BIC={bic}|CorrespAcc=0"); + } + + [Fact] + [Category("PayloadGenerator/RussiaPaymentOrder")] + public void russiapayment_generator_can_generate_payload_mandatory_fields() + { + var account = "40702810138250123017"; + var bic = "044525225"; + var bankName = "=ОАО \"БАНК\""; + var name = "ООО «Три кита»"; + var correspAcc = "30101810400000000225"; + var generator = new PayloadGenerator.RussiaPaymentOrder(PayloadGenerator.RussiaPaymentOrder.CharacterSets.utf_8, name, account, bankName, bic, correspAcc); + + generator + .ToString() + .ShouldBe($"ST00012|Name={name}|PersonalAcc={account}|BankName={bankName}|BIC={bic}|CorrespAcc={correspAcc}"); + } + + [Fact] + [Category("PayloadGenerator/RussiaPaymentOrder")] + public void russiapayment_generator_can_generate_payload_some_additional_fields() + { + var account = "40702810138250123017"; + var bic = "044525225"; + var bankName = "=ОАО \"БАНК\""; + var name = "ООО «Три кита»"; + var correspAcc = "30101810400000000225"; + var firstName = "Raffael"; + var lastName = "Herrmann"; + var sum = 999.95d; + + var generator = new PayloadGenerator.RussiaPaymentOrder(PayloadGenerator.RussiaPaymentOrder.CharacterSets.utf_8, name, account, bankName, bic, correspAcc, LastName: lastName, FirstName: firstName, Sum: sum); + + generator + .ToString() + .ShouldBe($"ST00012|Name={name}|PersonalAcc={account}|BankName={bankName}|BIC={bic}|CorrespAcc={correspAcc}|FirstName={firstName}|LastName={lastName}|Sum={sum}"); + } } } From e45437a1b8e9469491a4b9f3fa28b04cedc2b36b Mon Sep 17 00:00:00 2001 From: Raffael Herrmann Date: Wed, 3 Nov 2021 19:09:15 +0100 Subject: [PATCH 05/10] Refined testcases for RussiaPaymentOrder --- QRCoderTests/PayloadGeneratorTests.cs | 36 ++++++++++++++++++--------- 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/QRCoderTests/PayloadGeneratorTests.cs b/QRCoderTests/PayloadGeneratorTests.cs index e4236bab..0779f4e3 100644 --- a/QRCoderTests/PayloadGeneratorTests.cs +++ b/QRCoderTests/PayloadGeneratorTests.cs @@ -3058,53 +3058,65 @@ public void monero_generator_should_throw_no_address_exception() [Fact] [Category("PayloadGenerator/RussiaPaymentOrder")] - public void russiapayment_generator_can_generate_payload_minimal() + public void russiapayment_generator_can_generate_payload_mandatory_fields() { var account = "40702810138250123017"; var bic = "044525225"; var bankName = "=ОАО \"БАНК\""; var name = "ООО «Три кита»"; - var generator = new PayloadGenerator.RussiaPaymentOrder(PayloadGenerator.RussiaPaymentOrder.CharacterSets.utf_8, name, account, bankName, bic); + var correspAcc = "30101810400000000225"; + var generator = new PayloadGenerator.RussiaPaymentOrder(PayloadGenerator.RussiaPaymentOrder.CharacterSets.utf_8, name, account, bankName, bic, correspAcc); generator .ToString() - .ShouldBe($"ST00012|Name={name}|PersonalAcc={account}|BankName={bankName}|BIC={bic}|CorrespAcc=0"); + .ShouldBe($"ST00012|Name={name}|PersonalAcc={account}|BankName={bankName}|BIC={bic}|CorrespAcc={correspAcc}"); } [Fact] [Category("PayloadGenerator/RussiaPaymentOrder")] - public void russiapayment_generator_can_generate_payload_mandatory_fields() + public void russiapayment_generator_can_generate_payload_some_additional_fields() { var account = "40702810138250123017"; var bic = "044525225"; var bankName = "=ОАО \"БАНК\""; var name = "ООО «Три кита»"; var correspAcc = "30101810400000000225"; - var generator = new PayloadGenerator.RussiaPaymentOrder(PayloadGenerator.RussiaPaymentOrder.CharacterSets.utf_8, name, account, bankName, bic, correspAcc); + var optionalFields = new PayloadGenerator.RussiaPaymentOrder.OptionalFields() + { + FirstName = "Raffael", + LastName = "Herrmann", + Sum = "125000" + }; + + var generator = new PayloadGenerator.RussiaPaymentOrder(PayloadGenerator.RussiaPaymentOrder.CharacterSets.utf_8, name, account, bankName, bic, correspAcc, optionalFields: optionalFields); generator .ToString() - .ShouldBe($"ST00012|Name={name}|PersonalAcc={account}|BankName={bankName}|BIC={bic}|CorrespAcc={correspAcc}"); + .ShouldBe($"ST00012|Name={name}|PersonalAcc={account}|BankName={bankName}|BIC={bic}|CorrespAcc=0|Sum={optionalFields.Sum}|LastName={optionalFields.LastName}|FirstName={optionalFields.FirstName}"); } [Fact] [Category("PayloadGenerator/RussiaPaymentOrder")] - public void russiapayment_generator_can_generate_payload_some_additional_fields() + public void russiapayment_generator_can_generate_payload_all_additional_fields() { var account = "40702810138250123017"; var bic = "044525225"; var bankName = "=ОАО \"БАНК\""; var name = "ООО «Три кита»"; var correspAcc = "30101810400000000225"; - var firstName = "Raffael"; - var lastName = "Herrmann"; - var sum = 999.95d; + var optionalFields = new PayloadGenerator.RussiaPaymentOrder.OptionalFields() + { + Sum = "50000", + Purpose = "Your loan", + PayeeINN = "9909373824", + PayerINN = "1832090230" + }; - var generator = new PayloadGenerator.RussiaPaymentOrder(PayloadGenerator.RussiaPaymentOrder.CharacterSets.utf_8, name, account, bankName, bic, correspAcc, LastName: lastName, FirstName: firstName, Sum: sum); + var generator = new PayloadGenerator.RussiaPaymentOrder(PayloadGenerator.RussiaPaymentOrder.CharacterSets.utf_8, name, account, bankName, bic, correspAcc, optionalFields: optionalFields); generator .ToString() - .ShouldBe($"ST00012|Name={name}|PersonalAcc={account}|BankName={bankName}|BIC={bic}|CorrespAcc={correspAcc}|FirstName={firstName}|LastName={lastName}|Sum={sum}"); + .ShouldBe($"ST00012|Name={name}|PersonalAcc={account}|BankName={bankName}|BIC={bic}|CorrespAcc=0|Sum={optionalFields.Sum}|LastName={optionalFields.LastName}|FirstName={optionalFields.FirstName}"); } } } From 2bc17df8429f23549e78ae6d8bc4deba34abfa1f Mon Sep 17 00:00:00 2001 From: Raffael Herrmann Date: Wed, 3 Nov 2021 19:10:28 +0100 Subject: [PATCH 06/10] Added code documentation, input validation and refactored RussiaPaymentOrder --- QRCoder/PayloadGenerator.cs | 556 ++++++++++++++++++++++++++++-------- 1 file changed, 435 insertions(+), 121 deletions(-) diff --git a/QRCoder/PayloadGenerator.cs b/QRCoder/PayloadGenerator.cs index 919889f3..fdf4b180 100644 --- a/QRCoder/PayloadGenerator.cs +++ b/QRCoder/PayloadGenerator.cs @@ -4,6 +4,9 @@ using System.Globalization; using System.Text; using System.Text.RegularExpressions; +#if NETSTANDARD1_1 +using System.Reflection; +#endif namespace QRCoder { @@ -2437,139 +2440,69 @@ public override string ToString() public class RussiaPaymentOrder : Payload { + // Specification of RussianPaymentOrder //https://docs.cntd.ru/document/1200110981 //https://roskazna.gov.ru/upload/iblock/5fa/gost_r_56042_2014.pdf //https://sbqr.ru/standard/files/standart.pdf + // Specification of data types described in the above standard + // https://gitea.sergeybochkov.com/bochkov/emuik/src/commit/d18f3b550f6415ea4a4a5e6097eaab4661355c72/template/ed + + // Tool for QR validation + // https://www.sbqr.ru/validator/index.html + //base private CharacterSets characterSet; - // [Required(ErrorMessage = "Name must be a filled string max. 160 characters", AllowEmptyStrings = true)] - private readonly string Name;// { get; set; } // 1-160 char - private readonly string PersonalAcc; // 20 digit (UInt64) - private readonly string BankName; // 1-45 char - private readonly string BIC; // 9 digit (UInt32) - private readonly string CorrespAcc = "0"; // up to 20 digit (0-default) (UInt64) - //extend - private readonly string Sum; - private readonly string Purpose; - private readonly string PayeeINN; - private readonly string PayerINN; - private readonly string DrawerStatus; - private readonly string KPP; - private readonly string CBC; - private readonly string OKTMO; - private readonly string PaytReason; - private readonly string ТaxPeriod; - private readonly string DocNo; - private readonly DateTime? DocDate; - private readonly string TaxPaytKind; - //other - private readonly string LastName; - private readonly string FirstName; - private readonly string MiddleName; - private readonly string PayerAddress; - private readonly string PersonalAccount; - private readonly string DocIdx; - private readonly string PensAcc; - private readonly string Contract; - private readonly string PersAcc; - private readonly string Flat; - private readonly string Phone; - private readonly string PayerIdType; - private readonly string PayerIdNum; - private readonly string ChildFio; - private readonly DateTime? BirthDate; - private readonly string PaymTerm; - private readonly string PaymPeriod; - private readonly string Category; - private readonly string ServiceName; - private readonly string CounterId; - private readonly string CounterVal; - private readonly string QuittId; - private readonly DateTime? QuittDate; - private readonly string InstNum; - private readonly string ClassNum; - private readonly string SpecFio; - private readonly string AddAmount; - private readonly string RuleId; - private readonly string ExecId; - private readonly string RegType; - private readonly string UIN; - private readonly TechCode techCode; - - public RussiaPaymentOrder(CharacterSets characterSet, string Name, string PersonalAcc, string BankName, string BIC, string CorrespAcc = "0") + private MandatoryFields mFields; + private OptionalFields oFields; + + private RussiaPaymentOrder() { - this.characterSet = characterSet; - this.Name = Name; - this.PersonalAcc = PersonalAcc; - this.BankName = BankName; - this.BIC = BIC; - this.CorrespAcc = CorrespAcc; + mFields = new MandatoryFields(); + oFields = new OptionalFields(); } - public RussiaPaymentOrder(CharacterSets characterSet, string Name, string PersonalAcc, string BankName, string BIC, string CorrespAcc = "0", - string PayeeINN = "", string LastName = "", string FirstName = "", string MiddleName = "", string Purpose = "", string PayerAddress = "", string Sum = "0") + + public RussiaPaymentOrder(CharacterSets characterSet, string name, string personalAcc, string bankName, string BIC, string correspAcc, OptionalFields optionalFields = null) : this() { this.characterSet = characterSet; - this.Name = Name; - this.PersonalAcc = PersonalAcc; - this.BankName = BankName; - this.BIC = BIC; - this.CorrespAcc = CorrespAcc; - - if (!string.IsNullOrEmpty(PayeeINN) && !(PayeeINN.Length >= 10 && Regex.IsMatch(PayeeINN.Replace(" ", ""), @"^[0-9]+$"))) - throw new RussiaPaymentOrderException("PayeeINN must be a filled 1-10(12) digits."); - if (!string.IsNullOrEmpty(Sum) && !(Sum.Length <= 18 && Regex.IsMatch(Sum.Replace(" ", ""), @"^[0-9]+$"))) - throw new Exception("Sum must be a filled 1-18 digits (*including Sum/100 (the last two digits) without a separator sign)"); - - this.PayeeINN = PayeeINN; - this.LastName = LastName; - this.FirstName = FirstName; - this.MiddleName = MiddleName; - this.Purpose = Purpose; - this.PayerAddress = PayerAddress; - this.Sum = Sum; - - /* - if (string.IsNullOrEmpty(Name)) - throw new RussiaPaymentOrderException("Name must be a filled string max. 160 characters."); - if (string.IsNullOrEmpty(PersonalAcc)) - throw new RussiaPaymentOrderException("PersonalAcc must be a filled string max. 20 characters."); - if (string.IsNullOrEmpty(BankName)) - throw new RussiaPaymentOrderException("BankName must be a filled string max. 45 characters."); - if (string.IsNullOrEmpty(BIC)) - throw new RussiaPaymentOrderException("BIC must be a filled string max. 9 characters."); - if (string.IsNullOrEmpty(CorrespAcc)) - throw new RussiaPaymentOrderException("CorrespAcc must be a filled string max. 20 characters."); -*/ - + mFields.Name = validateInput(name, "Name", @"^.{1,160}$"); + mFields.PersonalAcc = validateInput(personalAcc, "PersonalAcc", @"^[1-9]\d{4}[0-9ABCEHKMPTX]\d{14}$"); + mFields.BankName = validateInput(bankName, "BankName", @"^.{1,45}$"); + mFields.BIC = validateInput(BIC, "BIC", @"^\d{9}$"); + mFields.CorrespAcc = validateInput(correspAcc, "CorrespAcc", @"^[1-9]\d{4}[0-9ABCEHKMPTX]\d{14}$"); + + if (optionalFields != null) + oFields = optionalFields; } public override string ToString() { - if (string.IsNullOrEmpty(Name) && PersonalAcc.Length <= 160) - throw new Exception("Name must be a filled string 1-160 characters"); - if (!(!string.IsNullOrEmpty(PersonalAcc) && PersonalAcc.Length == 20 && Regex.IsMatch(PersonalAcc.Replace(" ", ""), @"^[0-9]+$"))) - throw new Exception("PersonalAcc must be a filled strong 20 digits"); - if (string.IsNullOrEmpty(BankName) && BankName.Length <= 45) - throw new Exception("BankName must be a filled string 1-45 characters"); - if (!(!string.IsNullOrEmpty(BIC) && BIC.Length == 9 && Regex.IsMatch(BIC.Replace(" ", ""), @"^[0-9]+$"))) - throw new Exception("BIC must be a filled strong 9 digits"); - if (!(!string.IsNullOrEmpty(CorrespAcc) && CorrespAcc.Length <= 20 && Regex.IsMatch(CorrespAcc.Replace(" ", ""), @"^[0-9]+$"))) - throw new Exception("CorrespAcc must be a filled 1-20 digits or 0 value if empty"); - - string ret = $"ST0001" + ((int)characterSet).ToString() + $"|Name={this.Name}" + - $"|PersonalAcc={this.PersonalAcc}" + - $"|BankName={this.BankName}" + - $"|BIC={this.BIC}" + - $"|CorrespAcc={this.CorrespAcc}" + - $"|PayeeINN={this.PayeeINN}" + - $"|LastName={this.LastName}" + - $"|FirstName={this.FirstName}" + - $"|MiddleName={this.MiddleName}" + - $"|Purpose={this.Purpose}" + - $"|PayerAddress={this.PayerAddress}" + - $"|Sum={this.Sum}" - ; + string ret = $"ST0001" + ((int)characterSet).ToString() + $"|Name={mFields.Name}" + + $"|PersonalAcc={mFields.PersonalAcc}" + + $"|BankName={mFields.BankName}" + + $"|BIC={mFields.BIC}" + + $"|CorrespAcc={mFields.CorrespAcc}"; + + //Add optional fields, if filled + var optionalFieldsList = new List(); +#if NETSTANDARD1_1 + optionalFieldsList = oFields.GetType().GetRuntimeProperties() + .Where(field => field.GetValue(oFields) != null) + .Select(field => $"{field.Name}={field.GetValue(oFields)}") + .ToList(); +#else + optionalFieldsList = oFields.GetType().GetProperties() + .Where(field => field.GetValue(oFields, null) != null) + .Select(field => { + var objValue = field.GetValue(oFields, null); + var value = field.GetType().Equals(typeof(DateTime)) ? ((DateTime)objValue).ToString("dd.MM.YYYY") : objValue.ToString(); + return $"{field.Name}={value}"; + }) + .ToList(); +#endif + if (optionalFieldsList.Count > 0) + ret += $"|{string.Join("|", optionalFieldsList.ToArray())}"; + string page = this.characterSet.ToString().Replace("_", "-"); #if NETSTANDARD1_1 @@ -2581,10 +2514,391 @@ public override string ToString() } /// - /// Перечень значений технического кода платежа + /// Validates a string against a given Regex pattern. Returns input if it matches the Regex expression (=valid) or throws Exception in case there's a mismatch + /// + /// String to be validated + /// Name/descriptor of the string to be validated + /// A regex pattern to be used for validation + /// An optional error text. If null, a standard error text is generated + /// Input value (in case it is valid) + private static string validateInput(string input, string fieldname, string pattern, string errorText = null) + { + return validateInput(input, fieldname, new string[] { pattern }, errorText); + } + + /// + /// Validates a string against one or more given Regex patterns. Returns input if it matches all regex expressions (=valid) or throws Exception in case there's a mismatch + /// + /// String to be validated + /// Name/descriptor of the string to be validated + /// An array of regex patterns to be used for validation + /// An optional error text. If null, a standard error text is generated + /// Input value (in case it is valid) + private static string validateInput(string input, string fieldname, string[] patterns, string errorText = null) + { + if (input == null) + throw new RussiaPaymentOrderException($"The input for '{fieldname}' must not be null."); + foreach (var pattern in patterns) + { + if (!Regex.IsMatch(input, pattern)) + throw new RussiaPaymentOrderException(errorText ?? $"The input for '{fieldname}' ({input}) doesn't match the pattern {pattern}"); + } + return input; + } + + private class MandatoryFields + { + public string Name; + public string PersonalAcc; + public string BankName; + public string BIC; + public string CorrespAcc; + } + + public class OptionalFields + { + private string _sum; + /// + /// Payment amount, in kopecks (FTI’s Amount.) + /// Сумма платежа, в копейках + /// + public string Sum + { + get { return _sum; } + set { _sum = validateInput(value, "Sum", @"^\d{1,18}$"); } + } + + private string _purpose; + /// + /// Payment name (purpose) + /// Наименование платежа (назначение) + /// + public string Purpose + { + get { return _purpose; } + set { _purpose = validateInput(value, "Purpose", @"^.{1,160}$"); } + } + + private string _payeeInn; + /// + /// Payee's INN (Resident Tax Identification Number; Text, up to 12 characters.) + /// ИНН получателя платежа + /// + public string PayeeINN + { + get { return _payeeInn; } + set { _payeeInn = validateInput(value, "PayeeINN", @"^.{1,12}$"); } + } + + private string _payerInn; + /// + /// Payer's INN (Resident Tax Identification Number; Text, up to 12 characters.) + /// ИНН плательщика + /// + public string PayerINN + { + get { return _payerInn; } + set { _payerInn = validateInput(value, "PayerINN", @"^.{1,12}$"); } + } + + private string _drawerStatus; + /// + /// Status compiler payment document + /// Статус составителя платежного документа + /// + public string DrawerStatus + { + get { return _drawerStatus; } + set { _drawerStatus = validateInput(value, "DrawerStatus", @"^.{1,2}$"); } + } + + private string _kpp; + /// + /// KPP of the payee (Tax Registration Code; Text, up to 9 characters.) + /// КПП получателя платежа + /// + public string KPP + { + get { return _kpp; } + set { _kpp = validateInput(value, "KPP", @"^.{1,9}$"); } + } + + private string _cbc; + /// + /// CBC + /// КБК + /// + public string CBC + { + get { return _cbc; } + set { _cbc = validateInput(value, "CBC", @"^.{1,20}$"); } + } + + private string _oktmo; + /// + /// All-Russian classifier territories of municipal formations + /// Общероссийский классификатор территорий муниципальных образований + /// + public string OKTMO + { + get { return _oktmo; } + set { _oktmo = validateInput(value, "OKTMO", @"^.{1,11}$"); } + } + + private string _paytReason; + /// + /// Basis of tax payment + /// Основание налогового платежа + /// + public string PaytReason + { + get { return _paytReason; } + set { _paytReason = validateInput(value, "PaytReason", @"^.{1,2}$"); } + } + + private string _taxPeriod; + /// + /// Taxable period + /// Налоговый период + /// + public string TaxPeriod + { + get { return _taxPeriod; } + set { _taxPeriod = validateInput(value, "ТaxPeriod", @"^.{1,10}$"); } + } + + private string _docNo; + /// + /// Document number + /// Номер документа + /// + public string DocNo + { + get { return _docNo; } + set { _docNo = validateInput(value, "DocNo", @"^.{1,15}$"); } + } + + /// + /// Document date + /// Дата документа + /// + public DateTime? DocDate { get; set; } + + private string _taxPaytKind; + /// + /// Payment type + /// Тип платежа + /// + public string TaxPaytKind + { + get { return _taxPaytKind; } + set { _taxPaytKind = validateInput(value, "TaxPaytKind", @"^.{1,2}$"); } + } + + /************************************************************************** + * The following fiels are no further specified in the standard + * document (https://sbqr.ru/standard/files/standart.pdf) thus there + * is no addition input validation implemented. + * **************************************************************************/ + + /// + /// Payer's surname + /// Фамилия плательщика + /// + public string LastName { get; set; } + + /// + /// Payer's name + /// Имя плательщика + /// + public string FirstName { get; set; } + + /// + /// Payer's patronymic + /// Отчество плательщика + /// + public string MiddleName { get; set; } + + /// + /// Payer's address + /// Адрес плательщика + /// + public string PayerAddress { get; set; } + + /// + /// Personal account of a budget recipient + /// Лицевой счет бюджетного получателя + /// + public string PersonalAccount { get; set; } + + /// + /// Payment document index + /// Индекс платежного документа + /// + public string DocIdx { get; set; } + + /// + /// Personal account number in the personalized accounting system in the Pension Fund of the Russian Federation - SNILS + /// № лицевого счета в системе персонифицированного учета в ПФР - СНИЛС + /// + public string PensAcc { get; set; } + + /// + /// Number of contract + /// Номер договора + /// + public string Contract { get; set; } + + /// + /// Personal account number of the payer in the organization (in the accounting system of the PU) + /// Номер лицевого счета плательщика в организации (в системе учета ПУ) + /// + public string PersAcc { get; set; } + + /// + /// Apartment number + /// Номер квартиры + /// + public string Flat { get; set; } + + /// + /// Phone number + /// Номер телефона + /// + public string Phone { get; set; } + + /// + /// DUL payer type + /// Вид ДУЛ плательщика + /// + public string PayerIdType { get; set; } + + /// + /// DUL number of the payer + /// Номер ДУЛ плательщика + /// + public string PayerIdNum { get; set; } + + /// + /// FULL NAME. child / student + /// Ф.И.О. ребенка/учащегося + /// + public string ChildFio { get; set; } + + /// + /// Date of birth + /// Дата рождения + /// + public DateTime? BirthDate { get; set; } + + /// + /// Due date / Invoice date + /// Срок платежа/дата выставления счета + /// + public string PaymTerm { get; set; } + + /// + /// Payment period + /// Период оплаты + /// + public string PaymPeriod { get; set; } + + /// + /// Payment type + /// Вид платежа + /// + public string Category { get; set; } + + /// + /// Service code / meter name + /// Код услуги/название прибора учета + /// + public string ServiceName { get; set; } + + /// + /// Metering device number + /// Номер прибора учета + /// + public string CounterId { get; set; } + + /// + /// Meter reading + /// Показание прибора учета + /// + public string CounterVal { get; set; } + + /// + /// Notification, accrual, account number + /// Номер извещения, начисления, счета + /// + public string QuittId { get; set; } + + /// + /// Date of notification / accrual / invoice / resolution (for traffic police) + /// Дата извещения/начисления/счета/постановления (для ГИБДД) + /// + public DateTime? QuittDate { get; set; } + + /// + /// Institution number (educational, medical) + /// Номер учреждения (образовательного, медицинского) + /// + public string InstNum { get; set; } + + /// + /// Kindergarten / school class number + /// Номер группы детсада/класса школы + /// + public string ClassNum { get; set; } + + /// + /// Full name of the teacher, specialist providing the service + /// ФИО преподавателя, специалиста, оказывающего услугу + /// + public string SpecFio { get; set; } + + /// + /// Insurance / additional service amount / Penalty amount (in kopecks) + /// Сумма страховки/дополнительной услуги/Сумма пени (в копейках) + /// + public string AddAmount { get; set; } + + /// + /// Resolution number (for traffic police) + /// Номер постановления (для ГИБДД) + /// + public string RuleId { get; set; } + + /// + /// Enforcement Proceedings Number + /// Номер исполнительного производства + /// + public string ExecId { get; set; } + + /// + /// Type of payment code (for example, for payments to Rosreestr) + /// Код вида платежа (например, для платежей в адрес Росреестра) + /// + public string RegType { get; set; } + + /// + /// Unique accrual identifier + /// Уникальный идентификатор начисления + /// + public string UIN { get; set; } + + /// + /// The technical code recommended by the service provider. Maybe used by the receiving organization to call the appropriate processing IT system. + /// Технический код, рекомендуемый для заполнения поставщиком услуг. Может использоваться принимающей организацией для вызова соответствующей обрабатывающей ИТ-системы. + /// + public TechCode? TechCode { get; set; } + } + + /// /// (List of values of the technical code of the payment) + /// Перечень значений технического кода платежа /// - private enum TechCode + public enum TechCode { Мобильная_связь_стационарный_телефон = 01, Коммунальные_услуги_ЖКХAFN = 02, From 76df56fe3833475a449896ffd6577214d1dee254 Mon Sep 17 00:00:00 2001 From: Raffael Herrmann Date: Wed, 3 Nov 2021 19:14:46 +0100 Subject: [PATCH 07/10] Patched test-cases and fixed NETSTANDARD1_1 branch in RussiaPaymentOrder --- QRCoder/PayloadGenerator.cs | 6 +++++- QRCoderTests/PayloadGeneratorTests.cs | 4 ++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/QRCoder/PayloadGenerator.cs b/QRCoder/PayloadGenerator.cs index fdf4b180..29f086ef 100644 --- a/QRCoder/PayloadGenerator.cs +++ b/QRCoder/PayloadGenerator.cs @@ -2488,7 +2488,11 @@ public override string ToString() #if NETSTANDARD1_1 optionalFieldsList = oFields.GetType().GetRuntimeProperties() .Where(field => field.GetValue(oFields) != null) - .Select(field => $"{field.Name}={field.GetValue(oFields)}") + .Select(field => { + var objValue = field.GetValue(oFields, null); + var value = field.GetType().Equals(typeof(DateTime)) ? ((DateTime)objValue).ToString("dd.MM.YYYY") : objValue.ToString(); + return $"{field.Name}={value}"; + }) .ToList(); #else optionalFieldsList = oFields.GetType().GetProperties() diff --git a/QRCoderTests/PayloadGeneratorTests.cs b/QRCoderTests/PayloadGeneratorTests.cs index 0779f4e3..afe6866e 100644 --- a/QRCoderTests/PayloadGeneratorTests.cs +++ b/QRCoderTests/PayloadGeneratorTests.cs @@ -3092,7 +3092,7 @@ public void russiapayment_generator_can_generate_payload_some_additional_fields( generator .ToString() - .ShouldBe($"ST00012|Name={name}|PersonalAcc={account}|BankName={bankName}|BIC={bic}|CorrespAcc=0|Sum={optionalFields.Sum}|LastName={optionalFields.LastName}|FirstName={optionalFields.FirstName}"); + .ShouldBe($"ST00012|Name={name}|PersonalAcc={account}|BankName={bankName}|BIC={bic}|CorrespAcc={correspAcc}|Sum={optionalFields.Sum}|LastName={optionalFields.LastName}|FirstName={optionalFields.FirstName}"); } [Fact] @@ -3116,7 +3116,7 @@ public void russiapayment_generator_can_generate_payload_all_additional_fields() generator .ToString() - .ShouldBe($"ST00012|Name={name}|PersonalAcc={account}|BankName={bankName}|BIC={bic}|CorrespAcc=0|Sum={optionalFields.Sum}|LastName={optionalFields.LastName}|FirstName={optionalFields.FirstName}"); + .ShouldBe($"ST00012|Name={name}|PersonalAcc={account}|BankName={bankName}|BIC={bic}|CorrespAcc={correspAcc}|Sum={optionalFields.Sum}|LastName={optionalFields.LastName}|FirstName={optionalFields.FirstName}"); } } } From c6c511aa53f31db8915ae9a5aba5a86bd92dba03 Mon Sep 17 00:00:00 2001 From: Raffael Herrmann Date: Mon, 8 Nov 2021 17:35:25 +0100 Subject: [PATCH 08/10] More RussiaPaymentOrder test cases (100% coverage) --- QRCoderTests/PayloadGeneratorTests.cs | 282 ++++++++++++++++++++++++-- 1 file changed, 269 insertions(+), 13 deletions(-) diff --git a/QRCoderTests/PayloadGeneratorTests.cs b/QRCoderTests/PayloadGeneratorTests.cs index afe6866e..d82b7f98 100644 --- a/QRCoderTests/PayloadGeneratorTests.cs +++ b/QRCoderTests/PayloadGeneratorTests.cs @@ -3062,14 +3062,148 @@ public void russiapayment_generator_can_generate_payload_mandatory_fields() { var account = "40702810138250123017"; var bic = "044525225"; - var bankName = "=ОАО \"БАНК\""; + var bankName = "ОАО \"БАНК\""; + var name = "ООО «Три кита»"; + var correspAcc = "30101810965770000413"; + var generator = new PayloadGenerator.RussiaPaymentOrder(name, account, bankName, bic, correspAcc); + + generator + .ToString() + .ShouldBe($"ST00012|Name={name}|PersonalAcc={account}|BankName={bankName}|BIC={bic}|CorrespAcc={correspAcc}|"); + } + + [Fact] + [Category("PayloadGenerator/RussiaPaymentOrder")] + public void russiapayment_generator_can_generate_payload_encoding_win1251() + { + var account = "40702810138250123017"; + var bic = "044525225"; + var bankName = "ОАО \"БАНК\""; + var name = "ООО «Три кита»"; + var correspAcc = "30101810965770000413"; + var generator = new PayloadGenerator.RussiaPaymentOrder(name, account, bankName, bic, correspAcc, null, PayloadGenerator.RussiaPaymentOrder.CharacterSets.windows_1251); + + byte[] targetBytes = new byte[] { 83, 84, 48, 48, 48, 49, 49, 124, 78, 97, 109, 101, 61, 206, 206, 206, 32, 171, 210, 240, 232, 32, 234, 232, 242, 224, 187, 124, 80, 101, 114, 115, 111, 110, 97, 108, 65, 99, 99, 61, 52, 48, 55, 48, 50, 56, 49, 48, 49, 51, 56, 50, 53, 48, 49, 50, 51, 48, 49, 55, 124, 66, 97, 110, 107, 78, 97, 109, 101, 61, 206, 192, 206, 32, 34, 193, 192, 205, 202, 34, 124, 66, 73, 67, 61, 48, 52, 52, 53, 50, 53, 50, 50, 53, 124, 67, 111, 114, 114, 101, 115, 112, 65, 99, 99, 61, 51, 48, 49, 48, 49, 56, 49, 48, 57, 54, 53, 55, 55, 48, 48, 48, 48, 52, 49, 51, 124 }; + var payloadBytes = generator.ToBytes(); + + Assert.True(targetBytes.Length == payloadBytes.Length, $"Byte array lengths different. Expected: {targetBytes.Length}, Actual: {payloadBytes.Length}"); + for (int i = 0; i < targetBytes.Length; i++) + { + Assert.True(targetBytes[i] == payloadBytes[i], + $"Expected: '{targetBytes[i]}', Actual: '{payloadBytes[i]}' at offset {i}." + ); + } + } + + [Fact] + [Category("PayloadGenerator/RussiaPaymentOrder")] + public void russiapayment_generator_can_generate_payload_encoding_koi8() + { + var account = "40702810138250123017"; + var bic = "044525225"; + var bankName = "ОАО \"БАНК\""; + var name = "ООО «Три кита»"; + var correspAcc = "30101810965770000413"; + var generator = new PayloadGenerator.RussiaPaymentOrder(name, account, bankName, bic, correspAcc, null, PayloadGenerator.RussiaPaymentOrder.CharacterSets.koi8_r); + + byte[] targetBytes = new byte[] { 83, 84, 48, 48, 48, 49, 51, 124, 78, 97, 109, 101, 61, 239, 239, 239, 32, 60, 244, 210, 201, 32, 203, 201, 212, 193, 62, 124, 80, 101, 114, 115, 111, 110, 97, 108, 65, 99, 99, 61, 52, 48, 55, 48, 50, 56, 49, 48, 49, 51, 56, 50, 53, 48, 49, 50, 51, 48, 49, 55, 124, 66, 97, 110, 107, 78, 97, 109, 101, 61, 239, 225, 239, 32, 34, 226, 225, 238, 235, 34, 124, 66, 73, 67, 61, 48, 52, 52, 53, 50, 53, 50, 50, 53, 124, 67, 111, 114, 114, 101, 115, 112, 65, 99, 99, 61, 51, 48, 49, 48, 49, 56, 49, 48, 57, 54, 53, 55, 55, 48, 48, 48, 48, 52, 49, 51, 124 }; + var payloadBytes = generator.ToBytes(); + + Assert.True(targetBytes.Length == payloadBytes.Length, $"Byte array lengths different. Expected: {targetBytes.Length}, Actual: {payloadBytes.Length}"); + for (int i = 0; i < targetBytes.Length; i++) + { + Assert.True(targetBytes[i] == payloadBytes[i], + $"Expected: '{targetBytes[i]}', Actual: '{payloadBytes[i]}' at offset {i}." + ); + } + } + + [Fact] + [Category("PayloadGenerator/RussiaPaymentOrder")] + public void russiapayment_generator_can_generate_payload_custom_separator() + { + var account = "40702810138250123017"; + var bic = "044525225"; + var bankName = "ОАО | \"БАНК\""; var name = "ООО «Три кита»"; var correspAcc = "30101810400000000225"; - var generator = new PayloadGenerator.RussiaPaymentOrder(PayloadGenerator.RussiaPaymentOrder.CharacterSets.utf_8, name, account, bankName, bic, correspAcc); + var generator = new PayloadGenerator.RussiaPaymentOrder(name, account, bankName, bic, correspAcc); generator .ToString() - .ShouldBe($"ST00012|Name={name}|PersonalAcc={account}|BankName={bankName}|BIC={bic}|CorrespAcc={correspAcc}"); + .ShouldBe($"ST00012#Name={name}#PersonalAcc={account}#BankName={bankName}#BIC={bic}#CorrespAcc={correspAcc}#"); + } + + [Fact] + [Category("PayloadGenerator/RussiaPaymentOrder")] + public void russiapayment_generator_should_throw_no_separator_exception() + { + var account = "40702810138250123017"; + var bic = "044525225"; + var bankName = "ОАО | \"БАНК\""; + var name = "|@;:^_~{}!#$%&()*+,/"; //All chars that could be used as separator + var correspAcc = "30101810400000000225"; + var generator = new PayloadGenerator.RussiaPaymentOrder(name, account, bankName, bic, correspAcc); + + var exception = Record.Exception(() => generator.ToString()); + Assert.NotNull(exception); + Assert.IsType(exception); + exception.Message.ShouldBe("No valid separator found."); + } + + [Fact] + [Category("PayloadGenerator/RussiaPaymentOrder")] + public void russiapayment_generator_should_throw_data_too_long_exception() + { + var account = "40702810138250123017"; + var bic = "044525225"; + var bankName = "ОАО | \"БАНК\""; + var name = "A very very very very very very very very very very very very very very very very very very very very very very very very very very very very very long name"; + var correspAcc = "30101810400000000225"; + var optionalFields = new PayloadGenerator.RussiaPaymentOrder.OptionalFields() + { + FirstName = "Another long long long long long long long long long long long long long long firstname", + LastName = "Another long long long long long long long long long long long long long long lastname", + Sum = "125000" + }; + var generator = new PayloadGenerator.RussiaPaymentOrder(name, account, bankName, bic, correspAcc, optionalFields); + + var exception = Record.Exception(() => generator.ToString()); + Assert.NotNull(exception); + Assert.IsType(exception); + exception.Message.ShouldStartWith("Data too long"); + } + + [Fact] + [Category("PayloadGenerator/RussiaPaymentOrder")] + public void russiapayment_generator_should_throw_must_not_be_null_exception() + { + string account = null; + var bic = "044525225"; + var bankName = "ОАО | \"БАНК\""; + var name = "|@;:^_~{}!#$%&()*+,/"; + var correspAcc = "30101810400000000225"; + + var exception = Record.Exception(() => new PayloadGenerator.RussiaPaymentOrder(name, account, bankName, bic, correspAcc)); + Assert.NotNull(exception); + Assert.IsType(exception); + exception.Message.ShouldBe($"The input for 'PersonalAcc' must not be null."); + } + + [Fact] + [Category("PayloadGenerator/RussiaPaymentOrder")] + public void russiapayment_generator_should_throw_unmatched_pattern_exception() + { + string account = "40702810138250123017"; + var bic = "abcd"; //Invalid BIC + var bankName = "ОАО | \"БАНК\""; + var name = "|@;:^_~{}!#$%&()*+,/"; + var correspAcc = "30101810400000000225"; + + var exception = Record.Exception(() => new PayloadGenerator.RussiaPaymentOrder(name, account, bankName, bic, correspAcc)); + Assert.NotNull(exception); + Assert.IsType(exception); + exception.Message.ShouldBe("The input for 'BIC' (abcd) doesn't match the pattern ^\\d{9}$"); } [Fact] @@ -3088,16 +3222,108 @@ public void russiapayment_generator_can_generate_payload_some_additional_fields( Sum = "125000" }; - var generator = new PayloadGenerator.RussiaPaymentOrder(PayloadGenerator.RussiaPaymentOrder.CharacterSets.utf_8, name, account, bankName, bic, correspAcc, optionalFields: optionalFields); + var generator = new PayloadGenerator.RussiaPaymentOrder(name, account, bankName, bic, correspAcc, optionalFields); + + generator + .ToString() + .ShouldBe($"ST00012|Name={name}|PersonalAcc={account}|BankName={bankName}|BIC={bic}|CorrespAcc={correspAcc}|Sum={optionalFields.Sum}|LastName={optionalFields.LastName}|FirstName={optionalFields.FirstName}|"); + } + + [Fact] + [Category("PayloadGenerator/RussiaPaymentOrder")] + public void russiapayment_generator_can_generate_payload_all_additional_fields_pt1() + { + var account = "40702810138250123017"; + var bic = "044525225"; + var bankName = "=ОАО \"БАНК\""; + var name = "ООО «Три кита»"; + var correspAcc = "30101810400000000225"; + var optionalFields = new PayloadGenerator.RussiaPaymentOrder.OptionalFields() + { + FirstName = "R", + MiddleName = "C", + LastName = "Hann", + Sum = "1250", + AddAmount = "10", + BirthDate = new DateTime(1990, 1, 1), + Category = "1", + CBC = "CBC1", + ChildFio = "J Doe", + ClassNum = "1", + Contract = "99", + }; + + var generator = new PayloadGenerator.RussiaPaymentOrder(name, account, bankName, bic, correspAcc, optionalFields); + + generator + .ToString() + .ShouldBe($"ST00012|Name={name}|PersonalAcc={account}|BankName={bankName}|BIC={bic}|CorrespAcc={correspAcc}|Sum={optionalFields.Sum}|CBC=CBC1|LastName=Hann|FirstName=R|MiddleName=C|Contract=99|ChildFio=J Doe|BirthDate=01.01.1990|Category=1|ClassNum=1|AddAmount=10|"); + } + + [Fact] + [Category("PayloadGenerator/RussiaPaymentOrder")] + public void russiapayment_generator_can_generate_payload_all_additional_fields_pt2() + { + var account = "40702810138250123017"; + var bic = "044525225"; + var bankName = "=ОАО \"БАНК\""; + var name = "ООО «Три кита»"; + var correspAcc = "30101810400000000225"; + var optionalFields = new PayloadGenerator.RussiaPaymentOrder.OptionalFields() + { + CounterId = "1234", + CounterVal = "9999", + DocDate = new DateTime(2021, 11, 8), + DocIdx = "A1", + DocNo = "11", + DrawerStatus = "D1", + ExecId = "77", + Flat = "5a", + InstNum = "987", + KPP = "KPP1", + OKTMO = "112233" + }; + + var generator = new PayloadGenerator.RussiaPaymentOrder(name, account, bankName, bic, correspAcc, optionalFields); generator .ToString() - .ShouldBe($"ST00012|Name={name}|PersonalAcc={account}|BankName={bankName}|BIC={bic}|CorrespAcc={correspAcc}|Sum={optionalFields.Sum}|LastName={optionalFields.LastName}|FirstName={optionalFields.FirstName}"); + .ShouldBe($"ST00012|Name={name}|PersonalAcc={account}|BankName={bankName}|BIC={bic}|CorrespAcc={correspAcc}|DrawerStatus=D1|KPP=KPP1|OKTMO=112233|DocNo=11|DocDate=08.11.2021|DocIdx=A1|Flat=5a|CounterId=1234|CounterVal=9999|InstNum=987|ExecId=77|"); + } + + + [Fact] + [Category("PayloadGenerator/RussiaPaymentOrder")] + public void russiapayment_generator_can_generate_payload_all_additional_fields_pt3() + { + var account = "40702810138250123017"; + var bic = "044525225"; + var bankName = "=ОАО \"БАНК\""; + var name = "ООО «Три кита»"; + var correspAcc = "30101810400000000225"; + var optionalFields = new PayloadGenerator.RussiaPaymentOrder.OptionalFields() + { + PayeeINN = "INN1", + PayerAddress = "Street 1, 123 City", + PayerIdNum = "555", + PayerIdType = "X", + PayerINN = "INN2", + PaymPeriod = "12", + PaymTerm = "A", + PaytReason = "01", + PensAcc = "SNILS_NO" + }; + + var generator = new PayloadGenerator.RussiaPaymentOrder(name, account, bankName, bic, correspAcc, optionalFields); + + generator + .ToString() + .ShouldBe($"ST00012|Name={name}|PersonalAcc={account}|BankName={bankName}|BIC={bic}|CorrespAcc={correspAcc}|PayeeINN=INN1|PayerINN=INN2|PaytReason=01|PayerAddress=Street 1, 123 City|PensAcc=SNILS_NO|PayerIdType=X|PayerIdNum=555|PaymTerm=A|PaymPeriod=12|"); } [Fact] [Category("PayloadGenerator/RussiaPaymentOrder")] - public void russiapayment_generator_can_generate_payload_all_additional_fields() + public void russiapayment_generator_can_generate_payload_all_additional_fields_pt4() { var account = "40702810138250123017"; var bic = "044525225"; @@ -3105,18 +3331,48 @@ public void russiapayment_generator_can_generate_payload_all_additional_fields() var name = "ООО «Три кита»"; var correspAcc = "30101810400000000225"; var optionalFields = new PayloadGenerator.RussiaPaymentOrder.OptionalFields() - { - Sum = "50000", - Purpose = "Your loan", - PayeeINN = "9909373824", - PayerINN = "1832090230" + { + PersAcc = "2222", + PersonalAccount = "3333", + Phone = "0012345", + Purpose = "Test", + QuittDate = new DateTime(2021, 2, 1), + QuittId = "7", + RegType = "y", + RuleId = "2", + ServiceName = "Bank" + }; + + var generator = new PayloadGenerator.RussiaPaymentOrder(name, account, bankName, bic, correspAcc, optionalFields); + + generator + .ToString() + .ShouldBe($"ST00012|Name={name}|PersonalAcc={account}|BankName={bankName}|BIC={bic}|CorrespAcc={correspAcc}|Purpose=Test|PersonalAccount=3333|PersAcc=2222|Phone=0012345|ServiceName=Bank|QuittId=7|QuittDate=01.02.2021|RuleId=2|RegType=y|"); + } + + [Fact] + [Category("PayloadGenerator/RussiaPaymentOrder")] + public void russiapayment_generator_can_generate_payload_all_additional_fields_pt5() + { + var account = "40702810138250123017"; + var bic = "044525225"; + var bankName = "=ОАО \"БАНК\""; + var name = "ООО «Три кита»"; + var correspAcc = "30101810400000000225"; + var optionalFields = new PayloadGenerator.RussiaPaymentOrder.OptionalFields() + { + SpecFio = "T. Eacher", + TaxPaytKind = "99", + TaxPeriod = "31", + TechCode = PayloadGenerator.RussiaPaymentOrder.TechCode.ГИБДД_налоги_пошлины_бюджетные_платежи, + UIN = "1a2b" }; - var generator = new PayloadGenerator.RussiaPaymentOrder(PayloadGenerator.RussiaPaymentOrder.CharacterSets.utf_8, name, account, bankName, bic, correspAcc, optionalFields: optionalFields); + var generator = new PayloadGenerator.RussiaPaymentOrder(name, account, bankName, bic, correspAcc, optionalFields); generator .ToString() - .ShouldBe($"ST00012|Name={name}|PersonalAcc={account}|BankName={bankName}|BIC={bic}|CorrespAcc={correspAcc}|Sum={optionalFields.Sum}|LastName={optionalFields.LastName}|FirstName={optionalFields.FirstName}"); + .ShouldBe($"ST00012|Name={name}|PersonalAcc={account}|BankName={bankName}|BIC={bic}|CorrespAcc={correspAcc}|TaxPeriod=31|TaxPaytKind=99|SpecFio=T. Eacher|UIN=1a2b|TechCode=ГИБДД_налоги_пошлины_бюджетные_платежи|"); } } } From 7c602de2026ed574a8792d2c5d1939b51a8607a5 Mon Sep 17 00:00:00 2001 From: Raffael Herrmann Date: Mon, 8 Nov 2021 17:35:50 +0100 Subject: [PATCH 09/10] Added validation and re-fined logic for RussiaPaymentOrder --- QRCoder/PayloadGenerator.cs | 192 +++++++++++++++++++++++++++--------- QRCoder/QRCoder.csproj | 6 +- 2 files changed, 149 insertions(+), 49 deletions(-) diff --git a/QRCoder/PayloadGenerator.cs b/QRCoder/PayloadGenerator.cs index 29f086ef..4a5c796e 100644 --- a/QRCoder/PayloadGenerator.cs +++ b/QRCoder/PayloadGenerator.cs @@ -4,7 +4,7 @@ using System.Globalization; using System.Text; using System.Text.RegularExpressions; -#if NETSTANDARD1_1 +#if NETSTANDARD1_3 using System.Reflection; #endif @@ -2455,65 +2455,168 @@ public class RussiaPaymentOrder : Payload private CharacterSets characterSet; private MandatoryFields mFields; private OptionalFields oFields; + private string separator = "|"; private RussiaPaymentOrder() { mFields = new MandatoryFields(); oFields = new OptionalFields(); } - - public RussiaPaymentOrder(CharacterSets characterSet, string name, string personalAcc, string bankName, string BIC, string correspAcc, OptionalFields optionalFields = null) : this() + + /// + /// Generates a RussiaPaymentOrder payload + /// + /// Name of the payee (Наименование получателя платежа) + /// Beneficiary account number (Номер счета получателя платежа) + /// Name of the beneficiary's bank (Наименование банка получателя платежа) + /// BIC (БИК) + /// Box number / account payee's bank (Номер кор./сч. банка получателя платежа) + /// An (optional) object of additional fields + /// Type of encoding (default UTF-8) + public RussiaPaymentOrder(string name, string personalAcc, string bankName, string BIC, string correspAcc, OptionalFields optionalFields = null, CharacterSets characterSet = CharacterSets.utf_8) : this() { this.characterSet = characterSet; - mFields.Name = validateInput(name, "Name", @"^.{1,160}$"); - mFields.PersonalAcc = validateInput(personalAcc, "PersonalAcc", @"^[1-9]\d{4}[0-9ABCEHKMPTX]\d{14}$"); - mFields.BankName = validateInput(bankName, "BankName", @"^.{1,45}$"); - mFields.BIC = validateInput(BIC, "BIC", @"^\d{9}$"); - mFields.CorrespAcc = validateInput(correspAcc, "CorrespAcc", @"^[1-9]\d{4}[0-9ABCEHKMPTX]\d{14}$"); + mFields.Name = ValidateInput(name, "Name", @"^.{1,160}$"); + mFields.PersonalAcc = ValidateInput(personalAcc, "PersonalAcc", @"^[1-9]\d{4}[0-9ABCEHKMPTX]\d{14}$"); + mFields.BankName = ValidateInput(bankName, "BankName", @"^.{1,45}$"); + mFields.BIC = ValidateInput(BIC, "BIC", @"^\d{9}$"); + mFields.CorrespAcc = ValidateInput(correspAcc, "CorrespAcc", @"^[1-9]\d{4}[0-9ABCEHKMPTX]\d{14}$"); if (optionalFields != null) oFields = optionalFields; } + /// + /// Returns payload as string. + /// + /// ⚠ Attention: If CharacterSets was set to windows-1251 or koi8-r you should use ToBytes() instead of ToString() and pass the bytes to CreateQrCode()! + /// public override string ToString() { - string ret = $"ST0001" + ((int)characterSet).ToString() + $"|Name={mFields.Name}" + - $"|PersonalAcc={mFields.PersonalAcc}" + - $"|BankName={mFields.BankName}" + - $"|BIC={mFields.BIC}" + - $"|CorrespAcc={mFields.CorrespAcc}"; + var cp = characterSet.ToString().Replace("_", "-"); + var bytes = ToBytes(); + +#if !NET35 && !NET40 && !NETSTANDARD1_3_OR_GREATER + System.Text.Encoding.RegisterProvider(System.Text.CodePagesEncodingProvider.Instance); +#endif +#if NETSTANDARD1_3 + // TODO: Fix for NETSTANDARD1.1 + return Encoding.GetEncoding(cp).GetString(bytes,0,bytes.Length); +#else + return Encoding.GetEncoding(cp).GetString(bytes); +#endif + } + + /// + /// Returns payload as byte[]. + /// + /// Should be used if CharacterSets equals windows-1251 or koi8-r + /// + + public byte[] ToBytes() + { + //Calculate the seperator + separator = DetermineSeparator(); + + //Create the payload string + string ret = $"ST0001" + ((int)characterSet).ToString() + //(separator != "|" ? separator : "") + + $"{separator}Name={mFields.Name}" + + $"{separator}PersonalAcc={mFields.PersonalAcc}" + + $"{separator}BankName={mFields.BankName}" + + $"{separator}BIC={mFields.BIC}" + + $"{separator}CorrespAcc={mFields.CorrespAcc}"; //Add optional fields, if filled - var optionalFieldsList = new List(); -#if NETSTANDARD1_1 - optionalFieldsList = oFields.GetType().GetRuntimeProperties() + var optionalFieldsList = GetOptionalFieldsAsList(); + if (optionalFieldsList.Count > 0) + ret += $"|{string.Join("|", optionalFieldsList.ToArray())}"; + ret += separator; + + //Encode return string as byte[] with correct CharacterSet +#if !NET35_OR_GREATER + Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); +#endif + var cp = this.characterSet.ToString().Replace("_", "-"); + byte[] bytesOut = Encoding.Convert(Encoding.UTF8, Encoding.GetEncoding(cp), Encoding.UTF8.GetBytes(ret)); + if (bytesOut.Length > 300) + throw new RussiaPaymentOrderException($"Data too long. Payload must not exceed 300 bytes, but actually is {bytesOut.Length} bytes long. Remove additional data fields or shorten strings/values."); + return bytesOut; + } + + + /// + /// Determines a valid separator + /// + /// + private string DetermineSeparator() + { + // See chapter 5.2.1 of Standard (https://sbqr.ru/standard/files/standart.pdf) + + var mandatoryValues = GetMandatoryFieldsAsList(); + var optionalValues = GetOptionalFieldsAsList(); + + // Possible candidates for field separation + var separatorCandidates = new string[]{ "|", "#", ";", ":", "^", "_", "~", "{", "}", "!", "#", "$", "%", "&", "(", ")", "*", "+", ",", "/", "@" }; + foreach (var sepCandidate in separatorCandidates) + { + if (!mandatoryValues.Any(x => x.Contains(sepCandidate)) && !optionalValues.Any(x => x.Contains(sepCandidate))) + return sepCandidate; + } + throw new RussiaPaymentOrderException("No valid separator found."); + } + + /// + /// Takes all optional fields that are not null and returns their string represantion + /// + /// A List of strings + private List GetOptionalFieldsAsList() + { +#if NETSTANDARD1_3 + return oFields.GetType().GetRuntimeProperties() .Where(field => field.GetValue(oFields) != null) .Select(field => { var objValue = field.GetValue(oFields, null); - var value = field.GetType().Equals(typeof(DateTime)) ? ((DateTime)objValue).ToString("dd.MM.YYYY") : objValue.ToString(); + var value = field.PropertyType.Equals(typeof(DateTime?)) ? ((DateTime)objValue).ToString("dd.MM.yyyy") : objValue.ToString(); return $"{field.Name}={value}"; }) .ToList(); #else - optionalFieldsList = oFields.GetType().GetProperties() + return oFields.GetType().GetProperties() .Where(field => field.GetValue(oFields, null) != null) .Select(field => { var objValue = field.GetValue(oFields, null); - var value = field.GetType().Equals(typeof(DateTime)) ? ((DateTime)objValue).ToString("dd.MM.YYYY") : objValue.ToString(); + var value = field.PropertyType.Equals(typeof(DateTime?)) ? ((DateTime)objValue).ToString("dd.MM.yyyy") : objValue.ToString(); return $"{field.Name}={value}"; }) .ToList(); #endif - if (optionalFieldsList.Count > 0) - ret += $"|{string.Join("|", optionalFieldsList.ToArray())}"; + } - string page = this.characterSet.ToString().Replace("_", "-"); -#if NETSTANDARD1_1 - var bytes = Encoding.Convert(Encoding.UTF8, Encoding.GetEncoding(page), Encoding.GetEncoding(page).GetBytes(ret)); - return Encoding.GetEncoding(page).GetString(bytes,0,bytes.Length); + /// + /// Takes all mandatory fields that are not null and returns their string represantion + /// + /// A List of strings + private List GetMandatoryFieldsAsList() + { +#if NETSTANDARD1_3 + return mFields.GetType().GetRuntimeFields() + .Where(field => field.GetValue(mFields) != null) + .Select(field => { + var objValue = field.GetValue(mFields); + var value = field.FieldType.Equals(typeof(DateTime?)) ? ((DateTime)objValue).ToString("dd.MM.yyyy") : objValue.ToString(); + return $"{field.Name}={value}"; + }) + .ToList(); #else - return Encoding.GetEncoding(page).GetString(Encoding.Convert(Encoding.Default, Encoding.GetEncoding(page), Encoding.GetEncoding(page).GetBytes(ret))); + return mFields.GetType().GetFields() + .Where(field => field.GetValue(mFields) != null) + .Select(field => { + var objValue = field.GetValue(mFields); + var value = field.FieldType.Equals(typeof(DateTime?)) ? ((DateTime)objValue).ToString("dd.MM.yyyy") : objValue.ToString(); + return $"{field.Name}={value}"; + }) + .ToList(); #endif } @@ -2525,9 +2628,9 @@ public override string ToString() /// A regex pattern to be used for validation /// An optional error text. If null, a standard error text is generated /// Input value (in case it is valid) - private static string validateInput(string input, string fieldname, string pattern, string errorText = null) + private static string ValidateInput(string input, string fieldname, string pattern, string errorText = null) { - return validateInput(input, fieldname, new string[] { pattern }, errorText); + return ValidateInput(input, fieldname, new string[] { pattern }, errorText); } /// @@ -2538,7 +2641,7 @@ private static string validateInput(string input, string fieldname, string patte /// An array of regex patterns to be used for validation /// An optional error text. If null, a standard error text is generated /// Input value (in case it is valid) - private static string validateInput(string input, string fieldname, string[] patterns, string errorText = null) + private static string ValidateInput(string input, string fieldname, string[] patterns, string errorText = null) { if (input == null) throw new RussiaPaymentOrderException($"The input for '{fieldname}' must not be null."); @@ -2569,7 +2672,7 @@ public class OptionalFields public string Sum { get { return _sum; } - set { _sum = validateInput(value, "Sum", @"^\d{1,18}$"); } + set { _sum = ValidateInput(value, "Sum", @"^\d{1,18}$"); } } private string _purpose; @@ -2580,7 +2683,7 @@ public string Sum public string Purpose { get { return _purpose; } - set { _purpose = validateInput(value, "Purpose", @"^.{1,160}$"); } + set { _purpose = ValidateInput(value, "Purpose", @"^.{1,160}$"); } } private string _payeeInn; @@ -2591,7 +2694,7 @@ public string Purpose public string PayeeINN { get { return _payeeInn; } - set { _payeeInn = validateInput(value, "PayeeINN", @"^.{1,12}$"); } + set { _payeeInn = ValidateInput(value, "PayeeINN", @"^.{1,12}$"); } } private string _payerInn; @@ -2602,7 +2705,7 @@ public string PayeeINN public string PayerINN { get { return _payerInn; } - set { _payerInn = validateInput(value, "PayerINN", @"^.{1,12}$"); } + set { _payerInn = ValidateInput(value, "PayerINN", @"^.{1,12}$"); } } private string _drawerStatus; @@ -2613,7 +2716,7 @@ public string PayerINN public string DrawerStatus { get { return _drawerStatus; } - set { _drawerStatus = validateInput(value, "DrawerStatus", @"^.{1,2}$"); } + set { _drawerStatus = ValidateInput(value, "DrawerStatus", @"^.{1,2}$"); } } private string _kpp; @@ -2624,7 +2727,7 @@ public string DrawerStatus public string KPP { get { return _kpp; } - set { _kpp = validateInput(value, "KPP", @"^.{1,9}$"); } + set { _kpp = ValidateInput(value, "KPP", @"^.{1,9}$"); } } private string _cbc; @@ -2635,7 +2738,7 @@ public string KPP public string CBC { get { return _cbc; } - set { _cbc = validateInput(value, "CBC", @"^.{1,20}$"); } + set { _cbc = ValidateInput(value, "CBC", @"^.{1,20}$"); } } private string _oktmo; @@ -2646,7 +2749,7 @@ public string CBC public string OKTMO { get { return _oktmo; } - set { _oktmo = validateInput(value, "OKTMO", @"^.{1,11}$"); } + set { _oktmo = ValidateInput(value, "OKTMO", @"^.{1,11}$"); } } private string _paytReason; @@ -2657,7 +2760,7 @@ public string OKTMO public string PaytReason { get { return _paytReason; } - set { _paytReason = validateInput(value, "PaytReason", @"^.{1,2}$"); } + set { _paytReason = ValidateInput(value, "PaytReason", @"^.{1,2}$"); } } private string _taxPeriod; @@ -2668,7 +2771,7 @@ public string PaytReason public string TaxPeriod { get { return _taxPeriod; } - set { _taxPeriod = validateInput(value, "ТaxPeriod", @"^.{1,10}$"); } + set { _taxPeriod = ValidateInput(value, "ТaxPeriod", @"^.{1,10}$"); } } private string _docNo; @@ -2679,7 +2782,7 @@ public string TaxPeriod public string DocNo { get { return _docNo; } - set { _docNo = validateInput(value, "DocNo", @"^.{1,15}$"); } + set { _docNo = ValidateInput(value, "DocNo", @"^.{1,15}$"); } } /// @@ -2696,7 +2799,7 @@ public string DocNo public string TaxPaytKind { get { return _taxPaytKind; } - set { _taxPaytKind = validateInput(value, "TaxPaytKind", @"^.{1,2}$"); } + set { _taxPaytKind = ValidateInput(value, "TaxPaytKind", @"^.{1,2}$"); } } /************************************************************************** @@ -2931,17 +3034,10 @@ public enum CharacterSets public class RussiaPaymentOrderException : Exception { - public RussiaPaymentOrderException() - { - } public RussiaPaymentOrderException(string message) : base(message) { } - public RussiaPaymentOrderException(string message, Exception inner) - : base(message, inner) - { - } } } diff --git a/QRCoder/QRCoder.csproj b/QRCoder/QRCoder.csproj index ec6c4618..3e5f43c7 100644 --- a/QRCoder/QRCoder.csproj +++ b/QRCoder/QRCoder.csproj @@ -1,7 +1,7 @@  - net35;net40;netstandard1.1;netstandard2.0;net5.0;net5.0-windows + net35;net40;netstandard1.3;netstandard2.0;net5.0;net5.0-windows false true true @@ -42,6 +42,10 @@ + + + + From 91e37f054766023eae40a334444d482ed830e7f7 Mon Sep 17 00:00:00 2001 From: Raffael Herrmann Date: Mon, 8 Nov 2021 17:36:06 +0100 Subject: [PATCH 10/10] Fixed payload generator links to Wiki --- readme.md | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/readme.md b/readme.md index 6f75c4b4..bf034ab4 100644 --- a/readme.md +++ b/readme.md @@ -141,11 +141,12 @@ The PayloadGenerator supports the following types of payloads: * [Monero address/payment](https://github.com/codebude/QRCoder/wiki/Advanced-usage---Payload-generators#310-monero-addresspayment) * [One-Time-Password](https://github.com/codebude/QRCoder/wiki/Advanced-usage---Payload-generators#311-one-time-password) * [Phonenumber](https://github.com/codebude/QRCoder/wiki/Advanced-usage---Payload-generators#312-phonenumber) -* [Shadowsocks configuration](https://github.com/codebude/QRCoder/wiki/Advanced-usage---Payload-generators#313-shadowsocks-configuration) -* [Skype call](https://github.com/codebude/QRCoder/wiki/Advanced-usage---Payload-generators#314-skype-call) -* [SlovenianUpnQr](https://github.com/codebude/QRCoder/wiki/Advanced-usage---Payload-generators#315-slovenianupnqr) -* [SMS](https://github.com/codebude/QRCoder/wiki/Advanced-usage---Payload-generators#316-sms) -* [SwissQrCode (ISO-20022)](https://github.com/codebude/QRCoder/wiki/Advanced-usage---Payload-generators#317-swissqrcode-iso-20022) -* [URL](https://github.com/codebude/QRCoder/wiki/Advanced-usage---Payload-generators#318-url) -* [WhatsAppMessage](https://github.com/codebude/QRCoder/wiki/Advanced-usage---Payload-generators#319-whatsappmessage) -* [WiFi](https://github.com/codebude/QRCoder/wiki/Advanced-usage---Payload-generators#320-wifi) +* [RussiaPaymentOrder (ГОСТ Р 56042-2014)](https://github.com/codebude/QRCoder/wiki/Advanced-usage---Payload-generators#313-russiapaymentorder) +* [Shadowsocks configuration](https://github.com/codebude/QRCoder/wiki/Advanced-usage---Payload-generators#314-shadowsocks-configuration) +* [Skype call](https://github.com/codebude/QRCoder/wiki/Advanced-usage---Payload-generators#315-skype-call) +* [SlovenianUpnQr](https://github.com/codebude/QRCoder/wiki/Advanced-usage---Payload-generators#316-slovenianupnqr) +* [SMS](https://github.com/codebude/QRCoder/wiki/Advanced-usage---Payload-generators#317-sms) +* [SwissQrCode (ISO-20022)](https://github.com/codebude/QRCoder/wiki/Advanced-usage---Payload-generators#318-swissqrcode-iso-20022) +* [URL](https://github.com/codebude/QRCoder/wiki/Advanced-usage---Payload-generators#319-url) +* [WhatsAppMessage](https://github.com/codebude/QRCoder/wiki/Advanced-usage---Payload-generators#320-whatsappmessage) +* [WiFi](https://github.com/codebude/QRCoder/wiki/Advanced-usage---Payload-generators#321-wifi)