Import ClientFlow production v4928.1.5.132.4

This commit is contained in:
plx
2026-07-29 13:11:01 +00:00
parent 6445044ac6
commit 261d342057
405 changed files with 48373 additions and 1401 deletions

View File

@@ -171,6 +171,58 @@ def create_outbox_for_business_event(
return created_ids
def create_email_outbox_item(
*,
task_id: str,
opportunity_id: str = "",
to_email: str,
subject: str,
body: str,
draft_id: str = "",
selected_document_ids: Optional[List[str]] = None,
created_by: str = "operator",
) -> Optional[str]:
"""Create a safe email outbox item from an editable ClientFlow draft.
This does not send automatically. The existing outbox worker will only
process it if an email handler is explicitly implemented/enabled; until
then it remains visible for operator review instead of hiding the draft in
a task page.
"""
import hashlib
to_email = str(to_email or "").strip()
subject = str(subject or "").strip() or "Seguimento do processo"
body = str(body or "").strip()
if not to_email:
raise ValueError("Email do destinatário em falta.")
if not body:
raise ValueError("Mensagem vazia.")
fingerprint = hashlib.sha256((to_email + "\n" + subject + "\n" + body).encode("utf-8")).hexdigest()[:16]
idempotency_key = ":".join(["email_draft", str(task_id or ""), str(draft_id or "no_draft"), fingerprint])
payload = {
"channel": "email",
"mode": "operator_review_required",
"to": to_email,
"subject": subject,
"body": body,
"task_id": task_id,
"opportunity_id": opportunity_id or None,
"draft_id": draft_id or None,
"selected_document_ids": selected_document_ids or [],
"created_by": created_by,
"send_automatically": False,
}
return create_outbox_item(
business_event_id=task_id,
target_system="email",
action_type="send_email",
payload=payload,
idempotency_key=idempotency_key,
)
def list_outbox(
*,
status: Optional[str] = None,
@@ -272,13 +324,21 @@ def claim_pending_outbox(
This prevents two systemd timers/workers from processing the same pending
integration item at the same time. PostgreSQL SKIP LOCKED lets concurrent
workers take different rows without blocking each other.
Build the optional target filter explicitly instead of using
``(:target_system IS NULL OR target_system = :target_system)``. With
PostgreSQL + psycopg3, that expression can fail before execution with
``AmbiguousParameter: could not determine data type of parameter`` because
the bind parameter is used in an ``IS NULL`` expression.
"""
sql = text("""
target_system = str(target_system).strip() if target_system else None
target_filter = "AND target_system = :target_system" if target_system else ""
sql = text(f"""
WITH picked AS (
SELECT id
FROM integration_outbox
WHERE status = 'pending'
AND (:target_system IS NULL OR target_system = :target_system)
{target_filter}
ORDER BY created_at ASC
FOR UPDATE SKIP LOCKED
LIMIT :limit
@@ -308,12 +368,15 @@ def claim_pending_outbox(
io.locked_at,
io.lock_owner
""")
params = {
"limit": int(limit),
"lock_owner": str(lock_owner or "worker")[:120],
}
if target_system:
params["target_system"] = target_system
with engine.begin() as conn:
rows = conn.execute(sql, {
"limit": int(limit),
"target_system": target_system,
"lock_owner": str(lock_owner or "worker")[:120],
}).mappings().all()
rows = conn.execute(sql, params).mappings().all()
return [dict(row) for row in rows]