feat: add autonomous fiscal completion v1

This commit is contained in:
gti
2026-09-22 13:08:38 -03:00
parent eada0df37e
commit e3ac68c70e
28 changed files with 44630 additions and 0 deletions
@@ -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")
+59
View File
@@ -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)
+36
View File
@@ -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)}
+47
View File
@@ -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
+83
View File
@@ -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}
+30
View File
@@ -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)
+17
View File
@@ -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",
}
+49
View File
@@ -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))
+16
View File
@@ -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
+15
View File
@@ -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"]
+49
View File
@@ -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
+21
View File
@@ -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"