# Business rules — nataliehirschman

Evidence date: 2026-10-09. Source: `master` at `f9248b83`.

Rules below are what the code does. They are not an approved policy. Each rule is marked **Inferred** when it is only visible by reading the implementation. Nothing in the repository states these as written business policy.

## Site identity

1. The public name on the pages is "natalie hirschman Fine Art" / "Natalie Hirschman". **Inferred** from titles and the header in `index.html`. Documented on the pages themselves.
2. The admin sidebar and the mail helper use "Nathalie Hirschman". **Inferred** that this is the same artist. The spelling is inconsistent in code (`sidebar.php`, `mail_helper.php`). Which form is authoritative is **Unknown / Requires Confirmation**.

## What the public can see

3. About content is the `images` row joined to the `pages` row whose `page_name` is `About`. CV uses `page_name` `CV`. The works list uses `page_name` `Works`. **Inferred** from `ApiModel`. The names are exact. There is no code list of required pages.
4. The unfiltered works list is ordered by the `order` column ascending. **Inferred** from `getWorksImages`.
5. A works filter keeps rows whose `content_type` equals `image` and whose `category` or `id` equals the filter string. **Inferred** from `getWorksImage`.
6. The public site does not hide rows with `Active` unset or false. The flag is stored and ignored by the API. **Inferred** from `ImageModel` (writes `Active`) and `ApiModel` (no `Active` condition).
7. Desktop navigation does not offer Galleries, Landscape, or Still life as destinations. The Galleries file and the Landscape/StillLife category options in admin still exist. **Inferred** from `index.html` and `imageList.php`. Whether those categories are retired is **Unknown / Requires Confirmation**.
8. Figurative and Portraits in the menu both open `works.html` with no filter, so they show the same unfiltered Works query. **Inferred** from the hrefs. They do not select the admin categories of the same names.

## Contact and email list

9. A contact submission is stored only after Google reCAPTCHA reports `success`. The stored fields are the posted name (column `from`), email, phone, and comments. **Inferred** from `contact.php` and `Contact::create`.
10. The contact form's "required" labels are not enforced in `contact.php`. An empty email can be stored if reCAPTCHA passes. **Inferred**.
11. Storing a contact does not send email. `sendMail` in `Contact::create` is commented out. **Inferred**. The helper would send as `nataliehirschmanweb@gmail.com` with the display name "Nathalie Hirschman" if something called it.
12. A newsletter signup stores the posted email and a `created_at` timestamp of `Y-m-d h:i:s` (12-hour clock). No check for a valid address or a duplicate. **Inferred** from `Newsletter::create`.
13. The newsletter route is not in the anonymous exception list. Contact is. **Inferred** from `Filters.php`: an anonymous newsletter POST is sent to the login page instead of `create`. This conflicts with `newsletter.php`, which assumes the POST is accepted.
14. Home and archive subscribe forms do not use rule 12. They post to FASO or `nataliehirschman.com/webadmin/confirm`. **Inferred** from those forms. This repository has no `webadmin/confirm` script.

## Artwork records

15. Staff choose a page from the `pages` table, a content type of `Image` or `Content`, a category of Figurative, Portraits, Landscape, or StillLife, and a medium of Oil, Water, or Carole. **Inferred** from the options in `imageList.php`. "Carole" is the option value as written. Whether it should be "Charcoal" or another medium is **Unknown / Requires Confirmation**.
16. A new image row records `created_by` as the current date-time, the same value style as `created_at`, not the signed-in user. **Inferred** from `ImageModel::addImage`.
17. If a file name is posted, the admin saves the original and a 500-pixel thumbnail. The mime-type and 4096 KB checks are not on the path that saves the file. **Inferred** from `Upload::upload` (the `if ($validateImage)` block is commented out).
18. Update with a new file moves the original but does not rebuild the thumbnail. The stored thumbnail path on update is `public/assets/uploads/{name}`, not the thumbnail directory used on create. **Inferred** from `Upload::updateImageupload` and `ImageModel::updateImage`.
19. Deleting an article deletes the `images` row only. **Inferred** from `ImageModel::deleteArticle`. No file-delete call was found.

## Who may use the admin

20. Login is a username (the form field is named `inputEmail`) and a password checked with `password_verify`. **Inferred** from `Welcome::index`.
21. A successful login is sent to `articles`. An existing session that opens `/` is sent to `home`. **Inferred**. Those two destinations differ.
22. Wrong password and unknown user produce the same flash message: "Your ID or Password is Wrong". **Inferred**.
23. Anyone can open Register. The account is created with role id `1`. The seed names role id `1` "Developer" and gives that role the Dashboard, Users, Menu Management, and CRUD Generator menus. **Inferred** from `Welcome::registration` and the seeds. There is no approval and no second role in the seed.
24. The seeded user is `jaco@overdrive.co.za`, full name "Developer Tester", role `1`, password hash of the literal `123456`. **Inferred** from `Database/Seeds/Users.php`. Whether this row was loaded into the live database is **Unknown / Requires Confirmation**.
25. After login, a URL is allowed only when its first segment matches a menu the role can access. Unknown segments redirect to `/`. Denied segments redirect to `blocked`. **Inferred** from `Authorization`.
26. These URLs skip the login filter: `/`, `GetLogin`, `register`, `api`, `api/*`, `upload`, `contact`. The grant filter also skips `blocked`, `home`, and `Welcome/*`. **Inferred** from `Filters.php`.
27. Because `upload` and `contact` skip login, an anonymous client can post an upload or read and write contacts if those routes are reachable. **Inferred**. This is a consequence of the filter list, not a documented requirement.
28. The read API skips login. **Inferred**. That matches the public pages, which call it with no session.
29. CORS allows any origin and GET, POST, OPTIONS, PUT, and DELETE. OPTIONS ends the request immediately. **Inferred** from `Cors`.
30. CSRF protection is configured in `App.php` and is not enabled in `Filters.php` (the `csrf` entry is commented out). **Inferred**.

## Registration validation

31. Register requires a password, a confirmation that matches, and an email that is not already `users.username`. **Inferred** from the validation rules in `Welcome::registration`. Full name has no rule in that array.
32. Usernames are stored as the email string. Uniqueness is the CI `is_unique[users.username]` rule. **Inferred**.

## Data the migrations actually create

33. Migrations create `users`, `sessions`, `user_role`, `user_access`, `user_menu`, `user_submenu`. A later `User` migration file is empty. **Inferred** from `app/Database/Migrations`.
34. `pages`, `images`, `contacts`, `newsletter`, and `employees` are used by models and are not created in those migrations. **Inferred**. Their columns are whatever the database already has. The fields the PHP writes are listed in the technical document.

## Money, orders, and stock

No price, cart, tax, or stock rule exists in this repository. Enquire pages are static HTML. Nothing here takes payment.

## Conflicts to confirm

- Public newsletter handler versus the login filter (rules 12 and 13).
- Admin categories versus public filter labels (rules 5, 8, and 15).
- `Active` stored but not applied (rule 6).
- Contact mail commented out while an SMTP config and helper exist (rule 11).
- Public registration into the Developer role (rule 23).
- Anonymous `upload` and `contact` (rule 27).
- Natalie versus Nathalie (rule 2).
- Two subscribe mechanisms: this admin's `newsletter` table, and FASO `confirm.asp` / `webadmin/confirm` (rule 14).
