Cause: Nginx cannot connect to Gunicorn socket.
Diagnose:
sudo tail -20 /var/log/nginx/error.log
ls -la /var/www/yealinbilling/gunicorn.sock
sudo systemctl status gunicorn --no-pager
Fix — Permission denied on socket:
sudo usermod -aG crip www-data
sudo systemctl daemon-reload
sudo systemctl restart gunicorn nginx
Fix — Gunicorn not running:
sudo systemctl start gunicorn
sudo journalctl -u gunicorn -n 50 --no-pager # check why it failed
Cause 1: Token expired — log in again.
Cause 2: Portal views missing @authentication_classes([]) decorator.
Every portal view MUST have both decorators:
@api_view(['GET'])
@authentication_classes([]) # REQUIRED
@permission_classes([AllowAny])
def my_view(request):
...
Cause 3: Wrong token type — admin token used on portal endpoint or vice versa.
Cause: URL not registered in apps/api/urls.py.
# Check what URLs are registered
python manage.py show_urls 2>/dev/null || grep "path(" apps/api/urls.py
# Test SMTP connection
python manage.py shell
from django.core.mail import send_mail
send_mail('Test', 'Test', 'billing@yealin.com.au', ['test@gmail.com'], fail_silently=False)
Cause 1: EMAIL_BACKEND is console.EmailBackend — prints to terminal, doesn't send.
Fix: Change to smtp.EmailBackend in .env and restart Gunicorn.
Cause 2: Sending IP not in SPF record.
Fix: Add IP to yealin.com.au SPF TXT record in DNS.
Cause 3: Exchange Online quarantining.
Fix: Check https://security.microsoft.com → Quarantine.
Cause 1: Wrong webhook secret in .env.
The dev webhook secret changes every time stripe listen restarts.
Fix:
whsec_... in outputSTRIPE_WEBHOOK_SECRET in .envsudo systemctl restart gunicornCause 2: Stripe CLI not running on Windows.
Fix: Start C:\stripe\stripe.exe listen --forward-to http://10.0.0.41/api/webhooks/stripe/
Cause: CLI not registered or wrong E.164 format.
python manage.py shell
from apps.customers.models import CustomerCLI
# List all CLIs
for cli in CustomerCLI.objects.all():
print(f'{cli.cli} → {cli.customer.name} (billable: {cli.billable})')
Fix: Add missing CLI via Admin UI → Customer Detail → CLIs → Add CLI.
After adding CLIs, reprocess unmatched CDRs:
TOKEN=$(...)
curl -s -X POST https://billing.yealin.com.au/api/cdr/reprocess/ -H "Authorization: Bearer $TOKEN"
ls -la /var/www/yealinbilling/gunicorn.sock
# Should show: srwxrwx--- crip www-data
# If wrong:
sudo usermod -aG crip www-data
sudo systemctl daemon-reload
sudo systemctl restart gunicorn
cd /opt/wikijs
docker compose ps # check container status
docker compose logs wikijs # check error logs
docker compose up -d # restart if needed
sudo systemctl status docker # check Docker daemon
sudo systemctl status postgresql
sudo systemctl start postgresql
# Check if listening
sudo -u postgres psql -c "SELECT version();"
# Check Celery Beat is running
sudo systemctl status celerybeat
# Manually trigger refresh
python manage.py shell
from apps.core.tasks import refresh_fx_rate
result = refresh_fx_rate()
print(result)
git stash # stash local changes
git pull origin main # pull latest
git stash drop # discard stash
# Quick sanity check — runs before every deployment
python manage.py check
# Full security check (production only)
python manage.py check --deploy