Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions CustomControls.SwissQRBill/SwissQRBill/ChecksumValidator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
using System.Linq;

namespace CustomControls.SwissQRBill {

public static class ChecksumValidator {
static readonly int[][] Mod10RecursiveTable = new int[][] {
new[] {0,9,4,6,8,2,7,1,3,5},
new[] {9,4,6,8,2,7,1,3,5,0},
new[] {4,6,8,2,7,1,3,5,0,9},
new[] {6,8,2,7,1,3,5,0,9,4},
new[] {8,2,7,1,3,5,0,9,4,6},
new[] {2,7,1,3,5,0,9,4,6,8},
new[] {7,1,3,5,0,9,4,6,8,2},
new[] {1,3,5,0,9,4,6,8,2,7},
new[] {3,5,0,9,4,6,8,2,7,1},
new[] {5,0,9,4,6,8,2,7,1,3},
};

public static bool IsValidMod10Recursive(string reference) {
if(reference == null || reference.Length != 27 || !reference.All(c => c >= '0' && c <= '9'))
return false;

int carry = 0;
for(int i = 0; i < 26; i++)
carry = Mod10RecursiveTable[carry][reference[i] - '0'];

int checkDigit = (10 - carry) % 10;
return checkDigit == reference[26] - '0';
}

public static bool IsValidIso7064Mod97(string value) {
if(value == null || value.Length < 5)
return false;

string rearranged = value.Substring(4) + value.Substring(0, 4);
int remainder = 0;
foreach(char c in rearranged) {
int numericValue;
if(c >= '0' && c <= '9') {
numericValue = c - '0';
remainder = (remainder * 10 + numericValue) % 97;
} else if(char.IsLetter(c)) {
numericValue = char.ToUpperInvariant(c) - 'A' + 10;
remainder = (remainder * 100 + numericValue) % 97;
} else {
return false;
}
}
return remainder == 1;
}
}
}
76 changes: 54 additions & 22 deletions CustomControls.SwissQRBill/SwissQRBill/DataModels/Address.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System;
using System.Linq;
using System.ComponentModel;
using System.Text.RegularExpressions;
using DevExpress.Utils.Serializing;

