Skip to content

Commit

Permalink
Merge pull request #416 from nofrixion/bugfix/MOOV-3697-scan-validation
Browse files Browse the repository at this point in the history
Validate DestinationSortCode and DestinationAccountNumber
  • Loading branch information
donalnofrixion authored Sep 17, 2024
2 parents 44ba888 + 544c67f commit 1566127
Show file tree
Hide file tree
Showing 2 changed files with 145 additions and 51 deletions.
22 changes: 22 additions & 0 deletions src/NoFrixion.MoneyMoov/Models/Payouts/PayoutsValidator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,16 @@ public static class PayoutsValidator
/// </summary>
public const decimal BITCOIN_CURRENCY_RESOLUTION = 0.0000_0001M;

/// <summary>
/// The required length for a SCAN account number.
/// </summary>
private const int SCAN_REQUIRED_ACCOUNT_NUMBER_LENGTH = 8;

/// <summary>
/// The required length for a SCAN sort code.
/// </summary>
private const int SCAN_REQUIRED_SORT_CODE_LENGTH = 6;

public static bool ValidateIBAN(string iban)
{
if (string.IsNullOrEmpty(iban))
Expand Down Expand Up @@ -389,6 +399,18 @@ public static IEnumerable<ValidationResult> Validate(Payout payout, ValidationCo
{
yield return new ValidationResult($"Currency {payout.Currency} cannot be used with SCAN destinations.", new string[] { nameof(payout.Currency) });
}

if (payout.Type == AccountIdentifierType.SCAN && payout.Destination?.Identifier != null &&
!string.IsNullOrEmpty(payout.Destination?.Identifier?.AccountNumber) && payout.Destination.Identifier.AccountNumber.Length != SCAN_REQUIRED_ACCOUNT_NUMBER_LENGTH)
{
yield return new ValidationResult("Destination account number must be eight digits for a SCAN payout type.", new string[] { nameof(payout.Destination.Identifier.AccountNumber) });
}

if (payout.Type == AccountIdentifierType.SCAN && payout.Destination?.Identifier != null &&
!string.IsNullOrEmpty(payout.Destination?.Identifier?.SortCode) && payout.Destination.Identifier.SortCode.Length != SCAN_REQUIRED_SORT_CODE_LENGTH)
{
yield return new ValidationResult("Destination sort code must be six digits for a SCAN payout type.", new string[] { nameof(payout.Destination.Identifier.SortCode) });
}
}

if (payout.Destination?.Identifier != null)
Expand Down
174 changes: 123 additions & 51 deletions test/MoneyMoov.UnitTests/Models/PayoutsValidatorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,8 @@ public void PaymentsValidator_ValidateTheirReference_Success_Modulr(string their
var result = PayoutsValidator.ValidateTheirReference(theirReference, MoneyMoov.AccountIdentifierType.IBAN, PaymentProcessorsEnum.Modulr);

Assert.True(result);
}

}

/// <summary>
/// Tests that a payout property TheirReference is validated successfully.
/// </summary>
Expand All @@ -56,12 +56,12 @@ public void PaymentsValidator_ValidateTheirReference_Success_Modulr(string their
[InlineData("?./refe-12")]
[InlineData("r12 hsd-2")]
[InlineData("s-d73/sdf.4(8) ?:,'++")]
[InlineData("s-D7 K sdf -")]
[InlineData("Saldo F16 + F20")]
[InlineData("s-D7 K sdf -")]
[InlineData("Saldo F16 + F20")]
[InlineData("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD")]
public void PaymentsValidator_ValidateTheirReference_Success_Banking_Circle(string theirReference)
{
var result =
var result =
PayoutsValidator.ValidateTheirReference(theirReference, MoneyMoov.AccountIdentifierType.IBAN, PaymentProcessorsEnum.BankingCircle) &&
PayoutsValidator.ValidateTheirReference(theirReference, MoneyMoov.AccountIdentifierType.IBAN, PaymentProcessorsEnum.BankingCircleAgency);

Expand All @@ -83,8 +83,8 @@ public void PaymentsValidator_ValidateTheirReference_Fail(string theirReference,
var result = PayoutsValidator.ValidateTheirReference(theirReference, identifierType, PaymentProcessorsEnum.Modulr);

Assert.False(result);
}

}

/// <summary>
/// Tests that an invalid payout TheirReference fails the Banking Circle validation rules.
/// </summary>
Expand Down Expand Up @@ -132,8 +132,8 @@ public void PaymentsValidator_Modulr_Validate_Account_Name_Fails(string accountN
var result = PayoutsValidator.IsValidAccountName(accountName, PaymentProcessorsEnum.Modulr);

Assert.False(result);
}

}

