File size: 8,884 Bytes
ce8f04a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
"""
Strategy cascade recommend — pure, ≥4 paths, no LLM.

Feature flags
-------------
ENABLE_STRATEGY_CASCADE (default: true)
  When true (product default), recommend_strategy_paths runs the full 2026
  cascade scoring and returns cascade_enforced=true.
  When false, the same path list is still produced (scores, ≥4 ids, primary)
  but cascade_enforced=false so callers can treat cascade as advisory/off
  without losing the path surface.

ENABLE_TAX_PATHS (default: true) — see core.strategy.tax_paths
  When true, PATH_TAX (tax_psi_ulgi) gets tax checklists (PSI / ulga B+R /
  IP Box) via attach_tax_checklists. When false, PATH_TAX remains in the
  cascade without checklist payload; tax_paths_enabled=false on the result.
"""
from __future__ import annotations

import os
from typing import Any, Dict, List, Optional

from core.strategy.direct_eu import attach_direct_eu_meta
from core.strategy.tax_paths import attach_tax_checklists, tax_paths_enabled

# Canonical path ids (product)
PATH_TAX = "tax_psi_ulgi"
PATH_REGIONAL_FST = "regional_fst"
PATH_NATIONAL_FENG = "national_feng"
PATH_DEBT = "debt_loan_guarantee"
PATH_DIRECT_EU = "direct_eu_prep"

FLAG_STRATEGY_CASCADE = "ENABLE_STRATEGY_CASCADE"


def strategy_cascade_enabled() -> bool:
    """Thin flag helper — default ON so product cascade path stays active."""
    return os.environ.get(FLAG_STRATEGY_CASCADE, "true").lower() in ("1", "true", "yes")


