using System.Security.Cryptography;
var paymentReferenceService = new PaymentReferenceService();
var wrapperGlobalId = Guid.Parse("806b7c35-d19a-473e-bbdb-dc10987cbde1");
var paymentType = PaymentType.Vrp;
for (var i = 0; i < 10; i++)
var reference = paymentReferenceService.GenerateReference(amount, wrapperGlobalId, paymentType);
var payment = new Payment
SettlementAmount = amount,
WrapperGlobalId = wrapperGlobalId,
ReceivedAt = DateTime.UtcNow,
var valid = paymentReferenceService.ValidateReferenceForPayment(payment);
Console.WriteLine($"Reference: {reference}, Valid: {valid}");
public string Reference { get; init; }
public decimal SettlementAmount { get; init; }
public DateTime ReceivedAt { get; init; }
public Guid WrapperGlobalId { get; init; }
public class PaymentReferenceService
private const string Base36Chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
public string GenerateReference(decimal paymentAmount, Guid wrapperGlobalId, PaymentType paymentType)
var paymentTypeChar = paymentType.ToString()[0];
var amountEncoded = ConvertAmountToBase36(paymentAmount);
var wrapperGlobalIdString = wrapperGlobalId.ToString();
var wrapperGlobalIdEnding = wrapperGlobalIdString.Substring(wrapperGlobalIdString.Length - 4).ToUpper();
var randomSequence = GenerateRandomSequence(3);
var dataForChecksum = $"{paymentTypeChar}{wrapperGlobalIdEnding}{amountEncoded}{randomSequence}";
var checksum = ComputeChecksum(dataForChecksum, 4);
return $"{dataForChecksum}{checksum}";
public bool ValidateReferenceForPayment(Payment payment)
if (payment?.Reference is null) return false;
if (payment.Reference.Length != 16) return false;
var reference = payment.Reference;
var paymentType = reference[0];
var wrapperGlobalIdEnding = reference.Substring(1, 4);
var amountEncoded = reference.Substring(5, 4);
var randomSequence = reference.Substring(9, 3);
var receivedChecksum = reference.Substring(12, 4);
var receivedAmount = DecodeBase36ToDecimal(amountEncoded);
var dataForChecksum = $"{paymentType}{wrapperGlobalIdEnding}{amountEncoded}{randomSequence}";
var recomputedChecksum = ComputeChecksum(dataForChecksum, 4);
var isAmountValid = receivedAmount == payment.SettlementAmount;
var isWrapperGlobalIdValid = payment.WrapperGlobalId.ToString().EndsWith(wrapperGlobalIdEnding, StringComparison.OrdinalIgnoreCase);
var isChecksumValid = recomputedChecksum.Equals(receivedChecksum, StringComparison.OrdinalIgnoreCase);
return isAmountValid && isWrapperGlobalIdValid && isChecksumValid;
private string ComputeChecksum(string data, int length)
using var sha256 = SHA256.Create();
var hashBytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(data));
var hashPrefix = BitConverter.ToInt64(hashBytes, 0);
var positiveHashPrefix = Math.Abs(hashPrefix);
return ConvertToBase36(positiveHashPrefix).PadLeft(length, '0').Substring(0, length);
private string ConvertToBase36(long input)
if (input == 0) return "0";
var result = new StringBuilder();
result.Insert(0, Base36Chars[(int)(input % 36)]);
return result.ToString();
private string ConvertAmountToBase36(decimal amount)
var result = new StringBuilder();
var intAmount = (int)amount;
result.Insert(0, Base36Chars[intAmount % 36]);
return result.ToString().PadLeft(4, '0');
private decimal DecodeBase36ToDecimal(string base36EncodedAmount)
foreach (var c in base36EncodedAmount)
result += Base36Chars.IndexOf(c);
private string GenerateRandomSequence(int length)
var randomBytes = RandomNumberGenerator.GetBytes(length);
var result = new StringBuilder(length);
foreach (var b in randomBytes)
result.Append(Base36Chars[b % Base36Chars.Length]);
return result.ToString();