/// <summary>
/// Tests that a payout Account Name is validated successfully.
/// </summary>
Expand Down Expand Up @@ -249,10 +249,10 @@ public enum ComparisonMethod
[InlineData(AccountIdentifierType.IBAN, "-sD7&K.sdf./", "-sD7&K.sdf./", ComparisonMethod.Equals)] // If valid, gets left alone.
[InlineData(AccountIdentifierType.IBAN, "-sD7!&K.sdf./", "sD7Ksdf", ComparisonMethod.Equals)] // If invalid, non-ASCII chars get replaced.
[InlineData(AccountIdentifierType.IBAN, "Acme™ Corporation", "Acme Corporation", ComparisonMethod.Equals)] // Replace unicode.
[InlineData(AccountIdentifierType.SCAN, "Too long for SCAN which only likes short refs", "Too long for SCAN", ComparisonMethod.Equals)]
[InlineData(AccountIdentifierType.SCAN, "Just rght for SCAN", "Just rght for SCAN", ComparisonMethod.Equals)]
[InlineData(AccountIdentifierType.IBAN,
"Just right for IBAN processing making it simple and efficient for all your international banking needs. Ensure accuracy and security always.",
[InlineData(AccountIdentifierType.SCAN, "Too long for SCAN which only likes short refs", "Too long for SCAN", ComparisonMethod.Equals)]
[InlineData(AccountIdentifierType.SCAN, "Just rght for SCAN", "Just rght for SCAN", ComparisonMethod.Equals)]
[InlineData(AccountIdentifierType.IBAN,
"Just right for IBAN processing making it simple and efficient for all your international banking needs. Ensure accuracy and security always.",
"Just right for IBAN processing making it simple and efficient for all your international banking needs. Ensure accuracy and security always.",
ComparisonMethod.Equals)]
public void PaymentsValidator_Make_Safe_TheirReference_Modulr(AccountIdentifierType accountType,
Expand All @@ -274,7 +274,7 @@ public void PaymentsValidator_Make_Safe_TheirReference_Modulr(AccountIdentifierT
{
Assert.StartsWith(expectedTheirReference, safeTheirRef);
}
}
}

[Fact]
public void GetReferencesFromInvoices()
Expand Down Expand Up @@ -370,7 +370,7 @@ public void PayoutsValidator_Validate_GBP_Destination_Identifier_Success()
Identifier = new AccountIdentifier
{
SortCode = "123456",
AccountNumber = "00000070629907",
AccountNumber = "70629907",
Currency = CurrencyTypeEnum.GBP
}
};
Expand All @@ -397,6 +397,78 @@ public void PayoutsValidator_Validate_GBP_Destination_Identifier_Success()
Assert.True(result.IsEmpty);
}

[Fact]
public void PayoutsValidator_Validate_GBP_Destination_Identifier_AccountNumber_Fail()
{
var destination = new Counterparty
{
Name = "Joe Bloggs",
Identifier = new AccountIdentifier
{
SortCode = "123456",
AccountNumber = "7062990",
Currency = CurrencyTypeEnum.GBP
}
};

var payout = new Payout
{
ID = Guid.NewGuid(),
AccountID = Guid.Parse("B2DBB4E1-5F8A-4B07-82A0-EB033E6F3421"),
Type = AccountIdentifierType.SCAN,
Description = "Xero Invoice fgfg from Demo Company (Global).",
Currency = CurrencyTypeEnum.GBP,
Amount = 11.00M,
YourReference = "xero-18ead957-e3bc-4b12-b5c6-d12e4bef9d24",
TheirReference = "Placeholder",
Status = PayoutStatus.PENDING_INPUT,
InvoiceID = "18ead957-e3bc-4b12-b5c6-d12e4bef9d24",
Destination = destination
};

var result = payout.Validate();

_logger.LogDebug(result.ToTextErrorMessage());

Assert.False(result.IsEmpty);
}

[Fact]
public void PayoutsValidator_Validate_GBP_Destination_Identifier_SortCode_Fail()
{
var destination = new Counterparty
{
Name = "Joe Bloggs",
Identifier = new AccountIdentifier
{
SortCode = "1234567",
AccountNumber = "70629907",
Currency = CurrencyTypeEnum.GBP
}
};

var payout = new Payout
{
ID = Guid.NewGuid(),
AccountID = Guid.Parse("B2DBB4E1-5F8A-4B07-82A0-EB033E6F3421"),
Type = AccountIdentifierType.SCAN,
Description = "Xero Invoice fgfg from Demo Company (Global).",
Currency = CurrencyTypeEnum.GBP,
Amount = 11.00M,
YourReference = "xero-18ead957-e3bc-4b12-b5c6-d12e4bef9d24",
TheirReference = "Placeholder",
Status = PayoutStatus.PENDING_INPUT,
InvoiceID = "18ead957-e3bc-4b12-b5c6-d12e4bef9d24",
Destination = destination
};

var result = payout.Validate();

_logger.LogDebug(result.ToTextErrorMessage());

Assert.False(result.IsEmpty);
}

