# Invoice Review Module

**Folder:** `CRUD/invoice_review/` · **Reached from:** navbar → Invoice Review · **Entry page:** `invoice_dashboard_main.php`

Where hires turn into money. The module holds **three separate flows** that are easy to confuse because they all say "invoice":

| Flow | What it does | Folder |
|---|---|---|
| **Monthly invoice check** | Check what *should* be invoiced this month against the hire data | `invoice_review.php`, `invoice_dashboard*.php` |
| **Internal invoices** | Upload a batch of finished invoice PDFs, OCR them, match each page back to a hire | `internal_invoices/` |
| **Invoice prepare** | Build the document pack that goes out **with** an invoice (POD, timesheet, PO…) | `invoice_prepare/` |

> Documented to the format set by [sites.md](sites.md).

---

## How the three flows relate

```mermaid
flowchart TD
    HL[(hire_list<br/>+ timesheets, PODs, POs)] --> MC[Monthly Invoice Check<br/>invoice_review.php]
    MC --> ACC[Invoice raised in the<br/>accounts system]
    ACC --> BU[Batch upload of invoice PDFs<br/>internal_invoices/batch_upload.php]
    BU --> OCR[OCR + split into pages<br/>invoice_ocr_run.php]
    OCR --> BR[Batch review<br/>batch_review.php]
    BR --> MM[Match each page to a hire<br/>invoice_match_review.php]
    MM --> CONF[Confirmed<br/>batch_confirm.php]

    HL --> IP[Invoice Prepare queue<br/>invoice_prepare.php]
    IP --> PACK[Print pack:<br/>invoice + POD + timesheet + PO]

    CONF --> CLEAN[Monthly cleanup / archive<br/>admin/monthly_cleanup_panel.php]
```

---

## Dashboards

![Invoice Dashboard](../screenshots/invoices__dashboard_main.png)

- `invoice_dashboard_main.php` — the module entry point.
- `invoice_dashboard.php` — a secondary dashboard view.
- `admin/get_system_stats.php` — stats feed (JSON).

## Monthly invoice check — `invoice_review.php`

![Invoice Review](../screenshots/invoices__review.png)

Works off **`monthly_invoice_check`** (15,114 rows) — a per-hire, per-period record of what should be billed. This is where discrepancies get spotted before invoicing: missing PO, missing POD, missing fuel, wrong rate. The chasing pages in [Missing Info](../README.md) work the same data from the other end.

Related periods live in `invoice_monthly_report_periods` (12 rows) and the reporting side is in `CRUD/reports/invoice_monthly_reports/`.

---

## Internal invoices — `internal_invoices/`

![Batch List](../screenshots/invoices__batch_list.png)

Takes a **batch PDF of finished invoices**, splits it per page, OCRs each page and matches it back to the hire it belongs to.

| Page | Purpose |
|---|---|
| `batch_upload.php` | Upload the batch PDF |
| `batch_upload_save.php` | Store it and create the batch (handler) |
| `invoice_ocr_run.php` | Run OCR over the batch (API) |
| `batch_list.php` | All batches |
| `batch_review.php` | Review one batch (`?id_invoice_batch=`) |
| `page_view.php` | One invoice page (`?id_invoice_page=`; `&embed=1` for the viewer) |
| `page_save.php` / `page_confirm_quick.php` | Save / quick-confirm a page (handlers) |
| `invoice_match_list.php` | Pages awaiting a hire match |
| `invoice_match_review.php` | Match one page to a hire (`?id_invoice_page=`, optionally `&id_hire_list=`) |
| `invoice_match_report_selector.php` | Matching reports |
| `batch_confirm.php` | Confirm the whole batch (handler) |
| `download_confirmed_invoices.php` | Download confirmed invoices |
| `config.php` | **Local bootstrap** — pulls in `bootstrap.php`, `session.php` and `database.php` for every page in this folder |

![Batch Review](../screenshots/invoices__batch_review.png)

![Match List](../screenshots/invoices__match_list.png)

![Page View](../screenshots/invoices__page_view.png)

> **Auth note:** the pages here don't include `session.php` directly — they include `__DIR__/config.php`, which does. They are authenticated. (A naive "does this file mention session.php" check reports them as unauthenticated; the inventory tool now follows includes.)

**Tables:** `internal_invoice_batches` (3), `internal_invoice_pages` (180), `internal_invoice_ocr_templates` (0 — no OCR templates defined yet).

---

## Invoice prepare — `invoice_prepare/`

![Invoice Prepare](../screenshots/invoices__prepare.png)

