feat: GTI Gerenciador as the single source of truth for clients; bump to 1.1.2

Tenant gains source (LOCAL|GTI_GERENCIADOR), external_client_id, external_code,
active and synced_at (migration 0024). central/app/client_sync.py fetches from
the panel's new GET /internal/fiscal/clients (dedicated bearer token, never
MySQL credentials) and upserts Tenant rows, never touching a LOCAL row.
/agents/new now offers only active GTI_GERENCIADOR tenants, with a client-side
search box; the POST validates the same rule server-side, not just in the
dropdown. central/scripts/sync_clients_from_gerenciador.py is the operator-run
sync + reconciliation report.

GTI_FISCAL_VERSION/AGENT_VERSION bumped to 1.1.2 (release/VERSION single
source of truth), incorporating the installation_id persistence fix. Rebuilt
the EXE installer trio at 1.1.2 (scripts/build_windows_installer.ps1).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
gti
2026-09-23 22:38:58 -03:00
co-authored by Claude Sonnet 5
parent ac7b503c2f
commit e7d77beb26
29 changed files with 453 additions and 38 deletions
+87
View File
@@ -0,0 +1,87 @@
"""Client mirror from GTI Gerenciador (central/app/client_sync.py, web.py's
/agents/new). GTI Gerenciador is the single source of truth: Tenant rows
with source='GTI_GERENCIADOR' are upserted from it; source='LOCAL' rows
(pre-existing dev/test tenants) are never touched or offered."""
import dataclasses
import pytest
from sqlalchemy import select
from central.app import client_sync, config
from central.app.models import Tenant
from central.tests.test_control_plane import setup_db
CLIENTS = [
{"external_client_id": 1, "code": "000001", "display_name": "Mercado Ativo", "active": True},
{"external_client_id": 2, "code": "000002", "display_name": "Loja Inativa", "active": False},
]
@pytest.fixture
def factory():
return setup_db()
def test_sync_creates_tenants_from_gerenciador(factory):
with factory() as session:
result = client_sync.sync_from_gerenciador(session, CLIENTS)
session.commit()
assert result.created == 2 and result.updated == 0 and result.fetched == 2
assert result.active == 1 and result.inactive == 1
rows = {row.external_client_id: row for row in session.scalars(select(Tenant)).all()}
assert rows[1].name == "Mercado Ativo" and rows[1].source == "GTI_GERENCIADOR" and rows[1].active is True
assert rows[2].active is False and rows[2].external_code == "000002"
def test_sync_is_idempotent_and_updates_changed_fields(factory):
with factory() as session:
client_sync.sync_from_gerenciador(session, CLIENTS)
session.commit()
result = client_sync.sync_from_gerenciador(session, CLIENTS)
session.commit()
assert result.created == 0 and result.unchanged == 2
renamed = [{**CLIENTS[0], "display_name": "Mercado Ativo Renomeado"}, CLIENTS[1]]
result2 = client_sync.sync_from_gerenciador(session, renamed)
session.commit()
assert result2.updated == 1 and result2.unchanged == 1
row = session.scalar(select(Tenant).where(Tenant.external_client_id == 1))
assert row.name == "Mercado Ativo Renomeado"
def test_sync_never_touches_local_tenants(factory):
with factory() as session:
session.add(Tenant(name="Acme", source="LOCAL"))
session.commit()
client_sync.sync_from_gerenciador(session, CLIENTS)
session.commit()
local = session.scalar(select(Tenant).where(Tenant.name == "Acme"))
assert local.source == "LOCAL" and local.external_client_id is None
def test_sync_disambiguates_a_name_collision_with_an_existing_tenant(factory):
with factory() as session:
session.add(Tenant(name="Mercado Ativo", source="LOCAL"))
session.commit()
client_sync.sync_from_gerenciador(session, CLIENTS)
session.commit()
new_row = session.scalar(select(Tenant).where(Tenant.external_client_id == 1))
assert new_row.name == "Mercado Ativo (000001)" # the LOCAL tenant keeps the plain name
def test_deactivating_a_client_in_gerenciador_is_reflected_without_deleting_history(factory):
with factory() as session:
client_sync.sync_from_gerenciador(session, CLIENTS)
session.commit()
now_inactive = [{**CLIENTS[0], "active": False}, CLIENTS[1]]
client_sync.sync_from_gerenciador(session, now_inactive)
session.commit()
row = session.scalar(select(Tenant).where(Tenant.external_client_id == 1))
assert row.active is False # row (and every FK pointing at it) is kept — never deleted
def test_disabled_without_configured_url_or_token(monkeypatch):
monkeypatch.setattr(config, "settings", dataclasses.replace(config.settings, gerenciador_internal_url="", fiscal_internal_api_token=""))
assert client_sync.enabled() is False
with pytest.raises(client_sync.ClientSyncUnavailable):
client_sync.fetch_from_gerenciador()