Skip to content

Commit 1566127

Browse files
Merge pull request #416 from nofrixion/bugfix/MOOV-3697-scan-validation
Validate DestinationSortCode and DestinationAccountNumber
2 parents 44ba888 + 544c67f commit 1566127

File tree

2 files changed

+145
-51
lines changed

2 files changed

+145
-51
lines changed

src/NoFrixion.MoneyMoov/Models/Payouts/PayoutsValidator.cs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,16 @@ public static class PayoutsValidator
145145
/// </summary>
146146
public const decimal BITCOIN_CURRENCY_RESOLUTION = 0.0000_0001M;
147147

148+
/// <summary>
149+
/// The required length for a SCAN account number.
150+
/// </summary>
151+
private const int SCAN_REQUIRED_ACCOUNT_NUMBER_LENGTH = 8;
152+
153+
/// <summary>
154+
/// The required length for a SCAN sort code.
155+
/// </summary>
156+
private const int SCAN_REQUIRED_SORT_CODE_LENGTH = 6;
157+
148158
public static bool ValidateIBAN(string iban)
149159
{
150160
if (string.IsNullOrEmpty(iban))
@@ -389,6 +399,18 @@ public static IEnumerable<ValidationResult> Validate(Payout payout, ValidationCo
389399
{
390400
yield return new ValidationResult($"Currency {payout.Currency} cannot be used with SCAN destinations.", new string[] { nameof(payout.Currency) });
391401
}
402+
403+
if (payout.Type == AccountIdentifierType.SCAN && payout.Destination?.Identifier != null &&
404+
!string.IsNullOrEmpty(payout.Destination?.Identifier?.AccountNumber) && payout.Destination.Identifier.AccountNumber.Length != SCAN_REQUIRED_ACCOUNT_NUMBER_LENGTH)
405+
{
406+
yield return new ValidationResult("Destination account number must be eight digits for a SCAN payout type.", new string[] { nameof(payout.Destination.Identifier.AccountNumber) });
407+
}
408+
409+
if (payout.Type == AccountIdentifierType.SCAN && payout.Destination?.Identifier != null &&
410+
!string.IsNullOrEmpty(payout.Destination?.Identifier?.SortCode) && payout.Destination.Identifier.SortCode.Length != SCAN_REQUIRED_SORT_CODE_LENGTH)
411+
{
412+
yield return new ValidationResult("Destination sort code must be six digits for a SCAN payout type.", new string[] { nameof(payout.Destination.Identifier.SortCode) });
413+
}
392414
}
393415

394416
if (payout.Destination?.Identifier != null)

test/MoneyMoov.UnitTests/Models/PayoutsValidatorTests.cs

Lines changed: 123 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,8 @@ public void PaymentsValidator_ValidateTheirReference_Success_Modulr(string their
4646
var result = PayoutsValidator.ValidateTheirReference(theirReference, MoneyMoov.AccountIdentifierType.IBAN, PaymentProcessorsEnum.Modulr);
4747

4848
Assert.True(result);
49-
}
50-
49+
}
50+
5151
/// <summary>
5252
/// Tests that a payout property TheirReference is validated successfully.
5353
/// </summary>
@@ -56,12 +56,12 @@ public void PaymentsValidator_ValidateTheirReference_Success_Modulr(string their
5656
[InlineData("?./refe-12")]
5757
[InlineData("r12 hsd-2")]
5858
[InlineData("s-d73/sdf.4(8) ?:,'++")]
59-
[InlineData("s-D7 K sdf -")]
60-
[InlineData("Saldo F16 + F20")]
59+
[InlineData("s-D7 K sdf -")]
60+
[InlineData("Saldo F16 + F20")]
6161
[InlineData("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD")]
6262
public void PaymentsValidator_ValidateTheirReference_Success_Banking_Circle(string theirReference)
6363
{
64-
var result =
64+
var result =
6565
PayoutsValidator.ValidateTheirReference(theirReference, MoneyMoov.AccountIdentifierType.IBAN, PaymentProcessorsEnum.BankingCircle) &&
6666
PayoutsValidator.ValidateTheirReference(theirReference, MoneyMoov.AccountIdentifierType.IBAN, PaymentProcessorsEnum.BankingCircleAgency);
6767

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

8585
Assert.False(result);
86-
}
87-
86+
}
87+
8888
/// <summary>
8989
/// Tests that an invalid payout TheirReference fails the Banking Circle validation rules.
9090
/// </summary>
@@ -132,8 +132,8 @@ public void PaymentsValidator_Modulr_Validate_Account_Name_Fails(string accountN
132132
var result = PayoutsValidator.IsValidAccountName(accountName, PaymentProcessorsEnum.Modulr);
133133

134134
Assert.False(result);
135-
}
136-
135+
}
136+
137137
/// <summary>
138138
/// Tests that a payout Account Name is validated successfully.
139139
/// </summary>
@@ -249,10 +249,10 @@ public enum ComparisonMethod
249249
[InlineData(AccountIdentifierType.IBAN, "-sD7&K.sdf./", "-sD7&K.sdf./", ComparisonMethod.Equals)] // If valid, gets left alone.
250250
[InlineData(AccountIdentifierType.IBAN, "-sD7!&K.sdf./", "sD7Ksdf", ComparisonMethod.Equals)] // If invalid, non-ASCII chars get replaced.
251251
[InlineData(AccountIdentifierType.IBAN, "Acme™ Corporation", "Acme Corporation", ComparisonMethod.Equals)] // Replace unicode.
252-
[InlineData(AccountIdentifierType.SCAN, "Too long for SCAN which only likes short refs", "Too long for SCAN", ComparisonMethod.Equals)]
253-
[InlineData(AccountIdentifierType.SCAN, "Just rght for SCAN", "Just rght for SCAN", ComparisonMethod.Equals)]
254-
[InlineData(AccountIdentifierType.IBAN,
255-
"Just right for IBAN processing making it simple and efficient for all your international banking needs. Ensure accuracy and security always.",
252+
[InlineData(AccountIdentifierType.SCAN, "Too long for SCAN which only likes short refs", "Too long for SCAN", ComparisonMethod.Equals)]
253+
[InlineData(AccountIdentifierType.SCAN, "Just rght for SCAN", "Just rght for SCAN", ComparisonMethod.Equals)]
254+
[InlineData(AccountIdentifierType.IBAN,
255+
"Just right for IBAN processing making it simple and efficient for all your international banking needs. Ensure accuracy and security always.",
256256
"Just right for IBAN processing making it simple and efficient for all your international banking needs. Ensure accuracy and security always.",
257257
ComparisonMethod.Equals)]
258258
public void PaymentsValidator_Make_Safe_TheirReference_Modulr(AccountIdentifierType accountType,
@@ -274,7 +274,7 @@ public void PaymentsValidator_Make_Safe_TheirReference_Modulr(AccountIdentifierT
274274
{
275275
Assert.StartsWith(expectedTheirReference, safeTheirRef);
276276
}
277-
}
277+
}
278278

279279
[Fact]
280280
public void GetReferencesFromInvoices()
@@ -370,7 +370,7 @@ public void PayoutsValidator_Validate_GBP_Destination_Identifier_Success()
370370
Identifier = new AccountIdentifier
371371
{
372372
SortCode = "123456",
373-
AccountNumber = "00000070629907",
373+
AccountNumber = "70629907",
374374
Currency = CurrencyTypeEnum.GBP
375375
}
376376
};
@@ -397,6 +397,78 @@ public void PayoutsValidator_Validate_GBP_Destination_Identifier_Success()
397397
Assert.True(result.IsEmpty);
398398
}
399399

