Add Django site, Docker packaging, and beta/prod Gitea deploys.
Unignore site/ (was blocked by mkdocs /site rule), add compose/Docker/uv tooling, and split deploys so push to main goes to beta while prod stays manual.
This commit is contained in:
+35
-29
@@ -23,7 +23,7 @@ multiple active/active instances behind Nginx Proxy Manager (NPM).
|
||||
composer, social scheduler.
|
||||
- One mailing list of contacts. Form submissions auto-create/append contacts.
|
||||
- Per-channel, per-contact consent with easy opt-out (email unsubscribe link, SMS `STOP`).
|
||||
- Outreach over Email + SMS (SMTP2GO) and Postcard (pluggable provider, default Lob).
|
||||
- Outreach over Email + SMS (SMTP2GO) and Postcard (PCM Integrations, pluggable).
|
||||
- Social posting/scheduling to Facebook, Instagram, LinkedIn via native APIs.
|
||||
- Cheaper than PostcardMania + adds social automation.
|
||||
|
||||
@@ -74,7 +74,7 @@ flowchart TB
|
||||
|
||||
subgraph external [External APIs]
|
||||
SMTP2GO["SMTP2GO (email + SMS)"]
|
||||
Postcard["Postcard provider (Lob default)"]
|
||||
Postcard["Postcard provider (PCM Integrations)"]
|
||||
Meta["Meta Graph API (FB + IG)"]
|
||||
LinkedIn["LinkedIn API"]
|
||||
Recaptcha["Google reCAPTCHA"]
|
||||
@@ -230,30 +230,34 @@ a contact opt out of SMS while keeping email.
|
||||
writes a `Suppression`. A2P 10DLC registration is a client onboarding prerequisite (call out
|
||||
in Phase 3).
|
||||
|
||||
### 6.2 Postcards — pluggable provider (default Lob)
|
||||
### 6.2 Postcards — PCM Integrations (pluggable)
|
||||
|
||||
A small provider interface so the realtor isn't locked in and we can shop on price:
|
||||
Default provider is **PCM Integrations** (DirectMail API v3). A small provider interface
|
||||
keeps Lob / Click2Mail / PostGrid available behind `POSTCARD_PROVIDER` if needed later.
|
||||
|
||||
```python
|
||||
class PostcardProvider(Protocol):
|
||||
def send_postcard(self, *, to: PostalAddress, from_: PostalAddress,
|
||||
front: Asset, back: Asset, idempotency_key: str) -> ProviderResult: ...
|
||||
def get_status(self, provider_id: str) -> DeliveryStatus: ...
|
||||
def send_postcard(self, message) -> PostcardResult: ...
|
||||
def get_status(self, provider_id: str) -> str: ...
|
||||
```
|
||||
|
||||
Adapters live in `messaging/providers/postcard/`. Selected via `POSTCARD_PROVIDER` env var.
|
||||
|
||||
| Provider | Model | ~4x6 postcard | Best for | Notes |
|
||||
|----------|-------|---------------|----------|-------|
|
||||
| **Lob** (default) | API-first, tiered | Free dev tier ~$0.77; ~$0.51 on $260/mo Startup | Clean API, address verification, in-transit tracking | Best DX; monthly fee only worth it at volume |
|
||||
| Click2Mail | Pay-per-piece, no subscription | ~$0.35–0.70 | Low/occasional volume, no monthly fee | API less polished; great when volume is small |
|
||||
| PostGrid | API + dashboard, subscription | Contact sales (from ~$250/mo) | Compliance-heavy, templates | Overkill unless compliance-driven |
|
||||
| Stannp | Marketer-friendly, no minimums | Transparent per-piece | Non-dev fallback, EU | Good dashboard |
|
||||
| USPS EDDM | Postage-only saturation | Postage only | Whole-route blasts (no list) | No per-address API; manual/bulk option |
|
||||
| Provider | Model | Notes |
|
||||
|----------|-------|-------|
|
||||
| **PCM Integrations** (default) | DirectMail API v3 + embedded designer | Iframe editor (`POST /design/custom`, embed URL); orders with `designID` + recipients; inbound status webhooks |
|
||||
| Lob | API-first, tiered | Alternate behind same interface |
|
||||
| Click2Mail | Pay-per-piece | Low-volume alternate |
|
||||
| PostGrid | API + dashboard | Compliance-heavy alternate |
|
||||
|
||||
> Recommendation: start on **Lob Developer (free) tier** to validate, keep **Click2Mail** as the
|
||||
> low-volume cost option behind the same interface. Revisit once monthly volume is known — the
|
||||
> abstraction makes switching a config change.
|
||||
**Designer:** portal embeds PCM’s editor (no homemade layout tool). Saved designs become
|
||||
`MessageTemplate` rows with `postcard_front.design_id`.
|
||||
|
||||
**Webhooks:** `POST /portal/messaging/webhooks/postcard/` with Bearer `PCM_WEBHOOK_SECRET`.
|
||||
Correlate via `extRefNbr` (= `Message.pk`) or `orderID` (= `provider_message_id`).
|
||||
|
||||
**Campaign notify:** when any channel campaign reaches `completed`, one summary email goes
|
||||
to `created_by.email` or `CONTACT_EMAIL` (`Campaign.notify_sent_at` guard).
|
||||
|
||||
### 6.3 Social — native APIs (Facebook, Instagram, LinkedIn)
|
||||
Chosen over an aggregator (Ayrshare) to avoid per-profile monthly fees, since this is one
|
||||
@@ -324,10 +328,10 @@ monica_site/ # repo root
|
||||
│ ├── worker-entrypoint.sh # django.tasks backend worker (+ optional dispatch_due loop)
|
||||
│ └── validate-env.sh
|
||||
├── .gitea/workflows/
|
||||
│ ├── ci.yml # PR: uv sync + manage.py test
|
||||
│ ├── ci.yml # PR: uv sync + cd site && manage.py test
|
||||
│ ├── unittests.yml # master: tests gate deploy
|
||||
│ └── deploy.yml # calls server-infra/scripts/deploy.sh --app monica_site
|
||||
└── monica_site/ # Django project dir (manage.py lives here)
|
||||
└── site/ # Django project dir (manage.py lives here)
|
||||
├── manage.py
|
||||
├── monica_site/ # project package
|
||||
│ ├── __init__.py
|
||||
@@ -358,7 +362,7 @@ monica_site/ # repo root
|
||||
│ └── providers/
|
||||
│ ├── email/smtp2go.py
|
||||
│ ├── sms/smtp2go.py
|
||||
│ └── postcard/{base.py,lob.py,click2mail.py,postgrid.py}
|
||||
│ └── postcard/{__init__.py,pcm.py,lob.py,click2mail.py,postgrid.py}
|
||||
└── social/ # accounts, posts, scheduling
|
||||
├── models.py
|
||||
├── tasks.py # @task publish_social_target, etc.
|
||||
@@ -409,7 +413,7 @@ monica_site:
|
||||
|
||||
### 8.3 CI/CD flow
|
||||
Same as `company_site`:
|
||||
1. PR → `ci.yml` runs `uv sync` + `manage.py test`.
|
||||
1. PR → `ci.yml` runs `uv sync` + `cd site && manage.py test`.
|
||||
2. Merge to `main` → `unittests.yml` (containerized tests) → on green, `deploy.yml` calls
|
||||
`server-infra/scripts/deploy.sh --app monica_site --env prod --ref <sha>`.
|
||||
3. `deploy-apps.yml` checks out the ref on each app host, injects `.env`, `docker compose build`,
|
||||
@@ -464,9 +468,11 @@ EMAIL_USE_TLS=true
|
||||
# SMTP2GO SMS
|
||||
SMTP2GO_SMS_API_KEY=...
|
||||
|
||||
# Postcards
|
||||
POSTCARD_PROVIDER=lob # lob|click2mail|postgrid
|
||||
LOB_API_KEY=...
|
||||
# Postcards (PCM Integrations)
|
||||
POSTCARD_PROVIDER=pcm # pcm|lob|click2mail|postgrid
|
||||
PCM_API_KEY=...
|
||||
PCM_WEBHOOK_SECRET=...
|
||||
PCM_RETURN_ADDRESS={...}
|
||||
|
||||
# Social (native)
|
||||
META_APP_ID=...
|
||||
@@ -512,11 +518,11 @@ TIANJI_WEBSITE_ID=...
|
||||
- **Exit:** realtor sends a real email + SMS campaign to consented contacts; opt-out works.
|
||||
|
||||
### Phase 3 — Postcards
|
||||
- `messaging/providers/postcard/` interface + Lob adapter (+ Click2Mail adapter as the
|
||||
low-volume option); `POSTCARD_PROVIDER` switch.
|
||||
- Postcard campaign flow: pick template/artwork, select recipients (address required + Lob
|
||||
address verification), enqueue batch as Django Tasks, poll delivery status.
|
||||
- Client prerequisites called out: Lob account, A2P 10DLC for SMS.
|
||||
- `messaging/providers/postcard/` + PCM Integrations adapter (Lob/Click2Mail kept as
|
||||
alternates); `POSTCARD_PROVIDER=pcm`.
|
||||
- PCM iframe designer; save `design_id` on `MessageTemplate`; postcard campaign audience;
|
||||
inbound PCM webhooks → `ProviderEvent`; campaign completion notify email.
|
||||
- Client prerequisites: PCM Integrations account + API key, A2P 10DLC for SMS.
|
||||
- **Exit:** realtor mails a postcard batch and sees delivery tracking.
|
||||
|
||||
### Phase 4 — Social automation
|
||||
|
||||
Reference in New Issue
Block a user