# PFMIS Backend (PHP 8 + MariaDB/MySQL)

A dependency-free REST API for the Poultry Farm Management Information System.
No framework, no Composer packages — runs on any standard PHP 8 host (shared
hosting, XAMPP, or a VPS).

## What's inside

```
pfmis-backend/
├── public/
│   ├── index.php        # single entry point — all requests route here
│   └── .htaccess        # Apache rewrite → index.php
├── src/
│   ├── Core/            # Database (PDO), Router, Controller base, Http helpers
│   └── Controllers/     # one controller per module
├── config/config.php    # DB credentials + CORS origin
└── sql/
    └── schema.sql       # tables, indexes, foreign keys, the expenses view
```

## Setup (6 steps)

1. **Create the database, then the tables** — `schema.sql` creates tables
   only (no `CREATE DATABASE`/`USE`), so it imports cleanly into an
   already-created database. That's the only thing shared hosting like cPanel
   allows: you make the database in cPanel's MySQL wizard (it gets an account
   prefix, e.g. `youracct_pfmis`), select it in phpMyAdmin, then **Import**
   `sql/schema.sql`. Locally, create it first, then load the tables into it:
   ```bash
   mysql -u root -p -e "CREATE DATABASE IF NOT EXISTS pfmis CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci"
   mysql -u root -p pfmis < sql/schema.sql
   ```

2. **Set credentials** — copy `config/config.example.php` to `config/config.php`
   and fill in real values, or set env vars `DB_HOST DB_PORT DB_NAME DB_USER
   DB_PASS`. Set `CORS_ORIGIN` to your UI's origin in production (avoid `*`).

3. **Run it**
   - Local dev: `php -S localhost:8080 -t public`
   - Apache/shared hosting: point the document root at `public/`.

4. **Verify**: open `http://localhost:8080/healthz` → `{"ok":true,...}`

5. **Bootstrap your first login** — `schema.sql` creates empty `farms`,
   `roles` and `users` tables on purpose (no demo data ships anymore), so
   every route except `/auth/login` requires a session and every write
   requires a permission — including creating a farm. That means the very
   first farm/role/user have to be inserted directly, once:
   ```bash
   php -r "echo password_hash('choose-a-strong-password', PASSWORD_BCRYPT), PHP_EOL;"
   ```
   ```sql
   INSERT INTO farms (name) VALUES ('My Farm');

   INSERT INTO roles (farm_id, name, description, is_system, permissions) VALUES
     (1, 'Admin', 'Full access to every module', 1,
      '{"dashboard":{"create":true,"view":true,"update":true,"delete":true},"flocks":{"create":true,"view":true,"update":true,"delete":true},"daily":{"create":true,"view":true,"update":true,"delete":true},"eggs":{"create":true,"view":true,"update":true,"delete":true},"feed":{"create":true,"view":true,"update":true,"delete":true},"health":{"create":true,"view":true,"update":true,"delete":true},"vaccination":{"create":true,"view":true,"update":true,"delete":true},"inventory":{"create":true,"view":true,"update":true,"delete":true},"sales":{"create":true,"view":true,"update":true,"delete":true},"expenses":{"create":true,"view":true,"update":true,"delete":true},"hr":{"create":true,"view":true,"update":true,"delete":true},"payroll":{"create":true,"view":true,"update":true,"delete":true},"reports":{"create":true,"view":true,"update":true,"delete":true},"settings":{"create":true,"view":true,"update":true,"delete":true}}');

   INSERT INTO users (farm_id, role_id, name, email, password_hash, status) VALUES
     (1, 1, 'Your Name', 'you@example.com', '<paste the hash from the php command above>', 'active');
   ```
   Sign in with that email/password once the UI is pointed at the API (next
   step) — from there, use Settings → Users & Roles to add everyone else.

6. **Connect the UI**: in the app, set the API base URL (see "Connecting the UI").

## Connecting the UI

