Import ClientFlow production v4928.1.5.132.4
This commit is contained in:
@@ -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]
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user