Require a complete PCM return address before placing orders.
Empty PCM_RETURN_ADDRESS was sending SITE_NAME as firstName with blank street fields, which PCM rejects with 400; validate early and wire PCM env through compose.
This commit is contained in:
+3
-2
@@ -57,8 +57,9 @@ PCM_API_SECRET=
|
|||||||
# Paste all of them comma-separated (also accepts single PCM_WEBHOOK_SECRET=).
|
# Paste all of them comma-separated (also accepts single PCM_WEBHOOK_SECRET=).
|
||||||
PCM_WEBHOOK_SECRETS=
|
PCM_WEBHOOK_SECRETS=
|
||||||
# PCM_WEBHOOK_SECRET=
|
# PCM_WEBHOOK_SECRET=
|
||||||
# Return address on every postcard order (JSON). Example:
|
# Required return address on every postcard order.
|
||||||
# PCM_RETURN_ADDRESS={"firstName":"Monica","lastName":"Dhillon","address":"123 Main St","city":"Naperville","state":"IL","zipCode":"60540"}
|
# Quote the JSON (compose/shell break on bare {…, …}).
|
||||||
|
# PCM_RETURN_ADDRESS='{"firstName":"Monica","lastName":"Dhillon","address":"123 Main St","city":"Naperville","state":"IL","zipCode":"60540"}'
|
||||||
PCM_RETURN_ADDRESS=
|
PCM_RETURN_ADDRESS=
|
||||||
# Or set fields individually if JSON is empty:
|
# Or set fields individually if JSON is empty:
|
||||||
# PCM_RETURN_LINE1=
|
# PCM_RETURN_LINE1=
|
||||||
|
|||||||
+2
-2
@@ -66,8 +66,8 @@ PCM_API_SECRET=replace-me
|
|||||||
PCM_WEBHOOK_SECRETS=replace-me,replace-me-2
|
PCM_WEBHOOK_SECRETS=replace-me,replace-me-2
|
||||||
# PCM_WEBHOOK_SECRET=replace-me # optional single-secret alias
|
# PCM_WEBHOOK_SECRET=replace-me # optional single-secret alias
|
||||||
|
|
||||||
# JSON return address for postcard orders
|
# JSON return address for postcard orders (quote so compose keeps commas)
|
||||||
PCM_RETURN_ADDRESS={"firstName":"Monica","lastName":"Dhillon","address":"replace-me","city":"replace-me","state":"IL","zipCode":"replace-me"}
|
PCM_RETURN_ADDRESS='{"firstName":"Monica","lastName":"Dhillon","address":"replace-me","city":"replace-me","state":"IL","zipCode":"replace-me"}'
|
||||||
# LOB_API_KEY= # only if POSTCARD_PROVIDER=lob
|
# LOB_API_KEY= # only if POSTCARD_PROVIDER=lob
|
||||||
# CLICK2MAIL_API_KEY=
|
# CLICK2MAIL_API_KEY=
|
||||||
# POSTGRID_API_KEY=
|
# POSTGRID_API_KEY=
|
||||||
|
|||||||
@@ -45,6 +45,19 @@ services:
|
|||||||
EMAIL_BACKEND: ${EMAIL_BACKEND:-django.core.mail.backends.console.EmailBackend}
|
EMAIL_BACKEND: ${EMAIL_BACKEND:-django.core.mail.backends.console.EmailBackend}
|
||||||
SMTP2GO_SMS_API_KEY: ${SMTP2GO_SMS_API_KEY:-}
|
SMTP2GO_SMS_API_KEY: ${SMTP2GO_SMS_API_KEY:-}
|
||||||
SMTP2GO_WEBHOOK_SECRET: ${SMTP2GO_WEBHOOK_SECRET:-}
|
SMTP2GO_WEBHOOK_SECRET: ${SMTP2GO_WEBHOOK_SECRET:-}
|
||||||
|
POSTCARD_PROVIDER: ${POSTCARD_PROVIDER:-pcm}
|
||||||
|
PCM_API_KEY: ${PCM_API_KEY:-}
|
||||||
|
PCM_API_SECRET: ${PCM_API_SECRET:-}
|
||||||
|
PCM_CHILD_REF_NBR: ${PCM_CHILD_REF_NBR:-}
|
||||||
|
PCM_WEBHOOK_SECRETS: ${PCM_WEBHOOK_SECRETS:-}
|
||||||
|
PCM_WEBHOOK_SECRET: ${PCM_WEBHOOK_SECRET:-}
|
||||||
|
# Quote JSON in `.env`: PCM_RETURN_ADDRESS='{"firstName":"…",...}'
|
||||||
|
PCM_RETURN_ADDRESS: ${PCM_RETURN_ADDRESS:-}
|
||||||
|
PCM_RETURN_LINE1: ${PCM_RETURN_LINE1:-}
|
||||||
|
PCM_RETURN_LINE2: ${PCM_RETURN_LINE2:-}
|
||||||
|
PCM_RETURN_CITY: ${PCM_RETURN_CITY:-}
|
||||||
|
PCM_RETURN_STATE: ${PCM_RETURN_STATE:-}
|
||||||
|
PCM_RETURN_ZIP: ${PCM_RETURN_ZIP:-}
|
||||||
depends_on:
|
depends_on:
|
||||||
db:
|
db:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
@@ -70,6 +83,18 @@ services:
|
|||||||
EMAIL_BACKEND: ${EMAIL_BACKEND:-django.core.mail.backends.console.EmailBackend}
|
EMAIL_BACKEND: ${EMAIL_BACKEND:-django.core.mail.backends.console.EmailBackend}
|
||||||
SMTP2GO_SMS_API_KEY: ${SMTP2GO_SMS_API_KEY:-}
|
SMTP2GO_SMS_API_KEY: ${SMTP2GO_SMS_API_KEY:-}
|
||||||
SMTP2GO_WEBHOOK_SECRET: ${SMTP2GO_WEBHOOK_SECRET:-}
|
SMTP2GO_WEBHOOK_SECRET: ${SMTP2GO_WEBHOOK_SECRET:-}
|
||||||
|
POSTCARD_PROVIDER: ${POSTCARD_PROVIDER:-pcm}
|
||||||
|
PCM_API_KEY: ${PCM_API_KEY:-}
|
||||||
|
PCM_API_SECRET: ${PCM_API_SECRET:-}
|
||||||
|
PCM_CHILD_REF_NBR: ${PCM_CHILD_REF_NBR:-}
|
||||||
|
PCM_WEBHOOK_SECRETS: ${PCM_WEBHOOK_SECRETS:-}
|
||||||
|
PCM_WEBHOOK_SECRET: ${PCM_WEBHOOK_SECRET:-}
|
||||||
|
PCM_RETURN_ADDRESS: ${PCM_RETURN_ADDRESS:-}
|
||||||
|
PCM_RETURN_LINE1: ${PCM_RETURN_LINE1:-}
|
||||||
|
PCM_RETURN_LINE2: ${PCM_RETURN_LINE2:-}
|
||||||
|
PCM_RETURN_CITY: ${PCM_RETURN_CITY:-}
|
||||||
|
PCM_RETURN_STATE: ${PCM_RETURN_STATE:-}
|
||||||
|
PCM_RETURN_ZIP: ${PCM_RETURN_ZIP:-}
|
||||||
depends_on:
|
depends_on:
|
||||||
db:
|
db:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
|
|||||||
@@ -193,38 +193,82 @@ def pcm_request(
|
|||||||
|
|
||||||
|
|
||||||
def return_address_from_settings() -> dict[str, str]:
|
def return_address_from_settings() -> dict[str, str]:
|
||||||
"""Build PCM returnAddress from PCM_RETURN_ADDRESS JSON or CONTACT_* vars."""
|
"""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()
|
raw = (settings.PCM_RETURN_ADDRESS or "").strip()
|
||||||
if raw:
|
if raw:
|
||||||
data = json.loads(raw)
|
try:
|
||||||
if not isinstance(data, dict):
|
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")
|
raise PcmApiError("PCM_RETURN_ADDRESS must be a JSON object")
|
||||||
return {
|
data = parsed
|
||||||
"company": str(data.get("company") or ""),
|
|
||||||
"firstName": str(data.get("firstName") or data.get("first_name") or ""),
|
|
||||||
"lastName": str(data.get("lastName") or data.get("last_name") or ""),
|
|
||||||
"address": str(data.get("address") or data.get("line1") or ""),
|
|
||||||
"address2": str(data.get("address2") or data.get("line2") or ""),
|
|
||||||
"city": str(data.get("city") or ""),
|
|
||||||
"state": str(data.get("state") or ""),
|
|
||||||
"zipCode": str(data.get("zipCode") or data.get("zip") or ""),
|
|
||||||
}
|
|
||||||
|
|
||||||
name = (settings.SITE_NAME or "").strip()
|
name = (settings.SITE_NAME or "").strip()
|
||||||
parts = name.split(None, 1)
|
parts = name.split(None, 1)
|
||||||
first = parts[0] if parts else "Monica"
|
site_first = parts[0] if parts else ""
|
||||||
last = parts[1] if len(parts) > 1 else ""
|
site_last = parts[1] if len(parts) > 1 else ""
|
||||||
return {
|
|
||||||
"company": "",
|
address = {
|
||||||
"firstName": first,
|
"company": str(data.get("company") or "").strip(),
|
||||||
"lastName": last,
|
"firstName": str(
|
||||||
"address": str(getattr(settings, "PCM_RETURN_LINE1", "") or ""),
|
data.get("firstName") or data.get("first_name") or site_first or ""
|
||||||
"address2": str(getattr(settings, "PCM_RETURN_LINE2", "") or ""),
|
).strip(),
|
||||||
"city": str(getattr(settings, "PCM_RETURN_CITY", "") or ""),
|
"lastName": str(
|
||||||
"state": str(getattr(settings, "PCM_RETURN_STATE", "") or ""),
|
data.get("lastName") or data.get("last_name") or site_last or ""
|
||||||
"zipCode": str(getattr(settings, "PCM_RETURN_ZIP", "") 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]:
|
def contact_to_pcm_recipient(contact, *, ext_ref: str) -> dict[str, str]:
|
||||||
address = contact.postal_address or {}
|
address = contact.postal_address or {}
|
||||||
|
|||||||
@@ -1079,6 +1079,34 @@ class PcmAuthTests(TestCase):
|
|||||||
pcm_mod.login(force=True)
|
pcm_mod.login(force=True)
|
||||||
self.assertIn("PCM_API_SECRET", str(ctx.exception))
|
self.assertIn("PCM_API_SECRET", str(ctx.exception))
|
||||||
|
|
||||||
|
def test_return_address_requires_street_fields(self):
|
||||||
|
from messaging.providers.postcard import pcm as pcm_mod
|
||||||
|
|
||||||
|
with self.settings(
|
||||||
|
SITE_NAME="Monica Dhillon",
|
||||||
|
PCM_RETURN_ADDRESS="",
|
||||||
|
PCM_RETURN_LINE1="",
|
||||||
|
PCM_RETURN_CITY="",
|
||||||
|
PCM_RETURN_STATE="",
|
||||||
|
PCM_RETURN_ZIP="",
|
||||||
|
):
|
||||||
|
with self.assertRaises(pcm_mod.PcmApiError) as ctx:
|
||||||
|
pcm_mod.return_address_from_settings()
|
||||||
|
self.assertIn("PCM return address incomplete", str(ctx.exception))
|
||||||
|
|
||||||
|
def test_return_address_from_json(self):
|
||||||
|
from messaging.providers.postcard import pcm as pcm_mod
|
||||||
|
|
||||||
|
with self.settings(
|
||||||
|
PCM_RETURN_ADDRESS=(
|
||||||
|
'{"firstName":"Mo","lastName":"D","address":"1 Main",'
|
||||||
|
'"city":"Naperville","state":"IL","zipCode":"60540"}'
|
||||||
|
),
|
||||||
|
):
|
||||||
|
addr = pcm_mod.return_address_from_settings()
|
||||||
|
self.assertEqual(addr["address"], "1 Main")
|
||||||
|
self.assertEqual(addr["zipCode"], "60540")
|
||||||
|
|
||||||
|
|
||||||
class PostcardAddressDefaultConsentTests(TestCase):
|
class PostcardAddressDefaultConsentTests(TestCase):
|
||||||
def test_address_without_consent_is_postcard_eligible(self):
|
def test_address_without_consent_is_postcard_eligible(self):
|
||||||
|
|||||||
Reference in New Issue
Block a user