The React app is **offline-first**. With no API configured it uses browser
storage (the artifact keeps working standalone). To make the PHP backend the
source of truth, set the base URL before the app loads:

```html
<script>window.__PFMIS_API__ = "http://localhost:8080";</script>
```

or edit `API_BASE` at the top of the app file. When set, the typed `api` client
(also at the top of the app) calls these endpoints; if a call fails it falls
back to storage, so the UI never hard-crashes on a network blip.

## SMS notifications (optional)

Farms can opt into text alerts (Settings → Notifications → SMS alerts) for
mortality spikes, feed/mix stock hitting reorder level, sale receipts to
customers, and vaccinations due/overdue — sent through Pahappa's EgoSMS
gateway via `src/Core/Sms.php` (a plain JSON POST, no SDK/Composer dependency).
Skip this section entirely if you don't need SMS; every send site already
no-ops safely with nothing configured.

1. **Apply the migrations** (existing databases only — a fresh install's
   `schema.sql` already has this): `sql/migration-2026-07-sms-notifications.sql`
   adds `farms.phone` and the `sms_log` table (an audit trail that also backs
   the dedup checks — a farm is never texted twice for the same mortality
   entry, stock dip, sale, or vaccination); `sql/migration-2026-07-sms-multiple-numbers.sql`
   widens `farms.phone` to hold more than one number; and
   `sql/migration-2026-07-sms-triggers.sql` adds the `sms_triggers` table
   (per-trigger enable/disable + custom wording, see step 4).
2. **Add credentials to `config/config.php`** — see the `sms` block in
   `config/config.example.php`: `username` and `password` (your Pahappa/EgoSMS
   account login), `senderId` (≤11 chars), and `sandbox` (true while testing).
3. **Set each farm's alert phone number(s)** — Settings → Notifications →
   "Alert phone numbers" (one, or several separated by commas), then turn on
   "SMS alerts". This is the general/master switch: it has to be on for any
   trigger below to fire, and every number gets every alert that reaches it.
   Sale-receipt texts go to the customer's own phone on the invoice instead,
   but are still gated by this same toggle.
4. **Fine-tune individual triggers (optional)** — Configurations → "SMS
   message triggers" lets each of the 5 triggers (mortality, feed reorder,
   sale receipt, vaccination due, daily snapshot) be turned off independently,
   and its wording replaced with a custom `{token}` template (e.g. `{house}`,
   `{count}`, `{pct}` for mortality — the panel shows each trigger's own
   tokens). Leave a trigger's message blank to keep the built-in default.
5. **Schedule the two triggers that aren't tied to a save** (a due date or a
   new day don't "happen" on their own the way a save does). In cPanel:
   Cron Jobs → Add New Cron Job →
   - once daily → `php /home/<youraccount>/<path-to-this-repo>/cron/vaccination-reminders.php`
   - once daily, at whatever time you want farms to receive it →
     `php /home/<youraccount>/<path-to-this-repo>/cron/daily-snapshot.php`
     (live birds, mortality, eggs, feed, water, and the day's money figures —
     see `src/Core/FarmSnapshot.php` for exactly how each number is computed).
     There's also a "Send now" button next to it on the Configurations panel
     for an on-demand copy, independent of this schedule.

   The other three triggers (mortality, feed reorder, sale receipts) fire
   inline from the relevant save — no extra setup.

## AI Assistant

A chat page (Insight → AI Assistant) that answers plain-English questions
about a farm's own real records — "how's mortality this week", "what's our
feed stock", "summarize this month's profit" — from a fixed set of
read-only, farm-scoped data lookups (`src/Core/AiAssistant.php`), one per
data area (flocks, mortality, eggs, feed, money, sales, health,
vaccinations).

**Works with no setup at all**: with no OpenAI key configured, a question
is matched by keyword against those same lookups (`askLocally()`) and
answered directly — no external call, no cost, no account needed anywhere.

