# Customers Module

**Folder:** `CRUD/customers/` · **Main table:** `customers` (476 rows) · **Reached from:** navbar → Customers

A customer is the account a machine is hired to. Every hire, site, purchase order and invoice ultimately points at one. Alongside the customer records themselves, this module covers three sub-areas that all hang off the customer account code: **Debtors** (credit control), **Statements** (splitting a bulk statement PDF per account), and **Purchase Orders**.

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

---

## How the pieces fit together

```mermaid
flowchart TD
    A[Customers Dashboard<br/>dashboard.php] --> B[Customer List<br/>customers_index.php]
    A --> D[Debtor Dashboard<br/>debtors/index.php]
    A --> S[Statement Batches<br/>statements/statement_batch_list.php]
    A --> P[Upload POs<br/>po_upload/po_upload.php]

    B --> C[Create Customer]
    B --> R[View Customer]
    B --> E[Edit Customer<br/>+ Evidence Documents]

    D --> DL[All Debtors]
    DI[Import Debtors<br/>CSV from Sage] --> DL
    DL --> DD[Debtor Detail<br/>balance, promises, contact log]
    DD --> DA[Audit Trail]
    DL --> DR[Debtor Reports]

    SU[Upload Statements<br/>one bulk PDF] --> SV[Batch Review<br/>split + match to accounts]
    SV --> S
    S --> SD[Bulk Download]

    E -.insurance expiry.-> IR[Insurance Expiry Report<br/>reports/hickey_reports]
```

---

## Customers Dashboard — `dashboard.php`

![Customers Dashboard](../screenshots/customers__dashboard.png)

The hub for the whole module. Eight tiles grouped into sections, linking to the debtor tools, statement tools, the customer list and PO upload.

## Customer List — `customers_index.php`

![Customer List](../screenshots/customers__customer_list.png)

Every customer account in a DataTable. Unlike the site list this loads in one go (476 rows is small enough not to need server-side paging).

**What you can do here:** search by account code or name; see active/inactive status; row actions to view, edit, activate/deactivate and delete.

## Create Customer — `customers_create.php`

![Create Customer](../screenshots/customers__create_customer.png)

Adds a row to `customers`. The account code (`customer_acc`) is the key the rest of the portal joins on — hires, debtors and statements all match on it, so it must match the code used in Sage.

**Field groups:** identity (account code, name, VAT number, company number), accounts contact (name, phone, email, invoice email), **plant insurance** (insurer, expiry date, cover limit), **credit check** (rate, date, pass/fail), plus a notes field and two flags — `customer_requirePO` and `customer_active`.

> `customer_requirePO` is the flag that drives the Missing PO chasing elsewhere in the portal — if a customer requires a PO, hires without one get flagged.

## View Customer — `customers_read.php`

![View Customer](../screenshots/customers__view_customer.png)

Read-only view of one customer (`?id_customer=<id>`). An invalid or missing id redirects back to the list rather than erroring.

## Edit Customer — `customers_update.php`

![Edit Customer](../screenshots/customers__edit_customer.png)

Same fields as create (`?id_customer=<id>`), **plus an "Evidence Documents" section** at the bottom for uploading plant-insurance and credit-check paperwork against the customer.