400+
[Fact]
401+
public void PayoutsValidator_Validate_GBP_Destination_Identifier_AccountNumber_Fail()
402+
{
403+
var destination = new Counterparty
404+
{
405+
Name = "Joe Bloggs",
406+
Identifier = new AccountIdentifier
407+
{
408+
SortCode = "123456",
409+
AccountNumber = "7062990",
410+
Currency = CurrencyTypeEnum.GBP
411+
}
412+
};
413+
414+
var payout = new Payout
415+
{
416+
ID = Guid.NewGuid(),
417+
AccountID = Guid.Parse("B2DBB4E1-5F8A-4B07-82A0-EB033E6F3421"),
418+
Type = AccountIdentifierType.SCAN,
419+
Description = "Xero Invoice fgfg from Demo Company (Global).",
420+
Currency = CurrencyTypeEnum.GBP,
421+
Amount = 11.00M,
422+
YourReference = "xero-18ead957-e3bc-4b12-b5c6-d12e4bef9d24",
423+
TheirReference = "Placeholder",
424+
Status = PayoutStatus.PENDING_INPUT,
425+
InvoiceID = "18ead957-e3bc-4b12-b5c6-d12e4bef9d24",
426+
Destination = destination
427+
};
428+
429+
var result = payout.Validate();
430+
431+
_logger.LogDebug(result.ToTextErrorMessage());
432+
433+
Assert.False(result.IsEmpty);
434+
}
435+
436+
[Fact]
437+
public void PayoutsValidator_Validate_GBP_Destination_Identifier_SortCode_Fail()
438+
{
439+
var destination = new Counterparty
440+
{
441+
Name = "Joe Bloggs",
442+
Identifier = new AccountIdentifier
443+
{
444+
SortCode = "1234567",
445+
AccountNumber = "70629907",
446+
Currency = CurrencyTypeEnum.GBP
447+
}
448+
};
449+
450+
var payout = new Payout
451+
{
452+
ID = Guid.NewGuid(),
453+
AccountID = Guid.Parse("B2DBB4E1-5F8A-4B07-82A0-EB033E6F3421"),
454+
Type = AccountIdentifierType.SCAN,
455+
Description = "Xero Invoice fgfg from Demo Company (Global).",
456+
Currency = CurrencyTypeEnum.GBP,
457+
Amount = 11.00M,
458+
YourReference = "xero-18ead957-e3bc-4b12-b5c6-d12e4bef9d24",
459+
TheirReference = "Placeholder",
460+
Status = PayoutStatus.PENDING_INPUT,
461+
InvoiceID = "18ead957-e3bc-4b12-b5c6-d12e4bef9d24",
462+
Destination = destination
463+
};
464+
465+
var result = payout.Validate();
466+
467+
_logger.LogDebug(result.ToTextErrorMessage());
468+
469+
Assert.False(result.IsEmpty);
470+
}
471+
400472
[Fact]
401473
public void PayoutsValidator_Validate_EUR_Destination_Identifier_Failure()
402474
{
@@ -463,33 +535,33 @@ public void PayoutsValidator_Validate_GBP_Destination_Identifier_Failure()
463535
_logger.LogDebug(result.ToTextErrorMessage());
464536

465537
Assert.False(result.IsEmpty);
466-
}
467-
538+
}
539+
468540
/// <summary>
469541
/// Tests that the currency resolution check is working as expected.
470542
/// </summary>
471543
[Theory]
472-
[InlineData(CurrencyTypeEnum.EUR, 0.01, true)]
473-
[InlineData(CurrencyTypeEnum.EUR, 0.001, false)]
474-
[InlineData(CurrencyTypeEnum.EUR, 1.011, false)]
475-
[InlineData(CurrencyTypeEnum.GBP, 0.01, true)]
476-
[InlineData(CurrencyTypeEnum.GBP, 0.001, false)]
477-
[InlineData(CurrencyTypeEnum.GBP, 1.011, false)]
478-
[InlineData(CurrencyTypeEnum.BTC, 0.01, true)]
479-
[InlineData(CurrencyTypeEnum.BTC, 0.001, true)]
480-
[InlineData(CurrencyTypeEnum.BTC, 0.00000001, true)]
481-
[InlineData(CurrencyTypeEnum.BTC, 0.000000001, false)]
482-
[InlineData(CurrencyTypeEnum.BTC, 1.011, true)]
544+
[InlineData(CurrencyTypeEnum.EUR, 0.01, true)]
545+
[InlineData(CurrencyTypeEnum.EUR, 0.001, false)]
546+
[InlineData(CurrencyTypeEnum.EUR, 1.011, false)]
547+
[InlineData(CurrencyTypeEnum.GBP, 0.01, true)]
548+
[InlineData(CurrencyTypeEnum.GBP, 0.001, false)]
549+
[InlineData(CurrencyTypeEnum.GBP, 1.011, false)]
550+
[InlineData(CurrencyTypeEnum.BTC, 0.01, true)]
551+
[InlineData(CurrencyTypeEnum.BTC, 0.001, true)]
552+
[InlineData(CurrencyTypeEnum.BTC, 0.00000001, true)]
553+
[InlineData(CurrencyTypeEnum.BTC, 0.000000001, false)]
554+
[InlineData(CurrencyTypeEnum.BTC, 1.011, true)]
483555
[InlineData(CurrencyTypeEnum.BTC, 1.000000011, false)]
484556
public void Payout_Validator_Currency_Resolution(CurrencyTypeEnum currency, decimal amount, bool isValid)
485-
{
486-
AccountIdentifier identifier = currency switch
487-
{
488-
CurrencyTypeEnum.BTC => new AccountIdentifier { Currency = currency, BitcoinAddress = "abcdefg" },
489-
CurrencyTypeEnum.GBP => new AccountIdentifier { Currency = currency, SortCode = "123456", AccountNumber = "00001234" } ,
490-
_ => new AccountIdentifier { Currency = currency, IBAN = "IE78MOCK91012352877713" }
491-
};
492-
557+
{
558+
AccountIdentifier identifier = currency switch
559+
{
560+
CurrencyTypeEnum.BTC => new AccountIdentifier { Currency = currency, BitcoinAddress = "abcdefg" },
561+
CurrencyTypeEnum.GBP => new AccountIdentifier { Currency = currency, SortCode = "123456", AccountNumber = "00001234" } ,
562+
_ => new AccountIdentifier { Currency = currency, IBAN = "IE78MOCK91012352877713" }
563+
};
564+
493565
var payout = new Payout
494566
{
495567
ID = Guid.NewGuid(),
@@ -501,24 +573,24 @@ public void Payout_Validator_Currency_Resolution(CurrencyTypeEnum currency, deci
501573
TheirReference = "their ref",
502574
Status = PayoutStatus.PENDING_INPUT,
503575
InvoiceID = "18ead957-e3bc-4b12-b5c6-d12e4bef9d24",
504-
Destination = new Counterparty
505-
{
506-
Name = "Joe Bloggs",
507-
Identifier = identifier
576+
Destination = new Counterparty
577+
{
578+
Name = "Joe Bloggs",
579+
Identifier = identifier
508580
}
509581
};
510582

511-
var result = payout.Validate();
512-
513-
_logger.LogDebug(result.ToTextErrorMessage());
514-
515-
if(isValid)
516-
{
517-
Assert.True(result.IsEmpty);
518-
}
519-
else
520-
{
521-
Assert.False(result.IsEmpty);
583+
var result = payout.Validate();
584+
585+
_logger.LogDebug(result.ToTextErrorMessage());
586+
587+
if(isValid)
588+
{
589+
Assert.True(result.IsEmpty);
590+
}
591+
else
592+
{
593+
Assert.False(result.IsEmpty);
522594
}
523595
}
524596
}

0 commit comments

Comments
 (0)