[Fact]
public void PayoutsValidator_Validate_EUR_Destination_Identifier_Failure()
{
Expand Down Expand Up @@ -463,33 +535,33 @@ public void PayoutsValidator_Validate_GBP_Destination_Identifier_Failure()
_logger.LogDebug(result.ToTextErrorMessage());

Assert.False(result.IsEmpty);
}

}

/// <summary>
/// Tests that the currency resolution check is working as expected.
/// </summary>
[Theory]
[InlineData(CurrencyTypeEnum.EUR, 0.01, true)]
[InlineData(CurrencyTypeEnum.EUR, 0.001, false)]
[InlineData(CurrencyTypeEnum.EUR, 1.011, false)]
[InlineData(CurrencyTypeEnum.GBP, 0.01, true)]
[InlineData(CurrencyTypeEnum.GBP, 0.001, false)]
[InlineData(CurrencyTypeEnum.GBP, 1.011, false)]
[InlineData(CurrencyTypeEnum.BTC, 0.01, true)]
[InlineData(CurrencyTypeEnum.BTC, 0.001, true)]
[InlineData(CurrencyTypeEnum.BTC, 0.00000001, true)]
[InlineData(CurrencyTypeEnum.BTC, 0.000000001, false)]
[InlineData(CurrencyTypeEnum.BTC, 1.011, true)]
[InlineData(CurrencyTypeEnum.EUR, 0.01, true)]
[InlineData(CurrencyTypeEnum.EUR, 0.001, false)]
[InlineData(CurrencyTypeEnum.EUR, 1.011, false)]
[InlineData(CurrencyTypeEnum.GBP, 0.01, true)]
[InlineData(CurrencyTypeEnum.GBP, 0.001, false)]
[InlineData(CurrencyTypeEnum.GBP, 1.011, false)]
[InlineData(CurrencyTypeEnum.BTC, 0.01, true)]
[InlineData(CurrencyTypeEnum.BTC, 0.001, true)]
[InlineData(CurrencyTypeEnum.BTC, 0.00000001, true)]
[InlineData(CurrencyTypeEnum.BTC, 0.000000001, false)]
[InlineData(CurrencyTypeEnum.BTC, 1.011, true)]
[InlineData(CurrencyTypeEnum.BTC, 1.000000011, false)]
public void Payout_Validator_Currency_Resolution(CurrencyTypeEnum currency, decimal amount, bool isValid)
{
AccountIdentifier identifier = currency switch
{
CurrencyTypeEnum.BTC => new AccountIdentifier { Currency = currency, BitcoinAddress = "abcdefg" },
CurrencyTypeEnum.GBP => new AccountIdentifier { Currency = currency, SortCode = "123456", AccountNumber = "00001234" } ,
_ => new AccountIdentifier { Currency = currency, IBAN = "IE78MOCK91012352877713" }
};

{
AccountIdentifier identifier = currency switch
{
CurrencyTypeEnum.BTC => new AccountIdentifier { Currency = currency, BitcoinAddress = "abcdefg" },
CurrencyTypeEnum.GBP => new AccountIdentifier { Currency = currency, SortCode = "123456", AccountNumber = "00001234" } ,
_ => new AccountIdentifier { Currency = currency, IBAN = "IE78MOCK91012352877713" }
};

var payout = new Payout
{
ID = Guid.NewGuid(),
Expand All @@ -501,24 +573,24 @@ public void Payout_Validator_Currency_Resolution(CurrencyTypeEnum currency, deci
TheirReference = "their ref",
Status = PayoutStatus.PENDING_INPUT,
InvoiceID = "18ead957-e3bc-4b12-b5c6-d12e4bef9d24",
Destination = new Counterparty
{
Name = "Joe Bloggs",
Identifier = identifier
Destination = new Counterparty
{
Name = "Joe Bloggs",
Identifier = identifier
}
};

var result = payout.Validate();

_logger.LogDebug(result.ToTextErrorMessage());

if(isValid)
{
Assert.True(result.IsEmpty);
}
else
{
Assert.False(result.IsEmpty);
var result = payout.Validate();

_logger.LogDebug(result.ToTextErrorMessage());

if(isValid)
{
Assert.True(result.IsEmpty);
}
else
{
Assert.False(result.IsEmpty);
}
}
}

0 comments on commit 1566127

Please sign in to comment.