dodil/erp-brick-business-partner
tests/test_rules.py
"""The partner rules, with no database — named for the spec's stories and invariants
(modules/business-partner/module.yaml)."""
import pytest
from brick_bp.domain.partner import (BLOCKED, Edge, Partner, PartnerIncomplete, PartnerRuleBroken,
bp_id_for, check_link)
def test_bp02_the_same_key_derives_the_same_id_whatever_its_case():
assert bp_id_for("Acme.com ") == bp_id_for("acme.com")
assert Partner.new("ACME.com", "Acme AB", "se").bp_key == "acme.com"
def test_bp02_the_id_matches_the_deployed_reference_package():
"""Ids already in a bucket must stay valid: same blake2b-56 derivation as erp-business-partner v1."""
import hashlib
assert bp_id_for("acme.com") == int.from_bytes(
hashlib.blake2b(b"acme.com", digest_size=7).digest(), "big")
assert bp_id_for("acme.com") < 2 ** 63
def test_bp01_one_partner_two_roles():
p = Partner.new("acme.com", "Acme AB", "SE", roles=["customer"])
assert p.add_role("Supplier") and not p.add_role("customer")
assert p.roles == {"customer", "supplier"}
def test_a_colliding_key_is_refused_not_merged():
p = Partner.new("acme.com", "Acme AB", "SE")
with pytest.raises(PartnerRuleBroken, match="collides"):
p.same_party("other.example")
def test_describing_a_partner_never_changes_its_status():
p = Partner.new("acme.com", "Acme AB", "SE")
p.set_status(BLOCKED)
p.describe(legal_name="Acme Group AB")
assert p.status == BLOCKED and not p.can_transact
@pytest.mark.parametrize("field, value", [("legal_name", " "), ("country", "Sweden"), ("country", "S1")])
def test_a_partner_needs_a_name_and_a_country_code(field, value):
with pytest.raises(PartnerIncomplete):
Partner.new("acme.com", **{"legal_name": "Acme AB", "country": "SE", field: value})
def test_describe_reports_only_what_changed():
p = Partner.new("acme.com", "Acme AB", "SE", tax_id="se 5560 1234 01")
assert p.tax_id == "SE556012340" + "1"
assert p.describe(legal_name="Acme AB", country="se") == {}
assert p.describe(domain="ACME.COM") == {"domain": ("", "acme.com")}
# ── the family ─────────────────────────────────────────────────────────────────────────────
def test_a_partner_cannot_be_its_own_parent():
with pytest.raises(PartnerRuleBroken, match="own parent"):
check_link(Edge(1, 1, "subsidiary_of"), [], set())
def test_a_company_has_one_parent_company():
with pytest.raises(PartnerRuleBroken, match="one parent"):
check_link(Edge(2, 3, "subsidiary_of"), [1], set())
def test_a_framework_can_govern_alongside_others():
check_link(Edge(2, 3, "framework_governs"), [1], set())
def test_a_cycle_is_refused():
"""2 is below 1; making 2 the parent of 1 would make each the other's ancestor."""
with pytest.raises(PartnerRuleBroken, match="cycle"):
check_link(Edge(1, 2, "subsidiary_of"), [], {2, 5})
def test_linking_what_is_already_linked_is_fine():
check_link(Edge(2, 1, "subsidiary_of"), [1], set())
def test_an_unknown_relationship_is_refused():
with pytest.raises(PartnerRuleBroken, match="relationship"):
check_link(Edge(2, 1, "owns"), [], set())
# ── person partners (BP-05..BP-07) ─────────────────────────────────────────────────────────
from brick_bp.domain.partner import (ERASED_NAME, PeopleAreWrittenByHr, person_card, # noqa: E402
person_key)
CARD = {"person_id": "812", "legal_name": "Anna Berg", "preferred_name": "Anna", "work_email": "Anna@Co.example",
"work_phone": "+44 20 7946 0000", "roles": ["employee"], "works_for": None}
def test_bp05_a_person_is_keyed_by_hr_person_id_so_hr_knows_the_id():
p = Partner.person("812")
assert p.bp_key == "person:812" == person_key(" 812 ") and p.bp_id == bp_id_for("person:812")
assert p.kind == "person" and p.is_person
def test_bp05_the_card_makes_the_person_exactly_that_and_twice_changes_nothing():
p = Partner.person("812")
first = p.apply_card(person_card(CARD))["changed"]
assert "legal_name" in first and "role +employee" in first
assert (p.legal_name, p.preferred_name, p.work_email, p.roles) == ("Anna Berg", "Anna", "anna@co.example", {"employee"})
assert p.apply_card(person_card(CARD)) == {"changed": []}
later = p.apply_card(person_card({**CARD, "roles": ["worker", "supplier"]}))["changed"]
assert later == ["role +supplier", "role +worker", "role -employee"] and p.roles == {"worker", "supplier"}
@pytest.mark.parametrize("field", ["personal_email", "home_address", "personal_phone", "tax_id", "bank_account",
"iban", "ni_number", "date_of_birth"])
def test_bp05_a_private_field_is_refused_in_the_rules_words(field):
with pytest.raises(PartnerIncomplete, match="a name and a work contact only"):
person_card({**CARD, field: "x"})
def test_bp05_an_unknown_field_is_refused_not_dropped():
with pytest.raises(PartnerIncomplete, match="unknown field"):
person_card({**CARD, "shoe_size": 42})
with pytest.raises(PartnerIncomplete, match="roles are"):
person_card({**CARD, "roles": ["customer"]})
def test_bp05_a_steward_cannot_create_a_person_key():
with pytest.raises(PeopleAreWrittenByHr, match="people are written by hr"):
Partner.new("person:812", "Anna Berg", "GB")
def test_bp07_anonymise_keeps_the_id_and_roles_and_changes_nothing_twice():
p = Partner.person("812")
p.apply_card(person_card(CARD))
assert p.anonymise() and not p.anonymise()
assert (p.legal_name, p.work_email, p.work_phone, p.preferred_name) == (ERASED_NAME, "", "", "")
assert p.bp_id == bp_id_for("person:812") and p.roles == {"employee"}
with pytest.raises(PartnerRuleBroken, match="erased"):
p.apply_card(person_card(CARD))
with pytest.raises(PartnerRuleBroken, match="organisation"):
Partner.new("acme.com", "Acme AB", "SE").anonymise()