namespace CustomControls.SwissQRBill {
Expand All @@ -10,15 +11,21 @@ static string ToChar(AddressType addressType) {
switch(addressType) {
case AddressType.Structured:
return "S";
#pragma warning disable CS0618
case AddressType.Combined:
#pragma warning restore CS0618
return "K";
}
return null;
}
const string Switzerland = "CH";

const int DefaultPostalCode = 0;
const int DefaultBuildingNumber = 0;
string countryCode = string.Empty;
string buildingNumber = string.Empty;
string name = string.Empty;
string postalCode = string.Empty;
string street = string.Empty;
string town = string.Empty;

[DisplayName("Address Type")]
[Description("Address Type desription")]
Expand All @@ -31,13 +38,23 @@ static string ToChar(AddressType addressType) {
[Description("Country Code description")]
[DefaultValue("")]
[XtraSerializableProperty]
public string CountryCode { get; set; } = string.Empty;
public string CountryCode {
get => countryCode;
set {
if(!string.IsNullOrEmpty(value) && !Regex.IsMatch(value, "^[A-Z]{2}$"))
throw ValidationError.FieldException("Country", "Must be a two-letter country code (ISO 3166-1).");
countryCode = value ?? string.Empty;
}
}

[DisplayName("Building Number")]
[Description("Building Number description")]
[DefaultValue(DefaultBuildingNumber)]
[DefaultValue("")]
[XtraSerializableProperty]
public int BuildingNumber { get; set; } = DefaultBuildingNumber;
public string BuildingNumber {
get => buildingNumber;
set { FieldValidation.Validate(value, 16, "BuildingNumber"); buildingNumber = value ?? string.Empty; }
}

[DisplayName("Address Line 1")]
[Description("Address Line 1 description")]
Expand All @@ -55,36 +72,48 @@ static string ToChar(AddressType addressType) {
[Description("Name description")]
[DefaultValue("")]
[XtraSerializableProperty]
public string Name { get; set; } = string.Empty;
public string Name {
get => name;
set { FieldValidation.Validate(value, 70, "Name"); name = value ?? string.Empty; }
}

[DisplayName("Postal Code")]
[Description("Postal Code description")]
[DefaultValue(DefaultPostalCode)]
[DefaultValue("")]
[XtraSerializableProperty]
public int PostalCode { get; set; } = DefaultPostalCode;
public string PostalCode {
get => postalCode;
set { FieldValidation.Validate(value, 16, "PostalCode"); postalCode = value ?? string.Empty; }
}

[DisplayName("Street")]
[Description("Street description")]
[DefaultValue("")]
[XtraSerializableProperty]
public string Street { get; set; } = string.Empty;
public string Street {
get => street;
set { FieldValidation.Validate(value, 70, "Street"); street = value ?? string.Empty; }
}

[DisplayName("Town")]
[Description("Town description")]
[DefaultValue("")]
[XtraSerializableProperty]
public string Town { get; set; } = string.Empty;
public string Town {
get => town;
set { FieldValidation.Validate(value, 35, "Town"); town = value ?? string.Empty; }
}

public override void ConvertFromQRCodeDataString(string[] rawString) {
#pragma warning disable CS0618
AddressType = rawString[0] == "K" ? AddressType.Combined
: AddressType.Structured; //real value is empty or structured
: AddressType.Structured;
#pragma warning restore CS0618
Name = rawString[1];
if(AddressType == AddressType.Structured) {
Street = rawString[2];
if(int.TryParse(rawString[3], out int result))
BuildingNumber = result;
if(int.TryParse(rawString[4], out result))
PostalCode = result;
BuildingNumber = rawString[3];
PostalCode = rawString[4];
Town = rawString[5];
CountryCode = rawString[6];
} else {
Expand All @@ -98,10 +127,11 @@ public override string ConvertToPresentationString() {
if(IsEmpty())
return string.Empty;
if(AddressType == AddressType.Structured) {
string streetLine = string.IsNullOrEmpty(BuildingNumber) ? Street : $"{Street} {BuildingNumber}";
if(CountryCode == Switzerland) {
return string.Join(Environment.NewLine, Name, $"{Street} {BuildingNumber}", $"{PostalCode} {Town}");
return string.Join(Environment.NewLine, Name, streetLine, $"{PostalCode} {Town}");
} else {
return string.Join(Environment.NewLine, Name, $"{Street} {BuildingNumber}", $"{CountryCode} {PostalCode} {Town}");
return string.Join(Environment.NewLine, Name, streetLine, $"{CountryCode} {PostalCode} {Town}");
}
}
else
Expand All @@ -114,19 +144,21 @@ public override string ConvertToQRCodeDataString() {

if(AddressType == AddressType.Structured) {
return string.Join(Environment.NewLine, ToChar(AddressType), Name, Street, BuildingNumber, PostalCode, Town, CountryCode);
} else {
return string.Join(Environment.NewLine, ToChar(AddressType), Name, AddressLine1, AddressLine2, string.Empty, CountryCode);
}
throw ValidationError.FieldException("AddressType",
"Combined address type ('K') is no longer part of the Swiss QR Code data structure since Implementation Guidelines v2.3 (valid from 21 November 2025, mandatory since 30 September 2026). Use AddressType.Structured instead.");
}

bool IsEmpty() {
if(!string.IsNullOrEmpty(Name))
return false;

#pragma warning disable CS0618
return AddressType == AddressType.Combined
? string.IsNullOrEmpty(AddressLine1) && string.IsNullOrEmpty(AddressLine2)
: string.IsNullOrEmpty(Street) && string.IsNullOrEmpty(Town)
&& PostalCode == DefaultPostalCode && BuildingNumber == DefaultBuildingNumber;
&& string.IsNullOrEmpty(PostalCode) && string.IsNullOrEmpty(BuildingNumber);
#pragma warning restore CS0618
}

public override void Reset() {
Expand All @@ -135,8 +167,8 @@ public override void Reset() {
Street = string.Empty;
Town = string.Empty;
CountryCode = string.Empty;
PostalCode = DefaultPostalCode;
BuildingNumber = DefaultBuildingNumber;
PostalCode = string.Empty;
BuildingNumber = string.Empty;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -67,10 +67,16 @@ public override string ConvertToPresentationString() {

public override string ConvertToQRCodeDataString() {
List<string> result = new List<string>();
if(!IsFirstPairEmpty)
result.Add($"{Name1}{SplitterString}{Instruction1}");
if(!IsSecondPairEmpty)
result.Add($"{Name2}{SplitterString}{Instruction2}");
if(!IsFirstPairEmpty) {
string procedure1 = $"{Name1}{SplitterString}{Instruction1}";
FieldValidation.Validate(procedure1, 100, "AlternativeProcedures.Name1/Instruction1");
result.Add(procedure1);
}
if(!IsSecondPairEmpty) {
string procedure2 = $"{Name2}{SplitterString}{Instruction2}";
FieldValidation.Validate(procedure2, 100, "AlternativeProcedures.Name2/Instruction2");
result.Add(procedure2);
}
return string.Join(Environment.NewLine, result);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
namespace CustomControls.SwissQRBill {
public class CreditorAccountNumber : AccountNumber {
const string DefaultNumber = "CHXXXXXXXXXXXXXXXXXXX";
const string DefaultNumber = "CH5800791123000889012";

public CreditorAccountNumber(string ibanStr) : base(ibanStr) { }
public CreditorAccountNumber() : this(DefaultNumber) { }
static bool IsIban(string str) {
return str.Length == 21 && (str.StartsWith("CH") || str.StartsWith("LI"));
}
static bool IsQR_IBAN(string str) {
return IsIban(str) && str[4] == '3';
return IsIban(str) && int.TryParse(str.Substring(4, 5), out int iid) && iid >= 30000 && iid <= 31999;
}
protected override bool IsValid(string str) {
return IsIban(str) || IsQR_IBAN(str);
return (IsIban(str) || IsQR_IBAN(str)) && ChecksumValidator.IsValidIso7064Mod97(str);
}
protected override void IdentifyFormat() {
if(IsIban(Number))
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,27 @@
using System.Linq;
using System.Text.RegularExpressions;

namespace CustomControls.SwissQRBill {
public class PaymentReferenceAccountNumber : AccountNumber {
const string DefaultNumber = "RFXXXXXXXXXXXXXX";
const string DefaultNumber = "";

public PaymentReferenceAccountNumber() : this(DefaultNumber) { }
public PaymentReferenceAccountNumber(string ibanStr) : base(ibanStr) { }
static bool IsCreditorReference(string str) {
return str.Length == 16 && str.StartsWith("RF");
return str.Length >= 5 && str.Length <= 25
&& Regex.IsMatch(str, "^RF[0-9]{2}[A-Za-z0-9]+$");
}
static bool IsQR_Reference(string str) {
return str.Length == 27;
return str.Length == 27 && str.All(c => c >= '0' && c <= '9');
}
protected override bool IsValid(string str) {
return string.IsNullOrEmpty(str) || IsQR_Reference(str) || IsCreditorReference(str);
if(string.IsNullOrEmpty(str))
return true;
if(IsQR_Reference(str))
return ChecksumValidator.IsValidMod10Recursive(str);
if(IsCreditorReference(str))
return ChecksumValidator.IsValidIso7064Mod97(str);
return false;
}
protected override void IdentifyFormat() {
if(IsCreditorReference(Number))
Expand Down
Loading