313982c6f1
CI / detect-changes (push) Successful in 10s
CI / branch-name (push) Has been skipped
CI / guardrail-integrity (push) Has been skipped
CI / secret-scan (push) Has been skipped
CI / dep-audit (push) Has been skipped
CI / sbom-scan (push) Has been skipped
CI / validate-canonical-controls (push) Successful in 16s
CI / loc-budget (push) Successful in 19s
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / nodejs-build (push) Has been skipped
CI / test-go (push) Has been skipped
CI / iace-gt-coverage (push) Has been skipped
CI / test-python-backend (push) Successful in 39s
CI / test-python-document-crawler (push) Has been skipped
CI / test-python-dsms-gateway (push) Has been skipped
A) Cookie-Policy-Architecture-Block Fallback auf DSE-Text wenn cookie via P15 deduped wurde. Erkennt jetzt auch single-doc Sites (Safetykon-Pattern). B) Konkrete-Aufgaben-Liste: Per-Doc-Cap (3) entfernt + globaler Cap 10→20. Safetykon zeigt jetzt 7 statt 4 Aufgaben. C) business_type-Klassifizierer: B2B-Service-Cluster aus P14 als Boost. Bei 2+ Service-Indikatoren (CE-Zertifizierung/Compliance/Auditierung) wird b2b_score angehoben. Safetykon: "B2C consulting" → "B2B (consulting)". D) Vendor-Extract Fallback auf DSE-Text wenn cookie deduped + keine CMP- Payloads. LLM extrahiert dann Vendors aus dem DSE-Text. Safetykon: 0 → 1 Vendor (Google Analytics aus dem DSE-Text erkannt). Smoke-Test Safetykon: alle 4 Polish-Items wirken, kein Regression. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
435 lines
19 KiB
Python
435 lines
19 KiB
Python
"""
|
|
Business Profiler — detect business model from document texts.
|
|
|
|
Pure keyword-based detection (deterministic, no LLM). Analyzes
|
|
DSE, Impressum, AGB, Widerruf etc. together to build a profile
|
|
that drives context-aware compliance checks.
|
|
|
|
Example:
|
|
profile = await detect_business_profile({"dse": "...", "impressum": "..."})
|
|
profile.business_type # "b2c"
|
|
profile.has_online_shop # True
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
from dataclasses import dataclass, field
|
|
|
|
|
|
@dataclass
|
|
class BusinessProfile:
|
|
business_type: str = "unknown" # b2b, b2c, b2g, nonprofit, unknown
|
|
industry: str = "unknown" # it_services, retail, healthcare, legal, craft, public, unknown
|
|
has_online_shop: bool = False
|
|
has_editorial_content: bool = False
|
|
is_regulated_profession: bool = False
|
|
regulated_profession_type: str = "" # arzt, anwalt, steuerberater, architekt, ""
|
|
needs_odr: bool = False # Online-Streitbeilegung
|
|
detected_services: list[str] = field(default_factory=list)
|
|
confidence: float = 0.0
|
|
# Wenn True: die Site selbst schliesst KEINEN Direktkauf-Vertrag
|
|
# (typisch OEM-Konfigurator-Sites BMW/Audi/Mercedes — Vertrag laeuft
|
|
# ueber den Vertragshaendler, nicht die Hersteller-Webseite).
|
|
# Konsequenz: AGB/Widerruf/Nutzungsbedingungen sind NICHT PFLICHT
|
|
# auf der Website, sondern werden beim Haendler ausgehaendigt.
|
|
no_direct_sales: bool = False
|
|
|
|
|
|
# ── Keyword lists ────────────────────────────────────────────────────
|
|
|
|
_B2C_KEYWORDS = [
|
|
"verbraucher", "warenkorb", "bestellung", "lieferung", "widerruf",
|
|
"shop", "kaufpreis", "rueckgabe", "rückgabe", "endkunde", "kaeufer",
|
|
"käufer", "privatkunde", "zahlungspflichtig bestellen",
|
|
]
|
|
|
|
_B2B_KEYWORDS = [
|
|
# Discriminative — these don't appear in B2C consumer texts
|
|
"geschaeftskunden", "geschäftskunden", "firmenkunde", "b2b",
|
|
"industriekunden", "ausschliesslich gewerblich", "ausschließlich gewerblich",
|
|
"ausschliesslich unternehmer", "ausschließlich unternehmer",
|
|
"kein verbrauchergeschaeft", "kein verbrauchergeschäft",
|
|
# Note: "unternehmen", "beratung", "consulting", "dienstleistung"
|
|
# were removed — they match in any company text and bias toward B2B.
|
|
]
|
|
|
|
_B2G_KEYWORDS = [
|
|
"koerperschaft des oeffentlichen rechts", "körperschaft des öffentlichen rechts",
|
|
"gemeinde", "stadtverwaltung", "landesbehoerde", "landesbehörde",
|
|
"kommunal", "buergerservice", "bürgerservice", "rathaus",
|
|
"landesamt", "bundesamt", "oeffentliche verwaltung", "öffentliche verwaltung",
|
|
"oeffentlicher dienst", "öffentlicher dienst",
|
|
]
|
|
|
|
_NONPROFIT_KEYWORDS = [
|
|
"gemeinnuetzig", "gemeinnützig", "verein", "stiftung", "e.v.",
|
|
"spende", "ehrenamtlich", "satzung",
|
|
]
|
|
|
|
_REGULATED_PROFESSIONS = {
|
|
# Anwalt — nur spezifische Begriffe, nicht "anwalt" allein
|
|
# (matcht sonst Redaktionsanwalt, Justiziar etc.)
|
|
"rechtsanwalt": "anwalt",
|
|
"rechtsanwaeltin": "anwalt",
|
|
"rechtsanwältin": "anwalt",
|
|
"kanzlei": "anwalt",
|
|
"rechtsanwaltskammer": "anwalt",
|
|
"zugelassener anwalt": "anwalt",
|
|
# Arzt — "praxis" entfernt (matcht "in der Praxis")
|
|
"arztpraxis": "arzt",
|
|
"zahnarzt": "arzt",
|
|
"facharzt": "arzt",
|
|
"aerztekammer": "arzt",
|
|
"ärztekammer": "arzt",
|
|
"kassenärztlich": "arzt",
|
|
"kassenaerztlich": "arzt",
|
|
# Steuerberater
|
|
"steuerberater": "steuerberater",
|
|
"steuerberaterin": "steuerberater",
|
|
"steuerberaterkammer": "steuerberater",
|
|
# Architekt
|
|
"architekt": "architekt",
|
|
"architektin": "architekt",
|
|
"architektenkammer": "architekt",
|
|
# Notar
|
|
"notar": "notar",
|
|
"notariat": "notar",
|
|
# Apotheker
|
|
"apotheke": "apotheker",
|
|
"apotheker": "apotheker",
|
|
}
|
|
|
|
_ONLINE_SHOP_KEYWORDS = [
|
|
"warenkorb", "checkout", "bestellung", "lieferung", "versand",
|
|
"paypal", "kreditkarte", "klarna", "sofortueberweisung",
|
|
"sofortüberweisung", "zahlungsarten", "versandkosten",
|
|
"lieferzeit", "retour", "paketdienst",
|
|
]
|
|
|
|
_EDITORIAL_KEYWORDS = [
|
|
"blog", "ratgeber", "news", "redaktion", "artikel", "magazin",
|
|
"beitrag", "kommentar", "podcast", "newsletter", "autor",
|
|
]
|
|
|
|
_INDUSTRY_KEYWORDS = {
|
|
# "software/cloud/hosting" are often mentioned in privacy texts of any
|
|
# vendor (Cloud-Hosting fuer Newsletter, SaaS-Tools etc.) without making
|
|
# the company an IT-services vendor itself. Keep the list deliberately
|
|
# narrow: only patterns that strongly suggest IT/SaaS as the core business.
|
|
"it_services": ["saas-anbieter", "software-as-a-service",
|
|
"ihr saas", "ihre cloud", "hosting-provider",
|
|
"api-anbieter", "developer-portal"],
|
|
"retail": ["shop", "warenkorb", "versand", "lieferung", "einzelhandel"],
|
|
"healthcare": ["arzt", "praxis", "patient", "gesundheit", "therapie", "klinik"],
|
|
"legal": ["kanzlei", "rechtsanwalt", "mandant", "anwalt"],
|
|
"craft": ["handwerk", "meister", "werkstatt", "montage", "gewerk"],
|
|
"public": ["kommune", "stadtverwaltung", "buergerservice", "bürgerservice", "rathaus"],
|
|
"finance": ["bank", "versicherung", "finanz", "kredit", "anlage"],
|
|
"education": ["schule", "bildung", "unterricht", "lehrplan", "schueler", "schüler"],
|
|
"consulting": ["beratung", "consulting", "schulung", "seminar", "gutachten", "audit",
|
|
"arbeitssicherheit", "brandschutz", "sicherheitstechnik", "zertifizierung"],
|
|
"manufacturing": ["fertigung", "produktion", "maschinenbau", "anlagenbau", "zulieferer",
|
|
"werkzeugbau", "spritzguss", "cnc", "industrietechnik"],
|
|
"automotive": ["fahrzeug", "kraftfahrzeug", "kfz", "automobil", "neuwagen",
|
|
"gebrauchtwagen", "fahrzeugempfehlung", "modellreihe",
|
|
"modellpalette", "antriebs", "motor", "reifen", "elektroauto",
|
|
"verbrenner", "hybridfahrzeug", "leasing", "werkstatt",
|
|
"wartung und reparatur", "probefahrt", "bmw", "mercedes",
|
|
"audi", "volkswagen", "porsche", "opel"],
|
|
"media": ["redaktion", "verlag", "medien", "journalismus", "presse"],
|
|
}
|
|
|
|
# Terms that indicate "versicherung" / "bank" is only mentioned as a
|
|
# §34d/§34c GewO disclosure (Versicherungsvermittler / Finanzanlagenvermittler)
|
|
# rather than the core business. Used to suppress false finance matches.
|
|
_VERMITTLER_CONTEXT_TERMS = [
|
|
"versicherungsvermittler", "berufshaftpflichtversicherung",
|
|
"vermittlerregister", "§34d", "§ 34 d", "§34c", "§ 34 c",
|
|
"finanzanlagenvermittler", "ihk muenchen", "ihk münchen",
|
|
]
|
|
|
|
_TRACKING_SERVICES = {
|
|
"google analytics": "Google Analytics",
|
|
"google tag manager": "Google Tag Manager",
|
|
"matomo": "Matomo",
|
|
"facebook pixel": "Facebook Pixel",
|
|
"meta pixel": "Meta Pixel",
|
|
"hotjar": "Hotjar",
|
|
"hubspot": "HubSpot",
|
|
"mailchimp": "Mailchimp",
|
|
"linkedin insight": "LinkedIn Insight",
|
|
"google ads": "Google Ads",
|
|
"google adsense": "Google AdSense",
|
|
"google maps": "Google Maps",
|
|
"youtube": "YouTube",
|
|
"vimeo": "Vimeo",
|
|
"cloudflare": "Cloudflare",
|
|
"sentry": "Sentry",
|
|
"intercom": "Intercom",
|
|
"zendesk": "Zendesk",
|
|
"stripe": "Stripe",
|
|
"paypal": "PayPal",
|
|
}
|
|
|
|
|
|
# ── Detection logic ──────────────────────────────────────────────────
|
|
|
|
def _count_hits(text: str, keywords: list[str]) -> int:
|
|
return sum(1 for kw in keywords if kw in text)
|
|
|
|
|
|
async def detect_business_profile(documents: dict[str, str]) -> BusinessProfile:
|
|
"""Analyze all document texts together to detect business model.
|
|
|
|
Args:
|
|
documents: dict mapping doc_type -> text (e.g. {"dse": "...", "impressum": "..."})
|
|
"""
|
|
profile = BusinessProfile()
|
|
if not documents:
|
|
return profile
|
|
|
|
# Merge all texts for keyword search
|
|
full_text = "\n".join(documents.values()).lower()
|
|
full_text = full_text.replace("\xad", "") # strip soft hyphens
|
|
|
|
# ── Tracking services (use full service detector) ──────────
|
|
try:
|
|
from compliance.services.service_detector import detect_services_in_text
|
|
detected = detect_services_in_text(full_text)
|
|
profile.detected_services = [s["name"] for s in detected]
|
|
except Exception:
|
|
# Fallback to simple keyword list
|
|
for pattern, label in _TRACKING_SERVICES.items():
|
|
if pattern in full_text:
|
|
profile.detected_services.append(label)
|
|
|
|
# ── Online shop ──────────────────────────────────────────────
|
|
shop_hits = _count_hits(full_text, _ONLINE_SHOP_KEYWORDS)
|
|
profile.has_online_shop = shop_hits >= 3
|
|
|
|
# ── Editorial content ────────────────────────────────────────
|
|
editorial_hits = _count_hits(full_text, _EDITORIAL_KEYWORDS)
|
|
profile.has_editorial_content = editorial_hits >= 2
|
|
|
|
# ── Regulated profession ─────────────────────────────────────
|
|
# Only check impressum text (not full text) — keywords like "rechtsanwalt"
|
|
# appear as contact persons in DSI texts (e.g. Spiegel's "Rechtsanwalt Kruse")
|
|
# but that doesn't mean the company IS a law firm.
|
|
impressum_text = documents.get("impressum", "").lower().replace("\xad", "")
|
|
if not impressum_text:
|
|
impressum_text = full_text[:2000] # Fallback: first 2000 chars
|
|
for keyword, prof_type in _REGULATED_PROFESSIONS.items():
|
|
if keyword in impressum_text:
|
|
# Extra guard: "rechtsanwalt" must appear near the company description,
|
|
# not just as a contact person name
|
|
if keyword in ("rechtsanwalt", "rechtsanwaeltin", "rechtsanwältin"):
|
|
# Check if it's in the first 500 chars (company description area)
|
|
if keyword not in impressum_text[:500]:
|
|
continue
|
|
profile.is_regulated_profession = True
|
|
profile.regulated_profession_type = prof_type
|
|
break
|
|
|
|
# ── Business type ────────────────────────────────────────────
|
|
b2c_score = _count_hits(full_text, _B2C_KEYWORDS)
|
|
b2b_score = _count_hits(full_text, _B2B_KEYWORDS)
|
|
b2g_score = _count_hits(full_text, _B2G_KEYWORDS)
|
|
nonprofit_score = _count_hits(full_text, _NONPROFIT_KEYWORDS)
|
|
|
|
# P17-C: B2B-Dienstleister-Cluster (P14) als Boost — wenn ein Unternehmen
|
|
# CE-Zertifizierung / Compliance-Beratung / Auditierung / Schulungen anbietet,
|
|
# ist es i.d.R. B2B auch wenn die strikten B2B-Keywords nicht greifen.
|
|
b2b_service_boost = _count_hits(full_text, _B2B_SERVICE_POSITIVE)
|
|
if b2b_service_boost >= 2:
|
|
b2b_score += min(3, b2b_service_boost - 1)
|
|
|
|
# Missing documents as signal
|
|
has_agb = "agb" in documents
|
|
has_widerruf = "widerruf" in documents
|
|
if not has_agb:
|
|
b2c_score -= 1 # No AGB → less likely B2C
|
|
if not has_widerruf:
|
|
b2c_score -= 1 # No Widerruf → less likely B2C shop
|
|
if profile.has_online_shop:
|
|
b2c_score += 3 # Strong B2C signal
|
|
|
|
scores = {
|
|
"b2c": b2c_score,
|
|
"b2b": b2b_score,
|
|
"b2g": b2g_score,
|
|
"nonprofit": nonprofit_score,
|
|
}
|
|
best = max(scores, key=scores.get) # type: ignore[arg-type]
|
|
best_val = scores[best]
|
|
|
|
if best_val >= 2:
|
|
profile.business_type = best
|
|
total = sum(max(0, v) for v in scores.values())
|
|
profile.confidence = round(best_val / total, 2) if total > 0 else 0.5
|
|
else:
|
|
# Fallback: prefer B2C when the text mentions Verbraucherrechte,
|
|
# editorial content, or consumer-direction signals — even without
|
|
# checkout keywords. Only fall back to B2B if discriminative B2B
|
|
# markers fired (which the keyword list above already filtered to
|
|
# genuinely B2B-only terms).
|
|
consumer_hint = (
|
|
"verbraucher" in full_text
|
|
or "widerruf" in full_text
|
|
or "kunde" in full_text
|
|
or profile.has_editorial_content
|
|
)
|
|
if b2b_score >= 1 and not consumer_hint:
|
|
profile.business_type = "b2b"
|
|
profile.confidence = 0.4
|
|
elif consumer_hint:
|
|
profile.business_type = "b2c"
|
|
profile.confidence = 0.4
|
|
else:
|
|
profile.business_type = "unknown"
|
|
profile.confidence = 0.2
|
|
|
|
# ── ODR (Online-Streitbeilegung) ─────────────────────────────
|
|
# Required for B2C with online shop (EU Regulation 524/2013)
|
|
profile.needs_odr = (
|
|
profile.business_type == "b2c" and profile.has_online_shop
|
|
)
|
|
|
|
# ── Industry ─────────────────────────────────────────────────
|
|
industry_scores: dict[str, int] = {}
|
|
for industry, keywords in _INDUSTRY_KEYWORDS.items():
|
|
hits = _count_hits(full_text, keywords)
|
|
if hits >= 1:
|
|
industry_scores[industry] = hits
|
|
|
|
# Suppress finance/insurance false positives caused by §34d/§34c GewO
|
|
# disclosures (Versicherungsvermittler, Berufshaftpflicht, etc.) — these
|
|
# are pflichtangaben for many companies (e.g. BMW AG) without being
|
|
# actual financial services providers.
|
|
if industry_scores.get("finance"):
|
|
vermittler_hits = _count_hits(full_text, _VERMITTLER_CONTEXT_TERMS)
|
|
if vermittler_hits >= 2:
|
|
# Only the §34d boilerplate triggered the match — drop or shrink.
|
|
non_insurance_finance = _count_hits(
|
|
full_text, ["bank", "finanz", "kredit", "anlage"],
|
|
)
|
|
if non_insurance_finance == 0:
|
|
industry_scores.pop("finance", None)
|
|
else:
|
|
industry_scores["finance"] = non_insurance_finance
|
|
|
|
# Require a clear winner — if top score is 1 and there are ties, prefer
|
|
# "unknown" over guessing.
|
|
if industry_scores:
|
|
top = max(industry_scores.values())
|
|
winners = [k for k, v in industry_scores.items() if v == top]
|
|
if top >= 2 or len(winners) == 1:
|
|
profile.industry = winners[0]
|
|
else:
|
|
profile.industry = "unknown"
|
|
elif profile.is_regulated_profession:
|
|
prof_map = {"anwalt": "legal", "arzt": "healthcare",
|
|
"steuerberater": "finance", "architekt": "craft"}
|
|
profile.industry = prof_map.get(profile.regulated_profession_type, "unknown")
|
|
|
|
# ── no_direct_sales (OEM-Konfigurator-Pattern) ───────────────
|
|
# Hersteller-Sites die nur konfigurieren + zu Vertragshaendlern
|
|
# weiterleiten (BMW/Audi/Mercedes/VW/Porsche) schliessen KEINEN
|
|
# Direkt-Kaufvertrag. AGB/Widerruf/Nutzungsbedingungen sind dort
|
|
# nicht Pflicht — werden beim Haendler ausgehaendigt.
|
|
profile.no_direct_sales = _detect_no_direct_sales(full_text)
|
|
|
|
return profile
|
|
|
|
|
|
# P14: drei Cluster die jeweils unabhaengig no_direct_sales=True triggern.
|
|
|
|
# Cluster A: OEM-Konfigurator-Pattern (Auto-Hersteller mit Vertragshaendler-Netz)
|
|
_OEM_POSITIVE = [
|
|
"vertragshaendler", "vertragshändler", "vertragspartner",
|
|
"vertragswerkstatt", "haendlersuche", "händlersuche",
|
|
"niederlassung", "vertretung", "autorisierter haendler",
|
|
"autorisierter händler", "ihr haendler vor ort",
|
|
"ihr händler vor ort", "haendler in ihrer naehe",
|
|
"händler in ihrer nähe", "probefahrt vereinbaren",
|
|
"anfrage an haendler", "anfrage an händler",
|
|
"konfigurator", "fahrzeug konfigurieren",
|
|
"ihre individuelle anfrage",
|
|
"bmw vertriebs", "audi vertriebs", "mercedes-benz vertriebs",
|
|
"volkswagen vertriebs", "porsche zentrum",
|
|
# OEM-Markennamen im Pflichttext (Datenschutz erwaehnt Hersteller)
|
|
"bmw ag", "audi ag", "mercedes-benz ag", "volkswagen ag",
|
|
"porsche ag", "opel automobile gmbh",
|
|
]
|
|
|
|
# Cluster B: B2B-Dienstleister (Beratung / Compliance / Schulung / CE)
|
|
_B2B_SERVICE_POSITIVE = [
|
|
"ce-zertifizierung", "ce zertifizierung",
|
|
"ce-konformitaet", "ce-konformität",
|
|
"ce-kennzeichnung", "ce kennzeichnung",
|
|
"compliance-beratung", "compliance beratung",
|
|
"arbeitssicherheit", "product compliance",
|
|
"produktsicherheit", "produkthaftung",
|
|
"auditierung", "auditor", "auditierungen",
|
|
"schulungen", "workshops", "akademie",
|
|
"beratungsleistungen", "consultingleistungen",
|
|
"consulting services", "managementsystem",
|
|
"datenschutzbeauftragter (extern)",
|
|
"externer datenschutzbeauftragter",
|
|
"datenschutz-audit", "tisax", "iso 27001",
|
|
"iso 9001", "iso 14001", "iso 45001",
|
|
"gefaehrdungsbeurteilung", "gefährdungsbeurteilung",
|
|
"betriebsbeauftragter", "fachkraft fuer arbeitssicherheit",
|
|
"fachkraft für arbeitssicherheit",
|
|
]
|
|
|
|
# Cluster C: NGO / Verein / oeffentliche Verwaltung
|
|
_NONPROFIT_PUBLIC_POSITIVE = [
|
|
"spendenkonto", "vereinsregister", "gemeinnuetzig",
|
|
"gemeinnützig", "ehrenamtlich", "foerderverein",
|
|
"förderverein", "stiftung", "buergeramt", "bürgeramt",
|
|
"landratsamt", "kommunalverwaltung",
|
|
]
|
|
|
|
# Backwards-compat
|
|
_NO_DIRECT_SALES_POSITIVE = (
|
|
_OEM_POSITIVE + _B2B_SERVICE_POSITIVE + _NONPROFIT_PUBLIC_POSITIVE
|
|
)
|
|
|
|
# Indikatoren GEGEN no_direct_sales: echte Online-Shop-Funktionen.
|
|
_DIRECT_SALES_NEGATIVE = [
|
|
"in den warenkorb", "warenkorb hinzu", "zur kasse",
|
|
"jetzt kaufen", "kostenpflichtig bestellen",
|
|
"zahlungspflichtig bestellen", "sofort-kauf",
|
|
"online bestellen", "lieferadresse", "rechnungsadresse",
|
|
"versandkosten", "lieferzeit", "lieferbedingungen",
|
|
"checkout", "stueckpreis", "stückpreis",
|
|
]
|
|
|
|
|
|
def _detect_no_direct_sales(full_text: str) -> bool:
|
|
"""Heuristik: True wenn Site keinen Direkt-Vertrieb mit B2C-Kunden hat.
|
|
|
|
Trifft fuer 3 Cluster zu (jeweils mind. 2 Treffer im Cluster):
|
|
A) OEM-Konfigurator (Auto-Hersteller)
|
|
B) B2B-Dienstleister (Beratung/Compliance/Schulung)
|
|
C) NGO / oeffentliche Verwaltung
|
|
|
|
Negativ-Signale (echte Shop-Funktionen) zaehlen gegen den Cluster:
|
|
nur True wenn pos > neg.
|
|
"""
|
|
text = full_text.lower()
|
|
oem = sum(1 for k in _OEM_POSITIVE if k in text)
|
|
b2b = sum(1 for k in _B2B_SERVICE_POSITIVE if k in text)
|
|
npg = sum(1 for k in _NONPROFIT_PUBLIC_POSITIVE if k in text)
|
|
neg = sum(1 for k in _DIRECT_SALES_NEGATIVE if k in text)
|
|
# Jeder Cluster ist eigenstaendig: 2 Treffer + weniger Negativ-Signale
|
|
# als Cluster-Treffer.
|
|
if oem >= 2 and oem > neg:
|
|
return True
|
|
if b2b >= 2 and b2b > neg:
|
|
return True
|
|
if npg >= 2 and npg > neg:
|
|
return True
|
|
return False
|