Builds the **document pack that accompanies an invoice** — pulling together the POD, timesheet, purchase order and any other evidence for a hire, so the customer gets one bundle.

`invoice_prepare.php` is the queue screen (1,457 lines). Everything else is its API:

| Endpoint | Purpose |
|---|---|
| `invoice_prepare_list.php` | List the queue |
| `invoice_prepare_build.php` | Build the pack |
| `invoice_prepare_search_docs.php` | Find documents to attach |
| `invoice_prepare_save_selection.php` | Save the chosen documents |
| `invoice_prepare_replace.php` | Swap a document |
| `invoice_prepare_restore_original.php` | Revert to the original |
| `invoice_prepare_set_status.php` | Set queue status |
| `invoice_prepare_delete.php` | Remove from the queue |

**Tables:** `invoice_prepare_queue` (96 live, 434 archived).

## Invoice API — `invoice_api/`

Shared endpoints used by the screens above: `invoice_list.php`, `invoice_load.php`, `invoice_save.php`, `invoice_set_status.php`, and `invoice_create_print_pack.php` (produces the print pack).

---

## Monthly cleanup & archiving — `admin/`

![Cleanup Panel](../screenshots/invoices__cleanup_panel.png)

`monthly_cleanup.php` / `admin/monthly_cleanup_panel.php` archive finished work at month end into dated tables — visible in the schema as `invoice_pdf_reviews_archive_2026_07` (373 rows) and `invoice_prepare_queue_archive_2026_07` (434 rows), alongside the live `invoice_pdf_reviews` (3) and `invoice_prepare_queue` (96).

`admin/view_archives.php` browses archived data.

## Database

| Table | Rows | Notes |
|---|---|---|
| `monthly_invoice_check` | 15,114 | Per-hire billing check — the biggest table in the module |
| `internal_invoice_pages` | 180 | Split invoice pages awaiting/holding a hire match |
| `internal_invoice_batches` | 3 | Uploaded invoice batches |
| `internal_invoice_ocr_templates` | 0 | OCR field templates — none defined |
| `invoice_prepare_queue` | 96 (+434 archived) | Document packs being assembled |
| `invoice_pdf_reviews` | 3 (+373 archived) | PDF review records |
| `invoice_monthly_report_periods` | 12 | Reporting periods |

> The OCR steps depend on the **extractor service on `127.0.0.1:8012`** — the same service the [capture flows](capture-flows.md) use. Health at `CRUD/suppliers/supplier_invoices/ocr_status.php`.

---

## Known issues

Reviewed 2026-07-21.

**No bugs found.** Every page returned HTTP 200 with no PHP errors or warnings. The 302s were correct behaviour — `batch_review.php`, `page_view.php` and `invoice_match_review.php` redirect when their id parameter is missing, and all return 200 with a valid id.

| # | Observation | Status |
|---|---|---|
| 1 | Ten files in `internal_invoices/` initially appeared to have **no authentication**. They are in fact authenticated via `__DIR__/config.php`, which requires `session.php`. | **Not an issue** — verified; inventory tool corrected to follow includes |
| 2 | `admin/view_archives.php` (198 lines) is unreferenced. | **Open** — archive browsing may have been superseded by the cleanup panel |
| 3 | `internal_invoice_ocr_templates` is empty, so OCR field-matching has no templates defined. Combined with the extractor service being offline locally, **the OCR path cannot be exercised on a dev machine**. | **Environmental** — verify on live if OCR matching is expected to be in use |
| 4 | `invoice_match_review.php` takes `?id_invoice_page=`, while the sibling `batch_review.php` takes `?id_invoice_batch=`. Easy to confuse when linking. | **Open** — cosmetic |

### Cross-cutting: unauthenticated API endpoints

Improving the inventory's auth detection surfaced a gap in **earlier passes**: my per-module health checks only exercised `Type=Page` files, so **API endpoints were never requested**. 26 files across the portal still have no auth guard, and while most are legitimate libraries, several are genuine APIs — e.g. `forms/pdi/pdi_next_number.php`, `forms/pdi/pdi_upload_image.php`, `forms/timesheets/timesheet_upload.php`, `hire_list/send_loler/fetch_loler_emails.php`, `hs/api_get_accreditation_sections.php`.

Some may be deliberately open for tablet capture, but this is the same class as the **H1 finding** (unauthenticated debtors API) in the [security review](../security-audit-2026-07.md). **Recommend a dedicated sweep of all unauthenticated API endpoints** rather than handling them piecemeal per module.
