feat: Contexto Fiscal admin UI - reduces fiscal policy list to 3 real decisions

Adds an ADMIN-only "Contexto Fiscal" section to the Agent detail page: confirm/alter/clear the fiscal decisions
field_contract.resolve_product genuinely cannot derive from the ERP alone (icms_treatment, pis_cofins_profile,
entry_scope). Persists through the existing FiscalContextEntry table (the same one blocker_resolution already
writes operator answers into) - no new persistence model, no opaque JSON box. CSRF + RBAC + audit reuse the exact
mechanism already proven for /agents/{id}/features. Confirming or clearing flags every already-enriched product on
the installation for re-evaluation.

The list was reduced from 5 candidates to 3 by auditing real production data (Agent 2, 2026-09-27/28):
icms_rate/credit_rate and rounding were dropped - both proven NOT_APPLICABLE / ALREADY_DERIVED for this CRT=1
company from real field_contract dry-runs, not assumption. Real evidence surfaced in the UI is a dated, one-time
manual read-only measurement (disclosed in the card copy itself as such) - the Hub has no direct Firebird access,
so this is not a live query; a follow-up should pipe it through the heartbeat the same way CRT/CONTRIBUINTE_IPI
were wired.

Also found, during the same audit, that this client's NFC-e emission ignores the product's cadastral PIS/COFINS
CST/rate fields entirely (verified on the 3 real products that DO have them populated - the emitted documents never
carry the cadastral value) - documented as a strong OPEN recommendation for a future field_contract change, not
applied here, since changing canonical fiscal semantics needs accountant sign-off, not just my own inference.

