diff --git a/QRCoder/PayloadGenerator.cs b/QRCoder/PayloadGenerator.cs index d3b83c13..4a5c796e 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_3 +using System.Reflection; +#endif namespace QRCoder { @@ -2385,6 +2388,7 @@ public SlovenianUpnQr(string payerName, string payerAddress, string payerPlace, _recipientSiModel = LimitLength(recipientSiModel.Trim().ToUpper(), 4); _recipientSiReference = LimitLength(recipientSiReference.Trim(), 22); } + private string FormatAmount(double amount) { @@ -2431,7 +2435,613 @@ public override string ToString() _sb.AppendFormat("{0:000}", CalculateChecksum()).Append('\n'); return _sb.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; + private MandatoryFields mFields; + private OptionalFields oFields; + private string separator = "|"; + + private RussiaPaymentOrder() + { + mFields = new MandatoryFields(); + oFields = new OptionalFields(); + } + + /// + /// 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}$"); + + 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() + { + 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 = 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.PropertyType.Equals(typeof(DateTime?)) ? ((DateTime)objValue).ToString("dd.MM.yyyy") : objValue.ToString(); + return $"{field.Name}={value}"; + }) + .ToList(); +#else + return oFields.GetType().GetProperties() + .Where(field => field.GetValue(oFields, null) != null) + .Select(field => { + var objValue = field.GetValue(oFields, null); + var value = field.PropertyType.Equals(typeof(DateTime?)) ? ((DateTime)objValue).ToString("dd.MM.yyyy") : objValue.ToString(); + return $"{field.Name}={value}"; + }) + .ToList(); +#endif + } + + + /// + /// 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 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 + } + + /// + /// 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) + /// Перечень значений технического кода платежа + /// + public 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(string message) + : base(message) + { + } + } + + } + private static bool IsValidIban(string iban) { 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 @@ + + + + diff --git a/QRCoderTests/PayloadGeneratorTests.cs b/QRCoderTests/PayloadGeneratorTests.cs index 64717574..d82b7f98 100644 --- a/QRCoderTests/PayloadGeneratorTests.cs +++ b/QRCoderTests/PayloadGeneratorTests.cs @@ -3054,6 +3054,326 @@ 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_mandatory_fields() + { + var account = "40702810138250123017"; + var bic = "044525225"; + 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(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_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] + [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 optionalFields = new PayloadGenerator.RussiaPaymentOrder.OptionalFields() + { + FirstName = "Raffael", + LastName = "Herrmann", + Sum = "125000" + }; + + 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}|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_pt4() + { + var account = "40702810138250123017"; + var bic = "044525225"; + var bankName = "=ОАО \"БАНК\""; + var name = "ООО «Три кита»"; + var correspAcc = "30101810400000000225"; + var optionalFields = new PayloadGenerator.RussiaPaymentOrder.OptionalFields() + { + 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(name, account, bankName, bic, correspAcc, optionalFields); + + generator + .ToString() + .ShouldBe($"ST00012|Name={name}|PersonalAcc={account}|BankName={bankName}|BIC={bic}|CorrespAcc={correspAcc}|TaxPeriod=31|TaxPaytKind=99|SpecFio=T. Eacher|UIN=1a2b|TechCode=ГИБДД_налоги_пошлины_бюджетные_платежи|"); + } } } 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)