**Add an OpenAI key** (`OPENAI_API_KEY` from platform.openai.com, in
`config.php`'s `openai` block) to switch to letting the model itself pick
from those same tools for more open-ended questions (`askViaOpenAi()`) —
it never sees raw SQL and never receives or sends a farm id, so there's no
way for it to read (let alone write) anything outside the caller's own
farm. This mode costs a small amount per question (`gpt-4o-mini` by
default) and is rate-limited to 40 questions/hour per farm to bound that.

## Deploying updates

This repo is cloned into cPanel's own Git™ Version Control feature
(Repository Path: `repositories/pfmis-backend`, separate from the live site).
`.cpanel.yml` defines what "deploy" means: a plain `cp -R` from that clone
into the live path — safe by construction, since `config/config.php` and
`public/uploads/` are gitignored and so never exist in the cloned copy for
`cp` to overwrite. To push a change live: cPanel → Git™ Version Control →
**Manage** → **Pull or Deploy** → **Update from Remote** (fetches the latest
commit), then **Deploy HEAD Commit** (copies it into the live path). Two
clicks, no manual file upload, no FTP.

(A fully automatic version of this — GitHub webhook → cPanel auto-deploys
with zero clicks — is possible via cPanel's API, but was deliberately not
set up here: it needs a cPanel API token capable of full account access on
most cPanel versions, which is a bigger credential to protect than the
two-click workflow above is worth.)

## API reference

Base path is the server root. All bodies are JSON. Most collections filter by
`?farm_id=`.

| Method | Path | Purpose |
|--------|------|---------|
| GET | `/healthz` | liveness check |
| POST | `/auth/login` | `{email,password}` → `{token,user}` |
| GET/POST/PUT/DELETE | `/users[/{id}]`, `/roles[/{id}]` | accounts + permissions (needs `settings` permission) |
| GET/POST/PUT/DELETE | `/farms[/{id}]` | farms |
| POST | `/farms/{id}/logo` | upload a farm logo `{image:"data:image/png;base64,..."}` (PNG/JPEG/WEBP, ≤1.5MB) |
| DELETE | `/farms/{id}/logo` | remove the farm logo |
| GET/POST/PUT/DELETE | `/flocks[/{id}]` | flocks (`?farm_id=`) |
| GET/POST/PUT/DELETE | `/bird-usage[/{id}]` | live birds donated/consumed/other (`?farm_id=`) — "sold" is derived from sale line items, "died" from daily_entries, neither is ever written here |
| GET | `/daily-entries?farm_id=` | list daily entries |
| POST | `/daily-entries` | upsert production numbers `{farm_id,flock,date,...}` |
| POST | `/daily-entries/feed` | draw feed per component `{farm_id,flock,date,lines:[{type,qty}]}` |
| GET/POST/PUT/DELETE | `/feed-stock[/{id}]` | feed inventory |
| GET/POST/PUT/DELETE | `/eggs[/{id}]` | egg collections |
| GET | `/eggs/tally?farm_id=` | summed grade tally |
| GET/POST/PUT/DELETE | `/egg-usage[/{id}]` | how eggs leave stock (sold/donated/consumed/other) |
| GET/POST/PUT/DELETE | `/egg-store-transfers[/{id}]` | moves pooled egg stock between two egg stores |
| GET/POST/PUT/DELETE | `/health[/{id}]` | health events |
| GET/POST/PUT/DELETE | `/vaccinations[/{id}]` | vaccinations |
| GET/POST/PUT/DELETE | `/inventory[/{id}]` | inventory items |
| GET/POST/PUT/DELETE | `/sales[/{id}]` | sales invoices |
| GET/POST/PUT/DELETE | `/expenses[/{id}]` | expenses (GET includes payroll runs) |
| GET/POST/PUT/DELETE | `/employees[/{id}]` | employees |
| GET/POST | `/payroll` | payroll runs |
| GET | `/lists` | managed dropdown lists `{key:[values]}` |
| POST/DELETE | `/lists` | add/remove one `{key,value}` |
| GET | `/sms-triggers` | this farm's 5 SMS triggers, each with `enabled`/custom `message`/built-in `default` |
| PUT | `/sms-triggers/{kind}` | `{enabled,message}` — enable/disable and reword one trigger (`kind` is one of `mortality`, `feed_reorder`, `sale_receipt`, `vaccination_due`, `daily_snapshot`) |
| POST | `/sms-triggers/daily-snapshot/send` | sends the caller's farm an on-demand copy of the daily snapshot right now, independent of the scheduled cron job |
| GET | `/backup/export` | downloads a `.sql` file of every record tied to the caller's farm — restorable by pasting into phpMyAdmin's SQL tab against a database that already has this schema applied. Excludes users/roles/sessions, so it never contains a password hash. Needs `settings:edit`. |
| POST | `/ai-assistant/ask` | `{messages:[{role,content}, ...]}` — the AI Assistant chat. Rate-limited to 40 questions/hour per farm. |

### The two write paths on daily entries
Mirrors the UI's two save buttons. **Numbers** upserts one row per
farm/flock/date. **Feed** runs inside a DB transaction: it locks each feed
stock row (`FOR UPDATE`), checks there's enough, deducts the quantity, records a
`daily_entry_feed` line at that stock's unit cost, and rolls the totals into the
entry — so stock can never go negative and cost is always the real consumed cost.

## Design notes (for the next engineer)

- **Layering**: `index.php` (routing/CORS) → `Controller` (HTTP shape) →
  `Database` (PDO). No SQL is built from string concatenation of input —
  everything is a prepared statement, so injection isn't possible by construction.
- **Flock identity**: the UI uses business codes (`FL-2401`) as the flock key,
  so child tables reference `flock_code` and the API echoes `id` as that code
  (with `db_id` alongside for numeric joins).
- **Payroll → expenses**: rather than duplicating rows, `v_expenses_all` unions
  manual expenses with payroll runs at read time, matching the UI exactly.
- **SMS**: `src/Core/Sms.php` wraps Pahappa's EgoSMS gateway with plain cURL/
  `file_get_contents` (no Composer dependency). Every send is best-effort —
  failures are logged and swallowed, never allowed to break the request that
  triggered it — and every attempt is recorded in `sms_log`, which the same
  class reads back for its own dedup checks (`alertFarm`/`alertCustomer`).
  `Sms::TRIGGERS`/`DEFAULT_TEMPLATES` are the single source of truth for
  which triggers exist and their default `{token}` wording — `sms_triggers`
  only ever stores a farm's *override* of one, so adding a new trigger kind
  never needs a schema change. `src/Core/FarmSnapshot.php` computes the daily
  snapshot's numbers, deliberately mirroring pfmis-web's `src/lib/metrics.js`/
  `DashboardPage.jsx` formulas exactly (Live Birds, Sickbay, Revenue/Expenses/
  Net Profit) so the SMS never disagrees with what the farm sees in the app.
- **Recurring vaccinations**: `vaccinations.repeat_every_days` (also settable
  per item on a `vaccination_programs` template) supports an open-ended
  repeat like "ND/IB monthly during laying". Marking a row with this set
  'done' auto-creates its next occurrence that many days later
  (`VaccinationController::scheduleNext()`) — skipped for a flock whose
  status is already 'spent', so a retired flock doesn't keep generating
  reminders. This can't be the base `Controller::update()`, since that ends
  in `$this->show()`'s `exit()`, so the whole method is overridden.
- **Auth**: bearer-token sessions (`sessions` table, not JWT — see `src/Core/Auth.php`).
  Every route except `/auth/login` requires one; `Auth::requirePermission()` gates
  each module by the caller's role (`roles.permissions`, a `module → {create,view,update,delete}`
  boolean map — "None" is just all four false). A role saved under the old
  single-level `none|view|edit` format is normalized on the fly (`edit` → all
  four true, `view` → view only) until it's next saved, which rewrites it in
  the new shape. See "Bootstrap your first login" above for the chicken-and-egg
  first account.
```