> **There is a second copy of this page — `customers_update_ins.php`.** It is reached from the Insurance Expiry report and is byte-for-byte the same except that its Back/redirect links return to that report instead of the customer list. It is **missing the Evidence Documents section**. See [Known issues](#known-issues).

## Activate / Deactivate — `customers_toggle_active.php`

Small POST handler that flips `customer_active`. Customers are deactivated rather than deleted so historic hires keep resolving.

## Delete Customer — `customers_delete.php`

Hard delete, with a confirmation step. Use deactivate instead unless the record was created in error — deleting a customer that has hires against it will leave those hires orphaned.

## Evidence Documents — `customer_documents_*.php`

Insurance certificates and credit-check paperwork attached to a customer, stored in `customer_documents`.

- `customer_documents_helper.php` — renders the upload UI and its JS; included by the edit page (`render_customer_document_upload_ui()`, `add_customer_document_scripts()`).
- `customer_documents_modal.php` — the modal markup for viewing existing documents.
- `customer_documents_upload.php` — the AJAX upload endpoint. Validates via the shared upload guard (extension whitelist + magic bytes).

---

## Debtors (credit control)

A snapshot of the sales ledger imported from Sage, with the chasing activity recorded on top. Table: `debtors` (490 rows).

### Debtor Dashboard — `debtors/index.php`
![Debtor Dashboard](../screenshots/customers__debtor_dashboard.png)

Headline totals and ageing buckets — current, 0–30, 31–60, 61–90 and 90+ days.

### All Debtors — `debtors/debtors_list.php`
![All Debtors](../screenshots/customers__debtors_list.png)

Full ledger with balance, ageing, days overdue, status and who it's assigned to.

### Debtor Detail — `debtors/debtor_detail.php`
![Debtor Detail](../screenshots/customers__debtor_detail.png)

One account (`?id=<id_debtor>`): balances, amounts promised vs paid, last contact and next review date, assigned-to, and free-text notes.

### Import Debtors — `debtors/import_debtors.php`
![Import Debtors](../screenshots/customers__import_debtors.png)

Bulk-loads the ledger from a Sage export, matching on `customer_account`.

### Debtor Reports — `debtors/debtor_reports.php`
![Debtor Reports](../screenshots/customers__debtor_reports.png)

Ageing and collection reporting across the ledger.

### Audit Trail — `debtors/debtor_audit_trail.php`

Field-level change history from `debtor_audit_log` (field name, old value, new value, who, when) — a different, finer-grained shape than the whole-row snapshots used by `hire_list_audit`.

---

## Statements

Takes **one bulk statement PDF** (all customers in a single file, as produced by the accounts system), splits it per account and files each part. Tables: `customer_statement_batches` → `customer_statements`.

### Upload Statements — `statements/statement_batch_upload.php`
![Upload Statements](../screenshots/customers__statement_upload.png)

Upload the combined PDF. `statement_batch_upload_save.php` is the POST endpoint that stores it and creates the batch row.

### Statement Batches — `statements/statement_batch_list.php`
![Statement Batches](../screenshots/customers__statement_batches.png)

Every uploaded batch with its status, statement count and page count.

### Batch Review — `statements/statement_batch_review.php`

Opens a batch (`?id=<id_batch>`) to check the split — which pages became which customer's statement, and whether each matched an account code.

### Bulk Download / Delete
- `bulk_download.php` — downloads all statements in a batch together.
- `statement_batch_delete.php` — removes a batch and its split statements.

---

## Purchase Orders

### Upload Purchase Order — `po_upload/po_upload.php`
![Upload PO](../screenshots/customers__po_upload.png)

Drag-and-drop uploader that saves the file as `<PO number>.pdf` into `CRUD/customers/POs/`. Images are converted to PDF automatically.

**The filename is load-bearing** — `hire_list.php`, `documents_panel.php`, `missing_PO.php`, `site_move_read.php` and `history/server_processing.php` each build the path `/CRUD/customers/POs/<purchase_order>.pdf` by hand. If the name doesn't match the PO number on the hire, the document won't appear against that hire.

`POs/uploadpos.php` is the older uploader (plain multi-file save, keeps the original filename). It is kept on the [Legacy Uploads](../../CRUD/legacy_uploads.php) page.

---

## AJAX / helper endpoints (not user-facing)

| File | Method | Purpose |
|---|---|---|
| `customers_toggle_active.php` | POST | Flip `customer_active` |
| `customer_documents_upload.php` | POST | Upload an evidence document; returns JSON |
| `customer_documents_helper.php` | include | Renders document upload UI + scripts |
| `customer_documents_modal.php` | include | Document viewer modal markup |
| `statements/statement_batch_upload_save.php` | POST | Store the bulk PDF, create the batch |
| `statements/statement_batch_delete.php` | POST | Delete a batch and its statements |
| `statements/bulk_download.php` | GET | Zip/stream all statements in a batch |

## Database

| Table | Rows | Notes |
|---|---|---|
| `customers` | 476 | Master record. `customer_acc` is the join key used portal-wide |
| `debtors` | 490 | Sales-ledger snapshot imported from Sage, keyed by `customer_account` |
| `debtor_audit_log` | — | Field-level change history for debtor records |
| `customer_statement_batches` | 2 | One row per uploaded bulk PDF |
| `customer_statements` | 58 | One row per split statement, `id_batch` → the batch |
| `customer_documents` | 0 | Evidence documents (insurance / credit check) |

Key relationships:
- `site_address.site_customer_id` → `customers.id_customer` (see [sites.md](sites.md))
- `hire_list.customer_acc_id` → `customers.id_customer`; `hire_list.customer_acc` snapshots the code
- `debtors.customer_account` and `customer_statements.customer_account` match `customers.customer_acc` **by code, not by id**

---

## Known issues

Found during the review pass on 2026-07-21.

| # | Issue | Status |
|---|---|---|
| 1 | `customers_update_ins.php` is a near-duplicate of `customers_update.php` (differs only in back-links) and is **missing the Evidence Documents upload section** — so the route reached *from the Insurance Expiry report* is the one place you cannot upload a new insurance certificate. | **Open** — needs a decision (add the section, or de-duplicate both into one page taking a `return` parameter) |
| 2 | The dashboard's "Upload POs" tile pointed at the legacy uploader after the new drag-and-drop page was added. | **Fixed** — now points to `po_upload/po_upload.php` |
| 3 | `debtors/api/get_debtors.php` is unreferenced — the debtor pages query the database directly and never call it. It was also unauthenticated until the July security audit. | **Open** — candidate for deletion |
| 4 | `statements/api/download_by_account.php` is unreferenced. | **Open** — verify then delete |
| 5 | `customer_documents` table is empty (0 rows) while the upload feature exists. | Worth confirming the feature is actually in use |