def recommend_strategy_paths(
    *,
    eligibility: Optional[Dict[str, Any]] = None,
    company_data: Optional[Dict[str, Any]] = None,
    goal: str = "",
    region: str = "",
) -> Dict[str, Any]:
    """
    Returns ordered strategy paths with scores and reasons.
    Always returns at least four distinct path labels among the five canonical ids.

    cascade_enforced reflects ENABLE_STRATEGY_CASCADE (default true). Paths are
    always returned; when the flag is off, cascade_enforced is false.
    """
    elig = eligibility or {}
    cd = company_data or {}
    dm = elig.get("de_minimis") if isinstance(elig.get("de_minimis"), dict) else {}
    msp = elig.get("msp") if isinstance(elig.get("msp"), dict) else {}
    goal_l = (goal or "").lower()
    region_l = (
        region
        or cd.get("region")
        or cd.get("voivodeship")
        or (cd.get("address") or "")
    )
    region_l = str(region_l).lower()

    fst_regions = (
        "śląsk",
        "slask",
        "małopol",
        "malopol",
        "dolnośląsk",
        "dolnoslask",
        "wielkopol",
        "łódzk",
        "lodzk",
    )
    in_fst = any(r in region_l for r in fst_regions)

    paths: List[Dict[str, Any]] = []

    # Tax / PSI / ulgi — always relevant for investment-like goals
    tax_score = 55.0
    tax_reasons = ["Ulgi i PSI często pewniejsze niż konkurs grantowy (2026)."]
    if any(k in goal_l for k in ("inwestyc", "hala", "linia", "maszyn", "cit", "pit", "podat")):
        tax_score += 20
        tax_reasons.append("Cel inwestycyjny — sprawdź PSI / ulgę B+R przed naborem.")
    if any(k in goal_l for k in ("oprogram", "ip box", "patent", "b+r", "badaw")):
        tax_score += 15
        tax_reasons.append("B+R/IP — ulga B+R i IP Box jako fundament.")
    # Tax checklists attached after primary marking via attach_tax_checklists batch
    paths.append(
        {
            "id": PATH_TAX,
            "label": "Podatki / PSI / ulgi (B+R, IP Box)",
            "score": tax_score,
            "reasons": tax_reasons,
            "primary": False,
        }
    )

    # Regional / FST
    reg_score = 50.0
    reg_reasons = ["Programy regionalne FE — zwykle łatwiejsze wejście dla MŚP."]
    if in_fst:
        reg_score += 25
        reg_reasons.append("Siedziba w obszarze FST — wyższe % i łagodniejsze kryteria.")
    if any(k in goal_l for k in ("oze", "termo", "cyfryz", "szkolen", "lokaln")):
        reg_score += 10
    paths.append(
        {
            "id": PATH_REGIONAL_FST,
            "label": "Region / FST (programy wojewódzkie)",
            "score": reg_score,
            "reasons": reg_reasons,
            "primary": False,
        }
    )

    # National FENG / NCBR grants
    feng_score = 52.0
    feng_reasons = ["FENG/SMART/NCBR — główny trzon krajowych grantów innowacyjnych."]
    if any(k in goal_l for k in ("b+r", "innowac", "trl", "wdroż", "smart", "deeptech")):
        feng_score += 22
        feng_reasons.append("Cel B+R/innowacja — FENG/SMART lub NCBR.")
    if msp.get("is_large"):
        feng_score -= 10
        feng_reasons.append("Duża firma — część naborów MŚP niedostępna.")
    if dm.get("exhausted"):
        feng_score -= 5
        feng_reasons.append("De minimis wyczerpany — unikaj naborów opartych o de minimis.")
    paths.append(
        {
            "id": PATH_NATIONAL_FENG,
            "label": "Krajowe granty FENG / NCBR / PARP",
            "score": feng_score,
            "reasons": feng_reasons,
            "primary": False,
        }
    )

    # Debt / guarantee
    debt_score = 48.0
    debt_reasons = ["Instrumenty dłużne BGK/ARP — ciągłe nabory, pomost pod grant."]
    if dm.get("exhausted"):
        debt_score -= 25
        debt_reasons.append(
            "Limit de minimis wyczerpany — gwarancje de minimis niedostępne jako primary."
        )
    if any(k in goal_l for k in ("pożyczk", "kredyt", "gwaranc", "płynnoś", "kapitał obrot")):
        debt_score += 20
    if any(k in goal_l for k in ("kpo",)):
        debt_score += 15
        debt_reasons.append("KPO 2026: preferuj instrumenty dłużne zamiast nowych grantów KPO.")
    paths.append(
        {
            "id": PATH_DEBT,
            "label": "Dłużne / gwarancje (BGK, ARP)",
            "score": debt_score,
            "reasons": debt_reasons,
            "primary": False,
            "de_minimis_blocked": bool(dm.get("exhausted")),
        }
    )

    # Direct EU prep
    eu_score = 40.0
    eu_reasons = ["Direct EU (HE/EIC) lub Granty na Eurogranty jako przygotowanie."]
    if any(k in goal_l for k in ("horizon", "eic", "deeptech", "cleantech", "eurogrant", "horyzont")):
        eu_score += 30
        eu_reasons.append("Cel deeptech/UE — rozważ Horyzont/EIC i Eurogranty prep.")
    paths.append(
        {
            "id": PATH_DIRECT_EU,
            "label": "Direct EU / przygotowanie Eurograntu",
            "score": eu_score,
            "reasons": eu_reasons,
            "primary": False,
        }
    )

    # Sort and mark primary (highest that is not de_minimis_blocked when exhausted)
    paths.sort(key=lambda p: float(p.get("score") or 0), reverse=True)
    primary_set = False
    for p in paths:
        if p.get("de_minimis_blocked"):
            continue
        if not primary_set:
            p["primary"] = True
            primary_set = True
            break
    if not primary_set and paths:
        # all blocked weird case — still mark tax
        for p in paths:
            if p["id"] == PATH_TAX:
                p["primary"] = True
                break

    # Ensure ≥4 distinct path ids always present
    ids = {p["id"] for p in paths}
    assert len(ids) >= 4

    # Attach tax + Direct EU checklists (F3 / F6 product surface)
    paths = [attach_tax_checklists(p) for p in paths]
    paths = [attach_direct_eu_meta(p) for p in paths]

    # Filter primary recommendation list: do not list de-minimis-blocked as recommended primary
    recommended = [p for p in paths if not p.get("de_minimis_blocked")]
    if not recommended:
        recommended = [p for p in paths if p["id"] == PATH_TAX] or paths[:1]

    cascade_on = strategy_cascade_enabled()
    tax_on = tax_paths_enabled()

    notes = [
        "Kolejność kaskady 2026: podatki → region/FST → FENG → dłużne → Direct EU.",
        "Wynik jest rekomendacją produktową, nie opinią prawną.",
    ]
    if not cascade_on:
        notes.append(
            "ENABLE_STRATEGY_CASCADE=false — kaskada nie jest egzekwowana (paths nadal zwracane)."
        )
    if not tax_on:
        notes.append(
            "ENABLE_TAX_PATHS=false — checklisty PSI/ulga B+R/IP Box nie dołączone do PATH_TAX."
        )

    return {
        "version": 1,
        "paths": paths,
        "recommended_paths": recommended[:4],
        "primary_path_id": next((p["id"] for p in paths if p.get("primary")), recommended[0]["id"]),
        "cascade_order": [
            PATH_TAX,
            PATH_REGIONAL_FST,
            PATH_NATIONAL_FENG,
            PATH_DEBT,
            PATH_DIRECT_EU,
        ],
        "cascade_enforced": cascade_on,
        "tax_paths_enabled": tax_on,
        "eligibility_summary": {
            "msp": msp.get("status"),
            "de_minimis_exhausted": bool(dm.get("exhausted")),
            "de_minimis_used_eur": dm.get("used_eur"),
        },
        "notes": notes,
    }