feat: explain RTC blockers when no R3 candidates are eligible
This commit is contained in:
@@ -340,6 +340,19 @@ def blocker_filter_summary(session, *, tenant_id=None, agent_id=None, product_co
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def rtc_blocker_summary(session, agent_id: int) -> dict:
|
||||||
|
"""Read-only, installation-scoped RTC blocker aggregates for the cClassTrib page."""
|
||||||
|
query = _blockers_query(session, agent_id=agent_id, field="RTC", status="OPEN", scope="all")
|
||||||
|
blockers = int(session.scalar(query.with_only_columns(func.count(BlockedProduct.id)).order_by(None)) or 0)
|
||||||
|
products = int(session.scalar(query.with_only_columns(func.count(func.distinct(BlockedProduct.product_code))).order_by(None)) or 0)
|
||||||
|
reason = func.coalesce(BlockedProduct.evidence_summary["reason"].as_string(), BlockedProduct.reason_code)
|
||||||
|
reasons = session.execute(
|
||||||
|
query.with_only_columns(reason, func.count(func.distinct(BlockedProduct.product_code)))
|
||||||
|
.group_by(reason).order_by(None)
|
||||||
|
).all()
|
||||||
|
return {"open_blockers": blockers, "products_affected": products, "reason_counts": {str(code or ""): int(count) for code, count in reasons}}
|
||||||
|
|
||||||
|
|
||||||
def _blocker_breakdown_query(query):
|
def _blocker_breakdown_query(query):
|
||||||
"""Group on the source column itself for PostgreSQL-safe SQL semantics."""
|
"""Group on the source column itself for PostgreSQL-safe SQL semantics."""
|
||||||
return query.with_only_columns(BlockedProduct.field, func.count(BlockedProduct.id)).group_by(BlockedProduct.field).order_by(None)
|
return query.with_only_columns(BlockedProduct.field, func.count(BlockedProduct.id)).group_by(BlockedProduct.field).order_by(None)
|
||||||
|
|||||||
@@ -190,9 +190,13 @@ def _rtc_products_view(resolution, enabled):
|
|||||||
ready = int(resolution.get("ready_to_apply", 0))
|
ready = int(resolution.get("ready_to_apply", 0))
|
||||||
conflicts = int(resolution.get("conflicts", 0)) + int(resolution.get("official_mismatch", 0))
|
conflicts = int(resolution.get("conflicts", 0)) + int(resolution.get("official_mismatch", 0))
|
||||||
catalog = int(resolution.get("catalog_incompatible", 0))
|
catalog = int(resolution.get("catalog_incompatible", 0))
|
||||||
|
reasons = resolution.get("by_reason") if isinstance(resolution.get("by_reason"), dict) else {}
|
||||||
|
simple_count = sum(int(count or 0) for reason, count in reasons.items() if str(reason).endswith("SIMPLES_ORIENTATION_PENDING_OFFICIAL_NT"))
|
||||||
|
other_reason_max = max((int(count or 0) for reason, count in reasons.items() if not str(reason).endswith("SIMPLES_ORIENTATION_PENDING_OFFICIAL_NT")), default=0)
|
||||||
blocked = total - valid - ready
|
blocked = total - valid - ready
|
||||||
pending = total - valid
|
pending = total - valid
|
||||||
return {"total": total, "valid": valid, "ready": ready, "blocked": blocked, "pending": pending, "conflicts": conflicts, "catalog_incompatible": catalog, "suggestion_only": int(resolution.get("suggestion_only", 0)),
|
return {"total": total, "valid": valid, "ready": ready, "blocked": blocked, "pending": pending, "conflicts": conflicts, "catalog_incompatible": catalog, "suggestion_only": int(resolution.get("suggestion_only", 0)),
|
||||||
|
"simple_orientation_count": simple_count, "simple_orientation_dominant": simple_count > 0 and simple_count > other_reason_max,
|
||||||
"headline": f"Reforma Tributária ativa — cClassTrib pendente em {pending} produto{'s' if pending != 1 else ''}" if pending else "Reforma Tributária ativa — todos os produtos com cClassTrib válido",
|
"headline": f"Reforma Tributária ativa — cClassTrib pendente em {pending} produto{'s' if pending != 1 else ''}" if pending else "Reforma Tributária ativa — todos os produtos com cClassTrib válido",
|
||||||
"catalog": (resolution.get("catalog_compatibility") or {}).get("compatibility")}
|
"catalog": (resolution.get("catalog_compatibility") or {}).get("compatibility")}
|
||||||
|
|
||||||
|
|||||||
+15
-4
@@ -16,7 +16,7 @@ from .blocker_resolution import BlockerResolutionUnavailable, _company_key, reso
|
|||||||
from .context_keys import ALLOWED_KEYS, ENUMS
|
from .context_keys import ALLOWED_KEYS, ENUMS
|
||||||
from .installation_identity import reference_agent_version
|
from .installation_identity import reference_agent_version
|
||||||
from .panel_presentation import RTC_MODE_EXPLANATION, RTC_MODE_LABELS, abort_reason_label, audit_outcome_label, candidate_status_label, catalog_state_label, job_kind_label, job_status_label, rtc_rule_label, rtc_status_view
|
from .panel_presentation import RTC_MODE_EXPLANATION, RTC_MODE_LABELS, abort_reason_label, audit_outcome_label, candidate_status_label, catalog_state_label, job_kind_label, job_status_label, rtc_rule_label, rtc_status_view
|
||||||
from .control_plane import blocker_detail, blocker_filter_summary, dashboard_summary, installation_view, jobs_status_summary, list_blockers, list_installations, list_jobs, list_panel_events
|
from .control_plane import blocker_detail, blocker_filter_summary, dashboard_summary, installation_view, jobs_status_summary, list_blockers, list_installations, list_jobs, list_panel_events, rtc_blocker_summary
|
||||||
from .human_auth import create_session, current_session, require_csrf, verify_password
|
from .human_auth import create_session, current_session, require_csrf, verify_password
|
||||||
from .panel_presentation import format_ui_datetime
|
from .panel_presentation import format_ui_datetime
|
||||||
from .models import AdminUser, Agent, AgentEnrollment, AgentRelease, AgentSnapshot, BlockedProduct, FiscalContextEntry, Installation, Job, RtcCandidate, RtcWriteAudit, Tenant
|
from .models import AdminUser, Agent, AgentEnrollment, AgentRelease, AgentSnapshot, BlockedProduct, FiscalContextEntry, Installation, Job, RtcCandidate, RtcWriteAudit, Tenant
|
||||||
@@ -346,15 +346,26 @@ def router(session_factory):
|
|||||||
status = q.get("status") or "PENDING"
|
status = q.get("status") or "PENDING"
|
||||||
page = max(1, int(q.get("page", "1") or 1)) if str(q.get("page", "1")).isdigit() else 1
|
page = max(1, int(q.get("page", "1") or 1)) if str(q.get("page", "1")).isdigit() else 1
|
||||||
size = 50
|
size = 50
|
||||||
base = select(RtcCandidate).where(RtcCandidate.installation_id == installation.id)
|
base = select(RtcCandidate).where(RtcCandidate.installation_id == installation.id, RtcCandidate.rule_id == "RTC-R3")
|
||||||
counts = dict(session.execute(select(RtcCandidate.status, func.count()).where(RtcCandidate.installation_id == installation.id).group_by(RtcCandidate.status)).all())
|
counts = dict(session.execute(select(RtcCandidate.status, func.count()).where(RtcCandidate.installation_id == installation.id, RtcCandidate.rule_id == "RTC-R3").group_by(RtcCandidate.status)).all())
|
||||||
shown = base.where(RtcCandidate.status == status) if status in ("PENDING", "APPROVED", "REJECTED", "APPLIED", "STALE") else base
|
shown = base.where(RtcCandidate.status == status) if status in ("PENDING", "APPROVED", "REJECTED", "APPLIED", "STALE") else base
|
||||||
total = int(session.scalar(select(func.count()).select_from(shown.subquery())) or 0)
|
total = int(session.scalar(select(func.count()).select_from(shown.subquery())) or 0)
|
||||||
rows = list(session.scalars(shown.order_by(RtcCandidate.product_code).offset((page - 1) * size).limit(size)))
|
rows = list(session.scalars(shown.order_by(RtcCandidate.product_code).offset((page - 1) * size).limit(size)))
|
||||||
audits = list(session.scalars(select(RtcWriteAudit).where(RtcWriteAudit.installation_id == installation.id, RtcWriteAudit.outcome.in_(("APPLIED", "ROLLED_BACK"))).order_by(RtcWriteAudit.id.desc()).limit(25)))
|
audits = list(session.scalars(select(RtcWriteAudit).where(RtcWriteAudit.installation_id == installation.id, RtcWriteAudit.outcome.in_(("APPLIED", "ROLLED_BACK"))).order_by(RtcWriteAudit.id.desc()).limit(25)))
|
||||||
ops = _ops_view(session, installation, agent)
|
ops = _ops_view(session, installation, agent)
|
||||||
|
rtc_blockers = rtc_blocker_summary(session, agent_id)
|
||||||
|
rtc_products = ((ops.get("rtc") or {}).get("products") or {})
|
||||||
|
r3_candidates = int(rtc_products.get("ready", counts.get("PENDING", 0) + counts.get("APPROVED", 0)))
|
||||||
|
blocked_products = max(int(rtc_products.get("blocked", 0)), rtc_blockers["products_affected"])
|
||||||
|
reasons = rtc_blockers["reason_counts"]
|
||||||
|
simple_reason_count = sum(count for reason, count in reasons.items() if reason.endswith("SIMPLES_ORIENTATION_PENDING_OFFICIAL_NT"))
|
||||||
|
simple_reason_count = max(simple_reason_count, int(rtc_products.get("simple_orientation_count", 0)))
|
||||||
|
other_reason_max = max((count for reason, count in reasons.items() if not reason.endswith("SIMPLES_ORIENTATION_PENDING_OFFICIAL_NT")), default=0)
|
||||||
|
simple_orientation_dominant = bool(rtc_products.get("simple_orientation_dominant")) or simple_reason_count > other_reason_max
|
||||||
|
rtc_empty_state = {"show": r3_candidates == 0 and blocked_products > 0, "r3_candidates": r3_candidates,
|
||||||
|
"blocked_products": blocked_products, "simple_orientation_dominant": simple_orientation_dominant}
|
||||||
return render(request, "rtc_control.html", {"agent_id": agent_id, "installation": installation, "mode": installation.rtc_write_mode, "modes": RTC_MODE_LABELS, "explanation": RTC_MODE_EXPLANATION, "ops": ops,
|
return render(request, "rtc_control.html", {"agent_id": agent_id, "installation": installation, "mode": installation.rtc_write_mode, "modes": RTC_MODE_LABELS, "explanation": RTC_MODE_EXPLANATION, "ops": ops,
|
||||||
"privileged": _is_privileged(auth_data[1]), "rows": rows, "counts": counts, "status": status, "page": page, "pages": max(1, -(-total // size)), "total": total, "audits": audits,
|
"privileged": _is_privileged(auth_data[1]), "rows": rows, "counts": counts, "status": status, "page": page, "pages": max(1, -(-total // size)), "total": total, "audits": audits, "rtc_empty_state": rtc_empty_state,
|
||||||
"L": {"status": candidate_status_label, "outcome": audit_outcome_label, "reason": abort_reason_label, "rule": rtc_rule_label, "catalog": catalog_state_label}})
|
"L": {"status": candidate_status_label, "outcome": audit_outcome_label, "reason": abort_reason_label, "rule": rtc_rule_label, "catalog": catalog_state_label}})
|
||||||
finally: session.close()
|
finally: session.close()
|
||||||
|
|
||||||
|
|||||||
@@ -60,6 +60,11 @@ small { display: block; font-size: 0.78rem; }
|
|||||||
.list-row { display: flex; justify-content: space-between; align-items: center; gap: 12px; padding: 12px 8px; margin: 0 -8px; border-bottom: 1px solid var(--cor-borda); border-radius: 8px; text-decoration: none; color: inherit; transition: background var(--transicao); }
|
.list-row { display: flex; justify-content: space-between; align-items: center; gap: 12px; padding: 12px 8px; margin: 0 -8px; border-bottom: 1px solid var(--cor-borda); border-radius: 8px; text-decoration: none; color: inherit; transition: background var(--transicao); }
|
||||||
.list-row:last-child { border-bottom: 0; }
|
.list-row:last-child { border-bottom: 0; }
|
||||||
.list-row:hover { background: var(--cor-superficie-alt); }
|
.list-row:hover { background: var(--cor-superficie-alt); }
|
||||||
|
.rtc-empty-state { display: flex; align-items: center; justify-content: space-between; gap: 18px; border-left: 4px solid var(--cor-acento); background: var(--cor-superficie-alt); }
|
||||||
|
.rtc-empty-copy { min-width: 0; }
|
||||||
|
.rtc-empty-copy h2 { margin: 0 0 8px; }
|
||||||
|
.rtc-empty-copy p:last-child { margin-bottom: 0; }
|
||||||
|
.rtc-empty-state .botao { flex: 0 0 auto; margin: 0; }
|
||||||
.empty-state { color: var(--cor-texto-secundario); border: 1px dashed var(--cor-borda); border-radius: 8px; padding: 18px; margin: 0; }
|
.empty-state { color: var(--cor-texto-secundario); border: 1px dashed var(--cor-borda); border-radius: 8px; padding: 18px; margin: 0; }
|
||||||
|
|
||||||
/* ---------- badges: mesma forma do .badge do Gerenciador, cores semânticas dos tokens ---------- */
|
/* ---------- badges: mesma forma do .badge do Gerenciador, cores semânticas dos tokens ---------- */
|
||||||
@@ -139,6 +144,7 @@ td a { text-decoration: none; }
|
|||||||
}
|
}
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.cards, .cards.compact, .split { grid-template-columns: 1fr 1fr; }
|
.cards, .cards.compact, .split { grid-template-columns: 1fr 1fr; }
|
||||||
|
.rtc-empty-state { align-items: stretch; flex-direction: column; }
|
||||||
.coverage { grid-template-columns: repeat(2, 1fr); }
|
.coverage { grid-template-columns: repeat(2, 1fr); }
|
||||||
.form-grid { grid-template-columns: 1fr; }
|
.form-grid { grid-template-columns: 1fr; }
|
||||||
.form-grid .wide { grid-column: auto; }
|
.form-grid .wide { grid-column: auto; }
|
||||||
|
|||||||
@@ -15,6 +15,18 @@
|
|||||||
<article class="metric"><span>Catálogo incompatível</span><strong>{{ rtc.products.catalog_incompatible }}</strong></article>
|
<article class="metric"><span>Catálogo incompatível</span><strong>{{ rtc.products.catalog_incompatible }}</strong></article>
|
||||||
</section>
|
</section>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if rtc_empty_state.show %}
|
||||||
|
<section class="panel rtc-empty-state" role="status" aria-live="polite">
|
||||||
|
<div class="rtc-empty-copy">
|
||||||
|
<h2>Não há produtos elegíveis para preenchimento automático.</h2>
|
||||||
|
<p>{{ rtc_empty_state.blocked_products }} produto{{ 's' if rtc_empty_state.blocked_products != 1 else '' }} {% if rtc_empty_state.blocked_products != 1 %}precisam de atenção fiscal, mas não podem ser preenchidos{% else %}precisa de atenção fiscal, mas não pode ser preenchido{% endif %} automaticamente pelas regras atuais.</p>
|
||||||
|
{% if rtc_empty_state.simple_orientation_dominant %}
|
||||||
|
<p>Esta empresa está no Simples Nacional. Os produtos estão bloqueados porque o sistema ainda não possui fundamento oficial suficiente para determinar automaticamente o cClassTrib. Alterar o modo de preenchimento não remove esse bloqueio.</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<a class="botao" href="/blockers?scope=all&agent_id={{ agent_id }}&field=RTC&status=OPEN">Ver produtos bloqueados ({{ rtc_empty_state.blocked_products }})</a>
|
||||||
|
</section>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<section class="panel"><div class="panel-title"><div><h2>Modo de preenchimento</h2><p class="muted">Decidido centralmente para esta instalação. Padrão: Desativada. Nenhuma configuração local no servidor do cliente.</p></div></div>
|
<section class="panel"><div class="panel-title"><div><h2>Modo de preenchimento</h2><p class="muted">Decidido centralmente para esta instalação. Padrão: Desativada. Nenhuma configuração local no servidor do cliente.</p></div></div>
|
||||||
<form method="post" action="/agents/{{ agent_id }}/rtc-mode"><input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
<form method="post" action="/agents/{{ agent_id }}/rtc-mode"><input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||||
|
|||||||
@@ -705,3 +705,67 @@ def test_rtc_control_page_shows_human_labels_and_a_catalog_column_never_raw_enum
|
|||||||
assert header in page
|
assert header in page
|
||||||
for raw in ("PENDING</td>", "APPLIED</td>", "ABORTED"):
|
for raw in ("PENDING</td>", "APPLIED</td>", "ABORTED"):
|
||||||
assert raw not in page
|
assert raw not in page
|
||||||
|
|
||||||
|
|
||||||
|
def test_rtc_no_r3_candidates_shows_simples_explanation_and_filtered_blocker_link():
|
||||||
|
from central.app.models import BlockedProduct, RtcCandidate, RtcWriteAudit
|
||||||
|
|
||||||
|
client, agent_id, _ = _panel_client()
|
||||||
|
with client._session_factory() as session:
|
||||||
|
installation = session.scalar(select(Installation).where(Installation.agent_id == agent_id))
|
||||||
|
installation.rtc_write_mode = "ENABLED"
|
||||||
|
session.add(RtcCandidate(tenant_id=installation.tenant_id, installation_id=installation.id, product_code="R4-SUGGESTION", description="Sugestão", ncm="10063021", proposed="000001", cst_ibscbs="000", rule_id="RTC-R4", basis="suggestion only", status="PENDING"))
|
||||||
|
session.add_all([
|
||||||
|
BlockedProduct(
|
||||||
|
tenant_id=installation.tenant_id,
|
||||||
|
installation_id=installation.id,
|
||||||
|
company="Acme",
|
||||||
|
product_code=f"RTC-{index:04d}",
|
||||||
|
description=f"Produto RTC {index}",
|
||||||
|
blocker_type="BLOCKED_FISCAL_DATA",
|
||||||
|
field="RTC",
|
||||||
|
reason_code="CCLASSTRIB_MISSING_UNRESOLVED",
|
||||||
|
evidence_summary={"reason": "RTC-R1:SIMPLES_ORIENTATION_PENDING_OFFICIAL_NT"},
|
||||||
|
input_fingerprint=f"rtc-input-{index}",
|
||||||
|
blocker_fingerprint=f"rtc-blocker-{index}",
|
||||||
|
first_seen_at=datetime.now(timezone.utc),
|
||||||
|
last_seen_at=datetime.now(timezone.utc),
|
||||||
|
)
|
||||||
|
for index in range(521)
|
||||||
|
])
|
||||||
|
session.add(Job(agent_id=agent_id, kind="REPROCESS_ALL", status="COMPLETED", payload={}, result_summary={
|
||||||
|
"fiscal_context": {
|
||||||
|
"rtc_enabled": True,
|
||||||
|
"rtc_supported": True,
|
||||||
|
"rtc_catalog_valid": True,
|
||||||
|
"crt": "1",
|
||||||
|
"legal_effective_context": {"cclasstrib_orientation": "PENDING_OFFICIAL_NT"},
|
||||||
|
"rtc_resolution": {
|
||||||
|
"total": 521, "valid_existing": 0, "ready_to_apply": 0, "suggestion_only": 0,
|
||||||
|
"ambiguous": 0, "insufficient": 521,
|
||||||
|
"by_reason": {"SIMPLES_ORIENTATION_PENDING_OFFICIAL_NT": 521},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"rtc_auto_apply": {"mode": "ENABLED", "ready_total": 0, "preview": [], "write_attempts": 0, "write_successes": 0},
|
||||||
|
}, created_at=datetime.now(timezone.utc), finished_at=datetime.now(timezone.utc)))
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
page = client.get(f"/agents/{agent_id}/rtc")
|
||||||
|
|
||||||
|
assert page.status_code == 200
|
||||||
|
assert "Não há produtos elegíveis para preenchimento automático." in page.text
|
||||||
|
assert "521 produtos precisam de atenção fiscal, mas não podem ser preenchidos automaticamente pelas regras atuais." in page.text
|
||||||
|
assert "Esta empresa está no Simples Nacional." in page.text
|
||||||
|
assert "SIMPLES_ORIENTATION_PENDING_OFFICIAL_NT" not in page.text
|
||||||
|
assert "R4-SUGGESTION" not in page.text
|
||||||
|
assert 'href="/blockers?scope=all&agent_id=' + str(agent_id) + '&field=RTC&status=OPEN"' in page.text
|
||||||
|
assert "Nenhum candidato neste filtro." in page.text
|
||||||
|
|
||||||
|
blockers = client.get(f"/blockers?scope=all&agent_id={agent_id}&field=RTC&status=OPEN")
|
||||||
|
assert blockers.status_code == 200
|
||||||
|
assert "521 blockers" in blockers.text
|
||||||
|
with client._session_factory() as session:
|
||||||
|
assert session.scalar(select(Installation.rtc_write_mode).where(Installation.agent_id == agent_id)) == "ENABLED"
|
||||||
|
assert session.query(RtcCandidate).filter_by(installation_id=installation.id).count() == 1
|
||||||
|
assert session.query(RtcCandidate).filter_by(installation_id=installation.id, rule_id="RTC-R4").count() == 1
|
||||||
|
assert session.query(RtcWriteAudit).filter_by(installation_id=installation.id).count() == 0
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ from sqlalchemy.dialects import postgresql
|
|||||||
|
|
||||||
from central.app.audit import record_audit
|
from central.app.audit import record_audit
|
||||||
from central.app.blocked_products import upsert_blocker
|
from central.app.blocked_products import upsert_blocker
|
||||||
from central.app.control_plane import _blocker_breakdown_query, blocker_filter_summary, dashboard_summary, jobs_status_summary, list_blockers, list_installations, list_jobs
|
from central.app.control_plane import _blocker_breakdown_query, blocker_filter_summary, dashboard_summary, jobs_status_summary, list_blockers, list_installations, list_jobs, rtc_blocker_summary
|
||||||
from central.app.config import settings
|
from central.app.config import settings
|
||||||
from central.app.events import record_event
|
from central.app.events import record_event
|
||||||
from central.app.models import Agent, AgentEnrollment, AgentRelease, AgentSnapshot, AuditEvent, BlockedProduct, Installation, Job, Tenant
|
from central.app.models import Agent, AgentEnrollment, AgentRelease, AgentSnapshot, AuditEvent, BlockedProduct, Installation, Job, Tenant
|
||||||
@@ -238,6 +238,28 @@ def test_jobs_status_kpis_count_active_jobs_by_status_in_one_group():
|
|||||||
assert jobs_status_summary(session, scope="all")["FAILED"] == 2
|
assert jobs_status_summary(session, scope="all")["FAILED"] == 2
|
||||||
|
|
||||||
|
|
||||||
|
def test_rtc_blocker_summary_counts_products_and_simple_reason_server_side():
|
||||||
|
factory = setup_db()
|
||||||
|
with factory() as session:
|
||||||
|
_, agent, installation, _ = _installation(session, 1)
|
||||||
|
for index in range(521):
|
||||||
|
session.add(BlockedProduct(
|
||||||
|
tenant_id=installation.tenant_id, installation_id=installation.id, company="Client 1",
|
||||||
|
product_code=f"RTC-{index:04d}", description="RTC item", blocker_type="BLOCKED_FISCAL_DATA",
|
||||||
|
field="RTC", reason_code="CCLASSTRIB_MISSING_UNRESOLVED",
|
||||||
|
evidence_summary={"reason": "RTC-R1:SIMPLES_ORIENTATION_PENDING_OFFICIAL_NT"},
|
||||||
|
input_fingerprint=f"rtc-{index}", blocker_fingerprint=f"rtc-blocker-{index}",
|
||||||
|
first_seen_at=NOW, last_seen_at=NOW,
|
||||||
|
))
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
summary = rtc_blocker_summary(session, agent.id)
|
||||||
|
|
||||||
|
assert summary["open_blockers"] == 521
|
||||||
|
assert summary["products_affected"] == 521
|
||||||
|
assert summary["reason_counts"] == {"RTC-R1:SIMPLES_ORIENTATION_PENDING_OFFICIAL_NT": 521}
|
||||||
|
|
||||||
|
|
||||||
def test_attention_groups_rank_high_priority_before_limiting_recent_low_priority():
|
def test_attention_groups_rank_high_priority_before_limiting_recent_low_priority():
|
||||||
factory = setup_db()
|
factory = setup_db()
|
||||||
with factory() as session:
|
with factory() as session:
|
||||||
|
|||||||
Reference in New Issue
Block a user