📨 feat(integrations): escalar entregas do Brevo com destinatario dinamico
Consolidar as rotas de email por evento em um modelo global dinamico, resolvendo o destinatario a partir do cadastro do usuario e registrando recipient_email e recipient_name em cada entrega do outbox para melhorar rastreabilidade e operacao. Permitir captura opcional de email no Telegram, salvar o endereco no cadastro do usuario e reaproveitar esse dado em revisao, pedido e aluguel, incluindo prompts de consentimento e reenvio imediato do resumo apos a confirmacao. Ampliar a configuracao do provider Brevo e dos scripts operacionais com sender por rota, reply-to, cc, bcc, tags, headers, listagem de rotas e entregas, alem de migracoes de bootstrap e cobertura automatizada validada com 100 testes OK.main
parent
6837f00609
commit
31d02a7daa
@ -0,0 +1,34 @@
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
||||
if str(PROJECT_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(PROJECT_ROOT))
|
||||
|
||||
from app.services.integrations.events import SUPPORTED_EVENT_TYPES
|
||||
from app.services.integrations.service import SUPPORTED_DELIVERY_STATUSES, list_integration_deliveries
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description="Lista entregas do outbox de integracoes com filtros operacionais.")
|
||||
parser.add_argument("--status", action="append", choices=SUPPORTED_DELIVERY_STATUSES)
|
||||
parser.add_argument("--event", choices=SUPPORTED_EVENT_TYPES)
|
||||
parser.add_argument("--provider")
|
||||
parser.add_argument("--route-id", type=int)
|
||||
parser.add_argument("--limit", type=int, default=50)
|
||||
args = parser.parse_args()
|
||||
|
||||
deliveries = list_integration_deliveries(
|
||||
statuses=args.status,
|
||||
event_type=args.event,
|
||||
provider=args.provider,
|
||||
route_id=args.route_id,
|
||||
limit=args.limit,
|
||||
)
|
||||
print(json.dumps(deliveries, ensure_ascii=True, indent=2, sort_keys=True))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@ -0,0 +1,38 @@
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
||||
if str(PROJECT_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(PROJECT_ROOT))
|
||||
|
||||
from app.services.integrations.events import SUPPORTED_EVENT_TYPES
|
||||
from app.services.integrations.service import list_integration_routes
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description="Lista rotas de integracao configuradas.")
|
||||
parser.add_argument("--event", choices=SUPPORTED_EVENT_TYPES)
|
||||
parser.add_argument("--provider")
|
||||
enabled_group = parser.add_mutually_exclusive_group()
|
||||
enabled_group.add_argument("--enabled", action="store_true")
|
||||
enabled_group.add_argument("--disabled", action="store_true")
|
||||
args = parser.parse_args()
|
||||
|
||||
enabled = None
|
||||
if args.enabled:
|
||||
enabled = True
|
||||
if args.disabled:
|
||||
enabled = False
|
||||
|
||||
routes = list_integration_routes(
|
||||
event_type=args.event,
|
||||
provider=args.provider,
|
||||
enabled=enabled,
|
||||
)
|
||||
print(json.dumps(routes, ensure_ascii=True, indent=2, sort_keys=True))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@ -1,20 +1,56 @@
|
||||
import argparse
|
||||
import asyncio
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from app.services.integrations.service import process_pending_deliveries
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
||||
if str(PROJECT_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(PROJECT_ROOT))
|
||||
|
||||
from app.services.integrations.events import SUPPORTED_EVENT_TYPES
|
||||
from app.services.integrations.service import SUPPORTED_DELIVERY_STATUSES, process_pending_deliveries
|
||||
|
||||
async def _main_async(limit: int) -> None:
|
||||
deliveries = await process_pending_deliveries(limit=limit)
|
||||
|
||||
async def _main_async(
|
||||
*,
|
||||
limit: int,
|
||||
delivery_ids: list[int] | None,
|
||||
statuses: list[str] | None,
|
||||
event_type: str | None,
|
||||
provider: str | None,
|
||||
route_id: int | None,
|
||||
) -> None:
|
||||
deliveries = await process_pending_deliveries(
|
||||
limit=limit,
|
||||
delivery_ids=delivery_ids,
|
||||
statuses=statuses,
|
||||
event_type=event_type,
|
||||
provider=provider,
|
||||
route_id=route_id,
|
||||
)
|
||||
print(json.dumps(deliveries, ensure_ascii=True, indent=2, sort_keys=True))
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description="Processa entregas pendentes do outbox de integracoes.")
|
||||
parser.add_argument("--limit", type=int, default=20)
|
||||
parser.add_argument("--delivery-id", dest="delivery_ids", type=int, action="append")
|
||||
parser.add_argument("--status", action="append", choices=SUPPORTED_DELIVERY_STATUSES)
|
||||
parser.add_argument("--event", choices=SUPPORTED_EVENT_TYPES)
|
||||
parser.add_argument("--provider")
|
||||
parser.add_argument("--route-id", type=int)
|
||||
args = parser.parse_args()
|
||||
asyncio.run(_main_async(limit=args.limit))
|
||||
asyncio.run(
|
||||
_main_async(
|
||||
limit=args.limit,
|
||||
delivery_ids=args.delivery_ids,
|
||||
statuses=args.status,
|
||||
event_type=args.event,
|
||||
provider=args.provider,
|
||||
route_id=args.route_id,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@ -0,0 +1,100 @@
|
||||
import os
|
||||
import unittest
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
os.environ.setdefault("DEBUG", "false")
|
||||
|
||||
from app.core.settings import settings
|
||||
from app.services.integrations.providers import BrevoEmailProvider
|
||||
|
||||
|
||||
class _FakeResponse:
|
||||
def __init__(self, *, status_code: int = 201, payload: dict | None = None, text: str = "") -> None:
|
||||
self.status_code = status_code
|
||||
self._payload = payload or {}
|
||||
self.text = text
|
||||
|
||||
def json(self) -> dict:
|
||||
return self._payload
|
||||
|
||||
|
||||
class BrevoEmailProviderTests(unittest.IsolatedAsyncioTestCase):
|
||||
async def test_send_email_applies_provider_config_to_payload(self):
|
||||
response = _FakeResponse(payload={"messageId": "brevo-123"}, text='{"messageId":"brevo-123"}')
|
||||
post_mock = AsyncMock(return_value=response)
|
||||
fake_client = type("FakeClient", (), {"post": post_mock})()
|
||||
|
||||
with patch.object(settings, "brevo_api_key", "brevo-key"), patch.object(
|
||||
settings,
|
||||
"brevo_sender_email",
|
||||
"sender@empresa.com",
|
||||
), patch.object(settings, "brevo_sender_name", "Orquestrador"), patch(
|
||||
"app.services.integrations.providers.httpx.AsyncClient"
|
||||
) as client_cls:
|
||||
client_cls.return_value.__aenter__ = AsyncMock(return_value=fake_client)
|
||||
client_cls.return_value.__aexit__ = AsyncMock(return_value=None)
|
||||
provider = BrevoEmailProvider()
|
||||
result = await provider.send_email(
|
||||
to_email="destinatario@empresa.com",
|
||||
to_name="Operacoes",
|
||||
subject="Pedido criado",
|
||||
body="Pedido PED-1 criado com sucesso.",
|
||||
tags=["order.created"],
|
||||
provider_config={
|
||||
"sender": {"email": "noreply@empresa.com", "name": "Operacoes"},
|
||||
"reply_to": {"email": "atendimento@empresa.com", "name": "Atendimento"},
|
||||
"cc": ["financeiro@empresa.com", {"email": "gestor@empresa.com", "name": "Gestor"}],
|
||||
"bcc": ["auditoria@empresa.com"],
|
||||
"tags": ["ops", "order.created"],
|
||||
"headers": {"X-Canal": "orquestrador"},
|
||||
"html_content": "<strong>Pedido PED-1 criado com sucesso.</strong>",
|
||||
},
|
||||
)
|
||||
|
||||
payload = post_mock.await_args.kwargs["json"]
|
||||
self.assertEqual(payload["sender"], {"email": "noreply@empresa.com", "name": "Operacoes"})
|
||||
self.assertEqual(payload["replyTo"], {"email": "atendimento@empresa.com", "name": "Atendimento"})
|
||||
self.assertEqual(
|
||||
payload["cc"],
|
||||
[
|
||||
{"email": "financeiro@empresa.com"},
|
||||
{"email": "gestor@empresa.com", "name": "Gestor"},
|
||||
],
|
||||
)
|
||||
self.assertEqual(payload["bcc"], [{"email": "auditoria@empresa.com"}])
|
||||
self.assertEqual(payload["tags"], ["order.created", "ops"])
|
||||
self.assertEqual(payload["headers"], {"X-Canal": "orquestrador"})
|
||||
self.assertEqual(payload["htmlContent"], "<strong>Pedido PED-1 criado com sucesso.</strong>")
|
||||
self.assertEqual(result["message_id"], "brevo-123")
|
||||
|
||||
async def test_send_email_accepts_route_sender_without_global_sender_email(self):
|
||||
response = _FakeResponse(payload={"messageId": "brevo-456"}, text='{"messageId":"brevo-456"}')
|
||||
post_mock = AsyncMock(return_value=response)
|
||||
fake_client = type("FakeClient", (), {"post": post_mock})()
|
||||
|
||||
with patch.object(settings, "brevo_api_key", "brevo-key"), patch.object(
|
||||
settings,
|
||||
"brevo_sender_email",
|
||||
"",
|
||||
), patch.object(settings, "brevo_sender_name", "Orquestrador"), patch(
|
||||
"app.services.integrations.providers.httpx.AsyncClient"
|
||||
) as client_cls:
|
||||
client_cls.return_value.__aenter__ = AsyncMock(return_value=fake_client)
|
||||
client_cls.return_value.__aexit__ = AsyncMock(return_value=None)
|
||||
provider = BrevoEmailProvider()
|
||||
await provider.send_email(
|
||||
to_email="destinatario@empresa.com",
|
||||
to_name=None,
|
||||
subject="Pedido criado",
|
||||
body="Pedido PED-1 criado com sucesso.",
|
||||
provider_config={
|
||||
"sender": {"email": "noreply@empresa.com", "name": "Operacoes"},
|
||||
},
|
||||
)
|
||||
|
||||
payload = post_mock.await_args.kwargs["json"]
|
||||
self.assertEqual(payload["sender"], {"email": "noreply@empresa.com", "name": "Operacoes"})
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Loading…
Reference in New Issue