Skip to content

Commit

Permalink
Merge pull request #451 from nofrixion/MOOV-3875-invoice-payment-refe…
Browse files Browse the repository at this point in the history
…rence

Invoice payment reference
  • Loading branch information
sauravmaiti22 authored Nov 8, 2024
2 parents 646b22b + fb0fb9c commit ec44b89
Show file tree
Hide file tree
Showing 3 changed files with 180 additions and 94 deletions.
52 changes: 27 additions & 25 deletions src/NoFrixion.MoneyMoov/Mapping/PayrunInvoiceMapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,34 +20,36 @@ namespace NoFrixion.MoneyMoov;

public static class PayrunInvoiceMapper
{
/// <summary>
/// A safe TheirReference value that will pass validation sofr all currencies and processors.
/// Payrun invoices result in an auto-generating TheirReference when teh Payout is ultimately
/// created.
/// <summary>
/// A safe TheirReference value that will pass validation sofr all currencies and processors.
/// Payrun invoices result in an auto-generating TheirReference when teh Payout is ultimately
/// created.
/// </summary>
private const string SAFE_THEIR_REFERENCE = "Safe Reference";

public static Payout ToPayout(this PayrunInvoice invoice)
{
Guard.Against.Null(invoice, nameof(invoice));

return new Payout
{
Currency = invoice.Currency,
Type = invoice.Currency == CurrencyTypeEnum.GBP ? AccountIdentifierType.SCAN : AccountIdentifierType.IBAN,
Amount = invoice.TotalAmount,
TheirReference = SAFE_THEIR_REFERENCE,
Destination = new Counterparty
{
Name = invoice.DestinationAccountName,
Identifier = new AccountIdentifier
{
Currency = invoice.Currency,
IBAN = invoice.DestinationIban,
SortCode = invoice.DestinationSortCode,
AccountNumber = invoice.DestinationAccountNumber
},
}
};
{
Guard.Against.Null(invoice, nameof(invoice));

return new Payout
{
Currency = invoice.Currency,
Type = invoice.Currency == CurrencyTypeEnum.GBP ? AccountIdentifierType.SCAN : AccountIdentifierType.IBAN,
Amount = invoice.TotalAmount,
TheirReference = !string.IsNullOrWhiteSpace(invoice.PaymentReference)
? invoice.PaymentReference
: SAFE_THEIR_REFERENCE,
Destination = new Counterparty
{
Name = invoice.DestinationAccountName,
Identifier = new AccountIdentifier
{
Currency = invoice.Currency,
IBAN = invoice.DestinationIban,
SortCode = invoice.DestinationSortCode,
AccountNumber = invoice.DestinationAccountNumber
},
}
};
}
}
33 changes: 30 additions & 3 deletions src/NoFrixion.MoneyMoov/Models/Payruns/PayrunInvoice.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,16 +22,27 @@ namespace NoFrixion.MoneyMoov.Models;

public class PayrunInvoice : IValidatableObject
{
public const int PAYRUN_INVOICE_PAYMENT_REFERENCE_MAX_LENGTH = 18;

public Guid ID { get; set; }

public Guid PayRunID { get; set; }

public string? Name { get; set; }

[Obsolete("Please use Reference instead.")]
[Obsolete("Please use InvoiceReference instead.")]
public string? InvoiceNumber { get; set; }

public required string Reference { get; set; }
[Obsolete("Please use InvoiceReference instead.")]
public string? Reference
{
get => InvoiceReference;
init => InvoiceReference = value ?? string.Empty;
}

[Required]
[MinLength(1, ErrorMessage = "InvoiceReference cannot be empty.")]
public string InvoiceReference { get; set; } = null!;

public string? PaymentTerms { get; set; }

Expand Down Expand Up @@ -74,10 +85,26 @@ public class PayrunInvoice : IValidatableObject
public IEnumerable<InvoicePayment>? InvoicePayments { get; set; }

public bool IsEnabled { get; set; }

/// <summary>
/// Represents the reference used in the payout created for this invoice.
/// For a single destination (e.g., multiple invoices with the same IBAN),
/// the PaymentReference should remain consistent across all invoices.
/// If the PaymentReference is not set, one will be generated automatically.
/// </summary>
[MaxLength(PAYRUN_INVOICE_PAYMENT_REFERENCE_MAX_LENGTH, ErrorMessage = "PaymentReference cannot be longer than 18 characters.")]
public string? PaymentReference { get; set; }

public NoFrixionProblem Validate()
=> this.ToPayout().Validate();
{
var results = new List<ValidationResult>();
var contextInvoice = new ValidationContext(this, serviceProvider: null, items: null);
var isValid = Validator.TryValidateObject(this, contextInvoice, results, true);

return isValid ? NoFrixionProblem.Empty : new NoFrixionProblem("The PayrunInvoice model has one or more validation errors.", results);
}


public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
=> this.ToPayout().Validate(validationContext);
}
189 changes: 123 additions & 66 deletions test/MoneyMoov.UnitTests/Models/PayrunInvoiceValidatorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,13 @@ public PayrunInvoiceValidatorTests(ITestOutputHelper testOutputHelper)
[Theory]
[InlineData("A")]
[InlineData("1-A")]
[InlineData("1-A-2-c")]
[InlineData("1-A-2-c")]
[InlineData("NFXN...LTD")]
public void Validate_Account_Name_Success(string accountName)
{
var payrunInvoice = new PayrunInvoice
{
Reference = "ref-1",
InvoiceReference = "ref-1",
DestinationAccountName = accountName,
Currency = CurrencyTypeEnum.EUR,
TotalAmount = 11.00M,
Expand All @@ -71,65 +71,65 @@ public void Validate_Account_Name_Success(string accountName)
[InlineData(".-/&")] // No letter or number.
[InlineData("1-A-2-c + + dfg")] // Invalid character '+'.
[InlineData("Big Bucks £")]// Invalid character '£'.
[InlineData("Big Bucks €")]// Invalid character '€'.
[InlineData("Big Bucks €")]// Invalid character '€'.
[InlineData(":")] // Invalid initial character, can't be ':' or '-'.
[InlineData("1-A-2-c + ! + dfg")] // Invalid character '!'.
[InlineData(".-/& a")] // BC don't support '&' in account name.
[InlineData("/:")] // BC don't support '/' in account name.
[InlineData("1-A-2-c + ! + dfg")] // Invalid character '!'.
[InlineData(".-/& a")] // BC don't support '&' in account name.
[InlineData("/:")] // BC don't support '/' in account name.
[InlineData("TELECOMUNICAÇÕES S.A.")] // BC don't support unicode.
[InlineData("Ç_é")] // BC don't support unicode.
[InlineData("Ç_é")] // BC don't support unicode.
[InlineData("NFXN: LTD")] // Modulr don't support ':'
public void Validate_Account_Name_Fails(string accountName)
{
var payrunInvoice = new PayrunInvoice
{
Reference = "ref-1",
DestinationAccountName = accountName,
Currency = CurrencyTypeEnum.EUR,
TotalAmount = 11.00M,
DestinationIban = "IE83MOCK91012396989925"
{
InvoiceReference = "ref-1",
DestinationAccountName = accountName,
Currency = CurrencyTypeEnum.EUR,
TotalAmount = 11.00M,
DestinationIban = "IE83MOCK91012396989925"
};

var problem = payrunInvoice.Validate();

_logger.LogDebug(problem.Detail);

Assert.False(problem.IsEmpty);
}

}

/// <summary>
/// Validates that an invoice with a valid IBAN passes validation.
/// </summary>
[Fact]
public void Validate_Payout_Valid_IBAN_Success()
{
{
var payrunInvoice = new PayrunInvoice
{
Reference = "ref-1",
DestinationAccountName = "Some Biz",
DestinationIban = "IE83MOCK91012396989925",
Currency = CurrencyTypeEnum.EUR,
TotalAmount = 11.00M
{
InvoiceReference = "ref-1",
DestinationAccountName = "Some Biz",
DestinationIban = "IE83MOCK91012396989925",
Currency = CurrencyTypeEnum.EUR,
TotalAmount = 11.00M
};

var result = payrunInvoice.Validate();

Assert.True(result.IsEmpty);
}
}

/// <summary>
/// Validates that an invoice with an invalid IBAN fails validation.
/// </summary>
[Fact]
public void Validate_Payout_Invalid_IBAN_Fails()
{
{
var payrunInvoice = new PayrunInvoice
{
Reference = "ref-1",
DestinationAccountName = "Some Biz",
DestinationIban = "IE36ULSB98501017331006",
Currency = CurrencyTypeEnum.EUR,
TotalAmount = 11.00M
{
InvoiceReference = "ref-1",
DestinationAccountName = "Some Biz",
DestinationIban = "IE36ULSB98501017331006",
Currency = CurrencyTypeEnum.EUR,
TotalAmount = 11.00M
};

var result = payrunInvoice.Validate();
Expand All @@ -145,61 +145,118 @@ public void Validate_Payout_Invalid_IBAN_Fails()
/// </summary>
[Fact]
public void Validate_Payout_Valid_SCAN_Success()
{
{
var payrunInvoice = new PayrunInvoice
{
Reference = "ref-1",
DestinationAccountName = "Some Biz",
DestinationSortCode = "123456",
DestinationAccountNumber = "12345678",
Currency = CurrencyTypeEnum.GBP,
TotalAmount = 11.00M
{
InvoiceReference = "ref-1",
DestinationAccountName = "Some Biz",
DestinationSortCode = "123456",
DestinationAccountNumber = "12345678",
Currency = CurrencyTypeEnum.GBP,
TotalAmount = 11.00M
};

var result = payrunInvoice.Validate();

Assert.True(result.IsEmpty);
}

/// <summary>
/// Tests than a EUR invoice mssing an IBAN fails validation.
}

/// <summary>
/// Tests than a EUR invoice mssing an IBAN fails validation.
/// </summary>
[Fact]
public void Invoice_Missing_IBAN_Failure()
{
{
var payrunInvoice = new PayrunInvoice
{
Reference = "ref-1",
DestinationAccountName = "Some Biz",
Currency = CurrencyTypeEnum.EUR,
TotalAmount = 11.00M
};

{
InvoiceReference = "ref-1",
DestinationAccountName = "Some Biz",
Currency = CurrencyTypeEnum.EUR,
TotalAmount = 11.00M
};

var result = payrunInvoice.Validate();

_logger.LogDebug(result.ToJsonFormatted());

Assert.False(result.IsEmpty);
}

/// <summary>
/// Tests than a GBP invoice mssing SCAN details fails validation.
}

/// <summary>
/// Tests than a GBP invoice mssing SCAN details fails validation.
/// </summary>
[Fact]
public void Invoice_Missing_SCAN_Failure()
{
{
var payrunInvoice = new PayrunInvoice
{
InvoiceReference = "ref-1",
DestinationAccountName = "Some Biz",
Currency = CurrencyTypeEnum.GBP,
TotalAmount = 11.00M
};

var result = payrunInvoice.Validate();

_logger.LogDebug(result.ToJsonFormatted());

Assert.False(result.IsEmpty);
}

[Fact]
public void Invoice_With_PaymentReference_MaxLength_Validates_Success()
{
var payrunInvoice = new PayrunInvoice
{
InvoiceReference = "ref-1",
DestinationAccountName = "Some Biz",
Currency = CurrencyTypeEnum.EUR,
TotalAmount = 11.00M,
PaymentReference = "12345678901234567890" // More than 18 characters.
};

var result = payrunInvoice.Validate();

_logger.LogDebug(result.ToJsonFormatted());

Assert.False(result.IsEmpty);
}

[Fact]
public void Invoice_With_Empty_InvoiceReference_Returns_Error()
{
var payrunInvoice = new PayrunInvoice
{
Reference = "ref-1",
DestinationAccountName = "Some Biz",
Currency = CurrencyTypeEnum.GBP,
TotalAmount = 11.00M
};

var result = payrunInvoice.Validate();

_logger.LogDebug(result.ToJsonFormatted());

{
InvoiceReference = "",
DestinationAccountName = "Some Biz",
Currency = CurrencyTypeEnum.EUR,
DestinationIban = "IE83MOCK91012396989925",
TotalAmount = 11.00M
};

var result = payrunInvoice.Validate();

_logger.LogDebug(result.ToJsonFormatted());

Assert.False(result.IsEmpty);
}

[Fact]
public void Invoice_With_Obsolete_Reference_Field_Set_Success()
{
var payrunInvoice = new PayrunInvoice
{
#pragma warning disable CS0618 // Type or member is obsolete
Reference = "ref-1",
#pragma warning restore CS0618 // Type or member is obsolete
DestinationAccountName = "Some Biz",
Currency = CurrencyTypeEnum.EUR,
DestinationIban = "IE83MOCK91012396989925",
TotalAmount = 11.00M
};

var result = payrunInvoice.Validate();

Assert.True(result.IsEmpty);
}
}

0 comments on commit ec44b89

Please sign in to comment.