1129 tests passed (tests + central/tests + agent/tests), 0 failed. Version 1.3.9 (single GTI_FISCAL_VERSION). Hub
only - Agent unchanged, no new installer this release.
This commit is contained in:
gti
2026-09-27 22:09:41 -03:00
parent 4701ff8724
commit 1621ea4e0b
13 changed files with 229 additions and 18 deletions
+92
View File
@@ -0,0 +1,92 @@
"""Contexto Fiscal: the ADMIN-only, human-language view of the handful of fiscal decisions field_contract.resolve_product genuinely cannot
derive from the ERP alone (see central/app/enrichment/fiscal_completion.py). Reuses FiscalContextEntry (the SAME table blocker_resolution
already writes operator answers into) - no new persistence model, no opaque JSON box in the UI.
Each decision here was reduced from a larger candidate list by auditing REAL production data (Agent 2, 2026-09-27/28): icms_rate/credit_rate
and rounding were REMOVED (proven NOT_APPLICABLE / ALREADY_DERIVED for this CRT=1 company - see git history), leaving only the three that
real evidence could not resolve unattended. Evidence text below is a dated, one-time manual read-only measurement (see the audit trail in
git commit messages), not a live query - the Hub has no direct Firebird access; only the Agent does, and this evidence has not yet been
piped through the heartbeat. This is disclosed in the UI itself so the admin never mistakes it for a live number."""
from __future__ import annotations
DECISIONS = [
{
"key": "icms_treatment", "scope": "COMPANY", "title": "Tratamento padrão de ICMS dos produtos comuns",
"evidence": "CSOSN padrão do ERP (C000004.ICMS_CSOSN): 102. 487 de 521 produtos cadastrados já usam 102. As exceções existentes "
"(500 = ICMS-ST retido, em 5 produtos; 101 = com direito a crédito, em 4 produtos; 103, em 1 produto) são preservadas "
"e nunca sobrescritas - a confirmação abaixo só afeta os produtos com CSOSN em branco (28 hoje).",
"suggestion": "TAXED", "allowed_values": ["TAXED", "EXEMPT", "NOT_TAXED", "ST_WITHHELD", "SUSPENDED", "DEFERRED"],
"affected_fields": ["CST", "CSOSN"], "confidence": "Média (real, medida em produção; não é garantia de que todo produto novo seja 102)",
"measured_at": "2026-09-27",
},
{
"key": "pis_cofins_profile", "scope": "COMPANY", "title": "Perfil de PIS/COFINS (código e alíquota de entrada/saída)",
"evidence": "34.946 itens de NFC-e emitidos; 3.336 (9,5%) carregam um CST de PIS/COFINS na nota, sempre o código 01 - mas os "
"mesmos produtos têm os campos de cadastro (código de saída, alíquota) sempre em branco no SGV, incluindo os poucos "
"produtos que TÊM esses campos preenchidos manualmente (a nota emitida ignora o valor cadastrado). Isso indica que a "
"emissão não lê esse cadastro para este cliente - mas não é conclusivo o bastante para marcar os 6 campos como "
"não aplicável sem confirmação humana.", "suggestion": None,
"affected_fields": ["COD_ENTRADA_PIS", "COD_SAIDA_PIS", "ALIQ_PIS", "COD_ENTRADA_COFINS", "COD_SAIDA_COFINS", "ALIQ_COFINS"],
"confidence": "Baixa para um perfil único da empresa; considerar tratar como não aplicável ao cadastro (pendente de confirmação contábil)",
"measured_at": "2026-09-27",
},
{
"key": "entry_scope", "scope": "COMPANY", "title": "CFOP de entrada padrão (compras dentro/fora do estado)",
"evidence": "Apenas 12 documentos de compra reais no ERP para 526 produtos; 11 sem CFOP preenchido, 1 com CFOP intra-estadual. "
"Evidência real insuficiente para qualquer sugestão - o CFOP de entrada normalmente é definido pela operação/documento "
"de compra, não pelo cadastro do produto.", "suggestion": None,
"affected_fields": ["CFOP_ENTRADA"], "confidence": "Muito baixa - recomendação: tratar como definido no momento da operação, não como cadastro obrigatório",
"measured_at": "2026-09-27",
},
]
VALID_KEYS = {d["key"] for d in DECISIONS}
def set_fiscal_context_decision(session, *, tenant_id: int, company_key: str, key: str, value: str | None, reason: str, admin, installation_id: int):
"""Confirm/alter/clear one Contexto Fiscal decision. value=None clears it (the product/group falls back to whatever the engine can
prove on its own - never a silent default). Reuses the SAME table and validator the blocker-resolution workflow already writes
through; flags every already-enriched product on this installation for re-evaluation so the new (or cleared) policy takes effect."""
from datetime import datetime, timezone
from sqlalchemy import select
from . import context_keys
from .audit import record_audit
from .enrichment.status import mark_for_reevaluation
from .models import FiscalContextEntry
if key not in VALID_KEYS:
raise ValueError(f"unknown Contexto Fiscal key {key!r}")
entry = session.scalar(select(FiscalContextEntry).where(FiscalContextEntry.tenant_id == tenant_id, FiscalContextEntry.company_key == company_key,
FiscalContextEntry.scope == "COMPANY", FiscalContextEntry.scope_key == "", FiscalContextEntry.key == key))
before = entry.value if entry else None
if value is None:
if entry is not None:
session.delete(entry)
else:
normalized, error = context_keys.validate_entry({"scope": "COMPANY", "key": key, "value": value, "reason": reason})
if error:
raise ValueError(error)
if entry is None:
entry = FiscalContextEntry(tenant_id=tenant_id, company_key=company_key, source="OPERATOR", updated_at=datetime.now(timezone.utc), **normalized)
session.add(entry)
else:
entry.value, entry.reason, entry.source, entry.updated_at = normalized["value"], normalized["reason"], "OPERATOR", datetime.now(timezone.utc)
flagged = mark_for_reevaluation(session, installation_id)
record_audit(session, admin_user_id=admin.id, tenant_id=tenant_id, action="FISCAL_CONTEXT_DECISION", target_type="installation", target_id=company_key,
context={"key": key, "from": before, "to": value, "reason": reason, "operator": admin.display_name or admin.username, "role": admin.external_role or admin.auth_source, "products_flagged": flagged},
severity="WARNING")
def build_fiscal_context_view(session, tenant_id: int, company_key: str) -> list[dict]:
from sqlalchemy import select
from .models import FiscalContextEntry
current = {(r.scope, r.scope_key, r.key): r for r in session.scalars(
select(FiscalContextEntry).where(FiscalContextEntry.tenant_id == tenant_id, FiscalContextEntry.company_key == company_key))}
out = []
for d in DECISIONS:
row = current.get((d["scope"], "", d["key"]))
out.append({**d, "current_value": row.value if row else None, "current_reason": row.reason if row else None,
"last_changed": row.updated_at.isoformat() if row and getattr(row, "updated_at", None) else None,
"last_changed_by": row.source if row else None})
return out
+1 -1
View File
@@ -4,5 +4,5 @@ kept identical to this) and what an AgentRelease row's `version` column holds
once published — never a second, independent version number. Fiscal semantics
are versioned separately (ENGINE_VERSION/DECISION_VERSION in classification.py,
DECISION_ENGINE_VERSION etc.) and are NOT changed by a release bump."""
GTI_FISCAL_VERSION = "1.3.8"
GTI_FISCAL_VERSION = "1.3.9"
API_COMPAT_MAJOR_MINOR = "1.0" # an Agent 1.0.x/1.1.x talks to any Hub 1.0.x
+29 -1
View File
@@ -23,6 +23,7 @@ from .models import AdminUser, Agent, AgentEnrollment, AgentRelease, AgentSnapsh
from .jobs import create_job
from .audit import record_audit
from .enrichment import kb as kb_mod, status as enrichment_status
from .fiscal_context_ui import build_fiscal_context_view, set_fiscal_context_decision
from .enrichment.observability import coverage_metrics, kb_summary, official_sources, provider_sources
@@ -291,7 +292,8 @@ def router(session_factory):
for event in events:
event["occurred_display"] = format_ui_datetime(datetime.fromisoformat(event["occurred_at"]) if event["occurred_at"] else None)
agent_blockers = list_blockers(session, tenant_id=installation.tenant_id, agent_id=agent_id, page=1, page_size=25, scope="all")
return render(request, "agent_detail.html", {"detail": detail, "tab": tab, "blockers": agent_blockers, "blocker_breakdown": blocker_filter_summary(session, tenant_id=installation.tenant_id, agent_id=agent_id, status="OPEN", scope="all")["blocker_breakdown"], "jobs": list_jobs(session, agent_id=agent_id, page=1, page_size=25, scope="all"), "events": events, "event_page": max(1, event_page), "has_more": has_more, "severity": severity or "", "event_type": event_type or "", "job_id": job_id or "", "product_code": product_code or "", "since": since or "", "until": until or "", "event_query_base": query_base(tab=tab, severity=severity, event_type=event_type, job_id=job_id, product_code=product_code, since=since, until=until), "auth": auth_data, "csrf_token": request.cookies.get("gti_csrf", ""), "ops": _ops_view(session, installation, agent), "features": _features_view(session, installation, enrich_code), "enrich_code": enrich_code or "", "is_admin": _is_privileged(auth_data[1])})
fiscal_context = build_fiscal_context_view(session, installation.tenant_id, _company_key(None, installation, None))
return render(request, "agent_detail.html", {"detail": detail, "tab": tab, "blockers": agent_blockers, "blocker_breakdown": blocker_filter_summary(session, tenant_id=installation.tenant_id, agent_id=agent_id, status="OPEN", scope="all")["blocker_breakdown"], "jobs": list_jobs(session, agent_id=agent_id, page=1, page_size=25, scope="all"), "events": events, "event_page": max(1, event_page), "has_more": has_more, "severity": severity or "", "event_type": event_type or "", "job_id": job_id or "", "product_code": product_code or "", "since": since or "", "until": until or "", "event_query_base": query_base(tab=tab, severity=severity, event_type=event_type, job_id=job_id, product_code=product_code, since=since, until=until), "auth": auth_data, "csrf_token": request.cookies.get("gti_csrf", ""), "ops": _ops_view(session, installation, agent), "features": _features_view(session, installation, enrich_code), "enrich_code": enrich_code or "", "is_admin": _is_privileged(auth_data[1]), "fiscal_context": fiscal_context})
finally: session.close()
def _features_view(session, installation, enrich_code=None):
@@ -344,6 +346,32 @@ def router(session_factory):
return RedirectResponse(f"/agents/{agent_id}", status_code=303)
finally: session.close()
@routes.post("/agents/{agent_id}/fiscal-context")
async def set_fiscal_context(request: Request, agent_id: int):
"""Contexto Fiscal (ADMIN only, CSRF, audited): confirm/alter/clear one of the handful of real fiscal decisions field_contract
cannot derive from the ERP alone. Writes through FiscalContextEntry - the same table blocker_resolution already writes to."""
session, auth_data, redirect = page_auth(request)
if redirect: return redirect
try:
values = await form_values(request); web_session, admin = auth_data
require_csrf(web_session, values.get("csrf_token"))
agent, installation = _rtc_target(session, agent_id)
if not agent or not installation: return HTMLResponse("Agent not found", status_code=404)
if not _is_privileged(admin):
return _deny(session, admin, installation.tenant_id, "fiscal_context:set", installation.installation_id)
key = (values.get("key") or "").strip()
action = (values.get("fc_action") or "confirm").strip()
value = None if action == "clear" else (values.get("value") or "").strip() or None
reason = (values.get("reason") or "").strip() or f"{action} via painel"
company_key = _company_key(None, installation, None)
try:
set_fiscal_context_decision(session, tenant_id=installation.tenant_id, company_key=company_key, key=key, value=value, reason=reason, admin=admin, installation_id=installation.id)
except ValueError as error:
return HTMLResponse(str(error), status_code=422)
session.commit()
return RedirectResponse(f"/agents/{agent_id}#contexto-fiscal", status_code=303)
finally: session.close()
@routes.post("/agents/{agent_id}/enrichment/reevaluate")
async def reevaluate_enrichment(request: Request, agent_id: int):
"""Explicit, idempotent 'evaluate again' for one product (code=...) or every tracked product that is not enriched yet. KB/cache first; providers only when really needed."""
+1 -1
View File
@@ -1,6 +1,6 @@
[project]
name = "gti-fiscal-hub"
version = "1.3.8"
version = "1.3.9"
requires-python = ">=3.10"
dependencies = [
"fastapi>=0.115,<1",
+13
View File
@@ -10,6 +10,19 @@
{% if ops.enabled %}<form method="post" action="/agents/{{ detail.agent_id }}/pause" style="display:inline"><input type="hidden" name="csrf_token" value="{{ csrf_token }}"><button type="submit" class="caution" title="Mantém o heartbeat, mas não executa jobs">Pausar processamento</button></form>{% else %}<form method="post" action="/agents/{{ detail.agent_id }}/resume" style="display:inline"><input type="hidden" name="csrf_token" value="{{ csrf_token }}"><button type="submit" class="primary-action" title="Volta a executar jobs">Retomar processamento</button></form>{% endif %}
</div></div><dl class="facts"><dt>Processamento</dt><dd><span class="badge {{ '' if ops.enabled else 'critical' }}">{{ "Ativo" if ops.enabled else "Pausado" }}</span> · sincronização automática a cada {{ ops.interval_minutes }} min</dd><dt>Última sincronização</dt><dd>{{ ops.last_sync or "ainda não sincronizado" }}{% if ops.last_changed is not none %} · {{ ops.last_changed }} produto(s) alterado(s){% endif %}</dd><dt>Sincronização agendada</dt><dd>{{ ops.next_sync or ("aguardando o primeiro sync" if ops.enabled else "pausado") }}</dd>{% if ops.rtc %}<dt>Reforma Tributária</dt><dd><span class="badge {{ "" if ops.rtc.enabled else "muted" }}" title="{{ ops.rtc.title }}">{{ ops.rtc.label }}</span></dd>{% endif %}{% if ops.rtc and ops.rtc.enabled %}{% if ops.rtc.products and not ops.rtc.products.applicable %}<dt>cClassTrib</dt><dd>{{ ops.rtc.products.label }}</dd>{% elif ops.rtc.products %}<dt>Produtos RTC</dt><dd><b>{{ ops.rtc.products.headline }}</b><br><small>Total {{ ops.rtc.products.total }} · com cClassTrib válido {{ ops.rtc.products.valid }} · prontos para preenchimento {{ ops.rtc.products.ready }} · ambíguos/bloqueados {{ ops.rtc.products.blocked }}</small></dd>{% endif %}
<dt>Preenchimento automático (RTC, não é o Enriquecimento)</dt><dd>{{ ops.rtc.apply.state }}{% if ops.rtc.apply.applied_last_job %} · {{ ops.rtc.apply.applied_last_job }} preenchido(s) no último job{% endif %} · <a href="/agents/{{ detail.agent_id }}/rtc">Controle e candidatos cClassTrib</a></dd>{% endif %}<dt>Último job</dt><dd>{% if ops.last_job %}<a href="/jobs?scope=all&amp;job_id={{ ops.last_job.id }}">{{ ops.last_job.kind_label }}</a> · {{ ops.last_job.status_label }}{% else %}nenhum{% endif %}</dd></dl></section>
{% if is_admin %}<section class="panel" id="contexto-fiscal"><div class="panel-title"><div><h2>Contexto Fiscal</h2><p class="muted">Somente as decisões que o motor fiscal (field_contract) realmente não consegue provar sozinho no ERP. Visível apenas para administradores.</p></div></div>
{% for d in fiscal_context %}<article class="panel" style="margin-top:12px"><div class="panel-title"><h3>{{ d.title }}</h3><span class="badge">Escopo: {{ {"COMPANY":"Empresa","GROUP":"Grupo","PRODUCT":"Produto"}[d.scope] }}</span></div>
<dl class="facts"><dt>Valor atual</dt><dd>{{ d.current_value or "Não confirmado" }}{% if d.last_changed %} <small class="muted">· alterado em {{ d.last_changed }} por {{ d.last_changed_by }}</small>{% endif %}</dd>
<dt>Sugestão</dt><dd>{{ d.suggestion or "Sem sugestão automática — evidência insuficiente" }}</dd>
<dt>Evidência</dt><dd>{{ d.evidence }} <small class="muted">(medido manualmente em {{ d.measured_at }} — não é uma consulta ao vivo)</small></dd>
<dt>Confiança</dt><dd>{{ d.confidence }}</dd>
<dt>Campos afetados</dt><dd>{{ d.affected_fields|join(", ") }}</dd></dl>
<form method="post" action="/agents/{{ detail.agent_id }}/fiscal-context" class="filters compact-filters"><input type="hidden" name="csrf_token" value="{{ csrf_token }}"><input type="hidden" name="key" value="{{ d.key }}">
{% if d.allowed_values %}<label>Valor<select name="value">{% for v in d.allowed_values %}<option value="{{ v }}" {% if d.current_value==v %}selected{% endif %}>{{ v }}</option>{% endfor %}</select></label>{% else %}<label>Valor<input name="value" value="{{ d.current_value or '' }}" placeholder="ex.: {01,01,1.65,01,01,7.6} ou INTRA"></label>{% endif %}
<label>Motivo<input name="reason" placeholder="opcional" autocomplete="off"></label>
<button class="secondary" type="submit" name="fc_action" value="confirm">Confirmar</button>
<button class="secondary" type="submit" name="fc_action" value="clear" onclick="return confirm('Limpar esta decisão? Os produtos voltam a depender só do que o ERP prova.')">Limpar</button></form>
</article>{% endfor %}</section>{% endif %}
<section class="panel" id="funcionalidades"><div class="panel-title"><div><h2>Funcionalidades (Enriquecimento de produtos novos)</h2><p class="muted">Decididas no Hub para esta instalação — o Agent não tem chave local. O Enriquecimento de produtos novos NÃO é o preenchimento RTC, que tem controle próprio (<a href="/agents/{{ detail.agent_id }}/rtc">controle e candidatos</a>).</p></div></div>
{% if features.waiting %}<p class="coverage-status error">Aguardando configuração de funcionalidades: esta instalação ainda não processa nada.</p>{% endif %}
<dl class="facts"><dt>Corretor fiscal</dt><dd><span class="badge {{ '' if features.corrector_enabled else 'critical' }}">{{ "Ativo" if features.corrector_enabled else "Desativado" }}</span></dd><dt>Enriquecimento de produtos novos</dt><dd><span class="badge {{ '' if features.enrichment_mode != 'DISABLED' else 'critical' }}">{{ features.state_label }}</span></dd></dl>
+78
View File
@@ -0,0 +1,78 @@
"""Contexto Fiscal (Agent detail, ADMIN only): confirm/alter/clear one of the reduced set of real fiscal decisions
(icms_treatment/pis_cofins_profile/entry_scope). CSRF, RBAC and audit reuse the exact same mechanism already proven for
/agents/{id}/features; persistence reuses FiscalContextEntry, the same table blocker_resolution already writes to."""
from sqlalchemy import select
from central.app.models import AuditEvent, FiscalContextEntry, Installation, ProductEnrichment
from central.tests.test_control_panel_pages import _panel_client
from central.tests.test_rtc_security import _become, _post
def _company_key(client):
with client._session_factory() as s:
inst = s.scalars(select(Installation)).one()
return (inst.company_code or inst.company_name or "").strip(), inst.tenant_id, inst.id
def test_fiscal_context_tab_lists_only_the_three_reduced_decisions_with_evidence():
client, agent_id, _ = _panel_client()
_become(client, "ADMIN")
page = client.get(f"/agents/{agent_id}").text
assert "Contexto Fiscal" in page
for title in ("Tratamento padrão de ICMS", "Perfil de PIS/COFINS", "CFOP de entrada padrão"):
assert title in page
assert "487 de 521" in page # real evidence shown, not a raw internal key name
assert "<h3>Tratamento padrão de ICMS dos produtos comuns</h3>" in page # human title heads the card; internal key only appears as a hidden form field
def test_fiscal_context_hidden_from_non_admin():
client, agent_id, _ = _panel_client()
_become(client, "PADRAO")
page = client.get(f"/agents/{agent_id}").text
assert "Contexto Fiscal" not in page
def test_confirming_a_decision_is_csrf_admin_and_audited_and_flags_reevaluation():
client, agent_id, _ = _panel_client()
company_key, tenant_id, installation_id = _company_key(client)
with client._session_factory() as s:
s.add(ProductEnrichment(tenant_id=tenant_id, installation_id=installation_id, product_code="P1", status="RESOLVED", ui_status="PRONTO_PARA_PREENCHER"))
s.commit()
_become(client, "PADRAO")
assert _post(client, f"/agents/{agent_id}/fiscal-context", key="icms_treatment", value="TAXED", fc_action="confirm").status_code == 403
_become(client, "ADMIN")
assert client.post(f"/agents/{agent_id}/fiscal-context", data={"csrf_token": "bad", "key": "icms_treatment", "value": "TAXED", "fc_action": "confirm"}, follow_redirects=False).status_code in (400, 403)
assert _post(client, f"/agents/{agent_id}/fiscal-context", key="icms_treatment", value="TAXED", fc_action="confirm").status_code == 303
with client._session_factory() as s:
entry = s.scalars(select(FiscalContextEntry).where(FiscalContextEntry.key == "icms_treatment")).one()
assert entry.scope == "COMPANY" and entry.value == "TAXED" and entry.source == "OPERATOR"
acts = [e.action for e in s.scalars(select(AuditEvent))]
assert "FISCAL_CONTEXT_DECISION" in acts
row = s.scalars(select(ProductEnrichment)).one()
assert row.reevaluate is True # existing products get a chance to pick up the new policy - never a silent no-op
def test_clearing_a_decision_deletes_the_entry_never_a_default_value():
client, agent_id, _ = _panel_client()
company_key, tenant_id, installation_id = _company_key(client)
_become(client, "ADMIN")
assert _post(client, f"/agents/{agent_id}/fiscal-context", key="icms_treatment", value="TAXED", fc_action="confirm").status_code == 303
assert _post(client, f"/agents/{agent_id}/fiscal-context", key="icms_treatment", fc_action="clear").status_code == 303
with client._session_factory() as s:
assert s.scalars(select(FiscalContextEntry).where(FiscalContextEntry.key == "icms_treatment")).first() is None
def test_invalid_value_is_rejected_never_silently_stored():
client, agent_id, _ = _panel_client()
_become(client, "ADMIN")
resp = client.post(f"/agents/{agent_id}/fiscal-context", data={"csrf_token": client.cookies.get("gti_csrf"), "key": "icms_treatment", "value": "NOT_A_REAL_TREATMENT", "fc_action": "confirm"}, follow_redirects=False)
assert resp.status_code == 422
with client._session_factory() as s:
assert s.scalars(select(FiscalContextEntry)).first() is None
def test_unknown_key_is_refused():
client, agent_id, _ = _panel_client()
_become(client, "ADMIN")
resp = client.post(f"/agents/{agent_id}/fiscal-context", data={"csrf_token": client.cookies.get("gti_csrf"), "key": "not_a_real_key", "value": "x", "fc_action": "confirm"}, follow_redirects=False)
assert resp.status_code == 422
+1 -1
View File
@@ -17,7 +17,7 @@ def test_health_is_unavailable_when_database_is_down():
def test_version_endpoint_reports_release_and_fiscal_versions():
body = TestClient(create_app(database_probe=lambda: True)).get("/version").json()
assert body["version"] == "1.3.8" and body["api_compat"] == "1.0" and body["engine_version"] and body["decision_version"]
assert body["version"] == "1.3.9" and body["api_compat"] == "1.0" and body["engine_version"] and body["decision_version"]
def test_production_refuses_the_dev_token(monkeypatch):