feat(1c): decouple company intelligence from 1c names

This commit is contained in:
igor04091968
2026-05-22 17:22:10 +03:00
parent ba12146e43
commit 8af3fd27f4
11 changed files with 456 additions and 159 deletions
+66 -41
View File
@@ -463,6 +463,35 @@ def grafana_company_dashboard_url() -> str:
)
def resolve_company_portfolio_card(company_ref: str, infobase: str | None = None) -> dict[str, Any]:
client = ch_client()
filters = [
"("
+ " OR ".join(
[
f"company_entity_key = {q(company_ref)}",
f"counterparty = {q(company_ref)}",
f"company_name = {q(company_ref)}",
f"source_counterparty = {q(company_ref)}",
]
)
+ ")"
]
if infobase:
filters.append(f"infobase = {q(infobase)}")
sql = f"""
SELECT *
FROM analytics_1c.v_company_portfolio_overview
WHERE {' AND '.join(filters)}
ORDER BY last_company_snapshot_at DESC, amount_30d DESC
LIMIT 1
"""
rows = rows_to_dict(client.query(sql))
if not rows:
raise HTTPException(status_code=404, detail="company not found in analytics_1c.v_company_portfolio_overview")
return rows[0]
def fmt_number(value: Any) -> str:
if value is None or value == "":
return "-"
@@ -484,8 +513,8 @@ def severity_badge(severity: str) -> str:
return f'<span class="badge badge-{tone}">{html.escape(severity or "none")}</span>'
def company_detail_url(counterparty: str, infobase: str | None = None) -> str:
base = f"/manager/company/{quote(counterparty)}"
def company_detail_url(company_ref: str, infobase: str | None = None) -> str:
base = f"/manager/company/{quote(company_ref)}"
if infobase:
return f"{base}?infobase={quote(infobase)}"
return base
@@ -538,7 +567,7 @@ def render_manager_brief_html(payload: dict[str, Any]) -> str:
f"<div class=\"stack-card-head\"><h3>{html.escape(item.get('company', '-'))}</h3>{severity_badge(item.get('severity', ''))}</div>"
f"<p class=\"stack-card-body\">{html.escape(item.get('reason', '-'))}</p>"
f"<p class=\"stack-card-action\"><strong>Действие:</strong> {html.escape(item.get('recommended_action', '-'))}</p>"
f"<p class=\"stack-card-action\"><a class=\"inline-link\" href=\"{company_detail_url(item.get('company', '-'))}\">Открыть карточку компании</a></p>"
f"<p class=\"stack-card-action\"><a class=\"inline-link\" href=\"{company_detail_url(str(item.get('company_entity_key') or item.get('company') or '-'), str(item.get('infobase') or '') or None)}\">Открыть карточку компании</a></p>"
"</article>"
)
@@ -550,7 +579,7 @@ def render_manager_brief_html(payload: dict[str, Any]) -> str:
f"<div class=\"stack-card-head\"><h3>{html.escape(company)}</h3></div>"
f"<p class=\"metric-line\"><strong>Прогноз 30д:</strong> {html.escape(item.get('forecast_30d', '-'))}</p>"
f"<p class=\"stack-card-body\">{html.escape(item.get('interpretation', '-'))}</p>"
f"<a class=\"inline-link\" href=\"{company_detail_url(company)}\">Карточка компании</a>"
f"<a class=\"inline-link\" href=\"{company_detail_url(str(item.get('company_entity_key') or company), str(item.get('infobase') or '') or None)}\">Карточка компании</a>"
"</article>"
)
@@ -955,7 +984,7 @@ def problematic_companies(days: int = 7, limit: int = 50) -> list[dict[str, Any]
WITH recent AS (
SELECT
infobase,
counterparty,
counterparty AS company_entity_key,
max(generated_at) AS latest_signal_at,
max(score) AS max_score,
sum(score) AS total_score,
@@ -971,7 +1000,9 @@ def problematic_companies(days: int = 7, limit: int = 50) -> list[dict[str, Any]
)
SELECT
p.infobase AS infobase,
p.company_entity_key AS company_entity_key,
p.counterparty AS counterparty,
p.source_counterparty,
p.company_name,
p.normalized_counterparty,
p.registry_match_mode,
@@ -997,7 +1028,7 @@ def problematic_companies(days: int = 7, limit: int = 50) -> list[dict[str, Any]
FROM recent AS r
INNER JOIN analytics_1c.v_company_portfolio_overview AS p
ON p.infobase = r.infobase
AND p.counterparty = r.counterparty
AND p.company_entity_key = r.company_entity_key
ORDER BY r.max_score DESC, r.signals_total DESC, p.amount_30d DESC, p.counterparty
LIMIT {int(limit)}
"""
@@ -1089,7 +1120,7 @@ def render_problematic_companies_html(items: list[dict[str, Any]], days: int) ->
for item in items:
rows.append(
"<tr>"
f"<td><a class=\"inline-link\" href=\"{company_detail_url(str(item.get('counterparty', '-')), str(item.get('infobase', '')) if item.get('infobase') else None)}\">{html.escape(str(item.get('counterparty', '-')))}</a></td>"
f"<td><a class=\"inline-link\" href=\"{company_detail_url(str(item.get('company_entity_key') or item.get('counterparty') or '-'), str(item.get('infobase', '')) if item.get('infobase') else None)}\">{html.escape(str(item.get('counterparty', '-')))}</a></td>"
f"<td>{html.escape(str(item.get('normalized_counterparty') or '-'))}</td>"
f"<td>{severity_badge(str(item.get('top_severity') or item.get('signal_severity') or 'none'))}</td>"
f"<td>{fmt_number(item.get('max_score'))}</td>"
@@ -1231,7 +1262,7 @@ def render_brief_delta_html(payload: dict[str, Any]) -> str:
priority_tier = str(item.get("priority_tier") or "low")
rows.append(
"<tr>"
f"<td><a class=\"inline-link\" href=\"{company_detail_url(str(counterparty), str(infobase) if infobase else None)}\">{html.escape(str(counterparty or '-'))}</a></td>"
f"<td><a class=\"inline-link\" href=\"{company_detail_url(str(item.get('company_entity_key') or counterparty), str(infobase) if infobase else None)}\">{html.escape(str(counterparty or '-'))}</a></td>"
f"<td>{html.escape(tier_labels.get(priority_tier, priority_tier))}</td>"
f"<td>{delta_value(item.get('priority_score', 0))}</td>"
f"<td>{html.escape(str(item.get('change_type') or '-'))}</td>"
@@ -1538,7 +1569,7 @@ def render_weekly_digest_html(payload: dict[str, Any]) -> str:
f"<div class=\"stack-card-head\"><h3>{html.escape(company)}</h3>{severity_badge(str(item.get('priority') or 'low'))}</div>"
f"<p class=\"stack-card-body\">{html.escape(str(item.get('reason') or '-'))}</p>"
f"<p class=\"stack-card-action\"><strong>Действие:</strong> {html.escape(str(item.get('recommended_action') or '-'))}</p>"
f"<p class=\"stack-card-action\"><a class=\"inline-link\" href=\"{company_detail_url(company)}\">Карточка компании</a></p>"
f"<p class=\"stack-card-action\"><a class=\"inline-link\" href=\"{company_detail_url(str(item.get('company_entity_key') or company), str(item.get('infobase') or '') or None)}\">Карточка компании</a></p>"
"</article>"
)
@@ -1792,11 +1823,16 @@ def companies_overview(
sql = f"""
SELECT
infobase,
company_entity_key,
organization,
counterparty,
source_counterparty,
company_name,
normalized_counterparty,
registry_match_mode,
registry_company_key,
registry_binding_source,
registry_binding_note,
registry_assignee_name,
registry_status,
registry_share_text,
@@ -1833,39 +1869,27 @@ def companies_overview(
@app.get("/api/1/analytics-1c/companies/{counterparty}/summary")
def company_summary(counterparty: str, infobase: str | None = None) -> dict[str, Any]:
client = ch_client()
filters = [f"counterparty = {q(counterparty)}"]
if infobase:
filters.append(f"infobase = {q(infobase)}")
sql = f"""
SELECT *
FROM analytics_1c.v_company_portfolio_overview
WHERE {' AND '.join(filters)}
ORDER BY last_company_snapshot_at DESC, amount_30d DESC
LIMIT 1
"""
rows = rows_to_dict(client.query(sql))
if not rows:
raise HTTPException(status_code=404, detail="counterparty not found in analytics_1c.v_company_portfolio_overview")
card = rows[0]
card = resolve_company_portfolio_card(counterparty, infobase)
entity_key = str(card.get("company_entity_key") or "")
forecast_sql = f"""
SELECT metric, horizon_days, baseline_daily, trend_slope, predicted_daily, predicted_total, confidence, note
FROM analytics_1c.v_company_forecasts_current
WHERE counterparty = {q(counterparty)}
{"AND infobase = " + q(infobase) if infobase else ""}
WHERE counterparty = {q(entity_key)}
{"AND infobase = " + q(str(card.get('infobase') or infobase)) if (card.get('infobase') or infobase) else ""}
ORDER BY metric, horizon_days
"""
signals_sql = f"""
SELECT generated_at, severity, score, signal_type, summary
FROM analytics_1c.v_company_health_current
WHERE counterparty = {q(counterparty)}
{"AND infobase = " + q(infobase) if infobase else ""}
WHERE counterparty = {q(entity_key)}
{"AND infobase = " + q(str(card.get('infobase') or infobase)) if (card.get('infobase') or infobase) else ""}
ORDER BY score DESC, generated_at DESC
"""
timeline_sql = f"""
SELECT last_company_snapshot_at AS ts, infobase, company_name, owner_user, current_status, db_size_bytes, reglog_size_bytes, active_locks, current_activity_score
FROM analytics_1c.v_company_portfolio_overview
WHERE counterparty = {q(counterparty)}
{"AND infobase = " + q(infobase) if infobase else ""}
WHERE company_entity_key = {q(entity_key)}
{"AND infobase = " + q(str(card.get('infobase') or infobase)) if (card.get('infobase') or infobase) else ""}
ORDER BY ts DESC
LIMIT 1
"""
@@ -1875,14 +1899,14 @@ def company_summary(counterparty: str, infobase: str | None = None) -> dict[str,
timeline_sql = f"""
SELECT ts, infobase, doc_type, operation_type, amount, status, author
FROM analytics_1c.documents
WHERE counterparty = {q(counterparty)}
{"AND infobase = " + q(infobase) if infobase else ""}
WHERE infobase = {q(str(card.get('infobase') or infobase or ''))}
AND counterparty != ''
ORDER BY ts DESC
LIMIT 20
"""
timeline = rows_to_dict(client.query(timeline_sql))
essence = (
f"Компания {counterparty}: за 30 дней событий {card['docs_30d']}, суммарная активность {card['amount_30d']}, "
f"Компания {card['counterparty']}: за 30 дней событий {card['docs_30d']}, суммарная активность {card['amount_30d']}, "
f"прогноз активности на 30 дней {card['amount_forecast_30d']}, риск {card['signal_severity']}."
)
payload = {
@@ -1905,9 +1929,10 @@ def company_forecast(
horizon_days: int | None = Query(default=None, ge=1, le=365),
) -> dict[str, Any]:
client = ch_client()
filters = [f"counterparty = {q(counterparty)}"]
if infobase:
filters.append(f"infobase = {q(infobase)}")
card = resolve_company_portfolio_card(counterparty, infobase)
filters = [f"counterparty = {q(str(card.get('company_entity_key') or counterparty))}"]
if card.get("infobase") or infobase:
filters.append(f"infobase = {q(str(card.get('infobase') or infobase))}")
if horizon_days is not None:
filters.append(f"horizon_days = {int(horizon_days)}")
sql = f"""
@@ -1927,9 +1952,8 @@ def company_timeline(
limit: int = Query(default=100, ge=1, le=500),
) -> dict[str, Any]:
client = ch_client()
filters = [f"counterparty = {q(counterparty)}"]
if infobase:
filters.append(f"infobase = {q(infobase)}")
card = resolve_company_portfolio_card(counterparty, infobase)
filters = [f"infobase = {q(str(card.get('infobase') or infobase or ''))}", "counterparty != ''"]
sql = f"""
SELECT ts, infobase, organization, doc_type, doc_number, author, operation_type, amount, status, posted
FROM analytics_1c.documents
@@ -2076,13 +2100,14 @@ def render_company_detail_html(summary_payload: dict[str, Any], infobase: str |
title = card.get("counterparty", "Карточка компании")
subtitle = summary_payload.get("essence", "")
grafana_url = grafana_company_dashboard_url()
summary_url = f"/api/1/analytics-1c/companies/{quote(card['counterparty'])}/summary"
company_ref = str(card.get("company_entity_key") or card.get("counterparty") or "")
summary_url = f"/api/1/analytics-1c/companies/{quote(company_ref)}/summary"
if infobase:
summary_url += f"?infobase={quote(infobase)}"
timeline_url = f"/api/1/analytics-1c/companies/{quote(card['counterparty'])}/timeline"
timeline_url = f"/api/1/analytics-1c/companies/{quote(company_ref)}/timeline"
if infobase:
timeline_url += f"?infobase={quote(infobase)}"
forecast_url = f"/api/1/analytics-1c/companies/{quote(card['counterparty'])}/forecast"
forecast_url = f"/api/1/analytics-1c/companies/{quote(company_ref)}/forecast"
if infobase:
forecast_url += f"?infobase={quote(infobase)}"
+14 -5
View File
@@ -185,14 +185,14 @@ def snapshot_from_context(context: dict[str, Any]) -> dict[tuple[Any, Any], dict
snapshot_items = context.get("portfolio_snapshot") or []
if snapshot_items:
return {
(item.get("infobase"), item.get("counterparty")): item
(item.get("infobase"), item.get("company_entity_key") or item.get("counterparty")): item
for item in snapshot_items
}
merged: dict[tuple[Any, Any], dict[str, Any]] = {}
for source_name in ("top_risks", "top_forecasts", "watchlist", "busy_bases"):
for item in context.get(source_name, []):
key = (item.get("infobase"), item.get("counterparty"))
key = (item.get("infobase"), item.get("company_entity_key") or item.get("counterparty"))
if key not in merged:
merged[key] = dict(item)
else:
@@ -268,6 +268,7 @@ def build_context(client, top_limit: int, freshness_hours: int) -> dict[str, Any
f"""
SELECT
infobase,
company_entity_key,
counterparty,
normalized_counterparty,
registry_match_mode,
@@ -294,6 +295,7 @@ def build_context(client, top_limit: int, freshness_hours: int) -> dict[str, Any
f"""
SELECT
infobase,
company_entity_key,
counterparty,
normalized_counterparty,
registry_match_mode,
@@ -316,6 +318,7 @@ def build_context(client, top_limit: int, freshness_hours: int) -> dict[str, Any
f"""
SELECT
s.infobase,
p.company_entity_key,
s.counterparty,
p.normalized_counterparty,
p.registry_match_mode,
@@ -328,7 +331,7 @@ def build_context(client, top_limit: int, freshness_hours: int) -> dict[str, Any
round(p.amount_forecast_30d, 2) AS amount_forecast_30d
FROM analytics_1c.v_company_health_current AS s
LEFT JOIN analytics_1c.v_company_portfolio_overview AS p
ON p.infobase = s.infobase AND p.counterparty = s.counterparty
ON p.infobase = s.infobase AND p.company_entity_key = s.counterparty
WHERE s.signal_type IN ('inactive_company', 'amount_drop', 'docs_stopped')
ORDER BY s.score DESC, p.days_since_last_activity DESC, p.amount_30d DESC
LIMIT {int(top_limit)}
@@ -341,6 +344,7 @@ def build_context(client, top_limit: int, freshness_hours: int) -> dict[str, Any
f"""
SELECT
infobase,
company_entity_key,
counterparty,
normalized_counterparty,
current_status,
@@ -364,11 +368,14 @@ def build_context(client, top_limit: int, freshness_hours: int) -> dict[str, Any
SELECT
c.opened_at,
c.infobase,
p.company_entity_key,
c.entity_id AS counterparty,
c.title,
c.severity,
c.status
FROM analytics_1c.cases AS c
LEFT JOIN analytics_1c.v_company_portfolio_overview AS p
ON p.infobase = c.infobase AND p.company_entity_key = c.entity_id
WHERE c.entity_type = 'counterparty' AND c.status != 'closed'
ORDER BY c.opened_at DESC
LIMIT {int(top_limit)}
@@ -381,6 +388,7 @@ def build_context(client, top_limit: int, freshness_hours: int) -> dict[str, Any
"""
SELECT
infobase,
company_entity_key,
counterparty,
normalized_counterparty,
registry_match_mode,
@@ -424,8 +432,8 @@ def compute_delta_context(current: dict[str, Any], previous_artifact: dict[str,
previous = previous_artifact.get("context", {})
current_summary = current.get("portfolio_summary", {})
previous_summary = previous.get("portfolio_summary", {})
current_watchlist = {(item.get("infobase"), item.get("counterparty")) for item in current.get("watchlist", [])}
previous_watchlist = {(item.get("infobase"), item.get("counterparty")) for item in previous.get("watchlist", [])}
current_watchlist = {(item.get("infobase"), item.get("company_entity_key") or item.get("counterparty")) for item in current.get("watchlist", [])}
previous_watchlist = {(item.get("infobase"), item.get("company_entity_key") or item.get("counterparty")) for item in previous.get("watchlist", [])}
current_snapshot = snapshot_from_context(current)
previous_snapshot = snapshot_from_context(previous)
@@ -530,6 +538,7 @@ def compute_delta_context(current: dict[str, Any], previous_artifact: dict[str,
rank_top_change(
{
"infobase": current_item.get("infobase"),
"company_entity_key": current_item.get("company_entity_key"),
"company": current_item.get("counterparty"),
"normalized_counterparty": current_item.get("normalized_counterparty"),
"registry_match_mode": current_item.get("registry_match_mode"),
+4 -1
View File
@@ -146,7 +146,9 @@ def build_context(client, args: argparse.Namespace) -> dict[str, Any]:
)
SELECT
p.infobase AS infobase,
p.company_entity_key AS company_entity_key,
p.counterparty AS counterparty,
p.source_counterparty,
p.company_name,
p.normalized_counterparty,
p.registry_match_mode,
@@ -220,7 +222,7 @@ def render_deterministic_recovery(context: dict[str, Any]) -> dict[str, Any]:
]
top_incidents = []
for item in problematic[:6]:
company = str(item.get("counterparty") or "-")
company = str(item.get("counterparty") or item.get("company_name") or "-")
actions = [
"Проверить владельца и состав открытых кейсов по компании.",
"Подтвердить, что по компании есть план снижения хвоста в ближайшие 24 часа.",
@@ -231,6 +233,7 @@ def render_deterministic_recovery(context: dict[str, Any]) -> dict[str, Any]:
actions.append("Сначала подтвердить корректность manual-сопоставления.")
top_incidents.append(
{
"company_entity_key": str(item.get("company_entity_key") or ""),
"company": company,
"severity": str(item.get("signal_severity") or item.get("top_severity") or "critical"),
"diagnosis": (
@@ -130,18 +130,18 @@ def main() -> int:
daily_rows = query_rows(
client,
"""
SELECT infobase, organization, counterparty, d, docs_total, amount_total
FROM analytics_1c.v_counterparty_daily
ORDER BY infobase, counterparty, d
SELECT infobase, organization, company_entity_key, source_counterparty, d, docs_total, amount_total
FROM analytics_1c.v_company_activity_daily
ORDER BY infobase, company_entity_key, d
""",
)
if not daily_rows:
print("no counterparty rows in analytics_1c.v_counterparty_daily; nothing to refresh")
print("no company activity rows in analytics_1c.v_company_activity_daily; nothing to refresh")
return 0
grouped: dict[tuple[str, str, str], list[DailyPoint]] = defaultdict(list)
grouped: dict[tuple[str, str, str, str], list[DailyPoint]] = defaultdict(list)
for row in daily_rows:
key = (row["infobase"], row["organization"], row["counterparty"])
key = (row["infobase"], row["organization"], row["company_entity_key"], row.get("source_counterparty") or row["company_entity_key"])
grouped[key].append(
DailyPoint(
d=row["d"],
@@ -151,26 +151,26 @@ def main() -> int:
)
cases_map = {
(row["infobase"], row["counterparty"]): int(row["open_cases_total"] or 0)
(row["infobase"], row["company_entity_key"]): int(row["open_cases_total"] or 0)
for row in query_rows(
client,
"""
SELECT infobase, entity_id AS counterparty, countIf(status != 'closed') AS open_cases_total
SELECT infobase, entity_id AS company_entity_key, countIf(status != 'closed') AS open_cases_total
FROM analytics_1c.cases
WHERE entity_type = 'counterparty'
GROUP BY infobase, counterparty
GROUP BY infobase, company_entity_key
""",
)
}
detections_map = {
(row["infobase"], row["counterparty"]): int(row["detections_total"] or 0)
(row["infobase"], row["company_entity_key"]): int(row["detections_total"] or 0)
for row in query_rows(
client,
"""
SELECT infobase, entity_id AS counterparty, count() AS detections_total
SELECT infobase, entity_id AS company_entity_key, count() AS detections_total
FROM analytics_1c.detections
WHERE entity_type = 'counterparty' AND status != 'closed'
GROUP BY infobase, counterparty
GROUP BY infobase, company_entity_key
""",
)
}
@@ -205,8 +205,8 @@ def main() -> int:
forecast_rows: list[list[Any]] = []
signal_rows: list[list[Any]] = []
for (infobase, _organization, counterparty), points in grouped.items():
if normalize_company_key(counterparty) in excluded_company_keys:
for (infobase, _organization, company_entity_key, source_counterparty), points in grouped.items():
if normalize_company_key(source_counterparty) in excluded_company_keys:
continue
points.sort(key=lambda p: p.d)
filled = fill_daily_series(points)
@@ -223,8 +223,8 @@ def main() -> int:
amount_7d = float(sum(p.amount_total for p in last_7))
amount_prev_7d = float(sum(p.amount_total for p in prev_7))
days_since_last_activity = (date.today() - latest_day).days
open_cases_total = cases_map.get((infobase, counterparty), 0)
detections_total = detections_map.get((infobase, counterparty), 0)
open_cases_total = cases_map.get((infobase, company_entity_key), 0)
detections_total = detections_map.get((infobase, company_entity_key), 0)
company_state = company_state_map.get(infobase, {})
current_status = str(company_state.get("current_status") or "")
active_locks = int(company_state.get("active_locks") or 0)
@@ -245,7 +245,7 @@ def main() -> int:
generated_at,
latest_day,
infobase,
counterparty,
company_entity_key,
int(horizon),
metric,
float(baseline),
@@ -261,29 +261,29 @@ def main() -> int:
signals: list[tuple[str, int, str, str]] = []
if days_since_last_activity >= 14 and (docs_prev_7d > 0 or amount_prev_7d > 0):
signals.append(("inactive_company", 85, "high", f"Нет активности по компании {counterparty} уже {days_since_last_activity} дн."))
signals.append(("inactive_company", 85, "high", f"Нет активности по компании {source_counterparty} уже {days_since_last_activity} дн."))
if amount_prev_7d > 0 and amount_7d < amount_prev_7d * 0.5:
signals.append(("amount_drop", 70, "high", f"Активность по компании {counterparty} упала более чем на 50% неделя к неделе."))
signals.append(("amount_drop", 70, "high", f"Активность по компании {source_counterparty} упала более чем на 50% неделя к неделе."))
if docs_prev_7d > 0 and docs_7d == 0:
signals.append(("docs_stopped", 55, "medium", f"По компании {counterparty} прекратился поток документов за последние 7 дней."))
signals.append(("docs_stopped", 55, "medium", f"По компании {source_counterparty} прекратился поток документов за последние 7 дней."))
if current_status == "busy" or active_locks > 0 or temp_db_present > 0:
score = min(85, 45 + active_locks * 5 + temp_db_present * 10)
signals.append(("base_busy", score, severity_score_to_label(score), f"Файловая база компании {counterparty} занята: status={current_status}, locks={active_locks}, tempDb={temp_db_present}."))
signals.append(("base_busy", score, severity_score_to_label(score), f"Файловая база компании {source_counterparty} занята: status={current_status}, locks={active_locks}, tempDb={temp_db_present}."))
if scheduler_touched > 0 and current_activity_score >= 15:
signals.append(("scheduler_activity", 35, "medium", f"По компании {counterparty} есть активность scheduler и повышенный activity score {current_activity_score}."))
signals.append(("scheduler_activity", 35, "medium", f"По компании {source_counterparty} есть активность scheduler и повышенный activity score {current_activity_score}."))
if open_cases_total > 0:
signals.append(("open_cases", min(95, 40 + open_cases_total * 10), severity_score_to_label(min(95, 40 + open_cases_total * 10)), f"По компании {counterparty} есть открытые кейсы: {open_cases_total}."))
signals.append(("open_cases", min(95, 40 + open_cases_total * 10), severity_score_to_label(min(95, 40 + open_cases_total * 10)), f"По компании {source_counterparty} есть открытые кейсы: {open_cases_total}."))
if detections_total > 0:
signals.append(("open_detections", min(90, 35 + detections_total * 5), severity_score_to_label(min(90, 35 + detections_total * 5)), f"По компании {counterparty} есть активные detections: {detections_total}."))
signals.append(("open_detections", min(90, 35 + detections_total * 5), severity_score_to_label(min(90, 35 + detections_total * 5)), f"По компании {source_counterparty} есть активные detections: {detections_total}."))
for signal_type, score, severity, summary in signals:
signal_rows.append(
[
generated_at,
infobase,
counterparty,
f"{signal_type}:{infobase}:{counterparty}",
severity,
[
generated_at,
infobase,
company_entity_key,
f"{signal_type}:{infobase}:{company_entity_key}",
severity,
int(score),
signal_type,
summary,
@@ -0,0 +1,121 @@
#!/usr/bin/env python3
from __future__ import annotations
import argparse
import os
from datetime import UTC, datetime
from typing import Any
import clickhouse_connect
def parse_args() -> argparse.Namespace:
p = argparse.ArgumentParser(description="Refresh technical company->registry bindings for analytics_1c")
p.add_argument("--host", default=os.getenv("CLICKHOUSE_HOST", "localhost"))
p.add_argument("--port", type=int, default=int(os.getenv("CLICKHOUSE_PORT", "8123")))
p.add_argument("--user", default=os.getenv("CLICKHOUSE_USER", "default"))
p.add_argument("--password", default=os.getenv("CLICKHOUSE_PASSWORD", ""))
p.add_argument("--database", default=os.getenv("CLICKHOUSE_DB", "analytics_1c"))
return p.parse_args()
def ch_client(args: argparse.Namespace):
return clickhouse_connect.get_client(
host=args.host,
port=args.port,
username=args.user,
password=args.password,
database=args.database,
)
def query_rows(client, sql: str) -> list[dict[str, Any]]:
result = client.query(sql)
return [dict(zip(result.column_names, row)) for row in result.result_rows]
def main() -> int:
args = parse_args()
client = ch_client(args)
generated_at = datetime.now(UTC).replace(tzinfo=None, microsecond=0)
current_bindings = {
str(row["company_entity_key"]): row
for row in query_rows(
client,
"""
SELECT
company_entity_key,
registry_company_key,
registry_company_name,
binding_source
FROM analytics_1c.v_company_registry_bindings_current
""",
)
}
candidates = query_rows(
client,
"""
SELECT
company_entity_key,
infobase,
base_id,
base_path,
ifNull(base_path_key, '') AS base_path_key,
registry_company_key,
company_name,
registry_match_mode
FROM analytics_1c.v_company_portfolio_overview
WHERE registry_match_mode IN ('direct', 'alias', 'manual')
AND registry_company_key != ''
AND company_entity_key != ''
"""
)
inserts: list[list[Any]] = []
for row in candidates:
entity_key = str(row["company_entity_key"])
registry_key = str(row["registry_company_key"])
current = current_bindings.get(entity_key)
if current and str(current.get("registry_company_key") or "") == registry_key:
continue
inserts.append(
[
generated_at,
str(row["infobase"] or ""),
entity_key,
str(row["base_id"] or ""),
str(row["base_path"] or ""),
str(row["base_path_key"] or ""),
registry_key,
str(row["company_name"] or ""),
f"bootstrap_{row['registry_match_mode']}",
"autobound_from_portfolio",
]
)
if inserts:
client.insert(
"analytics_1c.company_registry_bindings",
inserts,
column_names=[
"ts",
"infobase",
"company_entity_key",
"base_id",
"base_path",
"base_path_key",
"registry_company_key",
"registry_company_name",
"binding_source",
"note",
],
)
print(f"company registry bindings refreshed: inserted={len(inserts)} generated_at={generated_at.isoformat()}")
return 0
if __name__ == "__main__":
raise SystemExit(main())