Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

TicketBAI - Introduced simplified invoices and improved error handling #244

Merged
merged 3 commits into from
Feb 6, 2024
Merged
Show file tree
Hide file tree
Changes from 2 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
79 changes: 52 additions & 27 deletions src/Basque/Mews.Fiscalizations.Basque.Tests/InvoiceTestData.cs
Original file line number Diff line number Diff line change
@@ -1,43 +1,79 @@
namespace Mews.Fiscalizations.Basque.Tests;

internal sealed class InvoiceTestData
internal static class InvoiceTestData
{
internal static SendInvoiceRequest CreateInvoiceRequest(Issuer issuer, Software software, bool localReceivers, bool negativeInvoice, OriginalInvoiceInfo originalInvoiceInfo = null)
internal static SendInvoiceRequest CreateCompleteInvoiceRequest(
Issuer issuer,
Software software,
bool localReceivers,
bool negativeInvoice,
OriginalInvoiceInfo originalInvoiceInfo = null)
{
return new SendInvoiceRequest(
subject: CreateSubject(issuer, localReceivers),
invoice: CreateInvoice(localReceivers, negativeInvoice),
invoiceFooter: new InvoiceFooter(software, originalInvoiceInfo: originalInvoiceInfo)
var taxSummary = CreateTaxSummary(negativeInvoice);
var randomString = String1To20.CreateUnsafe(Guid.NewGuid().ToString()[..19]);
return localReceivers.Match(
t => SendInvoiceRequest.CreateCompleteLocalReceiverInvoiceRequest(
issuer: issuer,
invoiceFooter: new InvoiceFooter(software, originalInvoiceInfo: originalInvoiceInfo),
receivers: CreateReceivers(localReceivers),
invoiceData: CreateInvoiceData(negativeInvoice),
taxSummary: taxSummary,
number: randomString,
issued: DateTime.Now,
series: randomString,
issuerType: IssuerType.IssuedByThirdParty
),
f => SendInvoiceRequest.CreateCompleteForeignReceiverInvoiceRequest(
issuer: issuer,
invoiceFooter: new InvoiceFooter(software, originalInvoiceInfo: originalInvoiceInfo),
receivers: CreateReceivers(localReceivers),
invoiceData: CreateInvoiceData(negativeInvoice),
taxBreakdown: OperationTypeTaxBreakdown.Create(delivery: taxSummary).Success.Get(),
number: randomString,
issued: DateTime.Now,
series: randomString,
issuerType: IssuerType.IssuedByThirdParty
)
);
}

private static Invoice CreateInvoice(bool localReceivers, bool negativeInvoice)
internal static SendInvoiceRequest CreateSimplifiedInvoiceRequest(
Issuer issuer,
Software software,
bool negativeInvoice,
OriginalInvoiceInfo originalInvoiceInfo = null)
{
return new Invoice(CreateHeader(), CreateInvoiceData(negativeInvoice), CreateTaxBreakdown(localReceivers, negativeInvoice));
var randomString = String1To20.CreateUnsafe(Guid.NewGuid().ToString()[..19]);
return SendInvoiceRequest.CreateSimplifiedInvoiceRequest(
issuer: issuer,
invoiceFooter: new InvoiceFooter(software, originalInvoiceInfo: originalInvoiceInfo),
invoiceData: CreateInvoiceData(negativeInvoice),
taxSummary: CreateTaxSummary(negativeInvoice),
number: randomString,
issued: DateTime.Now,
series: randomString,
issuerType: IssuerType.IssuedByThirdParty
);
}

private static TaxBreakdown CreateTaxBreakdown(bool localReceivers, bool negativeInvoice)
private static TaxSummary CreateTaxSummary(bool negativeInvoice)
{
var baseValue = negativeInvoice.Match(
t => -73.86m,
f => 73.86m
);
var taxSummary = TaxSummary.Create(taxed: CreateTaxRateSummary(21m, baseValue).ToEnumerable().ToArray()).Success.Get();
return localReceivers.Match(
t => new TaxBreakdown(taxSummary),
f => new TaxBreakdown(OperationTypeTaxBreakdown.Create(delivery: taxSummary).Success.Get())
);
return TaxSummary.Create(taxed: CreateTaxRateSummary(21m, baseValue).ToEnumerable().ToArray()).Success.Get();
}

private static InvoiceData CreateInvoiceData(bool negativeInvoice)
{
return InvoiceData.Create(
return new InvoiceData(
description: String1To250.CreateUnsafe("TicketBAI sample invoice test."),
items: CreateInvoiceItems(negativeInvoice),
totalAmount: negativeInvoice.Match(t => -89.36m, f => 89.36m),
taxModes: TaxMode.GeneralTaxRegimeActivity.ToEnumerable(),
transactionDate: DateTime.Now
).Success.Get();
);
}

private static INonEmptyEnumerable<InvoiceItem> CreateInvoiceItems(bool negativeInvoice)
Expand Down Expand Up @@ -88,17 +124,6 @@ private static INonEmptyEnumerable<InvoiceItem> CreateInvoiceItems(bool negative
);
}

private static InvoiceHeader CreateHeader()
{
var randomString = String1To20.CreateUnsafe(Guid.NewGuid().ToString().Substring(0, 19));
return new InvoiceHeader(number: randomString, issued: DateTime.Now, series: randomString);
}

private static Subject CreateSubject(Issuer issuer, bool localReceivers)
{
return Subject.Create(issuer, CreateReceivers(localReceivers), IssuerType.IssuedByThirdParty).Success.Get();
}

private static INonEmptyEnumerable<Receiver> CreateReceivers(bool localReceivers)
{
return NonEmptyEnumerable.Create(localReceivers.Match(
Expand Down
69 changes: 48 additions & 21 deletions src/Basque/Mews.Fiscalizations.Basque.Tests/SendInvoiceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,50 +4,77 @@
public class SendInvoiceTests
{
[Test]
[TestCase(Region.Araba, false, false, TestName = "Araba - Send invoice with local receiver")]
[TestCase(Region.Araba, true, false, TestName = "Araba - Send invoice with foreign receiver")]
[TestCase(Region.Araba, false, true, TestName = "Araba - Send negative invoice with local receiver")]
[TestCase(Region.Araba, true, true, TestName = "Araba - Send negative invoice with foreign receiver")]
[TestCase(Region.Gipuzkoa, false, false, TestName = "Gipuzkoa - Send invoice with local receiver")]
[TestCase(Region.Gipuzkoa, true, false, TestName = "Gipuzkoa - Send invoice with foreign receiver")]
[TestCase(Region.Gipuzkoa, false, true, TestName = "Gipuzkoa - Send negative invoice with local receiver")]
[TestCase(Region.Gipuzkoa, true, true, TestName = "Gipuzkoa - Send negative invoice with foreign receiver")]
[TestCase(Region.Bizkaia, false, false, TestName = "Bizkaia - Send invoice with local receiver")]
[TestCase(Region.Bizkaia, true, false, TestName = "Bizkaia - Send invoice with foreign receiver")]
[TestCase(Region.Bizkaia, false, true, TestName = "Bizkaia - Send negative invoice with local receiver")]
[TestCase(Region.Bizkaia, true, true, TestName = "Bizkaia - Send negative invoice with foreign receiver")]
[TestCase(Region.Araba, true, false, TestName = "Araba - Send complete invoice with local receiver")]
[TestCase(Region.Araba, false, false, TestName = "Araba - Send complete invoice with foreign receiver")]
[TestCase(Region.Araba, true, true, TestName = "Araba - Send negative complete invoice with local receiver")]
[TestCase(Region.Araba, false, true, TestName = "Araba - Send negative complete invoice with foreign receiver")]
[TestCase(Region.Gipuzkoa, true, false, TestName = "Gipuzkoa - Send complete invoice with local receiver")]
[TestCase(Region.Gipuzkoa, false, false, TestName = "Gipuzkoa - Send complete invoice with foreign receiver")]
[TestCase(Region.Gipuzkoa, true, true, TestName = "Gipuzkoa - Send complete negative invoice with local receiver")]
[TestCase(Region.Gipuzkoa, false, true, TestName = "Gipuzkoa - Send complete negative invoice with foreign receiver")]
[TestCase(Region.Bizkaia, true, false, TestName = "Bizkaia - Send complete invoice with local receiver")]
[TestCase(Region.Bizkaia, false, false, TestName = "Bizkaia - Send complete invoice with foreign receiver")]
[TestCase(Region.Bizkaia, true, true, TestName = "Bizkaia - Send complete negative invoice with local receiver")]
[TestCase(Region.Bizkaia, false, true, TestName = "Bizkaia - Send complete negative invoice with foreign receiver")]
[Retry(3)]
public async Task SendSimpleInvoiceSucceeds(Region region, bool localReceivers, bool negativeInvoice)
public async Task SendCompleteInvoiceSucceeds(Region region, bool localReceivers, bool negativeInvoice)
{
var testFixture = new TestFixture(region);
var client = testFixture.Client;
var request = InvoiceTestData.CreateInvoiceRequest(testFixture.Issuer, testFixture.Software, localReceivers, negativeInvoice);
var request = InvoiceTestData.CreateCompleteInvoiceRequest(testFixture.Issuer, testFixture.Software, localReceivers, negativeInvoice);
var tbaiData = client.GetTicketBaiInvoiceData(request);
var response = await client.SendInvoiceAsync(tbaiData);
TestFixture.AssertResponse(region, response, tbaiData);
}

[TestCase(Region.Araba, false, TestName = "Araba - Send simplified invoice")]
[TestCase(Region.Araba, true, TestName = "Araba - Send simplified negative invoice")]
[TestCase(Region.Gipuzkoa, false, TestName = "Gipuzkoa - Send simplified invoice")]
[TestCase(Region.Gipuzkoa, true, TestName = "Gipuzkoa - Send simplified negative invoice")]
[TestCase(Region.Bizkaia, false, TestName = "Bizkaia - Send simplified invoice")]
[TestCase(Region.Bizkaia, true, TestName = "Bizkaia - Send simplified negative invoice")]
[Retry(3)]
public async Task SendSimplifiedInvoiceSucceeds(Region region, bool negativeInvoice)
{
var testFixture = new TestFixture(region);
var client = testFixture.Client;
var request = InvoiceTestData.CreateSimplifiedInvoiceRequest(testFixture.Issuer, testFixture.Software, negativeInvoice);
var tbaiData = client.GetTicketBaiInvoiceData(request);
var response = await client.SendInvoiceAsync(tbaiData);
TestFixture.AssertResponse(region, response, tbaiData);
}

[Test]
[TestCase(Region.Araba, TestName = "Araba - Invoice chaining")]
[TestCase(Region.Gipuzkoa, TestName = "Gipuzkoa - Invoice chaining")]
[TestCase(Region.Bizkaia, TestName = "Bizkaia - Invoice chaining")]
[TestCase(Region.Araba, false, TestName = "Araba - Complete Invoice chaining")]
[TestCase(Region.Araba, true, TestName = "Araba - Simplified Invoice chaining")]
[TestCase(Region.Gipuzkoa, false, TestName = "Gipuzkoa - Complete Invoice chaining")]
[TestCase(Region.Gipuzkoa, true, TestName = "Gipuzkoa - Simplified Invoice chaining")]
[TestCase(Region.Bizkaia, false, TestName = "Bizkaia - Complete Invoice chaining")]
[TestCase(Region.Bizkaia, true, TestName = "Bizkaia - Simplified Invoice chaining")]
[Retry(3)]
public async Task SendChainedInvoiceSucceeds(Region region)
public async Task SendChainedInvoiceSucceeds(Region region, bool isSimplified)
{
var testFixture = new TestFixture(region);
var client = testFixture.Client;
var request1 = InvoiceTestData.CreateInvoiceRequest(testFixture.Issuer, testFixture.Software, localReceivers: true, negativeInvoice: false);
var request1 = isSimplified.Match(
t => InvoiceTestData.CreateSimplifiedInvoiceRequest(testFixture.Issuer, testFixture.Software, negativeInvoice: false),
f => InvoiceTestData.CreateCompleteInvoiceRequest(testFixture.Issuer, testFixture.Software, negativeInvoice: false, localReceivers: true)
);
var tbaiData1 = client.GetTicketBaiInvoiceData(request1);
var response1 = await client.SendInvoiceAsync(tbaiData1);
TestFixture.AssertResponse(region, response1, tbaiData1);

var originalInvoiceHeader = request1.Invoice.Header;
var request2 = InvoiceTestData.CreateInvoiceRequest(testFixture.Issuer, testFixture.Software, localReceivers: true, negativeInvoice: false, originalInvoiceInfo: new OriginalInvoiceInfo(
var originalInvoice = new OriginalInvoiceInfo(
number: originalInvoiceHeader.Number,
issueDate: originalInvoiceHeader.Issued,
signature: response1.SignatureValue,
series: originalInvoiceHeader.Series.GetOrNull()
));
);
var request2 = isSimplified.Match(
t => InvoiceTestData.CreateSimplifiedInvoiceRequest(testFixture.Issuer, testFixture.Software, negativeInvoice: false, originalInvoice),
f => InvoiceTestData.CreateCompleteInvoiceRequest(testFixture.Issuer, testFixture.Software, localReceivers: true, negativeInvoice: false, originalInvoice)
);
var tbaiData2 = client.GetTicketBaiInvoiceData(request2);
var response2 = await client.SendInvoiceAsync(tbaiData2);
TestFixture.AssertResponse(region, response2, tbaiData2);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ internal static SendInvoiceResponse Convert(
{
var record = response.Registros.Single();
var recordStatus = record.SituacionRegistro;
var invoiceIdentifier = record.Identificador.Item as IDFacturaType; // TODO: check if they would ever return byte[] ticketbai.
var invoiceIdentifier = record.Identificador.Item as IDFacturaType;
return new SendInvoiceResponse(
xmlRequestContent: xmlRequestContent,
xmlResponseContent: xmlResponseContent,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
<RepositoryUrl>https://github.com/MewsSystems/fiscalizations</RepositoryUrl>
<Icon>https://raw.githubusercontent.com/msigut/eet/master/receipt.png</Icon>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<PackageVersion>7.0.0</PackageVersion>
<PackageVersion>8.0.0</PackageVersion>
<LangVersion>12</LangVersion>
<ImplicitUsings>true</ImplicitUsings>
</PropertyGroup>
Expand Down
4 changes: 4 additions & 0 deletions src/Basque/Mews.Fiscalizations.Basque/Model/ErrorCode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,13 @@ public enum ErrorCode
// Invalid or missing chain.
InvalidOrMissingInvoiceChain = 09,

InvalidIssuerNif = 10,

// The rectified (Corrected) invoice is not indicated.
CorrectedInvoiceNotIndicated = 011,

InvalidIssuerNameOrNif = 12,

// Issuer NIF must be registered in the Araba region.
IssuerNifMustBeRegisteredInArabaRegion = 15,

Expand Down
50 changes: 49 additions & 1 deletion src/Basque/Mews.Fiscalizations.Basque/Model/Invoice/Invoice.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

public sealed class Invoice
{
public Invoice(InvoiceHeader header, InvoiceData invoiceData, TaxBreakdown taxBreakdown)
private Invoice(InvoiceHeader header, InvoiceData invoiceData, TaxBreakdown taxBreakdown)
{
Header = header;
InvoiceData = invoiceData;
Expand All @@ -14,4 +14,52 @@ public Invoice(InvoiceHeader header, InvoiceData invoiceData, TaxBreakdown taxBr
public InvoiceData InvoiceData { get; }

public TaxBreakdown TaxBreakdown { get; }

public static Invoice CreateSimplified(
InvoiceData invoiceData,
TaxSummary taxSummary,
String1To20 number,
DateTime issued,
bool? issuedInSubstitutionOfSimplifiedInvoice = null,
String1To20 series = null,
CorrectingInvoice correctingInvoice = null,
IEnumerable<CorrectedInvoice> correctedInvoices = null)
{
return new Invoice(
header: new InvoiceHeader(
number: number,
issued: issued,
isSimplified: true,
issuedInSubstitutionOfSimplifiedInvoice: issuedInSubstitutionOfSimplifiedInvoice,
series: series,
correctingInvoice: correctingInvoice,
correctedInvoices: correctedInvoices
),
invoiceData: invoiceData,
taxBreakdown: new TaxBreakdown(taxSummary)
);
}

public static Invoice CreateComplete(
InvoiceData invoiceData,
TaxBreakdown taxBreakdown,
String1To20 number,
DateTime issued,
String1To20 series = null,
CorrectingInvoice correctingInvoice = null,
IEnumerable<CorrectedInvoice> correctedInvoices = null)
{
return new Invoice(
header: new InvoiceHeader(
number: number,
issued: issued,
isSimplified: false,
series: series,
correctingInvoice: correctingInvoice,
correctedInvoices: correctedInvoices
),
invoiceData: invoiceData,
taxBreakdown: taxBreakdown
);
}
}
27 changes: 5 additions & 22 deletions src/Basque/Mews.Fiscalizations.Basque/Model/Invoice/InvoiceData.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@

public sealed class InvoiceData
{
private InvoiceData(
public InvoiceData(
String1To250 description,
IEnumerable<InvoiceItem> items,
INonEmptyEnumerable<InvoiceItem> items,
decimal totalAmount,
IEnumerable<TaxMode> taxModes,
INonEmptyEnumerable<TaxMode> taxModes,
decimal? supportWithheldAmount = null,
decimal? tax = null,
DateTime? transactionDate = null)
Expand All @@ -22,32 +22,15 @@ private InvoiceData(

public String1To250 Description { get; }

public IEnumerable<InvoiceItem> Items { get; }
public INonEmptyEnumerable<InvoiceItem> Items { get; }

public decimal TotalAmount { get; }

public IEnumerable<TaxMode> TaxModes { get; }
public INonEmptyEnumerable<TaxMode> TaxModes { get; }

public Option<decimal> SupportWithheldAmount { get; }

public Option<decimal> Tax { get; }

public Option<DateTime> TransactionDate { get; }

public static Try<InvoiceData, IReadOnlyList<Error>> Create(
String1To250 description,
IEnumerable<InvoiceItem> items,
decimal totalAmount,
IEnumerable<TaxMode> taxModes,
decimal? supportWithheldAmount = null,
decimal? tax = null,
DateTime? transactionDate = null)
{
return Try.Aggregate(
ObjectValidations.NotNull(description),
items.ToList().ToTry(i => i.Any() && i.Count <= 1000, _ => new Error($"{nameof(items)} count must be in range [1, 1000].")),
taxModes.ToTry(t => t.NonEmpty(), _ => new Error($"{nameof(taxModes)} shouldn't be empty.")),
(d, i, t) => new InvoiceData(d, i, totalAmount, t, supportWithheldAmount, tax, transactionDate)
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,15 @@ public sealed class InvoiceHeader
public InvoiceHeader(
String1To20 number,
DateTime issued,
bool? isSimplified = null,
bool isSimplified,
bool? issuedInSubstitutionOfSimplifiedInvoice = null,
String1To20 series = null,
CorrectingInvoice correctingInvoice = null,
IEnumerable<CorrectedInvoice> correctedInvoices = null)
{
Number = number;
Issued = issued;
IsSimplified = isSimplified.ToOption();
IsSimplified = isSimplified;
IssuedInSubstitutionOfSimplifiedInvoice = issuedInSubstitutionOfSimplifiedInvoice.ToOption();
Series = series.ToOption();
CorrectingInvoice = correctingInvoice.ToOption();
Expand All @@ -25,7 +25,7 @@ public InvoiceHeader(

public DateTime Issued { get; }

public Option<bool> IsSimplified { get; }
public bool IsSimplified { get; }

public Option<bool> IssuedInSubstitutionOfSimplifiedInvoice { get; }

Expand Down
Loading