File size: 4,720 Bytes
7ff7119
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
"""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  # parity watermark for ~5M


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)

        # 1. Missing change-of-control clause — value > threshold AND no mention
        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,
            ))

        # 2. Auto-renewal
        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,
            ))

        # 3. Non-compete / restrictive covenant
        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,
            ))

        # 4. Non-assignable contract
        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