Initial commit

This commit is contained in:
ai_ml_operations
2026-09-06 04:27:41 -07:00
commit 8a97e3fbe2
302 changed files with 34038 additions and 0 deletions
@@ -0,0 +1,42 @@
"""Pluggable postcard providers."""
from dataclasses import dataclass
from typing import Protocol
from django.conf import settings
@dataclass
class PostcardResult:
provider_id: str
class PostcardProvider(Protocol):
name: str
def send_postcard(self, message) -> PostcardResult: ...
def get_status(self, provider_id: str) -> str: ...
def get_postcard_provider() -> PostcardProvider:
name = (settings.POSTCARD_PROVIDER or "pcm").lower()
if name == "pcm":
from directmail.providers.postcard.pcm import PcmProvider
return PcmProvider()
if name == "click2mail":
from directmail.providers.postcard.click2mail import Click2MailProvider
return Click2MailProvider()
if name == "postgrid":
from directmail.providers.postcard.postgrid import PostGridProvider
return PostGridProvider()
if name == "lob":
from directmail.providers.postcard.lob import LobProvider
return LobProvider()
from directmail.providers.postcard.pcm import PcmProvider
return PcmProvider()
@@ -0,0 +1,23 @@
"""Click2Mail postcard adapter (low-volume pay-per-piece option)."""
from dataclasses import dataclass
from django.conf import settings
from directmail.providers.postcard import PostcardResult
@dataclass
class Click2MailProvider:
name: str = "click2mail"
def send_postcard(self, message) -> PostcardResult:
if not settings.CLICK2MAIL_API_KEY:
raise RuntimeError("CLICK2MAIL_API_KEY is not configured")
# Placeholder: wire full Click2Mail job API when account credentials are ready.
raise NotImplementedError(
"Click2Mail adapter stub — configure account then implement job submit"
)
def get_status(self, provider_id: str) -> str:
return "unknown"
+66
View File
@@ -0,0 +1,66 @@
"""Lob postcard adapter (default)."""
from dataclasses import dataclass
import requests
from django.conf import settings
from directmail.providers.postcard import PostcardResult
@dataclass
class LobProvider:
name: str = "lob"
def send_postcard(self, message) -> PostcardResult:
api_key = settings.LOB_API_KEY
if not api_key:
raise RuntimeError("LOB_API_KEY is not configured")
contact = message.contact
address = contact.postal_address or {}
if not address.get("line1"):
raise ValueError("Contact postal_address.line1 required for postcard")
# Minimal Lob create-postcard payload; artwork URLs come from template JSON.
template = message.campaign.template
front = (template.postcard_front if template else {}) or {}
back = (template.postcard_back if template else {}) or {}
payload = {
"description": f"campaign-{message.campaign_id}",
"to": {
"name": contact.full_name or contact.email or "Resident",
"address_line1": address.get("line1", ""),
"address_line2": address.get("line2", ""),
"address_city": address.get("city", ""),
"address_state": address.get("state", ""),
"address_zip": address.get("zip", ""),
"address_country": address.get("country", "US"),
},
"front": front.get("html") or front.get("url") or "<html></html>",
"back": back.get("html") or back.get("url") or "<html></html>",
}
response = requests.post(
"https://api.lob.com/v1/postcards",
json=payload,
auth=(api_key, ""),
timeout=60,
headers={"Idempotency-Key": str(message.pk)},
)
response.raise_for_status()
data = response.json()
return PostcardResult(provider_id=str(data.get("id") or message.pk))
def get_status(self, provider_id: str) -> str:
api_key = settings.LOB_API_KEY
if not api_key:
return "unknown"
response = requests.get(
f"https://api.lob.com/v1/postcards/{provider_id}",
auth=(api_key, ""),
timeout=30,
)
if not response.ok:
return "unknown"
return str(response.json().get("status") or "unknown")
+422
View File
@@ -0,0 +1,422 @@
"""PCM Integrations (DirectMail API v3) postcard adapter."""
from __future__ import annotations
import json
import logging
import threading
from dataclasses import dataclass
from datetime import datetime, timedelta, timezone
from typing import Any
import requests
from django.conf import settings
from directmail.providers.postcard import PostcardResult
logger = logging.getLogger(__name__)
PCM_API_BASE = "https://v3.pcmintegrations.com"
# Refresh a bit before PCM's expires timestamp.
_TOKEN_SKEW = timedelta(seconds=60)
# PCM size codes for custom designer designs.
PCM_SIZE_CHOICES = (
("46", "4.25 × 6"),
("68", "6 × 8.5"),
("69", "6 × 9"),
("611", "6 × 11"),
("811", "8.5 × 11"),
)
class PcmApiError(RuntimeError):
"""Raised when a PCM API call fails."""
_token_lock = threading.Lock()
_cached_token: str | None = None
_cached_expires: datetime | None = None
def _api_key() -> str:
return (settings.PCM_API_KEY or "").strip()
def _api_secret() -> str:
return (settings.PCM_API_SECRET or "").strip()
def _parse_expires(raw: Any) -> datetime:
"""Parse PCM expires timestamp; default to 55 minutes from now."""
if isinstance(raw, (int, float)):
# Unix seconds vs ms
ts = float(raw)
if ts > 1e12:
ts /= 1000.0
return datetime.fromtimestamp(ts, tz=timezone.utc)
if isinstance(raw, str) and raw.strip():
text = raw.strip().replace("Z", "+00:00")
try:
dt = datetime.fromisoformat(text)
except ValueError:
dt = None
if dt is not None:
if dt.tzinfo is None:
dt = dt.replace(tzinfo=timezone.utc)
return dt.astimezone(timezone.utc)
return datetime.now(timezone.utc) + timedelta(minutes=55)
def clear_token_cache() -> None:
"""Drop cached bearer token (tests / forced re-login)."""
global _cached_token, _cached_expires
with _token_lock:
_cached_token = None
_cached_expires = None
def login(*, force: bool = False) -> str:
"""POST /auth/login with apiKey + apiSecret → bearer token.
See https://docs.pcmintegrations.com/docs/directmail-api/ffef03a112bb0-logging-in
"""
global _cached_token, _cached_expires
with _token_lock:
now = datetime.now(timezone.utc)
if (
not force
and _cached_token
and _cached_expires
and now < (_cached_expires - _TOKEN_SKEW)
):
return _cached_token
key = _api_key()
secret = _api_secret()
if not key or not secret:
raise PcmApiError(
"PCM_API_KEY and PCM_API_SECRET are required "
"(POST /auth/login, then use the returned token)"
)
body: dict[str, str] = {"apiKey": key, "apiSecret": secret}
child = (getattr(settings, "PCM_CHILD_REF_NBR", None) or "").strip()
if child:
body["childRefNbr"] = child
response = requests.post(
f"{PCM_API_BASE}/auth/login",
headers={
"Accept": "application/json",
"Content-Type": "application/json",
},
json=body,
timeout=30,
)
if response.status_code >= 400:
detail = (response.text or "")[:500]
raise PcmApiError(
f"PCM POST /auth/login → {response.status_code}: {detail}"
)
try:
data = response.json()
except ValueError as exc:
raise PcmApiError("PCM login returned non-JSON") from exc
if not isinstance(data, dict):
raise PcmApiError("PCM login returned unexpected payload")
token = str(data.get("token") or "").strip()
if not token:
raise PcmApiError("PCM login response missing token")
_cached_token = token
_cached_expires = _parse_expires(data.get("expires"))
logger.info(
"PCM login ok; token expires %s",
_cached_expires.isoformat(),
)
return token
def _headers(*, force_login: bool = False) -> dict[str, str]:
token = login(force=force_login)
return {
"Accept": "application/json",
"Authorization": f"Bearer {token}",
"Content-Type": "application/json",
}
def pcm_request(
method: str,
path: str,
*,
params: dict[str, Any] | None = None,
json_body: dict[str, Any] | None = None,
timeout: int = 60,
) -> Any:
"""Call PCM v3 API. ``path`` is absolute under the API host (e.g. ``/design``)."""
url = f"{PCM_API_BASE}{path}"
response = requests.request(
method,
url,
headers=_headers(),
params=params,
json=json_body,
timeout=timeout,
)
# Expired/revoked session → login once and retry.
if response.status_code == 401:
clear_token_cache()
response = requests.request(
method,
url,
headers=_headers(force_login=True),
params=params,
json=json_body,
timeout=timeout,
)
if response.status_code >= 400:
detail = (response.text or "")[:500]
raise PcmApiError(
f"PCM {method} {path}{response.status_code}: {detail}"
)
if not response.content:
return {}
try:
return response.json()
except ValueError:
return {"raw": response.text}
def return_address_from_settings() -> dict[str, str]:
"""Build PCM returnAddress from PCM_RETURN_ADDRESS JSON and/or PCM_RETURN_* vars.
PCM requires address/city/state (and zip) whenever firstName is set — an empty
street with SITE_NAME-derived firstName causes a 400 from POST /order/postcard.
"""
data: dict[str, Any] = {}
raw = (settings.PCM_RETURN_ADDRESS or "").strip()
if raw:
try:
parsed = json.loads(raw)
except json.JSONDecodeError as exc:
raise PcmApiError(
"PCM_RETURN_ADDRESS must be valid JSON "
'(e.g. {"firstName":"","lastName":"","address":"",'
'"city":"","state":"IL","zipCode":""})'
) from exc
if not isinstance(parsed, dict):
raise PcmApiError("PCM_RETURN_ADDRESS must be a JSON object")
data = parsed
name = (settings.SITE_NAME or "").strip()
parts = name.split(None, 1)
site_first = parts[0] if parts else ""
site_last = parts[1] if len(parts) > 1 else ""
address = {
"company": str(data.get("company") or "").strip(),
"firstName": str(
data.get("firstName") or data.get("first_name") or site_first or ""
).strip(),
"lastName": str(
data.get("lastName") or data.get("last_name") or site_last or ""
).strip(),
"address": str(
data.get("address")
or data.get("line1")
or getattr(settings, "PCM_RETURN_LINE1", "")
or ""
).strip(),
# PCM treats blank address2 oddly; space matches recipient payload.
"address2": str(
data.get("address2")
or data.get("line2")
or getattr(settings, "PCM_RETURN_LINE2", "")
or ""
).strip()
or " ",
"city": str(
data.get("city") or getattr(settings, "PCM_RETURN_CITY", "") or ""
).strip(),
"state": str(
data.get("state") or getattr(settings, "PCM_RETURN_STATE", "") or ""
).strip(),
"zipCode": str(
data.get("zipCode")
or data.get("zip")
or getattr(settings, "PCM_RETURN_ZIP", "")
or ""
).strip(),
}
missing = [
field
for field in ("firstName", "address", "city", "state", "zipCode")
if not address.get(field, "").strip()
]
if missing:
raise PcmApiError(
"PCM return address incomplete (missing "
+ ", ".join(missing)
+ "). Set PCM_RETURN_ADDRESS JSON with firstName, lastName, "
"address, city, state, zipCode "
"(or PCM_RETURN_LINE1 / CITY / STATE / ZIP)."
)
return address
def contact_to_pcm_recipient(contact, *, ext_ref: str) -> dict[str, str]:
address = contact.postal_address or {}
line1 = (address.get("line1") or "").strip()
if not line1:
raise ValueError("Contact postal_address.line1 required for postcard")
first = (contact.first_name or "").strip()
last = (contact.last_name or "").strip()
if not first and not last:
# PCM requires name or company.
first = (contact.full_name or contact.email or "Resident").strip()
return {
"firstName": first,
"lastName": last,
"address": line1,
"address2": (address.get("line2") or "").strip() or " ",
"city": (address.get("city") or "").strip(),
"state": (address.get("state") or "").strip(),
"zipCode": (address.get("zip") or "").strip(),
"extRefNbr": ext_ref,
}
def list_designs(*, product_type: str = "postcard", page: int = 1, per_page: int = 50) -> list[dict]:
data = pcm_request(
"GET",
"/design",
params={
"productType": product_type,
"page": page,
"perPage": per_page,
},
)
if isinstance(data, dict):
results = data.get("results") or data.get("designs") or []
return results if isinstance(results, list) else []
return []
def create_custom_design(*, name: str, size: str) -> dict[str, Any]:
"""POST /design/custom → designID + embed url."""
return pcm_request(
"POST",
"/design/custom",
json_body={"name": name, "size": size},
)
def get_design_embed_url(design_id: int | str, *, duplicate: bool = False) -> str:
"""GET /design/{id}/edit?mode=embed → iframe URL."""
params: dict[str, Any] = {"mode": "embed"}
if duplicate:
params["duplicate"] = "true"
data = pcm_request("GET", f"/design/{design_id}/edit", params=params)
if not isinstance(data, dict):
raise PcmApiError("Unexpected embed response from PCM")
url = data.get("embed_url") or data.get("url") or ""
if not url:
raise PcmApiError("PCM did not return an embed URL")
return str(url)
def get_order(order_id: int | str) -> dict[str, Any]:
data = pcm_request("GET", f"/order/{order_id}")
return data if isinstance(data, dict) else {}
def place_postcard_order(
*,
design_id: int,
recipient: dict[str, str],
ext_ref: str,
mail_class: str = "FirstClass",
) -> str:
"""Place a one-recipient postcard order; return PCM orderID as string."""
payload = {
"designID": design_id,
"mailClass": mail_class,
"extRefNbr": ext_ref,
"returnAddress": return_address_from_settings(),
"recipients": [recipient],
}
# Product-specific path — plain POST /order is 404 ("Cannot POST /order").
# Docs: Place Postcard Order → POST /order/postcard
data = pcm_request("POST", "/order/postcard", json_body=payload)
if not isinstance(data, dict):
raise PcmApiError("Unexpected order response from PCM")
order_id = data.get("orderID") or data.get("orderId") or data.get("id")
if order_id is None and isinstance(data.get("results"), list) and data["results"]:
order_id = data["results"][0].get("orderID")
if order_id is None:
raise PcmApiError(f"PCM order response missing orderID: {data!r}"[:400])
return str(order_id)
def design_id_from_template(template) -> int | None:
"""Read design_id from MessageTemplate.postcard_front JSON."""
if not template:
return None
front = template.postcard_front or {}
if not isinstance(front, dict):
return None
raw = front.get("design_id") or front.get("designID")
if raw is None:
return None
try:
return int(raw)
except (TypeError, ValueError):
return None
@dataclass
class PcmProvider:
name: str = "pcm"
def send_postcard(self, message) -> PostcardResult:
template = message.campaign.template if message.campaign_id else None
design_id = design_id_from_template(template)
if not design_id:
raise ValueError(
"Postcard campaign template missing PCM design_id "
"(save a design from the postcard designer first)"
)
recipient = contact_to_pcm_recipient(
message.contact, ext_ref=str(message.pk)
)
mail_class = "FirstClass"
if template and isinstance(template.postcard_front, dict):
mail_class = (
template.postcard_front.get("mail_class") or mail_class
)
order_id = place_postcard_order(
design_id=design_id,
recipient=recipient,
ext_ref=str(message.pk),
mail_class=str(mail_class),
)
return PostcardResult(provider_id=order_id)
def get_status(self, provider_id: str) -> str:
try:
data = get_order(provider_id)
except PcmApiError:
logger.exception("PCM get_status failed for %s", provider_id)
return "unknown"
return str(data.get("status") or "unknown")
@@ -0,0 +1,22 @@
"""PostGrid postcard adapter."""
from dataclasses import dataclass
from django.conf import settings
from directmail.providers.postcard import PostcardResult
@dataclass
class PostGridProvider:
name: str = "postgrid"
def send_postcard(self, message) -> PostcardResult:
if not settings.POSTGRID_API_KEY:
raise RuntimeError("POSTGRID_API_KEY is not configured")
raise NotImplementedError(
"PostGrid adapter stub — configure account then implement send"
)
def get_status(self, provider_id: str) -> str:
return "unknown"