"""Упаковка встречных строк по одному правилу без БД (§5).""" import uuid from decimal import Decimal from src.domain.elimination_packing import MAX_SIDE_ROWS, PackRow, best_subsets, calculate_rule_totals, pack_rule_rows A = uuid.uuid4() B = uuid.uuid4() def _row(amount: str, *, debit: bool) -> PackRow: return PackRow((uuid.uuid4(),), A if debit else B, B if debit else A, Decimal(amount)) def test_knapsack_finds_exact_subset_with_fixed_size_and_keeps_ties() -> None: first, second, third = (_row(amount, debit=False) for amount in ("40", "60", "60")) choice = best_subsets([first, second, third], size=2, capacity=Decimal("100")) assert choice is not None assert choice.deviation == 0 assert choice.unique_rows is None assert choice.involved_keys == {first.key, second.key, third.key} def test_knapsack_keeps_both_nearest_sums_and_the_five_percent_lower_boundary() -> None: below, above = (_row(amount, debit=False) for amount in ("99.00", "101.00")) tie = best_subsets([below, above], size=1, capacity=Decimal("100.00")) assert tie is not None assert tie.deviation == Decimal("1.00") assert tie.unique_rows is None assert tie.involved_keys == {below.key, above.key} boundary = best_subsets([_row("100.00", debit=False)], size=1, capacity=Decimal("105.00")) assert boundary is not None assert boundary.deviation == Decimal("5.00") def test_exact_one_to_one_is_reserved_before_one_to_many() -> None: debit = [_row("100", debit=True), _row("50", debit=True)] credit = [_row("100", debit=False), _row("30", debit=False), _row("20", debit=False)] result = pack_rule_rows(debit, credit) assert [(len(group.debit_rows), len(group.credit_rows)) for group in result.groups] == [(1, 1), (1, 2)] assert result.used_keys == {row.key for row in [*debit, *credit]} assert result.ambiguous_keys == frozenset() def test_equal_exact_groups_sharing_a_row_go_to_manual() -> None: debit = [_row("100", debit=True)] credit = [_row("100", debit=False), _row("100", debit=False)] result = pack_rule_rows(debit, credit) assert result.groups == () assert result.ambiguous_keys == {row.key for row in [*debit, *credit]} def test_five_percent_boundary_and_no_many_to_many() -> None: debit = [_row("100", debit=True)] credit = [_row("21", debit=False) for _ in range(5)] result = pack_rule_rows(debit, credit) assert len(result.groups) == 1 assert len(result.groups[0].credit_rows) == MAX_SIDE_ROWS assert result.groups[0].deviation_percent == Decimal("5") many_to_many = pack_rule_rows( [_row("40", debit=True), _row("60", debit=True)], [_row("30", debit=False), _row("70", debit=False)], ) assert many_to_many.groups == () # суммы сторон равны, но допустимой группы 1:N или N:1 нет over_threshold = pack_rule_rows([_row("100", debit=True)], [_row("106", debit=False)]) assert over_threshold.groups == () def test_rule_totals_eliminate_minimum_within_five_percent() -> None: totals = calculate_rule_totals(Decimal("110429113.15"), Decimal("106311116.46"), manual=False) assert totals.amount_a == Decimal("110429113.15") assert totals.amount_b == Decimal("106311116.46") assert totals.deviation_amount == Decimal("4117996.69") assert totals.deviation_percent is not None assert totals.deviation_percent.quantize(Decimal("0.01")) == Decimal("3.87") assert totals.eliminated_amount == Decimal("106311116.46") assert totals.exceeds_threshold is False def test_rule_totals_over_five_percent_and_manual_eliminate_zero() -> None: over = calculate_rule_totals(Decimal("106"), Decimal("100"), manual=False) assert over.deviation_amount == Decimal("6") assert over.deviation_percent == Decimal("6") assert over.exceeds_threshold is True assert over.eliminated_amount == 0 manual = calculate_rule_totals(Decimal("100"), Decimal("100"), manual=True) assert manual.exceeds_threshold is False assert manual.eliminated_amount == 0 boundary = calculate_rule_totals(Decimal("105"), Decimal("100"), manual=False) assert boundary.exceeds_threshold is False assert boundary.eliminated_amount == Decimal("100") one_sided = calculate_rule_totals(Decimal("100"), Decimal("0"), manual=False) assert one_sided.deviation_percent is None assert one_sided.exceeds_threshold is True assert one_sided.eliminated_amount == 0