Files
govoplan-erp/tests/test_payables.py
T
zemion 51dc33d8ae
Module Package Release / publish-packages (push) Successful in 10s
docs(erp): complete German reference contract
2026-08-23 19:31:23 +02:00

137 lines
4.4 KiB
Python

from __future__ import annotations
from datetime import UTC, date, datetime
import json
import pytest
from govoplan_erp.backend.manifest import get_manifest
from govoplan_erp.backend.payables import (
BookingObservation,
ErpPayableProfile,
PayableExportInput,
build_payable_export_plan,
reconcile_booking,
)
def _profile() -> ErpPayableProfile:
return ErpPayableProfile(
profile_id="reviewed-target-v1",
provider_id="erp-target",
company_code="1000",
payable_schema="govoplan-neutral-payable",
payable_schema_version="1",
mapping_revision="mapping-3",
connection_ref="core-credential:erp-target",
booking_status_mapping=(
("RECEIVED", "received"),
("POSTED", "booked"),
("REJECTED", "rejected"),
("REVERSED", "reversed"),
),
)
def _payable() -> PayableExportInput:
return PayableExportInput(
tenant_id="tenant-a",
source_module="payments",
payable_id="payable-7",
payable_revision=3,
invoice_reference="invoice-2026-7",
creditor_reference="creditor-4",
currency="eur",
gross_amount_minor=123_45,
due_date=date(2026, 9, 15),
cost_center_reference="cost-12",
budget_reference="budget-2026",
invoice_document_sha256="a" * 64,
evidence_references=("xrechnung:handoff-8",),
)
def _observation(plan, *, status: str = "POSTED", plan_sha256: str | None = None):
return BookingObservation(
tenant_id=plan.tenant_id,
payable_id=plan.payable_id,
provider_id=plan.provider_id,
external_booking_id="booking-99",
raw_status=status,
external_revision="external-4",
observed_at=datetime(2026, 8, 23, 12, tzinfo=UTC),
exported_plan_sha256=plan_sha256 or plan.plan_sha256,
evidence_sha256="b" * 64,
currency="EUR",
gross_amount_minor=123_45,
)
def test_plan_is_canonical_digest_bound_and_effect_free() -> None:
first = build_payable_export_plan(_profile(), _payable())
second = build_payable_export_plan(_profile(), _payable())
assert first == second
assert first.dispatch_allowed is False
assert first.idempotency_key.startswith("erp-payable:")
payload = json.loads(first.payload_json)
assert payload["payable"]["gross_amount_minor"] == 12345
assert payload["mapping_revision"] == "mapping-3"
def test_money_rejects_floating_point_and_invalid_currency() -> None:
values = _payable().to_payload()
with pytest.raises(ValueError, match="integer minor units"):
PayableExportInput(
**{
**values,
"due_date": date(2026, 9, 15),
"gross_amount_minor": 123.45,
"evidence_references": (),
}
)
@pytest.mark.parametrize(
("raw_status", "outcome", "action"),
[
("RECEIVED", "pending", "wait"),
("POSTED", "booked", "record_booking"),
("REJECTED", "rejected", "record_rejection"),
("REVERSED", "reversed", "record_reversal"),
],
)
def test_reconciliation_maps_reviewed_statuses(raw_status, outcome, action) -> None:
profile = _profile()
plan = build_payable_export_plan(profile, _payable())
decision = reconcile_booking(profile, plan, _observation(plan, status=raw_status))
assert decision.outcome == outcome
assert decision.action == action
assert len(decision.decision_sha256) == 64
def test_unknown_or_wrong_plan_observation_is_quarantined() -> None:
profile = _profile()
plan = build_payable_export_plan(profile, _payable())
unknown = reconcile_booking(profile, plan, _observation(plan, status="NEW-VENDOR-STATE"))
wrong_plan = reconcile_booking(
profile,
plan,
_observation(plan, plan_sha256="c" * 64),
)
assert (unknown.outcome, unknown.action) == ("conflict", "quarantine")
assert (wrong_plan.outcome, wrong_plan.action) == ("conflict", "quarantine")
def test_manifest_documents_unconfigured_provider_and_german_workflow() -> None:
manifest = get_manifest()
assert manifest.version == "0.1.20"
assert manifest.external_providers[0].id == "erp.payables_target"
assert manifest.architecture is not None
assert manifest.architecture.target_tested_providers == ()
assert manifest.documentation[0].translations["de"]["title"]