diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..4acb363
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,247 @@
+# Remove the line below if you want to inherit .editorconfig settings from higher directories
+root = true
+
+# All files
+[*]
+end_of_line = lf
+charset = utf-8
+trim_trailing_whitespace = true
+insert_final_newline = true
+indent_style = space
+indent_size = 4
+
+# C# files
+[*.cs]
+
+#### Core EditorConfig Options ####
+
+# Indentation and spacing
+indent_size = 4
+indent_style = space
+tab_width = 4
+
+# New line preferences
+end_of_line = crlf
+insert_final_newline = true
+
+#### .NET Coding Conventions ####
+
+# Organize usings
+dotnet_separate_import_directive_groups = false
+dotnet_sort_system_directives_first = true
+file_header_template = unset
+
+# this. and Me. preferences
+dotnet_style_qualification_for_event = false
+dotnet_style_qualification_for_field = false
+dotnet_style_qualification_for_method = false
+dotnet_style_qualification_for_property = false
+
+# Language keywords vs BCL types preferences
+dotnet_style_predefined_type_for_locals_parameters_members = true
+dotnet_style_predefined_type_for_member_access = true
+
+# Parentheses preferences
+dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity
+dotnet_style_parentheses_in_other_binary_operators = always_for_clarity
+dotnet_style_parentheses_in_other_operators = never_if_unnecessary
+dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity
+
+# Modifier preferences
+dotnet_style_require_accessibility_modifiers = for_non_interface_members
+
+# Expression-level preferences
+dotnet_style_coalesce_expression = true
+dotnet_style_collection_initializer = true
+dotnet_style_explicit_tuple_names = true
+dotnet_style_namespace_match_folder = true
+dotnet_style_null_propagation = true
+dotnet_style_object_initializer = true
+dotnet_style_operator_placement_when_wrapping = beginning_of_line
+dotnet_style_prefer_auto_properties = true
+dotnet_style_prefer_compound_assignment = true
+dotnet_style_prefer_conditional_expression_over_assignment = true
+dotnet_style_prefer_conditional_expression_over_return = true
+dotnet_style_prefer_foreach_explicit_cast_in_source = when_strongly_typed
+dotnet_style_prefer_inferred_anonymous_type_member_names = true
+dotnet_style_prefer_inferred_tuple_names = true
+dotnet_style_prefer_is_null_check_over_reference_equality_method = true
+dotnet_style_prefer_simplified_boolean_expressions = true
+dotnet_style_prefer_simplified_interpolation = true
+
+# Field preferences
+dotnet_style_readonly_field = true
+
+# Parameter preferences
+dotnet_code_quality_unused_parameters = all
+
+# Suppression preferences
+dotnet_remove_unnecessary_suppression_exclusions = 0
+
+# New line preferences
+dotnet_style_allow_multiple_blank_lines_experimental = true
+dotnet_style_allow_statement_immediately_after_block_experimental = true
+
+#### C# Coding Conventions ####
+
+# var preferences
+csharp_style_var_elsewhere = false
+csharp_style_var_for_built_in_types = false
+csharp_style_var_when_type_is_apparent = false
+
+# Expression-bodied members
+csharp_style_expression_bodied_accessors = true
+csharp_style_expression_bodied_constructors = false
+csharp_style_expression_bodied_indexers = true
+csharp_style_expression_bodied_lambdas = true
+csharp_style_expression_bodied_local_functions = false
+csharp_style_expression_bodied_methods = false
+csharp_style_expression_bodied_operators = false
+csharp_style_expression_bodied_properties = true
+
+# Pattern matching preferences
+csharp_style_pattern_matching_over_as_with_null_check = true
+csharp_style_pattern_matching_over_is_with_cast_check = true
+csharp_style_prefer_extended_property_pattern = true
+csharp_style_prefer_not_pattern = true
+csharp_style_prefer_pattern_matching = true
+csharp_style_prefer_switch_expression = true
+
+# Null-checking preferences
+csharp_style_conditional_delegate_call = true
+
+# Modifier preferences
+csharp_prefer_static_local_function = true
+csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async
+
+# Code-block preferences
+csharp_prefer_braces = true
+csharp_prefer_simple_using_statement = true
+csharp_style_namespace_declarations = block_scoped
+csharp_style_prefer_method_group_conversion = true
+csharp_style_prefer_top_level_statements = true
+
+# Expression-level preferences
+csharp_prefer_simple_default_expression = true
+csharp_style_deconstructed_variable_declaration = true
+csharp_style_implicit_object_creation_when_type_is_apparent = true
+csharp_style_inlined_variable_declaration = true
+csharp_style_prefer_index_operator = true
+csharp_style_prefer_local_over_anonymous_function = true
+csharp_style_prefer_null_check_over_type_check = true
+csharp_style_prefer_range_operator = true
+csharp_style_prefer_tuple_swap = true
+csharp_style_prefer_utf8_string_literals = true
+csharp_style_throw_expression = true
+csharp_style_unused_value_assignment_preference = discard_variable
+csharp_style_unused_value_expression_statement_preference = discard_variable
+
+# 'using' directive preferences
+csharp_using_directive_placement = outside_namespace
+
+# New line preferences
+csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = true
+csharp_style_allow_blank_lines_between_consecutive_braces_experimental = true
+csharp_style_allow_embedded_statements_on_same_line_experimental = true
+
+#### C# Formatting Rules ####
+
+# New line preferences
+csharp_new_line_before_catch = true
+csharp_new_line_before_else = true
+csharp_new_line_before_finally = true
+csharp_new_line_before_members_in_anonymous_types = true
+csharp_new_line_before_members_in_object_initializers = true
+csharp_new_line_before_open_brace = all
+csharp_new_line_between_query_expression_clauses = true
+
+# Indentation preferences
+csharp_indent_block_contents = true
+csharp_indent_braces = false
+csharp_indent_case_contents = true
+csharp_indent_case_contents_when_block = true
+csharp_indent_labels = one_less_than_current
+csharp_indent_switch_labels = true
+
+# Space preferences
+csharp_space_after_cast = false
+csharp_space_after_colon_in_inheritance_clause = true
+csharp_space_after_comma = true
+csharp_space_after_dot = false
+csharp_space_after_keywords_in_control_flow_statements = true
+csharp_space_after_semicolon_in_for_statement = true
+csharp_space_around_binary_operators = before_and_after
+csharp_space_around_declaration_statements = false
+csharp_space_before_colon_in_inheritance_clause = true
+csharp_space_before_comma = false
+csharp_space_before_dot = false
+csharp_space_before_open_square_brackets = false
+csharp_space_before_semicolon_in_for_statement = false
+csharp_space_between_empty_square_brackets = false
+csharp_space_between_method_call_empty_parameter_list_parentheses = false
+csharp_space_between_method_call_name_and_opening_parenthesis = false
+csharp_space_between_method_call_parameter_list_parentheses = false
+csharp_space_between_method_declaration_empty_parameter_list_parentheses = false
+csharp_space_between_method_declaration_name_and_open_parenthesis = false
+csharp_space_between_method_declaration_parameter_list_parentheses = false
+csharp_space_between_parentheses = false
+csharp_space_between_square_brackets = false
+
+# Wrapping preferences
+csharp_preserve_single_line_blocks = true
+csharp_preserve_single_line_statements = true
+
+#### Naming styles ####
+
+# Naming rules
+
+dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion
+dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface
+dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i
+
+dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion
+dotnet_naming_rule.types_should_be_pascal_case.symbols = types
+dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case
+
+dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion
+dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members
+dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case
+
+dotnet_naming_rule.private_or_internal_field_should_be_underscore.severity = suggestion
+dotnet_naming_rule.private_or_internal_field_should_be_underscore.symbols = private_or_internal_field
+dotnet_naming_rule.private_or_internal_field_should_be_underscore.style = underscore
+
+# Symbol specifications
+
+dotnet_naming_symbols.interface.applicable_kinds = interface
+dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
+dotnet_naming_symbols.interface.required_modifiers =
+
+dotnet_naming_symbols.private_or_internal_field.applicable_kinds = field
+dotnet_naming_symbols.private_or_internal_field.applicable_accessibilities = internal, private, private_protected
+dotnet_naming_symbols.private_or_internal_field.required_modifiers =
+
+dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum
+dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
+dotnet_naming_symbols.types.required_modifiers =
+
+dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method
+dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
+dotnet_naming_symbols.non_field_members.required_modifiers =
+
+# Naming styles
+
+dotnet_naming_style.pascal_case.required_prefix =
+dotnet_naming_style.pascal_case.required_suffix =
+dotnet_naming_style.pascal_case.word_separator =
+dotnet_naming_style.pascal_case.capitalization = pascal_case
+
+dotnet_naming_style.begins_with_i.required_prefix = I
+dotnet_naming_style.begins_with_i.required_suffix =
+dotnet_naming_style.begins_with_i.word_separator =
+dotnet_naming_style.begins_with_i.capitalization = pascal_case
+
+dotnet_naming_style.underscore.required_prefix = _
+dotnet_naming_style.underscore.required_suffix =
+dotnet_naming_style.underscore.word_separator =
+dotnet_naming_style.underscore.capitalization = camel_case
diff --git a/Directory.Build.props b/Directory.Build.props
new file mode 100644
index 0000000..f83fc80
--- /dev/null
+++ b/Directory.Build.props
@@ -0,0 +1,14 @@
+
+
+
+ enable
+ enable
+ latest
+ latest
+
+
+
+ true
+
+
+
diff --git a/src/Inflop.VatSharp/Inflop.VatSharp.csproj b/src/Inflop.VatSharp/Inflop.VatSharp.csproj
index 5587442..89be1a1 100644
--- a/src/Inflop.VatSharp/Inflop.VatSharp.csproj
+++ b/src/Inflop.VatSharp/Inflop.VatSharp.csproj
@@ -2,8 +2,6 @@
net8.0;net9.0;net10.0
- enable
- enable
diff --git a/tests/Inflop.VatSharp.Tests/DirectEngineTests.cs b/tests/Inflop.VatSharp.Tests/DirectEngineTests.cs
index b18a150..3913ef1 100644
--- a/tests/Inflop.VatSharp.Tests/DirectEngineTests.cs
+++ b/tests/Inflop.VatSharp.Tests/DirectEngineTests.cs
@@ -111,6 +111,10 @@ public void Create_SumOfLineItemVatAmounts_PennyDifference()
var m3 = engine.Calculate(ArticleItems, VatCalculationMethod.SumOfLineItemVatAmounts);
(m1.TotalVat.Value - m3.TotalVat.Value).Should().Be(0.01m);
+
+ // M.I łamie sum(LineItems.VatAmount) == TotalVat — zachowanie definiujące różnicę
+ // między M.I (VAT od sumy netto) a M.III (suma VAT per linia).
+ m1.LineItems.Sum(li => li.VatAmount.Value).Should().NotBe(m1.TotalVat.Value);
}
[Fact]
diff --git a/tests/Inflop.VatSharp.Tests/DiscountTests.cs b/tests/Inflop.VatSharp.Tests/DiscountTests.cs
index d822499..877e957 100644
--- a/tests/Inflop.VatSharp.Tests/DiscountTests.cs
+++ b/tests/Inflop.VatSharp.Tests/DiscountTests.cs
@@ -619,6 +619,99 @@ public void PerUnit_DiscountPerUnitRoundsUpPastUnitPrice_ClampedToZero()
result.TotalVat.Value.Should().Be(0m);
result.TotalGross.Value.Should().Be(0m);
}
+
+ // ── Combinatorial PerUnit gaps ───────────────────────────────────────
+
+ [Fact]
+ public void MethodI_PerUnit_MultiRate_NonDivisibleQuantity_CorrectPerRateAmounts()
+ {
+ // Linia 1 — niepodzielna: discount/qty = 5/3 = 1.6666... → round = 1.67
+ // effectiveUnit = 8.33; net = 24.99; discount net = 30 − 24.99 = 5.01
+ // Linia 2 — podzielna: discount/qty = 2/2 = 1.00
+ // effectiveUnit = 19.00; net = 38.00; discount net = 40 − 38.00 = 2.00
+ var items = new InvoiceLineItem[]
+ {
+ new(UnitPrice.Net(10m), Quantity.Of(3), VatRate.Of(23), Discount.OfAmount(5.00m)),
+ new(UnitPrice.Net(20m), Quantity.Of(2), VatRate.Of(8), Discount.OfAmount(2.00m)),
+ };
+
+ var result = _perUnit.Calculate(items, VatCalculationMethod.FromSumOfNetValues);
+
+ result.VatRateSummaries.Should().HaveCount(2);
+ result.VatRateSummaries.Single(s => s.VatRate == VatRate.Of(23)).TotalNet.Value.Should().Be(24.99m);
+ result.VatRateSummaries.Single(s => s.VatRate == VatRate.Of(8)).TotalNet.Value.Should().Be(38.00m);
+
+ result.TotalNet.Value.Should().Be(62.99m);
+
+ // VAT M.I per rate: round(VatFromNet(sumNet))
+ // 23%: round(24.99 × 0.23) = round(5.7477) = 5.75
+ // 8%: round(38.00 × 0.08) = 3.04
+ result.VatRateSummaries.Single(s => s.VatRate == VatRate.Of(23)).TotalVat.Value.Should().Be(5.75m);
+ result.VatRateSummaries.Single(s => s.VatRate == VatRate.Of(8)).TotalVat.Value.Should().Be(3.04m);
+ result.TotalVat.Value.Should().Be(8.79m);
+
+ result.TotalDiscount.Value.Should().Be(7.01m); // 5.01 + 2.00
+ }
+
+ [Fact]
+ public void MethodIII_PerUnit_DivisibleQuantity_MatchesFromTotal()
+ {
+ // discount/qty = 4/4 = 1.00 (dokładnie podzielna) — PerUnit ≡ FromTotal dla M.III
+ var item = new InvoiceLineItem(
+ UnitPrice.Net(10m), Quantity.Of(4), VatRate.Of(23), Discount.OfAmount(4.00m));
+
+ var resultPerUnit = _perUnit.Calculate([item], VatCalculationMethod.SumOfLineItemVatAmounts);
+ var resultFromTotal = _fromTotal.Calculate([item], VatCalculationMethod.SumOfLineItemVatAmounts);
+
+ resultPerUnit.TotalNet.Should().Be(resultFromTotal.TotalNet);
+ resultPerUnit.TotalVat.Should().Be(resultFromTotal.TotalVat);
+ resultPerUnit.TotalDiscount.Should().Be(resultFromTotal.TotalDiscount);
+ }
+
+ [Fact]
+ public void MethodIII_PerUnit_MultiRate_PerLineVat_CorrectTotals()
+ {
+ // Linia 1 niepodzielna: net = 24.99, VAT M.III per linia = round(24.99 × 0.23) = 5.75
+ // Linia 2 podzielna: net = 38.00, VAT M.III per linia = round(38.00 × 0.08) = 3.04
+ var items = new InvoiceLineItem[]
+ {
+ new(UnitPrice.Net(10m), Quantity.Of(3), VatRate.Of(23), Discount.OfAmount(5.00m)),
+ new(UnitPrice.Net(20m), Quantity.Of(2), VatRate.Of(8), Discount.OfAmount(2.00m)),
+ };
+
+ var result = _perUnit.Calculate(items, VatCalculationMethod.SumOfLineItemVatAmounts);
+
+ // Definiująca własność M.III: sum per-line VAT == TotalVat
+ var sumPerLineVat = result.LineItems.Aggregate(Money.Zero, (acc, li) => acc + li.VatAmount);
+ sumPerLineVat.Should().Be(result.TotalVat);
+
+ result.VatRateSummaries.Single(s => s.VatRate == VatRate.Of(23)).TotalVat.Value.Should().Be(5.75m);
+ result.VatRateSummaries.Single(s => s.VatRate == VatRate.Of(8)).TotalVat.Value.Should().Be(3.04m);
+ result.TotalVat.Value.Should().Be(8.79m);
+ }
+
+ [Fact]
+ public void CalculateLineItem_PerUnitEngine_AbsoluteDiscount_NonDivisibleQuantity()
+ {
+ // discount/qty = 1/3 = 0.3333... → round = 0.33
+ // effectiveUnit = 12.50 − 0.33 = 12.17; totalNet = 36.51
+ // VAT = round(36.51 × 0.23) = round(8.3973) = 8.40
+ // Gross = 36.51 + 8.40 = 44.91
+ // DiscountAmount net = 12.50×3 − 36.51 = 0.99
+ var item = new InvoiceLineItem(UnitPrice.Net(12.50m), Quantity.Of(3), VatRate.Of(23), Discount.OfAmount(1.00m));
+
+ var result = _perUnit.CalculateLineItem(item);
+
+ result.NetValue.Value.Should().Be(36.51m);
+ result.VatAmount.Value.Should().Be(8.40m);
+ result.GrossValue.Value.Should().Be(44.91m);
+ result.DiscountAmount.Value.Should().Be(0.99m);
+
+ // Kontrast: FromTotal dałoby NetValue = 36.50, DiscountAmount = 1.00
+ var fromTotalResult = _fromTotal.CalculateLineItem(item);
+ fromTotalResult.NetValue.Value.Should().Be(36.50m);
+ fromTotalResult.DiscountAmount.Value.Should().Be(1.00m);
+ }
}
// ═══════════════════════════════════════════════════════════════════════════
@@ -631,8 +724,7 @@ public class FullDiscountTests
public void MethodI_100PercentDiscount_AllAmountsZero()
{
var engine = VatCalculationEngine.Create();
- var item = new InvoiceLineItem(
- UnitPrice.Net(100m), Quantity.One, VatRate.Of(23), Discount.OfPercentage(100m));
+ var item = new InvoiceLineItem(UnitPrice.Net(100m), Quantity.One, VatRate.Of(23), Discount.OfPercentage(100m));
var result = engine.Calculate([item], VatCalculationMethod.FromSumOfNetValues);
@@ -646,8 +738,7 @@ public void MethodI_100PercentDiscount_AllAmountsZero()
public void MethodII_100PercentDiscount_AllAmountsZero()
{
var engine = VatCalculationEngine.Create();
- var item = new InvoiceLineItem(
- UnitPrice.Gross(123m), Quantity.One, VatRate.Of(23), Discount.OfPercentage(100m));
+ var item = new InvoiceLineItem(UnitPrice.Gross(123m), Quantity.One, VatRate.Of(23), Discount.OfPercentage(100m));
var result = engine.Calculate([item], VatCalculationMethod.FromSumOfGrossValues);
@@ -660,8 +751,7 @@ public void MethodII_100PercentDiscount_AllAmountsZero()
public void MethodIII_100PercentDiscount_AllAmountsZero()
{
var engine = VatCalculationEngine.Create();
- var item = new InvoiceLineItem(
- UnitPrice.Net(100m), Quantity.One, VatRate.Of(23), Discount.OfPercentage(100m));
+ var item = new InvoiceLineItem(UnitPrice.Net(100m), Quantity.One, VatRate.Of(23), Discount.OfPercentage(100m));
var result = engine.Calculate([item], VatCalculationMethod.SumOfLineItemVatAmounts);
@@ -685,11 +775,7 @@ public void MethodII_GrossPrice_AbsoluteDiscount_ReducesTaxableBase()
// VatFromGross: 113.00 × 23/123 = 21.13008 → 21.13 (rounded)
// Net: 113.00 − 21.13 = 91.87
var engine = VatCalculationEngine.Create();
- var item = new InvoiceLineItem(
- UnitPrice.Gross(123m),
- Quantity.One,
- VatRate.Of(23),
- Discount.OfAmount(10m));
+ var item = new InvoiceLineItem(UnitPrice.Gross(123m), Quantity.One, VatRate.Of(23), Discount.OfAmount(10m));
var result = engine.Calculate([item], VatCalculationMethod.FromSumOfGrossValues);
diff --git a/tests/Inflop.VatSharp.Tests/ForeignCurrencyCalculatorTests.cs b/tests/Inflop.VatSharp.Tests/ForeignCurrencyCalculatorTests.cs
index 14df85a..b221d57 100644
--- a/tests/Inflop.VatSharp.Tests/ForeignCurrencyCalculatorTests.cs
+++ b/tests/Inflop.VatSharp.Tests/ForeignCurrencyCalculatorTests.cs
@@ -370,4 +370,64 @@ public void MethodIII_Fcy_ProducesDifferentVatBaseThanMethodI_ForMultipleItemsSa
resultIII.TotalVatBase.Value.Should().Be(30.23m); // round(7.14 × 4.2345)
resultI.TotalVatBase.Value.Should().Be(30.18m); // round(131.23 × 0.23) — derived from NetBase
}
+
+ // ── FCY + PerUnit discount behavior ──────────────────────────────────
+
+ [Fact]
+ public void MethodI_Fcy_PerUnit_NonDivisibleQuantity_CorrectBaseAmounts()
+ {
+ // Tests that discountBehavior is properly propagated from
+ // VatCalculationEngine.Create(...) through to the FCY pipeline.
+ var engine = VatCalculationEngine.Create(discountBehavior: Strategies.Discount.PerUnitAbsoluteDiscountBehavior.Instance);
+
+ var item = new InvoiceLineItem(UnitPrice.Net(10m), Quantity.Of(3), VatRate.Of(23), Discount.OfAmount(5.00m));
+ // PerUnit: discount/qty = 5/3 = 1.6666... → round = 1.67
+ // effectiveUnit = 8.33; totalNet = 24.99
+ // VAT (M.I) = round(24.99 × 0.23) = round(5.7477) = 5.75
+ // Gross = 30.74
+
+ var result = engine.Calculate([item], VatCalculationMethod.FromSumOfNetValues, EurPln);
+
+ // FCY (EUR)
+ result.TotalNet.Value.Should().Be(24.99m);
+ result.TotalVat.Value.Should().Be(5.75m);
+ result.TotalGross.Value.Should().Be(30.74m);
+
+ // Base (PLN) — strategy.BuildSummaryFcy dla M.I:
+ // NetBase = round(24.99 × 4.2345) = round(105.820155) = 105.82
+ // VatBase = round(105.82 × 0.23) = round(24.3386) = 24.34
+ // GrossBase = 105.82 + 24.34 = 130.16
+ // DiscountBase = round(5.01 × 4.2345) = round(21.214845) = 21.21
+ result.TotalNetBase.Value.Should().Be(105.82m);
+ result.TotalVatBase.Value.Should().Be(24.34m);
+ result.TotalGrossBase.Value.Should().Be(130.16m);
+ result.TotalDiscountBase.Value.Should().Be(21.21m);
+ }
+
+ // ── ToBaseDocumentAmounts — LineItems pozostają w walucie obcej ──────
+
+ [Fact]
+ public void ToBaseDocumentAmounts_LineItems_RemainInForeignCurrency()
+ {
+ // Per art. 106e ust. 11 ustawy o VAT i art. 91 dyrektywy 2006/112/EC:
+ // tylko VAT totals i per-rate summaries muszą być w walucie rozliczeniowej.
+ // LineItems celowo pozostają w walucie faktury — bez tej własności
+ // ktoś nieświadomy mógłby "naprawić" pozorną niespójność.
+ var engine = VatCalculationEngine.Create();
+ var item = new InvoiceLineItem(UnitPrice.Net(100m), Quantity.One, VatRate.Of(23));
+
+ var fcy = engine.Calculate([item], VatCalculationMethod.FromSumOfNetValues, EurPln);
+ var asBase = fcy.ToBaseDocumentAmounts();
+
+ // Totale i VAT summaries są w PLN (base currency)
+ asBase.TotalNet.Value.Should().Be(fcy.TotalNetBase.Value);
+ asBase.TotalVat.Value.Should().Be(fcy.TotalVatBase.Value);
+ asBase.TotalGross.Value.Should().Be(fcy.TotalGrossBase.Value);
+ asBase.VatRateSummaries.Single().TotalNet.Value
+ .Should().Be(fcy.VatRateSummaries.Single().TotalNetBase.Value);
+
+ // ALE LineItems pozostają w EUR — art. 106e ust. 11 ustawy o VAT
+ asBase.LineItems.Should().BeEquivalentTo(fcy.LineItems);
+ asBase.LineItems.Single().NetValue.Value.Should().Be(100m); // EUR, nie PLN
+ }
}
diff --git a/tests/Inflop.VatSharp.Tests/Inflop.VatSharp.Tests.csproj b/tests/Inflop.VatSharp.Tests/Inflop.VatSharp.Tests.csproj
index 5b198c7..5fff8cf 100644
--- a/tests/Inflop.VatSharp.Tests/Inflop.VatSharp.Tests.csproj
+++ b/tests/Inflop.VatSharp.Tests/Inflop.VatSharp.Tests.csproj
@@ -2,9 +2,8 @@
net10.0
- enable
- enable
false
+ false
diff --git a/tests/Inflop.VatSharp.Tests/InvariantTests.cs b/tests/Inflop.VatSharp.Tests/InvariantTests.cs
new file mode 100644
index 0000000..0e738ae
--- /dev/null
+++ b/tests/Inflop.VatSharp.Tests/InvariantTests.cs
@@ -0,0 +1,199 @@
+using FluentAssertions;
+using Inflop.VatSharp.Enums;
+using Inflop.VatSharp.Exceptions;
+using Inflop.VatSharp.Mapping;
+using Inflop.VatSharp.ValueObjects;
+using Xunit;
+
+namespace Inflop.VatSharp.Tests;
+
+// ═══════════════════════════════════════════════════════════════════════════
+// Structural invariants and per-method properties
+// ═══════════════════════════════════════════════════════════════════════════
+
+public class InvariantTests
+{
+ private static readonly InvoiceLineItem[] NetItems =
+ [
+ new(UnitPrice.Net(9.99m), Quantity.Of(7), VatRate.Of(23), Discount.OfPercentage(10m)),
+ new(UnitPrice.Net(14.50m), Quantity.Of(3), VatRate.Of(23)),
+ new(UnitPrice.Net(5.25m), Quantity.Of(5), VatRate.Of(8), Discount.OfAmount(2.00m)),
+ ];
+
+ private static readonly InvoiceLineItem[] GrossItems =
+ [
+ new(UnitPrice.Gross(12.29m), Quantity.Of(7), VatRate.Of(23), Discount.OfPercentage(10m)),
+ new(UnitPrice.Gross(17.84m), Quantity.Of(3), VatRate.Of(23)),
+ new(UnitPrice.Gross(5.67m), Quantity.Of(5), VatRate.Of(8), Discount.OfAmount(2.00m)),
+ ];
+
+ private static readonly InvoiceLineItem[] ArticleItems =
+ [
+ new(UnitPrice.Net(4.58m), Quantity.Of(4), VatRate.Of(23)),
+ new(UnitPrice.Net(7.22m), Quantity.Of(5), VatRate.Of(23)),
+ new(UnitPrice.Net(12.74m), Quantity.Of(2), VatRate.Of(8)),
+ ];
+
+ private readonly LineItemCalculationEngine _engine = VatCalculationEngine.Create();
+
+ public static IEnumerable