feat: add autonomous fiscal completion v1
This commit is contained in:
@@ -0,0 +1,52 @@
|
||||
"""Add central AI usage ledger and external GTIN cache."""
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
revision = "0015_ai_budget_and_product_cache"
|
||||
down_revision = "0014_ai_eval_cache"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
op.create_table(
|
||||
"ai_usage_ledger",
|
||||
sa.Column("id", sa.Integer(), primary_key=True),
|
||||
sa.Column("provider", sa.String(length=64), nullable=False),
|
||||
sa.Column("model", sa.String(length=64), nullable=False),
|
||||
sa.Column("run_id", sa.String(length=128), nullable=False),
|
||||
sa.Column("tenant", sa.String(length=128), nullable=False),
|
||||
sa.Column("items", sa.Integer(), nullable=False, server_default="0"),
|
||||
sa.Column("input_tokens", sa.Integer(), nullable=False, server_default="0"),
|
||||
sa.Column("output_tokens", sa.Integer(), nullable=False, server_default="0"),
|
||||
sa.Column("cost", sa.Float(), nullable=False, server_default="0"),
|
||||
sa.Column("timestamp", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False),
|
||||
)
|
||||
op.create_index("ix_ai_usage_ledger_run_id", "ai_usage_ledger", ["run_id"])
|
||||
op.create_index("ix_ai_usage_ledger_tenant", "ai_usage_ledger", ["tenant"])
|
||||
op.create_index("ix_ai_usage_ledger_timestamp", "ai_usage_ledger", ["timestamp"])
|
||||
op.create_table(
|
||||
"external_product_cache",
|
||||
sa.Column("id", sa.Integer(), primary_key=True),
|
||||
sa.Column("gtin", sa.String(length=16), nullable=False),
|
||||
sa.Column("provider", sa.String(length=64), nullable=False),
|
||||
sa.Column("provider_record_version", sa.String(length=64), nullable=False),
|
||||
sa.Column("retrieved_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False),
|
||||
sa.Column("raw_hash", sa.String(length=64), nullable=False),
|
||||
sa.Column("normalized_data", sa.JSON(), nullable=False),
|
||||
sa.Column("cache_allowed", sa.Boolean(), nullable=False, server_default=sa.false()),
|
||||
sa.Column("redistribution_allowed", sa.Boolean(), nullable=False, server_default=sa.false()),
|
||||
sa.UniqueConstraint("gtin", "provider", "provider_record_version", name="uq_external_product_cache_record"),
|
||||
)
|
||||
op.create_index("ix_external_product_cache_gtin", "external_product_cache", ["gtin"])
|
||||
|
||||
|
||||
def downgrade():
|
||||
op.drop_index("ix_external_product_cache_gtin", table_name="external_product_cache")
|
||||
op.drop_table("external_product_cache")
|
||||
op.drop_index("ix_ai_usage_ledger_timestamp", table_name="ai_usage_ledger")
|
||||
op.drop_index("ix_ai_usage_ledger_tenant", table_name="ai_usage_ledger")
|
||||
op.drop_index("ix_ai_usage_ledger_run_id", table_name="ai_usage_ledger")
|
||||
op.drop_table("ai_usage_ledger")
|
||||
@@ -0,0 +1,59 @@
|
||||
"""Persistent-boundary-compatible budget guard; provider invocation happens after this check."""
|
||||
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timezone
|
||||
from threading import Lock
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class BudgetLimits:
|
||||
max_run_cost_usd: float = 0.54
|
||||
max_daily_cost_usd: float = 1.00
|
||||
max_monthly_cost_usd: float = 10.00
|
||||
max_requests_per_day: int = 100
|
||||
max_new_products_per_run: int = 750
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class BudgetDecision:
|
||||
allowed: bool
|
||||
reason: str
|
||||
reserved_cost_usd: float
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class UsageEntry:
|
||||
run_id: str
|
||||
tenant: str
|
||||
cost_usd: float
|
||||
requests: int
|
||||
new_products: int
|
||||
timestamp: str
|
||||
|
||||
|
||||
class BudgetLedger:
|
||||
"""Atomic in-process ledger with an explicit persistence boundary for Hub storage."""
|
||||
|
||||
def __init__(self, limits: BudgetLimits):
|
||||
self.limits = limits
|
||||
self.entries: list[UsageEntry] = []
|
||||
self._lock = Lock()
|
||||
|
||||
def check_and_reserve(self, run_id: str, tenant: str, cost_usd: float, requests: int, new_products: int) -> BudgetDecision:
|
||||
with self._lock:
|
||||
run_cost = sum(e.cost_usd for e in self.entries if e.run_id == run_id)
|
||||
day = datetime.now(timezone.utc).date()
|
||||
day_entries = [e for e in self.entries if e.timestamp[:10] == day.isoformat()]
|
||||
month_entries = [e for e in self.entries if e.timestamp[:7] == day.strftime("%Y-%m")]
|
||||
if run_cost + cost_usd > self.limits.max_run_cost_usd:
|
||||
return BudgetDecision(False, "AI_BUDGET_EXHAUSTED", 0.0)
|
||||
if sum(e.cost_usd for e in day_entries) + cost_usd > self.limits.max_daily_cost_usd:
|
||||
return BudgetDecision(False, "AI_BUDGET_EXHAUSTED", 0.0)
|
||||
if sum(e.cost_usd for e in month_entries) + cost_usd > self.limits.max_monthly_cost_usd:
|
||||
return BudgetDecision(False, "AI_BUDGET_EXHAUSTED", 0.0)
|
||||
if sum(e.requests for e in day_entries) + requests > self.limits.max_requests_per_day:
|
||||
return BudgetDecision(False, "AI_BUDGET_EXHAUSTED", 0.0)
|
||||
if sum(e.new_products for e in self.entries if e.run_id == run_id) + new_products > self.limits.max_new_products_per_run:
|
||||
return BudgetDecision(False, "AI_BUDGET_EXHAUSTED", 0.0)
|
||||
self.entries.append(UsageEntry(run_id, tenant, cost_usd, requests, new_products, datetime.now(timezone.utc).isoformat()))
|
||||
return BudgetDecision(True, "RESERVED", cost_usd)
|
||||
@@ -0,0 +1,36 @@
|
||||
"""Conservative deterministic role, ST, and inbound scope resolution."""
|
||||
|
||||
|
||||
def resolve_product_role(signals: dict) -> dict:
|
||||
if signals.get("recipe") and signals.get("production"):
|
||||
return {"value": "OWN_PRODUCTION", "status": "CONFIRMED", "evidence": ["recipe", "production"]}
|
||||
if signals.get("purchase") and signals.get("sale") and not signals.get("recipe"):
|
||||
return {"value": "RESALE", "status": "CONFIRMED", "evidence": ["purchase", "sale"]}
|
||||
if signals.get("input_consumed") and signals.get("recipe"):
|
||||
return {"value": "INPUT", "status": "CONFIRMED", "evidence": ["input_consumed", "recipe"]}
|
||||
if signals.get("service_flag") and signals.get("service_transactions"):
|
||||
return {"value": "SERVICE", "status": "CONFIRMED", "evidence": ["service_flag", "service_transactions"]}
|
||||
return {"value": None, "status": "PENDING_PRODUCT_ROLE", "evidence": []}
|
||||
|
||||
|
||||
def resolve_st_status(signals: dict) -> dict:
|
||||
if not signals.get("official_cest"):
|
||||
return {"value": "NOT_SUBJECT_TO_ST", "status": "CONFIRMED", "evidence": []}
|
||||
if signals.get("purchase_retained") is True:
|
||||
return {"value": "ST_ALREADY_WITHHELD_IN_PURCHASE", "status": "CONFIRMED", "evidence": ["official_cest", "purchase_retained"]}
|
||||
if signals.get("purchase_retained") is False:
|
||||
return {"value": "PRODUCT_SUBJECT_TO_ST", "status": "CONFIRMED", "evidence": ["official_cest", "purchase_not_retained"]}
|
||||
return {"value": None, "status": "PENDING_ST_WITHHELD", "evidence": ["official_cest"]}
|
||||
|
||||
|
||||
def resolve_entry_scope(supplier_ufs: list[str], company_uf: str) -> dict:
|
||||
observed = {str(uf).strip().upper() for uf in supplier_ufs if uf}
|
||||
if not observed:
|
||||
return {"value": None, "status": "PENDING_ENTRY_SCOPE", "evidence": []}
|
||||
if len(observed - {company_uf.upper()}) and company_uf.upper() in observed:
|
||||
value = "MIXED_OR_DYNAMIC"
|
||||
elif observed - {company_uf.upper()}:
|
||||
value = "INTER"
|
||||
else:
|
||||
value = "INTRA"
|
||||
return {"value": value, "status": "CONFIRMED", "evidence": sorted(observed)}
|
||||
@@ -0,0 +1,47 @@
|
||||
"""Conservative aggregation of supplier/document observations."""
|
||||
|
||||
from dataclasses import dataclass
|
||||
from collections import defaultdict
|
||||
from typing import Any, Iterable
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Observation:
|
||||
product_id: str
|
||||
attribute: str
|
||||
value: str
|
||||
supplier_id: str
|
||||
document_id: str
|
||||
observed_at: str
|
||||
provenance: str = "SUPPLIER_OBSERVATION"
|
||||
|
||||
|
||||
def aggregate_observations(observations: Iterable[Observation]) -> dict[tuple[str, str], dict[str, Any]]:
|
||||
grouped: dict[tuple[str, str], list[Observation]] = defaultdict(list)
|
||||
seen: set[tuple[str, str, str, str]] = set()
|
||||
for observation in observations:
|
||||
key = (observation.product_id, observation.attribute)
|
||||
dedupe = (observation.product_id, observation.attribute, observation.supplier_id, observation.document_id)
|
||||
if dedupe not in seen and observation.value not in (None, ""):
|
||||
grouped[key].append(observation)
|
||||
seen.add(dedupe)
|
||||
|
||||
output = {}
|
||||
for key, rows in grouped.items():
|
||||
values = {row.value for row in rows}
|
||||
counts = {value: sum(row.value == value for row in rows) for value in values}
|
||||
supplier_count = len({row.supplier_id for row in rows})
|
||||
agreement_rate = max(counts.values()) / len(rows) if rows else 0.0
|
||||
conflict_count = max(0, len(values) - 1)
|
||||
strong = len(rows) >= 2 and supplier_count >= 2 and len(values) == 1
|
||||
output[key] = {
|
||||
"value": next(iter(values)) if strong else None,
|
||||
"observation_count": len(rows),
|
||||
"supplier_count": supplier_count,
|
||||
"agreement_rate": agreement_rate,
|
||||
"latest_observation": max(row.observed_at for row in rows),
|
||||
"conflict_count": conflict_count,
|
||||
"strong_evidence": strong,
|
||||
"provenance": sorted({row.provenance for row in rows}),
|
||||
}
|
||||
return output
|
||||
@@ -0,0 +1,83 @@
|
||||
"""Pure requiredness and fiscal completeness evaluation."""
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
|
||||
VALID_CLASSIFICATIONS = {
|
||||
"REQUIRED", "CONDITIONAL", "OPTIONAL_REFERENCE", "NOT_APPLICABLE", "ERP_NOT_USED"
|
||||
}
|
||||
|
||||
|
||||
def load_requiredness_matrix(path: str | Path) -> dict[str, Any]:
|
||||
matrix = json.loads(Path(path).read_text(encoding="utf-8"))
|
||||
fields = matrix.get("fields", {})
|
||||
if set(matrix.get("classifications", [])) != VALID_CLASSIFICATIONS:
|
||||
raise ValueError("requiredness matrix classifications are incomplete")
|
||||
if len(fields) != 23 or any(rule.get("classification") not in VALID_CLASSIFICATIONS for rule in fields.values()):
|
||||
raise ValueError("requiredness matrix must define 23 valid field rules")
|
||||
return matrix
|
||||
|
||||
|
||||
def _has_value(value: Any) -> bool:
|
||||
return value is not None and (not isinstance(value, str) or bool(value.strip()))
|
||||
|
||||
|
||||
def _conditional_state(field: str, context: dict[str, Any]) -> str:
|
||||
if field == "IPI":
|
||||
return "REQUIRED" if context.get("contribuinte_ipi") == "S" and context.get("has_numeric_tipi") else "NOT_APPLICABLE"
|
||||
if field == "EX_TIPI":
|
||||
return "REQUIRED" if context.get("has_supported_ex") and context.get("product_matches_ex") else "NOT_APPLICABLE"
|
||||
if field == "CEST":
|
||||
return "REQUIRED" if context.get("has_official_cest") or context.get("st_subject") else "NOT_APPLICABLE"
|
||||
if field in {"CFOP", "SITUACAO_TRIBUTARIA"}:
|
||||
return "REQUIRED" if context.get("operation", "SALE") == "SALE" else "NOT_APPLICABLE"
|
||||
if field in {"CFOP_ENTRADA", "CST_ENTRADA", "COD_ENTRADA_PIS", "COD_ENTRADA_COFINS"}:
|
||||
return "REQUIRED" if context.get("operation") == "PURCHASE" and context.get("erp_consumes_entry_fields") else "NOT_APPLICABLE"
|
||||
if field == "CST":
|
||||
return "REQUIRED" if context.get("regime") == "NORMAL" and context.get("operation", "SALE") == "SALE" else "NOT_APPLICABLE"
|
||||
if field == "CSOSN":
|
||||
return "REQUIRED" if context.get("regime") == "SIMPLES" and context.get("operation", "SALE") == "SALE" else "NOT_APPLICABLE"
|
||||
if field == "ALIQUOTA":
|
||||
return "REQUIRED" if context.get("icms_taxed") else "NOT_APPLICABLE"
|
||||
if field == "COD_ORIGEM_MERCADORIA":
|
||||
return "REQUIRED" if context.get("origin_proven") else "NOT_APPLICABLE"
|
||||
if field == "CST_IPI":
|
||||
return "REQUIRED" if context.get("contribuinte_ipi") == "S" and context.get("erp_consumes_ipi_cst") else "NOT_APPLICABLE"
|
||||
if field in {"COD_SAIDA_PIS", "COD_SAIDA_COFINS"}:
|
||||
return "REQUIRED" if context.get("operation", "SALE") == "SALE" and context.get("piscofins_profile_proven") else "NOT_APPLICABLE"
|
||||
if field in {"ALIQ_PIS", "ALIQ_COFINS"}:
|
||||
return "REQUIRED" if context.get("piscofins_rate_applies") else "NOT_APPLICABLE"
|
||||
if field in {"INCIDENCIA_PISCOFINS", "PISCOFINS"}:
|
||||
return "REQUIRED" if context.get("piscofins_profile_proven") else "NOT_APPLICABLE"
|
||||
return "NOT_APPLICABLE"
|
||||
|
||||
|
||||
def evaluate_field(field: str, value: Any, context: dict[str, Any], matrix: dict[str, Any]) -> dict[str, Any]:
|
||||
rule = matrix["fields"][field]
|
||||
classification = rule["classification"]
|
||||
if classification == "ERP_NOT_USED":
|
||||
status = "ERP_NOT_SUPPORTED"
|
||||
valid = True
|
||||
elif classification == "NOT_APPLICABLE":
|
||||
status = "NOT_APPLICABLE"
|
||||
valid = True
|
||||
elif classification == "OPTIONAL_REFERENCE":
|
||||
status = "OPTIONAL_VALID" if _has_value(value) else "OPTIONAL_EMPTY"
|
||||
valid = True
|
||||
elif classification == "REQUIRED":
|
||||
status = "REQUIRED_AND_VALID" if _has_value(value) else "REQUIRED_MISSING"
|
||||
valid = _has_value(value)
|
||||
else:
|
||||
conditional = _conditional_state(field, context)
|
||||
if conditional == "NOT_APPLICABLE":
|
||||
status, valid = "CONDITIONAL_NOT_TRIGGERED", True
|
||||
else:
|
||||
status, valid = ("CONDITIONAL_AND_VALID", True) if _has_value(value) else ("REQUIRED_MISSING", False)
|
||||
return {"field": field, "classification": classification, "status": status, "valid": valid, "value_present": _has_value(value), "source": rule["source"]}
|
||||
|
||||
|
||||
def evaluate_product(product: dict[str, Any], context: dict[str, Any], matrix: dict[str, Any]) -> dict[str, Any]:
|
||||
fields = {field: evaluate_field(field, product.get(field), context, matrix) for field in matrix["fields"]}
|
||||
return {"FISCALLY_COMPLETE": all(result["valid"] for result in fields.values()), "fields": fields}
|
||||
@@ -464,3 +464,33 @@ class AiEvalCache(Base):
|
||||
answer: Mapped[dict] = mapped_column(JSON)
|
||||
gold_ncm: Mapped[str | None] = mapped_column(String(8), nullable=True)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||
|
||||
|
||||
class AiUsageLedger(Base):
|
||||
"""Central persistent usage ledger reserved before any future provider call."""
|
||||
__tablename__ = "ai_usage_ledger"
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
provider: Mapped[str] = mapped_column(String(64))
|
||||
model: Mapped[str] = mapped_column(String(64))
|
||||
run_id: Mapped[str] = mapped_column(String(128), index=True)
|
||||
tenant: Mapped[str] = mapped_column(String(128), index=True)
|
||||
items: Mapped[int] = mapped_column(Integer, default=0)
|
||||
input_tokens: Mapped[int] = mapped_column(Integer, default=0)
|
||||
output_tokens: Mapped[int] = mapped_column(Integer, default=0)
|
||||
cost: Mapped[float] = mapped_column(default=0.0)
|
||||
timestamp: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), index=True)
|
||||
|
||||
|
||||
class ExternalProductCache(Base):
|
||||
"""Versioned GTIN cache; legal/provider flags remain explicit metadata."""
|
||||
__tablename__ = "external_product_cache"
|
||||
__table_args__ = (UniqueConstraint("gtin", "provider", "provider_record_version", name="uq_external_product_cache_record"),)
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
gtin: Mapped[str] = mapped_column(String(16), index=True)
|
||||
provider: Mapped[str] = mapped_column(String(64))
|
||||
provider_record_version: Mapped[str] = mapped_column(String(64))
|
||||
retrieved_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||
raw_hash: Mapped[str] = mapped_column(String(64))
|
||||
normalized_data: Mapped[dict] = mapped_column(JSON, default=dict)
|
||||
cache_allowed: Mapped[bool] = mapped_column(Boolean, default=False)
|
||||
redistribution_allowed: Mapped[bool] = mapped_column(Boolean, default=False)
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
"""Evidence-gated origin resolution; CST digits are never an origin source."""
|
||||
|
||||
|
||||
def resolve_origin(product_id: str, aggregate: dict, kb_facts: dict) -> dict:
|
||||
if aggregate.get("conflict_count", 0):
|
||||
status = "ORIGIN_CONFLICT"
|
||||
elif aggregate.get("strong_evidence") and aggregate.get("value") in {str(i) for i in range(9)}:
|
||||
status = "ORIGIN_CONFIRMED"
|
||||
else:
|
||||
status = "ORIGIN_NEEDS_EVIDENCE"
|
||||
return {
|
||||
"product_id": product_id,
|
||||
"status": status,
|
||||
"value": aggregate.get("value") if status == "ORIGIN_CONFIRMED" else None,
|
||||
"provenance": aggregate.get("provenance", []),
|
||||
"reason": "explicit supplier observations only; CST digit ignored",
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
"""Offline-safe product data provider boundary and permanent cache record."""
|
||||
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timezone
|
||||
import hashlib
|
||||
import json
|
||||
from typing import Any, Protocol
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ProviderRecord:
|
||||
provider: str
|
||||
provider_record_version: str
|
||||
gtin: str
|
||||
retrieved_at: str
|
||||
raw_hash: str
|
||||
normalized_data: dict[str, Any]
|
||||
fiscal_authority: str = "OBSERVATION_ONLY"
|
||||
cache_allowed: bool = False
|
||||
redistribution_allowed: bool = False
|
||||
|
||||
|
||||
class ProductDataProvider(Protocol):
|
||||
def lookup_gtin(self, gtin: str) -> ProviderRecord | None: ...
|
||||
|
||||
|
||||
def normalize_record(provider: str, version: str, gtin: str, data: dict[str, Any]) -> ProviderRecord:
|
||||
normalized = {str(key): value for key, value in sorted(data.items()) if value not in (None, "")}
|
||||
raw_hash = hashlib.sha256(json.dumps(normalized, ensure_ascii=False, sort_keys=True, default=str).encode("utf-8")).hexdigest()
|
||||
return ProviderRecord(provider, version, str(gtin), datetime.now(timezone.utc).isoformat(), raw_hash, normalized)
|
||||
|
||||
|
||||
def cache_key(gtin: str, provider: str, provider_record_version: str) -> str:
|
||||
return f"{gtin}:{provider}:{provider_record_version}"
|
||||
|
||||
|
||||
def result_cache_key(product: dict[str, Any], candidates: list[str], datasets: dict[str, str], prompt: str, model: str) -> str:
|
||||
payload = {"product": product, "candidates": candidates, "datasets": datasets, "prompt": prompt, "model": model}
|
||||
return hashlib.sha256(json.dumps(payload, ensure_ascii=False, sort_keys=True, default=str).encode("utf-8")).hexdigest()
|
||||
|
||||
|
||||
class LocalBulkDatasetProvider:
|
||||
"""Offline provider backed by explicitly supplied records; never performs I/O."""
|
||||
|
||||
def __init__(self, records: dict[str, ProviderRecord] | None = None):
|
||||
self.records = records or {}
|
||||
|
||||
def lookup_gtin(self, gtin: str) -> ProviderRecord | None:
|
||||
return self.records.get(str(gtin))
|
||||
@@ -0,0 +1,16 @@
|
||||
from central.app.ai.budget_ledger import BudgetLedger, BudgetLimits
|
||||
|
||||
|
||||
def test_budget_rejects_before_provider_when_daily_limit_is_exceeded():
|
||||
ledger = BudgetLedger(BudgetLimits(max_run_cost_usd=2.0, max_daily_cost_usd=1.0))
|
||||
assert ledger.check_and_reserve("r1", "t1", 0.75, 1, 0).allowed is True
|
||||
decision = ledger.check_and_reserve("r2", "t1", 0.30, 1, 0)
|
||||
|
||||
assert decision.allowed is False
|
||||
assert decision.reason == "AI_BUDGET_EXHAUSTED"
|
||||
|
||||
|
||||
def test_budget_allows_exact_limit_and_counts_new_products():
|
||||
ledger = BudgetLedger(BudgetLimits(max_run_cost_usd=1.0, max_new_products_per_run=2))
|
||||
assert ledger.check_and_reserve("r1", "t1", 1.0, 1, 2).allowed is True
|
||||
assert ledger.check_and_reserve("r1", "t1", 0.0, 0, 1).allowed is False
|
||||
@@ -0,0 +1,15 @@
|
||||
from central.app.product_data_provider import result_cache_key
|
||||
|
||||
|
||||
def test_same_classification_inputs_have_same_cache_key():
|
||||
first = result_cache_key({"gtin": "789", "description": "X"}, ["12345678"], {"ncm": "v1"}, "prompt-v1", "model-v1")
|
||||
second = result_cache_key({"description": "X", "gtin": "789"}, ["12345678"], {"ncm": "v1"}, "prompt-v1", "model-v1")
|
||||
|
||||
assert first == second
|
||||
|
||||
|
||||
def test_changed_dataset_changes_cache_key():
|
||||
first = result_cache_key({"gtin": "789"}, ["12345678"], {"ncm": "v1"}, "prompt-v1", "model-v1")
|
||||
second = result_cache_key({"gtin": "789"}, ["12345678"], {"ncm": "v2"}, "prompt-v1", "model-v1")
|
||||
|
||||
assert first != second
|
||||
@@ -0,0 +1,16 @@
|
||||
from central.app.deterministic_resolution import resolve_entry_scope, resolve_product_role, resolve_st_status
|
||||
|
||||
|
||||
def test_product_role_requires_two_independent_strong_signals():
|
||||
assert resolve_product_role({"recipe": True, "production": True})["value"] == "OWN_PRODUCTION"
|
||||
assert resolve_product_role({"single_flag": True})["value"] is None
|
||||
|
||||
|
||||
def test_st_distinguishes_subject_from_withheld():
|
||||
assert resolve_st_status({"official_cest": True, "purchase_retained": True})["value"] == "ST_ALREADY_WITHHELD_IN_PURCHASE"
|
||||
assert resolve_st_status({"official_cest": True, "purchase_retained": None})["status"] == "PENDING_ST_WITHHELD"
|
||||
|
||||
|
||||
def test_entry_scope_is_dynamic_when_both_ufs_are_observed():
|
||||
assert resolve_entry_scope(["BA", "SP"], "BA")["value"] == "MIXED_OR_DYNAMIC"
|
||||
assert resolve_entry_scope(["BA"], "BA")["value"] == "INTRA"
|
||||
@@ -0,0 +1,38 @@
|
||||
from central.app.evidence_aggregation import Observation, aggregate_observations
|
||||
from scripts.analyze_inbound_observations import summarize_rows
|
||||
|
||||
|
||||
def test_two_suppliers_agree_without_conflict():
|
||||
rows = [
|
||||
Observation("P1", "origin", "0", "S1", "D1", "2026-01-01"),
|
||||
Observation("P1", "origin", "0", "S2", "D2", "2026-02-01"),
|
||||
]
|
||||
|
||||
result = aggregate_observations(rows)[("P1", "origin")]
|
||||
|
||||
assert result["observation_count"] == 2
|
||||
assert result["supplier_count"] == 2
|
||||
assert result["agreement_rate"] == 1.0
|
||||
assert result["strong_evidence"] is True
|
||||
assert result["conflict_count"] == 0
|
||||
|
||||
|
||||
def test_supplier_disagreement_is_conflict_not_majority():
|
||||
rows = [
|
||||
Observation("P1", "origin", "0", "S1", "D1", "2026-01-01"),
|
||||
Observation("P1", "origin", "0", "S2", "D2", "2026-02-01"),
|
||||
Observation("P1", "origin", "1", "S3", "D3", "2026-03-01"),
|
||||
]
|
||||
|
||||
result = aggregate_observations(rows)[("P1", "origin")]
|
||||
|
||||
assert result["conflict_count"] == 1
|
||||
assert result["strong_evidence"] is False
|
||||
assert result["value"] is None
|
||||
|
||||
|
||||
def test_inbound_rows_become_supplier_observations_without_raw_identity():
|
||||
result = summarize_rows([("P1", "S1", "2026-01-01", "12345678", "1102", "060")])
|
||||
|
||||
assert result["observation_count"] == 3
|
||||
assert result["sources"] == ["SUPPLIER_OBSERVATION"]
|
||||
@@ -0,0 +1,49 @@
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from central.app.fiscal_completeness import evaluate_field, evaluate_product, load_requiredness_matrix
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
|
||||
|
||||
def test_requiredness_matrix_covers_contract_fields_once():
|
||||
contract = json.loads((ROOT / "docs/FISCAL_FIELD_CONTRACT_V1.json").read_text(encoding="utf-8"))
|
||||
matrix = load_requiredness_matrix(ROOT / "docs/FISCAL_REQUIREDNESS_MATRIX_V1.json")
|
||||
|
||||
assert set(matrix["fields"]) == set(contract["fields"])
|
||||
assert len(matrix["fields"]) == 23
|
||||
assert set(matrix["classifications"]) == {
|
||||
"REQUIRED", "CONDITIONAL", "OPTIONAL_REFERENCE", "NOT_APPLICABLE", "ERP_NOT_USED"
|
||||
}
|
||||
|
||||
|
||||
def test_conditional_empty_when_not_triggered_is_not_missing():
|
||||
matrix = load_requiredness_matrix(ROOT / "docs/FISCAL_REQUIREDNESS_MATRIX_V1.json")
|
||||
|
||||
result = evaluate_field("IPI", None, {"contribuinte_ipi": "N", "has_numeric_tipi": False}, matrix)
|
||||
|
||||
assert result["status"] == "CONDITIONAL_NOT_TRIGGERED"
|
||||
assert result["valid"] is True
|
||||
|
||||
|
||||
def test_required_missing_makes_product_incomplete():
|
||||
matrix = load_requiredness_matrix(ROOT / "docs/FISCAL_REQUIREDNESS_MATRIX_V1.json")
|
||||
|
||||
result = evaluate_product(
|
||||
{"CLASSIFICACAO_FISCAL": None, "CFOP": "5102"},
|
||||
{"product_role": "RESALE", "operation": "SALE"},
|
||||
matrix,
|
||||
)
|
||||
|
||||
assert result["FISCALLY_COMPLETE"] is False
|
||||
assert result["fields"]["CLASSIFICACAO_FISCAL"]["status"] == "REQUIRED_MISSING"
|
||||
|
||||
|
||||
def test_erp_not_used_is_not_missing():
|
||||
matrix = load_requiredness_matrix(ROOT / "docs/FISCAL_REQUIREDNESS_MATRIX_V1.json")
|
||||
|
||||
result = evaluate_field("IAT", None, {"erp_uses_iat": False}, matrix)
|
||||
|
||||
assert result["status"] == "ERP_NOT_SUPPORTED"
|
||||
assert result["valid"] is True
|
||||
@@ -0,0 +1,21 @@
|
||||
from central.app.origin_resolution import resolve_origin
|
||||
|
||||
|
||||
def test_origin_requires_strong_supplier_evidence():
|
||||
result = resolve_origin("P1", {"value": "0", "strong_evidence": True, "conflict_count": 0}, {})
|
||||
|
||||
assert result["status"] == "ORIGIN_CONFIRMED"
|
||||
assert result["value"] == "0"
|
||||
|
||||
|
||||
def test_origin_conflict_stays_pending():
|
||||
result = resolve_origin("P1", {"value": None, "strong_evidence": False, "conflict_count": 1}, {})
|
||||
|
||||
assert result["status"] == "ORIGIN_CONFLICT"
|
||||
assert result["value"] is None
|
||||
|
||||
|
||||
def test_origin_does_not_use_cst_digit():
|
||||
result = resolve_origin("P1", {}, {"cst": "040"})
|
||||
|
||||
assert result["status"] == "ORIGIN_NEEDS_EVIDENCE"
|
||||
@@ -0,0 +1,16 @@
|
||||
from central.app.product_data_provider import ProviderRecord, cache_key, normalize_record
|
||||
|
||||
|
||||
def test_provider_record_has_stable_gtin_cache_key_and_hash():
|
||||
record = normalize_record("GS1", "v1", "789", {"brand": "Acme", "description": "Produto"})
|
||||
|
||||
assert isinstance(record, ProviderRecord)
|
||||
assert record.raw_hash
|
||||
assert cache_key(record.gtin, record.provider, record.provider_record_version).startswith("789:")
|
||||
|
||||
|
||||
def test_provider_normalization_does_not_claim_fiscal_authority():
|
||||
record = normalize_record("COMMERCIAL", "v1", "789", {"ncm": "12345678"})
|
||||
|
||||
assert record.normalized_data["ncm"] == "12345678"
|
||||
assert record.fiscal_authority == "OBSERVATION_ONLY"
|
||||
Reference in New Issue
Block a user