dodil/erp-brick-contracts

src/brick_contracts/infrastructure/mail.py

"""Anti-corruption layer: mail.send (capabilities.yaml), never a vendor.

A signing link, a reminder and the executed copy reach people through the mail.send capability. With NO provider in
the composition (the first demo) the platform hands back a ManualTask (rule 8): contracts keeps it as a delivery row
for the owner with the exact recipient and message — WITHOUT the link, because a link is shown once and never stored;
the owner's "copy the signing link" reissues it (CT-52). A provider wired later makes the same delivery automatic:
nothing in a template or a type changes.

idempotency_key = contract_id|signer_no|<kind>|<seq> (CT-52): a retried tick sends one mail.
"""
from dataclasses import dataclass
from typing import Optional

from erp_platform import capabilities as caps

MAIL = caps.CapabilitySeam("mail.send")


@dataclass
class Sent:
    status: str                       # sent | manual | retry | failed
    provider_ref: Optional[str] = None
    message: str = ""
    plan: str = ""


def provider() -> Optional[str]:
    return MAIL.provider()


def send(key: str, *, from_email: str, to: str, subject: str, body: str, actor: str = "") -> Sent:
    data = {"from_email": from_email or "", "to": [to], "cc": [], "subject": subject, "body_text": body,
            "body_html": None}
    try:
        r = MAIL.call(key, data, actor=actor)
    except caps.InvalidCapabilityInput as e:
        return Sent("manual", message=f"cannot be mailed as it stands ({str(e)[:200]}); send it by hand")
    except caps.CapabilityRefused as e:
        return Sent("failed", message=f"mail.send refused the call: {e.reason[:200]}")
    if isinstance(r, caps.ManualTask):
        return Sent("manual", message=r.reason, plan=r.plan)
    if r.status == "done":
        return Sent("sent", r.provider_ref or (r.output or {}).get("message_id"), r.message)
    if r.status in ("in_progress",) or (r.status == "failed" and r.retryable):
        return Sent("retry", message=r.message)
    return Sent("failed", message=r.message)


__all__ = ["MAIL", "send", "provider", "Sent"]