diff --git a/CLAUDE.md b/CLAUDE.md index fe7f271..90c9f09 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -2,7 +2,7 @@ > **Audience:** AI assistants and developers working on this codebase. > **Purpose:** Authoritative reference for architecture, conventions, gotchas, and decisions. -> **Last reviewed:** June 2026 (Phase 19 complete + mobile API gap-fill Phases A–E + customer UI refinements + Phase 22 comment visibility + Phase 23 support chat/tickets + inspector performance Excel export + inspection list filters + customer issue logging + AI chatbot + dashboard grouped sections + issues/inspections PDF export + date/ID filters + Reports expansion Phases R1–R4 + Phase 24 issue_created notify defaults + Phase 25 inspection GPS + Phase 26 issue vendor fields + Phase 27 facility score alerts + **MT-0 through MT-8 complete; self-service signup; trial enforcement; billing emails; invoice history; superadmin billing controls; per-tenant backup CLI; health dashboard; MT-9 iOS pending**) +> **Last reviewed:** June 2026 (Phase 19 complete + mobile API gap-fill Phases A–E + customer UI refinements + Phase 22 comment visibility + Phase 23 support chat/tickets + inspector performance Excel export + inspection list filters + customer issue logging + AI chatbot + dashboard grouped sections + issues/inspections PDF export + date/ID filters + Reports expansion Phases R1–R4 + Phase 24 issue_created notify defaults + Phase 25 inspection GPS + Phase 26 issue vendor fields + Phase 27 facility score alerts + **MT-0 through MT-8 complete; self-service signup; trial enforcement; billing emails; invoice history; superadmin billing controls; per-tenant backup CLI; health dashboard; fail2ban brute-force protection; welcome email on signup; dunning sequence day-3/7/14; MT-9 iOS pending**) --- @@ -193,8 +193,12 @@ lt_janitorial_quality_control/ │ │ ├── health.py # GET /health/ — monitoring dashboard (tenant status, schema, trials) │ │ ├── wsgi_panel.py # WSGI entry point — Gunicorn on port 8001 │ │ └── templates/panel/ # base.html, login.html, tenants_list.html, tenant_detail.html, provision.html, health.html -│ └── migrations/ # control Alembic chain (control{N}_…) -│ └── versions/control0001_init.py ← HEAD +│ └── migrations/ # control Alembic chain: control0001_init → control0002_billing → control0003_trial_reminder_sent → control0004_dunning_tracking ← HEAD +│ └── versions/control0001_init.py … control0004_dunning_tracking ← HEAD +├── deploy/ +│ └── fail2ban/ # Fail2ban filter + jail configs for login brute-force protection +│ ├── filter.d/jqc-login.conf # Matches WARNING LOGIN_FAILED from app log +│ └── jail.d/jqc.conf # maxretry=5 / findtime=300s / bantime=3600s └── ... ``` @@ -448,7 +452,7 @@ api_device_tokens: id, user_id, device_id, apns_token, device_name, app_version | `inspections` | `/inspections` | list, start, execute, view, PDF export, flag-issue, save-draft (AJAX), flag-followup, reinspect, upload-photo (AJAX) | | `templates` | `/templates` | list, create, edit, delete, form editor, preview | | `issues` | `/issues` | list, view, create, update, verify, comment, follow/unfollow, verification queue, bulk-verify, delete, quick-assign | -| `notifications` | `/notifications` | list, mark-read, preferences, send-digest (cron), check-sla (cron), cleanup-tokens (cron) | +| `notifications` | `/notifications` | list, mark-read, preferences, send-digest (cron), check-sla (cron), cleanup-tokens (cron), trial-reminders (cron), dunning-reminders (cron) | | `audit` | `/audit` | list (admin only), view, purge | | `reports` | `/reports` | index, facility report, scorecard, CSV/PDF/Excel export, issues-aging, sla-compliance, followup-closure, facility summary PDF | | `scheduled_reports` | `/scheduled-reports` | CRUD + manual trigger (accessible via Reports sub-nav) | @@ -661,6 +665,8 @@ EVENT_SCORE_ALERT = 'score_alert' ← Phase 27 | `POST /notifications/check-sla` | SLA breach/at-risk alerts | `*/30 * * * *` | | `POST /notifications/cleanup-tokens` | Purge expired API tokens | `0 3 * * *` | | `POST /notifications/check-score-trends` | Facility score drop alerts (Phase 27) | `0 8 * * *` | +| `POST /notifications/trial-reminders` | Trial-ending warning emails (≤3 days left) | `0 9 * * *` | +| `POST /notifications/dunning-reminders` | Payment-failure escalation emails (day 3/7/14) | `0 10 * * *` | --- @@ -1112,6 +1118,10 @@ set -a; . /etc/jqc/control.env; set +a -d "secret=SECRET" 0 8 * * * curl -s -X POST https://your-domain.com/notifications/check-score-trends \ -d "token=SECRET" +0 9 * * * curl -s -X POST https://your-domain.com/notifications/trial-reminders \ + -d "token=SECRET" +0 10 * * * curl -s -X POST https://your-domain.com/notifications/dunning-reminders \ + -d "token=SECRET" ``` --- @@ -1389,7 +1399,7 @@ Stripe-backed subscription billing. Controlled by `BILLING_ENABLED` env var (def ### Trial period - New tenants provisioned via `create_tenant()` get `subscription_status='trial'`, `trial_ends_at = now + 14 days` by default. -- Self-service signup (`/signup`) provisions a tenant immediately with a 14-day trial. +- Self-service signup (`/signup`) provisions a tenant immediately with a 14-day trial and sends a `welcome` email. - Trial enforcement is in `_billing_gate()` — no Stripe required until they subscribe. ### Billing emails (`app/billing/emails.py`) @@ -1398,12 +1408,27 @@ Stripe-backed subscription billing. Controlled by `BILLING_ENABLED` env var (def | `event_type` | Trigger | Required context keys | |---|---|---| -| `payment_failed` | `invoice.payment_failed` Stripe webhook | `portal_url` | -| `trial_ending` | Cron (not yet wired — see §11 cron table) | `trial_ends_at`, `subscribe_url` | +| `welcome` | `POST /signup` success | `tenant_name`, `login_url`, `trial_days`, `trial_ends_at`, `plan_name` | +| `payment_failed` | `invoice.payment_failed` Stripe webhook (day 0) | `portal_url` | +| `payment_reminder` | Dunning cron (day 3 + day 7) | `portal_url`, `tenant_name`, `days_overdue` | +| `payment_final` | Dunning cron (day 14) | `portal_url`, `tenant_name`, `days_overdue` | +| `trial_ending` | Cron `/notifications/trial-reminders` | `trial_ends_at`, `subscribe_url`, `days_left`, `tenant_name` | | `subscription_cancelled` | `customer.subscription.deleted` Stripe webhook | `portal_url` | HTML templates live in `app/templates/billing/email/`. `base.html` provides the branded layout; each event type extends it. +### Dunning sequence + +Three `Tenant` columns track payment-failure escalation state: + +| Column | Type | Purpose | +|---|---|---| +| `past_due_since` | `DATETIME NULL` | Set when payment first fails (webhook `_on_payment_failed`). Reset to `NULL` on recovery. | +| `dunning_stage` | `TINYINT DEFAULT 0` | 0=none, 1=day-3 sent, 2=day-7 sent, 3=day-14 sent. Reset to 0 on recovery. | +| `dunning_sent_at` | `DATETIME NULL` | Timestamp of the last dunning email. Reset to `NULL` on recovery. | + +Cron endpoint `POST /notifications/dunning-reminders` runs daily, checks `elapsed = now - past_due_since`, and advances the stage if behind schedule. Migration: `control0004_dunning_tracking`. + ### Plan seeding ```bash diff --git a/SERVER_DUPLICATION.md b/SERVER_DUPLICATION.md index 3c04d88..7a8de6a 100644 --- a/SERVER_DUPLICATION.md +++ b/SERVER_DUPLICATION.md @@ -186,6 +186,27 @@ ufw enable ufw status ``` +### 1.5b Install Fail2ban (brute-force protection) + +```bash +apt install -y fail2ban + +# Copy the JQC filter and jail configs from the repository +cp /home/jqc/deploy/fail2ban/filter.d/jqc-login.conf /etc/fail2ban/filter.d/ +cp /home/jqc/deploy/fail2ban/jail.d/jqc.conf /etc/fail2ban/jail.d/ + +# Confirm logpath in jqc.conf matches your actual log file +# Default is /var/www/jqc/logs/jqc.log — update if your path differs: +# sed -i 's|/var/www/jqc/logs/jqc.log|/home/jqc/logs/jqc.log|g' /etc/fail2ban/jail.d/jqc.conf + +systemctl enable fail2ban +systemctl restart fail2ban + +# Verify the jail is active +fail2ban-client status jqc-login +# Expected: "Status for the jail: jqc-login" with Banned IP counts +``` + ### 1.6 Create the `jqc` system user ```bash @@ -1206,6 +1227,18 @@ SHELL=/bin/bash 0 3 * * * curl -s -X POST https://YOUR_DOMAIN/notifications/cleanup-tokens \ -d "token=YOUR_DIGEST_SECRET" >> /home/jqc/logs/cron.log 2>&1 +# ---------------------------------------------------------------- +# Trial-ending reminder emails — 9:00 AM Eastern +# ---------------------------------------------------------------- +0 9 * * * curl -s -X POST https://YOUR_DOMAIN/notifications/trial-reminders \ + -d "token=YOUR_DIGEST_SECRET" >> /home/jqc/logs/cron.log 2>&1 + +# ---------------------------------------------------------------- +# Payment dunning reminders (day 3 / 7 / 14 escalation) — 10:00 AM Eastern +# ---------------------------------------------------------------- +0 10 * * * curl -s -X POST https://YOUR_DOMAIN/notifications/dunning-reminders \ + -d "token=YOUR_DIGEST_SECRET" >> /home/jqc/logs/cron.log 2>&1 + # ---------------------------------------------------------------- # Nightly database backup — 2:00 AM Eastern # ---------------------------------------------------------------- @@ -1384,6 +1417,7 @@ Work through this list after DNS propagation is confirmed. Check each item in th - [ ] Log rotation is working: `/home/jqc/logs/` is not filling up unexpectedly - [ ] First nightly backup completed (check `/var/backups/jqc/` the morning after cutover) - [ ] Firewall is active: `ufw status` shows correct rules +- [ ] Fail2ban is active: `fail2ban-client status jqc-login` shows the jail running ### Decommission old server (after 48 hours)