Add 'fiscal/' from commit '102ba84500dbd9655a0471288c3e278652593ca9'
git-subtree-dir: fiscal git-subtree-mainline:d95165fc8fgit-subtree-split:102ba84500
This commit is contained in:
@@ -0,0 +1,368 @@
|
||||
"""Minimal guarded Firebird write executor for the MVP.
|
||||
|
||||
Scope is intentionally narrow: a single allowlisted table/field
|
||||
(C000025.IPI), a single authorized database file, and an optimistic
|
||||
compare-before-write. This is not a generic ORM/executor framework.
|
||||
|
||||
Per CLAUDE.md's "Firebird write policy": writes are only ever permitted
|
||||
against the disposable test database TESTESOFICIAIS.FDB. Any other path,
|
||||
or any table/field outside the allowlist, fails closed before touching
|
||||
the connection.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
AUTHORIZED_WRITE_DATABASE_PATH = Path(r"D:\PROJETOS\GTISISTEMAS\Projeto\TESTESOFICIAIS.FDB")
|
||||
|
||||
# table -> allowed writable fields. Anything not listed here is refused.
|
||||
_FIELD_ALLOWLIST: dict[str, set[str]] = {
|
||||
# C000025.ALIQ_IPI is VARCHAR(1) and unused (0/521 rows) — confirmed via
|
||||
# the vendor's own DDL scripts and live schema inspection to NOT be the
|
||||
# IPI rate field. C000025.IPI NUMERIC(15,2) is. See Mission notes.
|
||||
"C000025": {"IPI"},
|
||||
}
|
||||
# Local completion mission: every field with WRITE_ALLOWED=true in docs/FISCAL_FIELD_CONTRACT_V1.json (kept equal to
|
||||
# central.app.field_contract.WRITE_FIELDS by a test). Still C000025 only; still exact-path authorized.
|
||||
_LOCAL_COMPLETION_FIELDS = {"CLASSIFICACAO_FISCAL", "IPI", "EX_TIPI", "CEST", "SITUACAO_TRIBUTARIA", "CFOP", "CFOP_ENTRADA", "CST", "CST_ENTRADA", "CSOSN", "ALIQUOTA", "COD_ORIGEM_MERCADORIA",
|
||||
"CST_IPI", "COD_ENTRADA_PIS", "COD_SAIDA_PIS", "ALIQ_PIS", "COD_ENTRADA_COFINS", "COD_SAIDA_COFINS", "ALIQ_COFINS", "INCIDENCIA_PISCOFINS", "PISCOFINS", "IAT", "IPPT"}
|
||||
_LOCAL_ALLOWLIST_FILE = Path(__file__).resolve().parents[1] / "docs" / "LOCAL_WRITE_ALLOWLIST_V1.json"
|
||||
|
||||
_KEY_FIELD = "CODIGO"
|
||||
|
||||
|
||||
class WriteRefused(Exception):
|
||||
"""Raised whenever a guard rejects the write before it reaches the database."""
|
||||
|
||||
|
||||
def _normalized_path(path: os.PathLike[str] | str) -> str:
|
||||
return os.path.normcase(os.path.normpath(str(Path(path).resolve(strict=False))))
|
||||
|
||||
|
||||
def assert_authorized_write_path(
|
||||
path: os.PathLike[str] | str,
|
||||
authorized_path: os.PathLike[str] | str | None = None,
|
||||
) -> Path:
|
||||
"""Fail closed unless `path` is exactly the authorized write target.
|
||||
|
||||
`authorized_path` defaults to the development disposable test database
|
||||
(`AUTHORIZED_WRITE_DATABASE_PATH`) for backward compatibility with the
|
||||
dev environment. A pilot/client installation MUST pass its own
|
||||
`authorized_path` explicitly (from that installation's own config,
|
||||
e.g. `AUTHORIZED_WRITE_DATABASE_PATH` env var) — authorization is never
|
||||
inferred from the filename or extension alone, and never defaults to
|
||||
"any .FDB is fine". If a caller sets FIREBIRD_WRITES=1 in a pilot
|
||||
environment without also setting an explicit authorized_path, this
|
||||
still falls back to the dev-only path and therefore fails closed for
|
||||
any real client database, which is the safe behavior.
|
||||
"""
|
||||
candidate = Path(path)
|
||||
authorized = Path(authorized_path) if authorized_path else AUTHORIZED_WRITE_DATABASE_PATH
|
||||
if _normalized_path(candidate) != _normalized_path(authorized):
|
||||
raise WriteRefused(f"path is not the authorized write target: {candidate}")
|
||||
return authorized
|
||||
|
||||
|
||||
def assert_schema_fingerprint_authorized(observed_fingerprint: str | None, expected_fingerprint: str | None) -> None:
|
||||
"""Fail closed unless the live schema fingerprint matches what this
|
||||
installation was registered/configured to expect. Prevents writing
|
||||
against a database whose schema silently changed or was never verified."""
|
||||
if not expected_fingerprint:
|
||||
raise WriteRefused("no expected schema fingerprint configured for this installation")
|
||||
if observed_fingerprint != expected_fingerprint:
|
||||
raise WriteRefused(f"schema fingerprint mismatch: expected {expected_fingerprint!r}, observed {observed_fingerprint!r}")
|
||||
|
||||
|
||||
def assert_allowed_field(table: str, field: str) -> None:
|
||||
allowed = _FIELD_ALLOWLIST.get(table.upper())
|
||||
if not allowed or field.upper() not in allowed:
|
||||
raise WriteRefused(f"{table}.{field} is not in the write allowlist")
|
||||
|
||||
|
||||
def assert_write_eligible(proposal: dict[str, Any]) -> None:
|
||||
if not proposal.get("write_eligible"):
|
||||
raise WriteRefused(
|
||||
f"proposal for {proposal.get('product_code')}/{proposal.get('field')} "
|
||||
f"is not write_eligible (status={proposal.get('status')})"
|
||||
)
|
||||
|
||||
|
||||
def assert_schema_ok(cursor: Any, table: str, field: str, proposed_value: Any = None) -> None:
|
||||
"""Confirm the target column exists and, if it is a fixed-length text field,
|
||||
that the proposed value actually fits without truncation. Never coerce or
|
||||
truncate a value to make it fit."""
|
||||
cursor.execute(
|
||||
"SELECT f.RDB$FIELD_TYPE, f.RDB$FIELD_LENGTH FROM RDB$RELATION_FIELDS rf "
|
||||
"JOIN RDB$FIELDS f ON f.RDB$FIELD_NAME = rf.RDB$FIELD_SOURCE "
|
||||
"WHERE rf.RDB$RELATION_NAME = ? AND rf.RDB$FIELD_NAME = ?",
|
||||
(table, field),
|
||||
)
|
||||
row = cursor.fetchone()
|
||||
if row is None:
|
||||
raise WriteRefused(f"schema guard failed: {table}.{field} not found in live schema")
|
||||
field_type, field_length = row
|
||||
# Firebird character types (CHAR=14, VARCHAR=37): reject a value that would truncate.
|
||||
if proposed_value is not None and field_type in (14, 37) and field_length is not None:
|
||||
if len(str(proposed_value)) > field_length:
|
||||
raise WriteRefused(
|
||||
f"schema guard failed: {table}.{field} is {field_length} char(s) wide, "
|
||||
f"proposed value {proposed_value!r} ({len(str(proposed_value))} chars) would truncate"
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class WriteAuditRecord:
|
||||
product_code: str
|
||||
field: str
|
||||
before_value: Any
|
||||
proposed_value: Any
|
||||
after_value: Any
|
||||
rowcount: int
|
||||
source_dataset: str | None
|
||||
source_version: str | None
|
||||
timestamp: str
|
||||
|
||||
def as_dict(self) -> dict[str, Any]:
|
||||
return {
|
||||
"product_code": self.product_code,
|
||||
"field": self.field,
|
||||
"before_value": self.before_value,
|
||||
"proposed_value": self.proposed_value,
|
||||
"after_value": self.after_value,
|
||||
"rowcount": self.rowcount,
|
||||
"source_dataset": self.source_dataset,
|
||||
"source_version": self.source_version,
|
||||
"timestamp": self.timestamp,
|
||||
}
|
||||
|
||||
|
||||
def execute_guarded_update(
|
||||
connection: Any,
|
||||
*,
|
||||
database_path: os.PathLike[str] | str,
|
||||
proposal: dict[str, Any],
|
||||
table: str = "C000025",
|
||||
key_field: str = _KEY_FIELD,
|
||||
now: str,
|
||||
authorized_path: os.PathLike[str] | str | None = None,
|
||||
) -> WriteAuditRecord:
|
||||
"""Perform one allowlisted, compare-before-write UPDATE and verify it.
|
||||
|
||||
Never commits if any guard fails. Returns an audit record with no secrets.
|
||||
`authorized_path` defaults to the dev disposable test database; a pilot
|
||||
installation must pass its own registered path explicitly.
|
||||
"""
|
||||
assert_authorized_write_path(database_path, authorized_path=authorized_path)
|
||||
assert_write_eligible(proposal)
|
||||
field = proposal["field"]
|
||||
assert_allowed_field(table, field)
|
||||
|
||||
product_code = proposal["product_code"]
|
||||
expected_before = proposal["current_value"]
|
||||
proposed_value = proposal["proposed_value"]
|
||||
if proposed_value is None:
|
||||
raise WriteRefused("refuse to write a null proposed_value")
|
||||
|
||||
cursor = connection.cursor()
|
||||
try:
|
||||
assert_schema_ok(cursor, table, field, proposed_value)
|
||||
|
||||
cursor.execute(
|
||||
f"SELECT {key_field}, {field} FROM {table} WHERE {key_field} = ?",
|
||||
(product_code,),
|
||||
)
|
||||
row = cursor.fetchone()
|
||||
if row is None:
|
||||
raise WriteRefused(f"product {product_code} not found in {table}")
|
||||
live_before = row[1]
|
||||
if live_before != expected_before:
|
||||
raise WriteRefused(
|
||||
f"drift detected for {product_code}.{field}: expected {expected_before!r}, "
|
||||
f"found {live_before!r}"
|
||||
)
|
||||
|
||||
if expected_before is None:
|
||||
sql = f"UPDATE {table} SET {field} = ? WHERE {key_field} = ? AND {field} IS NULL"
|
||||
params = (proposed_value, product_code)
|
||||
else:
|
||||
sql = f"UPDATE {table} SET {field} = ? WHERE {key_field} = ? AND {field} = ?"
|
||||
params = (proposed_value, product_code, expected_before)
|
||||
cursor.execute(sql, params)
|
||||
rowcount = cursor.rowcount
|
||||
if rowcount != 1:
|
||||
connection.rollback()
|
||||
raise WriteRefused(f"expected rowcount 1, got {rowcount}; rolled back")
|
||||
|
||||
connection.commit()
|
||||
|
||||
cursor.execute(f"SELECT {field} FROM {table} WHERE {key_field} = ?", (product_code,))
|
||||
after_row = cursor.fetchone()
|
||||
after_value = after_row[0] if after_row else None
|
||||
if after_value != proposed_value:
|
||||
raise WriteRefused(
|
||||
f"post-write verification failed for {product_code}.{field}: "
|
||||
f"expected {proposed_value!r}, found {after_value!r}"
|
||||
)
|
||||
|
||||
return WriteAuditRecord(
|
||||
product_code=product_code,
|
||||
field=field,
|
||||
before_value=live_before,
|
||||
proposed_value=proposed_value,
|
||||
after_value=after_value,
|
||||
rowcount=rowcount,
|
||||
source_dataset=proposal.get("source_dataset"),
|
||||
source_version=proposal.get("source_version"),
|
||||
timestamp=now,
|
||||
)
|
||||
except Exception:
|
||||
try:
|
||||
connection.rollback()
|
||||
except Exception:
|
||||
pass
|
||||
raise
|
||||
finally:
|
||||
cursor.close()
|
||||
|
||||
|
||||
def execute_guarded_restore(
|
||||
connection: Any,
|
||||
*,
|
||||
database_path: os.PathLike[str] | str,
|
||||
table: str,
|
||||
field: str,
|
||||
key_field: str,
|
||||
product_code: str,
|
||||
written_value: Any,
|
||||
restore_value: Any,
|
||||
now: str,
|
||||
) -> WriteAuditRecord:
|
||||
"""Restore a field to `restore_value`, guarded the same way as the forward write."""
|
||||
assert_authorized_write_path(database_path)
|
||||
assert_allowed_field(table, field)
|
||||
|
||||
cursor = connection.cursor()
|
||||
try:
|
||||
assert_schema_ok(cursor, table, field)
|
||||
cursor.execute(f"SELECT {field} FROM {table} WHERE {key_field} = ?", (product_code,))
|
||||
row = cursor.fetchone()
|
||||
if row is None:
|
||||
raise WriteRefused(f"product {product_code} not found in {table}")
|
||||
if row[0] != written_value:
|
||||
raise WriteRefused(f"drift before restore: expected {written_value!r}, found {row[0]!r}")
|
||||
|
||||
if restore_value is None:
|
||||
sql = f"UPDATE {table} SET {field} = NULL WHERE {key_field} = ? AND {field} = ?"
|
||||
params = (product_code, written_value)
|
||||
else:
|
||||
sql = f"UPDATE {table} SET {field} = ? WHERE {key_field} = ? AND {field} = ?"
|
||||
params = (restore_value, product_code, written_value)
|
||||
cursor.execute(sql, params)
|
||||
rowcount = cursor.rowcount
|
||||
if rowcount != 1:
|
||||
connection.rollback()
|
||||
raise WriteRefused(f"expected rowcount 1 on restore, got {rowcount}; rolled back")
|
||||
connection.commit()
|
||||
|
||||
cursor.execute(f"SELECT {field} FROM {table} WHERE {key_field} = ?", (product_code,))
|
||||
after_row = cursor.fetchone()
|
||||
after_value = after_row[0] if after_row else None
|
||||
if after_value != restore_value:
|
||||
raise WriteRefused(
|
||||
f"post-restore verification failed: expected {restore_value!r}, found {after_value!r}"
|
||||
)
|
||||
|
||||
return WriteAuditRecord(
|
||||
product_code=product_code,
|
||||
field=field,
|
||||
before_value=written_value,
|
||||
proposed_value=restore_value,
|
||||
after_value=after_value,
|
||||
rowcount=rowcount,
|
||||
source_dataset=None,
|
||||
source_version=None,
|
||||
timestamp=now,
|
||||
)
|
||||
except Exception:
|
||||
try:
|
||||
connection.rollback()
|
||||
except Exception:
|
||||
pass
|
||||
raise
|
||||
finally:
|
||||
cursor.close()
|
||||
|
||||
|
||||
def load_local_allowlist(path=None):
|
||||
"""Exact canonical paths authorized for the local completion mission (never by directory/extension)."""
|
||||
import json
|
||||
doc = json.loads(Path(path or _LOCAL_ALLOWLIST_FILE).read_text(encoding="utf-8"))
|
||||
return {os.path.normcase(os.path.abspath(d["path"])) for d in doc["databases"]}
|
||||
|
||||
|
||||
def assert_local_completion_target(database_path, allowlist=None) -> None:
|
||||
allow = allowlist if allowlist is not None else load_local_allowlist()
|
||||
if os.path.normcase(os.path.abspath(str(database_path))) not in allow:
|
||||
raise WriteRefused(f"{database_path} is not in the exact local-completion write allowlist")
|
||||
|
||||
|
||||
def execute_guarded_product_update(connection, *, database_path, product_code, changes: dict, now: str, allowlist=None, table: str = "C000025", key_field: str = _KEY_FIELD, restore_before: bool = False):
|
||||
"""One transaction per product: every field is compare-before-write, ONE UPDATE, rowcount must be 1, then an independent re-read.
|
||||
changes: {field: (expected_before, proposed)}. restore_before=True (rollback of an audited write to its recorded BEFORE) additionally allows a NULL target. Refuses any field outside the local-completion field set, any null proposal,
|
||||
any path outside the exact allowlist, and any value that would truncate. Returns an audit dict (no secrets)."""
|
||||
assert_local_completion_target(database_path, allowlist)
|
||||
if table != "C000025":
|
||||
raise WriteRefused("only C000025 may be written")
|
||||
if not changes:
|
||||
raise WriteRefused("no changes")
|
||||
for field, (_, proposed) in changes.items():
|
||||
if field not in _LOCAL_COMPLETION_FIELDS:
|
||||
raise WriteRefused(f"{table}.{field} is not a contract write target")
|
||||
if proposed is None and not restore_before:
|
||||
raise WriteRefused(f"refuse to write a null proposed_value for {field}")
|
||||
cursor = connection.cursor()
|
||||
try:
|
||||
for field, (_, proposed) in changes.items():
|
||||
if proposed is not None:
|
||||
assert_schema_ok(cursor, table, field, proposed)
|
||||
fields = list(changes)
|
||||
cursor.execute(f"SELECT {', '.join(fields)} FROM {table} WHERE {key_field} = ?", (product_code,))
|
||||
row = cursor.fetchone()
|
||||
if row is None:
|
||||
raise WriteRefused(f"product {product_code} not found in {table}")
|
||||
for field, live in zip(fields, row):
|
||||
expected = changes[field][0]
|
||||
if _norm(live) != _norm(expected):
|
||||
raise WriteRefused(f"drift detected for {product_code}.{field}: expected {expected!r}, found {live!r}")
|
||||
sets = ", ".join(f"{f} = ?" for f in fields)
|
||||
cursor.execute(f"UPDATE {table} SET {sets} WHERE {key_field} = ?", tuple(changes[f][1] for f in fields) + (product_code,))
|
||||
if cursor.rowcount != 1:
|
||||
connection.rollback()
|
||||
raise WriteRefused(f"expected rowcount 1, got {cursor.rowcount}; rolled back")
|
||||
connection.commit()
|
||||
cursor.execute(f"SELECT {', '.join(fields)} FROM {table} WHERE {key_field} = ?", (product_code,))
|
||||
after = cursor.fetchone()
|
||||
bad = [f for f, a in zip(fields, after) if _norm(a) != _norm(changes[f][1])]
|
||||
if bad:
|
||||
raise WriteRefused(f"post-write verification failed for {product_code}: {bad}")
|
||||
return {"product_code": product_code, "rowcount": 1, "fields": {f: {"before": changes[f][0], "proposed": changes[f][1], "after": a} for f, a in zip(fields, after)}, "timestamp": now}
|
||||
except Exception:
|
||||
try:
|
||||
connection.rollback()
|
||||
except Exception:
|
||||
pass
|
||||
raise
|
||||
finally:
|
||||
cursor.close()
|
||||
|
||||
|
||||
def _norm(v):
|
||||
"""Comparable form: text trimmed, numbers as float (Decimal/int/float from the driver), None stays None."""
|
||||
if v is None:
|
||||
return None
|
||||
if isinstance(v, (int, float)) or v.__class__.__name__ == "Decimal":
|
||||
return round(float(v), 6)
|
||||
return str(v).strip()
|
||||
Reference in New Issue
Block a user