| """09: DD red flags (M&A best practice) — A/B level, universal. |
| |
| 4 red flags: |
| 1. Missing change-of-control clause for high-value contracts (MEDIUM) |
| — value > 4.83M parity watermark |
| 2. Auto-renewal (MEDIUM) — unpredictable obligation |
| 3. Non-compete clause (MEDIUM) — buyer flexibility constraint |
| 4. Non-assignable contract (HIGH) — critical for M&A |
| """ |
|
|
| from __future__ import annotations |
|
|
| from domain_checks.base import make_risk |
| from domain_checks.check_08_gdpr_28 import _get_full_text, _text_contains_any |
| from graph.states.pipeline_state import Risk |
| from utils.numbers import coerce_number |
|
|
|
|
| _REGULATION = "M&A DD best practice" |
| _VALUE_THRESHOLD = 4_830_000 |
|
|
|
|
| class DDRedFlagsCheck: |
| check_id = "check_09_dd_red_flags" |
| regulation = _REGULATION |
| is_hu_specific = False |
| applies_to = {"contract"} |
|
|
| def apply(self, extracted: dict) -> list[Risk]: |
| risks: list[Risk] = [] |
|
|
| full_text = _get_full_text(extracted) |
|
|
| |
| value_dict = extracted.get("value") or {} |
| if isinstance(value_dict, dict) and value_dict: |
| total = coerce_number(value_dict.get("amount")) |
| else: |
| total = coerce_number(extracted.get("total_value")) |
|
|
| has_coc = _text_contains_any(full_text, [ |
| "change of control", "change-of-control", "ownership change", |
| "acquisition", "buyout", |
| "tulajdonosváltozás", "irányításváltozás", "változás az irányításban", |
| "kontrollváltozás", "felvasárl", "akvizíció", |
| "Kontrollwechsel", "Eigentümerwechsel", |
| ]) |
| if total is not None and total > _VALUE_THRESHOLD and not has_coc: |
| risks.append(make_risk( |
| description="Missing change-of-control clause in a high-value contract", |
| severity="medium", |
| rationale=( |
| f"Contract value is {total:,.0f}, but no change-of-control " |
| f"clause is present. In an acquisition, the contract's " |
| f"future would be uncertain." |
| ), |
| regulation=_REGULATION, |
| source_check_id=self.check_id, |
| )) |
|
|
| |
| has_auto_renewal = _text_contains_any(full_text, [ |
| "auto-renewal", "automatic renewal", "evergreen clause", |
| "automatically renewed", |
| "automatikusan megújul", "hallgatólagos megújítás", "meghosszabbodik", |
| "automatische Verlängerung", |
| ]) |
| if has_auto_renewal: |
| risks.append(make_risk( |
| description="Auto-renewal clause detected", |
| severity="medium", |
| rationale=( |
| "The contract contains an auto-renewal clause. From a DD " |
| "perspective, this creates an open-ended obligation." |
| ), |
| regulation=_REGULATION, |
| source_check_id=self.check_id, |
| )) |
|
|
| |
| has_non_compete = _text_contains_any(full_text, [ |
| "non-compete", "non compete", "restrictive covenant", |
| "may not engage in", |
| "versenytilalm", "versenykorlátozás", "versenytilalom", "nem folytathat", |
| "Wettbewerbsverbot", |
| ]) |
| if has_non_compete: |
| risks.append(make_risk( |
| description="Non-compete clause detected", |
| severity="medium", |
| rationale=( |
| "The contract contains a non-compete clause. In an M&A " |
| "context, EU practice limits these to a maximum of 2 years." |
| ), |
| regulation=_REGULATION, |
| source_check_id=self.check_id, |
| )) |
|
|
| |
| has_no_assignment = _text_contains_any(full_text, [ |
| "not assignable", "assignment prohibited", "no assignment", |
| "may not be assigned", |
| "nem ruházható át", "nem engedményezhető", "átruházás tilalma", |
| "nicht übertragbar", |
| ]) |
| if has_no_assignment: |
| risks.append(make_risk( |
| description="Contract assignment restriction", |
| severity="high", |
| rationale=( |
| "The contract is non-assignable. After an acquisition, the " |
| "new owner cannot automatically step into the contract." |
| ), |
| regulation=_REGULATION, |
| source_check_id=self.check_id, |
| )) |
|
|
| return risks |
|
|