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
@@ -0,0 +1,35 @@
"""Tenant mirror identity from GTI Gerenciador.
GTI Gerenciador (the panel) becomes the source of truth for clients.
tenants gains source ('LOCAL' | 'GTI_GERENCIADOR'), external_client_id
(Gerenciador's CLIENTE.ID), external_code, active and synced_at. Existing
rows (pre-dating this sync) become source='LOCAL' and are never offered for
a new installation, but keep every foreign key that already points at them.
Revision ID: 0024_tenant_gerenciador_mirror
Revises: 0023_install_codes
"""
from alembic import op
import sqlalchemy as sa
revision = "0024_tenant_gerenciador_mirror"
down_revision = "0023_install_codes"
branch_labels = None
depends_on = None
def upgrade():
op.add_column("tenants", sa.Column("source", sa.String(16), nullable=False, server_default="LOCAL"))
op.add_column("tenants", sa.Column("external_client_id", sa.Integer(), nullable=True))
op.add_column("tenants", sa.Column("external_code", sa.String(16), nullable=True))
op.add_column("tenants", sa.Column("active", sa.Boolean(), nullable=False, server_default="true"))
op.add_column("tenants", sa.Column("synced_at", sa.DateTime(timezone=True), nullable=True))
op.create_index("ix_tenants_external_client_id", "tenants", ["external_client_id"])
op.create_unique_constraint("uq_tenant_external_identity", "tenants", ["source", "external_client_id"])
def downgrade():
op.drop_constraint("uq_tenant_external_identity", "tenants", type_="unique")
op.drop_index("ix_tenants_external_client_id", table_name="tenants")
for column in ("synced_at", "active", "external_code", "external_client_id", "source"):
op.drop_column("tenants", column)