Internet
│
├── billing.yealin.com.au (51.161.136.89)
│ ├── Nginx (80/443) — reverse proxy
│ ├── Gunicorn — Django WSGI server (3 workers)
│ ├── Celery Worker — background tasks
│ ├── Celery Beat — scheduled tasks
│ ├── PostgreSQL 17 — primary database
│ ├── Redis 8 — message broker + cache
│ └── Docker — Wiki.js container
│
├── wiki.yealin.com.au (51.161.136.89, Docker)
│ └── Wiki.js 2 + PostgreSQL (Docker)
│
└── aum.bylaw.com.au (51.161.193.85)
└── cPanel/Exim — SMTP relay
LAN (behind MikroTik 202.47.178.140)
└── 10.0.0.41 — Development server (Proxmox VM)
Browser → Nginx (SSL termination)
├── /api/* → Gunicorn → Django → PostgreSQL
├── /admin/* → Gunicorn → Django → PostgreSQL
├── /static/* → staticfiles/ (cached 30 days)
├── /media/* → media/ (PDF invoices)
└── /* → React SPA (index.html)
Twilio AU1 (api.sydney.au1.twilio.com)
↓ Celery task (sync_account_task)
CDR normaliser → CLI matcher → destination classifier → pricing engine
↓
CallRecord created in PostgreSQL
↓ (when billing run executed)
BillingRun → Invoice → InvoiceLineItem → WeasyPrint PDF
↓
Stripe Checkout → Payment → Invoice marked paid
↓ (optional)
cPanel SMTP → Customer email with PDF + Pay Now link
Customer clicks Pay Now (portal or admin)
↓
POST /api/invoices/{id}/pay/
↓
Stripe Checkout Session created
↓
Customer → Stripe hosted payment page
↓
Payment completed
↓
Stripe POST → /api/webhooks/stripe/
↓
Invoice status = paid, Payment record created
| Layer | Technology | Purpose |
|---|---|---|
| Frontend | React 18 + Vite + Tailwind | Admin UI + Customer Portal |
| API | Django REST Framework | All API endpoints |
| Auth | SimpleJWT | 8hr JWT tokens |
| Background | Celery + Redis | CDR sync, FX refresh |
| Scheduler | Celery Beat | Hourly FX rate, daily sync |
| Database | PostgreSQL | All persistent data |
| Cache | Redis | Celery broker + Django cache |
| WeasyPrint | Invoice generation | |
| Web server | Nginx + Gunicorn | HTTP + WSGI |
| SSL | Let's Encrypt | Auto-renewing HTTPS |
| Containers | Docker | Wiki.js isolation |
Twilio US1 (api.twilio.com)
→ Master account calls (44 calls, standard Voice)
→ Auth: API Key SID + Secret
Twilio AU1 (api.sydney.au1.twilio.com)
→ Campbells Legal Services (5,601 SIP trunking calls)
→ All Australian customer trunks
→ Auth: Account SID + AU1 Auth Token
Critical: AU1 calls cannot be accessed via US1 endpoint. Each region requires separate authentication credentials.
CDR imported (API or CSV)
call_sid, from_number, to_number, called_via, direction, duration, price_usd
CLI Matcher
from_number (outbound) or called_via/to_number (inbound)
→ matched to CustomerCLI → Customer assigned
Destination Classifier
to_number prefix matched against Destination table
Longest prefix wins: +614 beats +61 beats +
Pricing Engine
cost_price_aud = abs(twilio_price_usd) × effective_fx_rate
sell_price_aud = (duration_minutes × sell_rate) + connection_fee
effective_fx_rate = live_rate × (1 + markup_percent / 100)
Billing Run (per calendar month)
All billable=True, invoiced=False CDRs for period
→ grouped into Invoice with InvoiceLineItems
→ WeasyPrint PDF generated
→ CDRs marked invoiced=True
Payment
Stripe Checkout → webhook → Invoice paid
OR Manual payment recorded by admin
/var/www/yealinbilling/
├── apps/ Django applications
│ ├── core/ Settings, FX tasks, base models
│ ├── customers/ Customer, CustomerGroup, CustomerCLI
│ ├── cdr/ CallRecord, SyncLog, TwilioAccount, pricing
│ ├── products/ Destination, Tariff, RateCard, ServiceCharge
│ ├── billing/ BillingRun, Invoice, PDF generation
│ ├── payments/ Payment, Stripe integration
│ ├── portal/ CustomerPortalUser, portal API
│ └── api/ All views and URL routing
├── config/ Django project settings
│ └── settings/ base.py, development.py, production.py
├── frontend/ React source (builds to staticfiles/)
├── staticfiles/ Collected static + React build
├── media/ PDF invoices
├── logs/ Application logs
└── wiki_sync.py Wiki.js page sync script
| Decision | Choice | Reason |
|---|---|---|
| OS | Debian 13.5 | Ubuntu cloud-init broke OVH failover IPs |
| WeasyPrint | Pure Python, no browser dependency | |
| cPanel SMTP | Existing infrastructure, SPF configured | |
| Payments | Stripe only | GoCardless deferred |
| PDF charts | Inline SVG | WeasyPrint cannot render JavaScript |
| Auth | JWT (8hr) | Balance between security and UX |
| CDR sync | Celery background tasks | Avoids HTTP timeout on large syncs |
| Billing | Per calendar month | Clean invoicing, no period overlap |