Add 'fiscal/' from commit '102ba84500dbd9655a0471288c3e278652593ca9'
git-subtree-dir: fiscal git-subtree-mainline:d95165fc8fgit-subtree-split:102ba84500
This commit is contained in:
@@ -0,0 +1,168 @@
|
||||
"""Validated, auditable context resolution for a single blocked product."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from sqlalchemy import select
|
||||
|
||||
from .audit import record_audit
|
||||
from .context_keys import validate_entry
|
||||
from .models import Agent, BlockedProduct, FiscalContextEntry, Installation, Tenant, Job
|
||||
|
||||
|
||||
WORKFLOW_SCOPES = {"PRODUCT", "GROUP", "COMPANY"}
|
||||
ENTRY_FIELDS = {"scope", "scope_key", "key", "value", "reason"}
|
||||
MAX_VALUE_LENGTH = 512
|
||||
MAX_REASON_LENGTH = 2000
|
||||
MAX_COMPANY_KEY_LENGTH = 64
|
||||
|
||||
|
||||
class BlockerResolutionUnavailable(LookupError):
|
||||
"""The blocker exists, but has no proven installation to target safely."""
|
||||
|
||||
|
||||
def validate_resolution_input(payload):
|
||||
"""Validate only the structured context accepted by the blocker workflow."""
|
||||
if not isinstance(payload, dict):
|
||||
return None, "resolution input must be an object"
|
||||
unexpected = set(payload) - ENTRY_FIELDS
|
||||
if unexpected:
|
||||
return None, f"unsupported resolution fields: {sorted(unexpected)}"
|
||||
if not isinstance(payload.get("value"), str):
|
||||
return None, "value must be a string"
|
||||
if not isinstance(payload.get("reason"), str):
|
||||
return None, "reason must be a string"
|
||||
if len(payload["value"].encode("utf-8")) > MAX_VALUE_LENGTH:
|
||||
return None, f"value must be at most {MAX_VALUE_LENGTH} UTF-8 bytes"
|
||||
if len(payload["reason"].encode("utf-8")) > MAX_REASON_LENGTH:
|
||||
return None, f"reason must be at most {MAX_REASON_LENGTH} UTF-8 bytes"
|
||||
|
||||
normalized, error = validate_entry(payload)
|
||||
if error:
|
||||
return None, error
|
||||
if normalized["scope"] not in WORKFLOW_SCOPES:
|
||||
return None, "scope must be one of ('PRODUCT', 'GROUP', 'COMPANY')"
|
||||
if not normalized["reason"]:
|
||||
return None, "reason is required"
|
||||
return normalized, None
|
||||
|
||||
|
||||
def _company_key(blocker, installation, tenant):
|
||||
if installation is not None:
|
||||
return (installation.company_code or installation.company_name or installation.customer_code or "").strip()
|
||||
return (blocker.company or (tenant.name if tenant else "")).strip()
|
||||
|
||||
|
||||
def _entry_view(row):
|
||||
if row is None:
|
||||
return None
|
||||
return {
|
||||
"company_key": row.company_key,
|
||||
"scope": row.scope,
|
||||
"scope_key": row.scope_key,
|
||||
"key": row.key,
|
||||
"value": row.value,
|
||||
"source": row.source,
|
||||
"reason": row.reason,
|
||||
}
|
||||
|
||||
|
||||
def resolve_blocker_with_context(session, blocker_id, admin, payload, request_ip=None):
|
||||
"""Persist context, enqueue one product reprocess, and audit atomically."""
|
||||
normalized, error = validate_resolution_input(payload)
|
||||
if error:
|
||||
raise ValueError(error)
|
||||
|
||||
try:
|
||||
blocker = session.get(BlockedProduct, blocker_id)
|
||||
if blocker is None:
|
||||
raise LookupError("blocker not found")
|
||||
|
||||
installation = None
|
||||
if blocker.installation_id is not None:
|
||||
installation = session.get(Installation, blocker.installation_id)
|
||||
if installation is None:
|
||||
raise LookupError("blocker installation not found")
|
||||
if installation.tenant_id != blocker.tenant_id:
|
||||
raise LookupError("blocker installation tenant mismatch")
|
||||
agent = session.get(Agent, installation.agent_id)
|
||||
if agent is None:
|
||||
raise LookupError("blocker installation agent not found")
|
||||
if agent.tenant_id != blocker.tenant_id:
|
||||
raise LookupError("blocker agent tenant mismatch")
|
||||
else:
|
||||
raise BlockerResolutionUnavailable(
|
||||
"Blocker legado de empresa sem vínculo comprovado com um servidor; não é possível oferecer o reprocessamento de contexto com segurança."
|
||||
)
|
||||
|
||||
tenant = session.get(Tenant, blocker.tenant_id)
|
||||
company_key = _company_key(blocker, installation, tenant)
|
||||
if not company_key:
|
||||
raise ValueError("company context is unavailable")
|
||||
if len(company_key) > MAX_COMPANY_KEY_LENGTH:
|
||||
raise ValueError(f"company key must be at most {MAX_COMPANY_KEY_LENGTH} characters")
|
||||
if normalized["scope"] == "PRODUCT" and normalized["scope_key"] != blocker.product_code:
|
||||
raise ValueError("PRODUCT scope_key must match the blocker product code")
|
||||
|
||||
query = select(FiscalContextEntry).where(
|
||||
FiscalContextEntry.tenant_id == blocker.tenant_id,
|
||||
FiscalContextEntry.company_key == company_key,
|
||||
FiscalContextEntry.scope == normalized["scope"],
|
||||
FiscalContextEntry.scope_key == normalized["scope_key"],
|
||||
FiscalContextEntry.key == normalized["key"],
|
||||
)
|
||||
entry = session.scalar(query)
|
||||
before = _entry_view(entry)
|
||||
now = datetime.now(timezone.utc)
|
||||
if entry is None:
|
||||
entry = FiscalContextEntry(
|
||||
tenant_id=blocker.tenant_id,
|
||||
company_key=company_key,
|
||||
source="OPERATOR",
|
||||
updated_at=now,
|
||||
**normalized,
|
||||
)
|
||||
session.add(entry)
|
||||
else:
|
||||
entry.value = normalized["value"]
|
||||
entry.reason = normalized["reason"]
|
||||
entry.source = "OPERATOR"
|
||||
entry.updated_at = now
|
||||
|
||||
job = Job(agent_id=agent.id, kind="REPROCESS_PRODUCTS", status="PENDING", payload={"codigos": [blocker.product_code]})
|
||||
session.add(job)
|
||||
session.flush()
|
||||
audit = record_audit(
|
||||
session,
|
||||
admin_user_id=admin.id,
|
||||
tenant_id=blocker.tenant_id,
|
||||
action="RESOLVE_BLOCKER_WITH_CONTEXT",
|
||||
target_type="blocker",
|
||||
target_id=str(blocker.id),
|
||||
context={
|
||||
"admin_user_id": admin.id,
|
||||
"tenant_id": blocker.tenant_id,
|
||||
"client": tenant.name if tenant else None,
|
||||
"company_key": company_key,
|
||||
"installation_id": installation.installation_id if installation else None,
|
||||
"installation_db_id": installation.id if installation else None,
|
||||
"product_code": blocker.product_code,
|
||||
"blocker_id": blocker.id,
|
||||
"key": normalized["key"],
|
||||
"scope": normalized["scope"],
|
||||
"scope_key": normalized["scope_key"],
|
||||
"value": normalized["value"],
|
||||
"reason": normalized["reason"],
|
||||
"before": before,
|
||||
"after": _entry_view(entry),
|
||||
"reprocess_job_id": job.id,
|
||||
"request_ip": request_ip,
|
||||
},
|
||||
)
|
||||
session.flush()
|
||||
session.commit()
|
||||
return {"blocker_id": blocker.id, "job_id": job.id, "audit_id": audit.id}
|
||||
except Exception:
|
||||
session.rollback()
|
||||
raise
|
||||
Reference in New Issue
Block a user