feat: product enrichment foundation (GTIN, KB evidence, providers, quota gateway, resolver, migration 0029)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
gti
2026-09-25 18:35:42 -03:00
co-authored by Claude Sonnet 5
parent 4a12640031
commit 27f992d9e0
10 changed files with 1360 additions and 0 deletions
+121
View File
@@ -285,6 +285,11 @@ class Installation(Base):
# RTC auto-apply is authorized centrally per installation and is OFF until an operator explicitly turns it on (the Agent has its own second switch).
rtc_auto_apply_authorized: Mapped[bool] = mapped_column(Boolean, default=False, server_default="false") # legacy (0027); rtc_write_mode is the source of truth
rtc_write_mode: Mapped[str] = mapped_column(String(24), default="DISABLED", server_default="DISABLED") # DISABLED | DRY_RUN | APPROVAL_REQUIRED | ENABLED
# Per-installation FUNCTIONS (central, no local switch). Existing installations keep working exactly as before (corrector on, features configured);
# a NEW installation starts WAITING_FEATURE_CONFIGURATION (features_configured=False): it heartbeats but processes nothing until an admin chooses.
corrector_enabled: Mapped[bool] = mapped_column(Boolean, default=False, server_default="true")
enrichment_mode: Mapped[str] = mapped_column(String(16), default="DISABLED", server_default="DISABLED") # DISABLED | DRY_RUN | ENABLED
features_configured: Mapped[bool] = mapped_column(Boolean, default=False, server_default="true")
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
@@ -447,6 +452,10 @@ class AgentEnrollment(Base):
hostname_seen: Mapped[str | None] = mapped_column(String(255), nullable=True)
hostname_mismatch: Mapped[bool] = mapped_column(Boolean, default=False, server_default="false")
agent_id: Mapped[int | None] = mapped_column(ForeignKey("agents.id"), nullable=True, index=True)
# Functions chosen when the installation was created (copied to the Installation at enrollment). Nothing chosen => WAITING_FEATURE_CONFIGURATION.
features_selected: Mapped[bool] = mapped_column(Boolean, default=False, server_default="false")
feature_corrector: Mapped[bool] = mapped_column(Boolean, default=False, server_default="false")
feature_enrichment_mode: Mapped[str] = mapped_column(String(16), default="DISABLED", server_default="DISABLED")
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
@@ -620,6 +629,7 @@ class KbKnowledge(Base):
dataset_versions: Mapped[dict] = mapped_column(JSON, default=dict)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
validated_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
status: Mapped[str] = mapped_column(String(16), default="EVIDENCE_ONLY", server_default="EVIDENCE_ONLY", index=True) # UNVERIFIED | EVIDENCE_ONLY | VERIFIED_KB | CONFLICT | STALE
class KbObservation(Base):
@@ -786,3 +796,114 @@ class RtcCandidate(Base):
basis: Mapped[str | None] = mapped_column(String(255), nullable=True)
status: Mapped[str] = mapped_column(String(16), default="PENDING", server_default="PENDING", index=True) # PENDING | APPROVED | REJECTED | APPLIED | STALE
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
class ProductEnrichment(Base):
"""Per (installation, product) state of the new-product enrichment. Historical products never get a row (no mass provider use)."""
__tablename__ = "product_enrichments"
__table_args__ = (UniqueConstraint("installation_id", "product_code", name="uq_product_enrichment"),)
id: Mapped[int] = mapped_column(primary_key=True)
tenant_id: Mapped[int] = mapped_column(ForeignKey("tenants.id"), index=True)
installation_id: Mapped[int] = mapped_column(ForeignKey("installations.id"), index=True)
product_code: Mapped[str] = mapped_column(String(64))
gtin: Mapped[str | None] = mapped_column(String(16), nullable=True, index=True)
description: Mapped[str | None] = mapped_column(String(255), nullable=True)
status: Mapped[str] = mapped_column(String(32), default="PENDING", server_default="PENDING", index=True)
# PENDING | RESOLVED | APPLIED | PARTIAL | NOT_RESOLVED | CONFLICT | PENDING_PROVIDER_QUOTA | PENDING_PROVIDERS_OFFLINE | INVALID_GTIN
decisions: Mapped[dict] = mapped_column(JSON, default=dict)
fields_applied: Mapped[dict] = mapped_column(JSON, default=dict)
human_overridden: Mapped[dict] = mapped_column(JSON, default=dict)
attempts: Mapped[int] = mapped_column(Integer, default=0, server_default="0")
next_attempt_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True, index=True)
latency_ms: Mapped[int | None] = mapped_column(Integer, nullable=True)
detected_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
class ProductFieldAudit(Base):
"""Append-only audit of every field the enrichment wrote (or refused to). Never holds secrets."""
__tablename__ = "product_field_audits"
id: Mapped[int] = mapped_column(primary_key=True)
tenant_id: Mapped[int] = mapped_column(ForeignKey("tenants.id"), index=True)
installation_id: Mapped[int] = mapped_column(ForeignKey("installations.id"), index=True)
agent_id: Mapped[int] = mapped_column(ForeignKey("agents.id"), index=True)
product_code: Mapped[str] = mapped_column(String(64), index=True)
gtin: Mapped[str | None] = mapped_column(String(16), nullable=True)
description_snapshot: Mapped[str | None] = mapped_column(String(255), nullable=True)
field: Mapped[str] = mapped_column(String(48))
old_value: Mapped[str | None] = mapped_column(String(64), nullable=True)
new_value: Mapped[str | None] = mapped_column(String(64), nullable=True)
rule: Mapped[str | None] = mapped_column(String(64), nullable=True)
kb_status: Mapped[str | None] = mapped_column(String(16), nullable=True)
evidence: Mapped[dict] = mapped_column(JSON, default=dict)
dataset_versions: Mapped[dict] = mapped_column(JSON, default=dict)
authorization_origin: Mapped[str | None] = mapped_column(String(48), nullable=True)
job_id: Mapped[int | None] = mapped_column(Integer, nullable=True)
outcome: Mapped[str] = mapped_column(String(24), default="APPLIED", server_default="APPLIED") # APPLIED | NO_WRITE_REQUIRED | ABORTED | HUMAN_OVERRIDDEN
reason: Mapped[str | None] = mapped_column(String(96), nullable=True)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
class ProviderState(Base):
"""Live state of each external product provider (owned by the Hub: quota belongs to the Hub, never to Agents)."""
__tablename__ = "provider_states"
provider: Mapped[str] = mapped_column(String(32), primary_key=True)
configured: Mapped[bool] = mapped_column(Boolean, default=False, server_default="false")
status: Mapped[str] = mapped_column(String(16), default="NOT_CONFIGURED", server_default="NOT_CONFIGURED") # ONLINE | DEGRADED | RATE_LIMITED | AUTH_ERROR | OFFLINE | NOT_CONFIGURED
quota_kind: Mapped[str] = mapped_column(String(16), default="DOCUMENTED_RATE", server_default="DOCUMENTED_RATE") # REMOTE_QUOTA | LOCAL_ESTIMATE | DOCUMENTED_RATE
daily_limit: Mapped[int | None] = mapped_column(Integer, nullable=True)
remaining: Mapped[int | None] = mapped_column(Integer, nullable=True)
reset_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
rate_per_minute: Mapped[int | None] = mapped_column(Integer, nullable=True)
circuit_open_until: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
consecutive_failures: Mapped[int] = mapped_column(Integer, default=0, server_default="0")
last_request_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
last_success_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
last_error: Mapped[str | None] = mapped_column(String(160), nullable=True)
last_error_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
last_latency_ms: Mapped[int | None] = mapped_column(Integer, nullable=True)
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
class ProviderUsageDaily(Base):
__tablename__ = "provider_usage_daily"
__table_args__ = (UniqueConstraint("provider", "day", name="uq_provider_usage_day"),)
id: Mapped[int] = mapped_column(primary_key=True)
provider: Mapped[str] = mapped_column(String(32), index=True)
day: Mapped[str] = mapped_column(String(10), index=True)
requests: Mapped[int] = mapped_column(Integer, default=0, server_default="0")
success: Mapped[int] = mapped_column(Integer, default=0, server_default="0")
not_found: Mapped[int] = mapped_column(Integer, default=0, server_default="0")
errors: Mapped[int] = mapped_column(Integer, default=0, server_default="0")
rate_limited: Mapped[int] = mapped_column(Integer, default=0, server_default="0")
timeouts: Mapped[int] = mapped_column(Integer, default=0, server_default="0")
cache_hits: Mapped[int] = mapped_column(Integer, default=0, server_default="0")
cache_misses: Mapped[int] = mapped_column(Integer, default=0, server_default="0")
class GtinLookupCache(Base):
"""Positive and negative (NOT_FOUND) provider results per GTIN, with TTL. Single place that spares quota across all installations."""
__tablename__ = "gtin_lookup_cache"
__table_args__ = (UniqueConstraint("gtin", "provider", name="uq_gtin_lookup_cache"),)
id: Mapped[int] = mapped_column(primary_key=True)
gtin: Mapped[str] = mapped_column(String(16), index=True)
provider: Mapped[str] = mapped_column(String(32))
found: Mapped[bool] = mapped_column(Boolean, default=False)
data: Mapped[dict] = mapped_column(JSON, default=dict)
fetched_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
expires_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), index=True)
class SourceRefreshRun(Base):
"""Observability of official fiscal sources (NCM Siscomex, ...): one row per refresh attempt."""
__tablename__ = "source_refresh_runs"
id: Mapped[int] = mapped_column(primary_key=True)
source: Mapped[str] = mapped_column(String(32), index=True)
source_url: Mapped[str] = mapped_column(Text)
started_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
finished_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
status: Mapped[str] = mapped_column(String(16), default="RUNNING", server_default="RUNNING") # RUNNING | UPDATED | UNCHANGED | FAILED
sha256: Mapped[str | None] = mapped_column(String(64), nullable=True)
version_label: Mapped[str | None] = mapped_column(String(64), nullable=True)
record_count: Mapped[int | None] = mapped_column(Integer, nullable=True)
error: Mapped[str | None] = mapped_column(String(300), nullable=True)