137 lines
4.4 KiB
Python
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.19"
|
|
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"]
|