# Nirvaan — install

## 1. Database

```sql
CREATE USER 'nirvaan_app'@'localhost' IDENTIFIED BY '<strong password>';
```
```bash
mysql -u root -p < install/schema.sql
mysql -u root -p < install/seed_cms.sql
mysql -u root -p < install/seed_phase2.sql
mysql -u root -p < install/seed_phase3.sql
mysql -u root -p < install/seed_phase4.sql
mysql -u root -p < install/migrate_phase5.sql
mysql -u root -p < install/seed_phase6.sql
mysql -u root -p < install/seed_phase7.sql
mysql -u root -p < install/seed_phase8.sql
mysql -u root -p -e "GRANT SELECT,INSERT,UPDATE,DELETE ON nirvaan.* TO 'nirvaan_app'@'localhost'"
```

## 2. Configuration

Edit `config/config.php`:

- `DB_PASS` — the password you just set
- `ENC_KEY` — generate with `openssl rand -base64 32`, paste inside `base64_decode('…')`
- `BRAND`, `BASE_URL` — your names
- Gateway, SES, MSG91 and Exotel credentials

**The encryption key is not recoverable.** Every chat message, consultation note
and prescription is encrypted with it. Back it up somewhere other than this server,
and never rotate it without re-encrypting first.

## 3. Web server

Document root points at `public/`. Everything else — `config/`, `lib/`, `uploads/`,
`install/`, `cron/` — must sit outside the document root or be denied by the server.

```
DocumentRoot /var/www/nirvaan/public
```

Nginx equivalent of the `.htaccess` rewrites:

```nginx
location ~ ^/tools/([a-z0-9-]+)/?$        { try_files $uri /tools/view.php?t=$1; }
location ~ ^/consult/([a-z0-9-]+)/?$      { try_files $uri /speciality.php?slug=$1; }
location ~ ^/sexologist/([a-z0-9-]+)/?$   { try_files $uri /city.php?slug=$1; }
location ~ ^/learn/([a-z0-9-]+)/?$        { try_files $uri /post.php?slug=$1; }
location = /sitemap.xml                   { try_files $uri /sitemap.php; }
```

## 4. Cron

```cron
* * * * * for i in 0 10 20 30 40 50; do (sleep $i; php /var/www/nirvaan/cron/meter.php) & done
* * * * * php /var/www/nirvaan/cron/queue.php
0 6 * * 1 php /var/www/nirvaan/cron/payouts.php
```

Two of these are load-bearing. Alert on both:

- **meter.php** bills consultations. If it stops, live sessions keep running
  and nobody is charged.
- **queue.php** sends every outbound message and, every fifth minute,
  reconciles payments left pending. If it stops, patients pay the gateway and
  never see the credit — and they will blame you, correctly.

## 4a. Webhooks

Register these with each provider. Both are idempotent and both record every
event in `webhook_events`.

| Provider | URL |
|---|---|
| Cashfree | `https://yourdomain/api/webhook/cashfree.php` |
| Exotel   | `https://yourdomain/api/webhook/exotel.php` |

Cashfree callbacks are HMAC-signed and verified before the body is parsed.
Exotel does not sign its callbacks, so the handler accepts only a `CallSid`
that already exists in `consultations`. Restrict that endpoint to Exotel's
published IP ranges at the web server for a second layer.

## 4b. Prescribing

Prescriptions are gated in code against the Telemedicine Practice Guidelines,
2020 — see `lib/rx.php`. Only a doctor whose `tier` is `rmp` **and** whose
registration has been verified can issue one. Drug lists live in `settings`
(`rx_prohibited`, `rx_list_a`, `rx_list_b`) and can only ADD to the built-in
lists, never shorten them.

Get your medical advisor to review those lists before go-live. The built-in
set is a working subset for this category, not a complete formulary.

## 5. Before go-live

- Change every seeded password. The seed ships `Nirvaan@2026` for
  `admin@nirvaan.in`, `ops@`, `finance@`, `content@` and the two test doctors.
- Delete the two test doctor rows.
- Set `billing_descriptor` to whatever your gateway actually prints.
- Confirm `mask_names_for_doctors` and `mask_mobile_for_doctors` are both on.
- Point `robots.txt` and `llms.txt` at your real domain.
- HTTPS only. Sessions are set `secure`, so nothing works over plain HTTP.

## Brand assets

`public/assets/img/` ships placeholder marks generated from the palette —
`logo.png`, `logo.svg`, `og-default.png` (the 1200x630 share card) and
`favicon.png`. Replace them when you have real artwork; the paths are
referenced from the Organization schema and every Open Graph tag, so keep the
filenames.

## Consent

No consultation can start without a recorded teleconsult consent — the check is
in `consultRequest()`, not in the UI, so it cannot be bypassed. Consent text is
versioned in `lib/consent.php`. **When you change the wording, bump the version**;
anyone whose stored consent predates it is asked again. Without that, you can
prove somebody agreed to something but not what.

## The shop

Anything flagged `rx_required` cannot be bought without a prescription issued
**on this platform** in the last 90 days, matched on molecule rather than brand.
The check runs at add-to-cart and again at checkout, because a cart can sit in a
cookie while a prescription expires.

Product copy goes through the same DMR Act gate as marketing copy. The seeded
catalog shows the phrasings that pass.

`uploads/media/` must be web-readable — unlike `uploads/docs/` and `uploads/rx/`,
which stay denied. Product photos are public; verification documents are not.

## Roles

| Role | Can do |
|---|---|
| superadmin | everything, including settings |
| ops | doctor verification, consultations, tickets |
| finance | payouts, refunds, payments |
| content | CMS, SEO, assessment tools |
| support | tickets, consultations, read-only user and doctor data |
