# PRD — laravel

Evidence date: 2026-10-10.

Source: `git@bitbucket.org:overdrive/laravel.git`, `master` at `10c0b9fa` (2023-08-15, "Restore prod .env").

There is no prior PRD in the source repository or in Silicon Ark. Requirements below are recovered from the implementation. Provenance is labelled. Committed secrets (application key, database passwords, JWT secret, FCM key, Mailgun key, Google Maps key, Pusher secret) are not reproduced.

`vendor/`, `bvendor/`, `public/`, and `storage/` were not fully read. See `project-plan.md` for the inspection limit.

## Product purpose

**Implemented.** The production `.env` names the app Freshtrade and points it at `https://market.hellochoice.co.za`. `config/application.php` calls the site HelloChoice. The deploy path is `/mnt/efs/www/html/market.hellochoice.co.za/`.

The system is a produce marketplace. Sellers list lots. Buyers bid, buy at a bin price, or reserve donation and special-offer lots. Administrators maintain products, users, and listings. A JWT API repeats the trading actions for a client that sends device headers.

**Documented** only as configuration and code comments. There is no README.

**Unknown / Requires Confirmation:** the commercial owner of Freshtrade versus HelloChoice, and whether both names are still in use.

## Users and roles

Roles are fixed in `config/application.php` and enforced with Spatie permission middleware plus `AuthUserAccess` and `AuthAdminAccess`.

| ID | Name | Who | Web access |
|---|---|---|---|
| 1 | `admin` | Administrator | `/admin`, middleware `admin` |
| 2 | `seller` | Seller | Middleware `user`, then `role:seller\|both` for selling |
| 3 | `buyer` | Buyer | Middleware `user`, then `role:buyer\|both` for buying |
| 4 | `guest_user` | Guest | Middleware `user`. Display name default `Guest`. Company code `GUE` |
| 5 | `both` | Buyer and seller | Selling routes and buying routes |

`User` account status: `0` inactive, `1` active (`User::$ACCOUNT_STATUS_INACTIVE` / `ACTIVE`).

`AuthUserAccess` (`app/Http/Middleware/AuthUserAccess.php`):

- A user whose role is not seller, buyer, guest, or both is redirected to `/admin`.
- An inactive user who is not a guest is logged out. Flash text: "Your account has been deactivated". Ajax receives `{logout: Y}`.
- If the session role id no longer matches the user's role, the user is logged out. Flash text: "Your account info is changed."
- Guests stay signed in even when `account_status` is not active. `IS_GUEST_USER` is set to `1` and `ROLE_ID` becomes `selected_role_id`.
- Daily, weekly, and monthly bidding mandates are read from `userInformation` into constants.

`AuthAdminAccess`: a signed-in user without the admin role is redirected to `/`. A guest of `/admin` is redirected to `/admin/login`.

API registration offers seller, buyer, and both (`api_register_role_ids`). It does not offer guest or admin.

Permissions named in config: `show_company_information` (id 1) and `branded` (id 2). Provenance: **Implemented** as config keys. How Spatie assigns them in production data is **Unknown** (no seed data; `DatabaseSeeder` is empty).

## Systems the product talks to

| System | Evidence | Direction | Used? |
|---|---|---|---|
| MySQL `mysql`, database `market_prod`, prefix `ft_`, port 3306, host an RDS endpoint in `eu-west-1` | Production `.env`, `config/database.php` | App reads and writes | Yes for market data. Host string not copied |
| MySQL `mysql2` | Same host, database, and prefix in the production `.env` | `OnlineUser` and `UserHistory` only | Configured. In that `.env` it is the same database as `mysql` |
| Browser session | `SESSION_DRIVER=file`, lifetime 120 minutes | Server | Yes |
| File cache, sync queue | Production `.env` | Server | Yes as the configured drivers |
| SMTP `mail.freshtrades.co.za:465`, encryption `tls` | Production `.env` `MAIL_DRIVER=smtp` | Outbound mail | Configured. Laravel's mail driver is SMTP |
| Mailgun domain `hellochoice.co.za` | `config/application.php` `mailgun_domain` and a hardcoded API key | Outbound, if a call site uses the Mailgun SDK | Key not copied. Which messages use Mailgun rather than SMTP was not traced |
| Firebase Realtime Database | `config/application.php` paths; events `LotUpdateFirebase`, `LotBidUpdateFirebase`, `LotBinQuantityUpdateFirebase`, `LotSpecialDonationStatusUpdateFirebase` | App writes lot, bid, bin, and donation state | Implemented listeners. Production paths are `/lots`, `/lot_bids`, `/users/`, `/bin_lots`, `/bin_lot_bids`, `/donation_special_lot`. Local `APP_ENV` uses a `freshtrade/testing/...` prefix |
| FCM | `brozot/laravel-fcm`, `app/Library/FirebaseFCM.php`, `FCM_SERVER_KEY` in `.env` | Push to device tokens | Implemented. Key not copied |
| Pusher cluster `ap2` | Production `.env` `BROADCAST_DRIVER=pusher` | Broadcast | Configured. No app broadcast class was found beyond the framework setup |
| Redis `127.0.0.1:6379` | Production `.env` | None of cache, session, or queue | Present and unused by those drivers |
| Google Maps | `config/application.php` `google_map_key`; default map centre `-26.2041028, 28.0473051` | Browser | Key is hardcoded and not copied |
| YouTube | `how_to_buy_url`, `how_to_sell_url` | Outbound links | Documented in config |
| HelloChoice FAQ | `https://www.hellochoice.co.za/faqs/` | Outbound link | Documented in config |
| WordPress | Comment on `GET /get_all_market_lots` | Inbound HTTP | Route exists. The WordPress repo is not here |
| AWS CodeDeploy / EFS | `appspec.yml` | Deploy | Spec exists. Live deploy status is **Unknown** |
| S3 | `config/filesystems.php` disk `s3`, `FILESYSTEM_CLOUD` default `s3` | Optional | Production `.env` has no `AWS_*` keys. `DEV.env` has empty `AWS_BUCKET`. No application upload path in `config/application.php` points at S3 |

Contact addresses configured in `config/application.php` (not secrets): `noreply@hellochoice.net`, `loadlisting@hellochoice.net`, `useraccounts@hellochoice.net`, `connect@hellochoice.co.za`, `contact@hellochoice.co.za`, `offers@hellochoice.co.za`, phone `0338151036`. Finance mail is `finance@hellochoice.co.za` unless `APP_ENV` is `dev`, in which case it is `petrive@hellochoice.co.za`. Also `onefarmshare@hellochoice.co.za`, `registrations@hellochoice.co.za`, `shelly@saharvest.com`, `khamil@foodforward.com`. Transport-quote recipients are `grantj@amkelainvestments.co.za` with BCC `olivias@hellochoice.co.za`, `wayneb@ishack.co.za`, and `transportquote@hellochoice.co.za`.

## Major workflows

### WF-1 — Register and activate

Public routes, before the `user` middleware:

- `GET/POST register/buyer`, `register/seller`, `register/both` (`Auth\RegisterController`).
- `GET /activate/{token}` (`Auth\UserActivation@activate`).
- `GET user-info`, `user-info/skip`, `POST save-user-personal-info`, `POST save-user-company-info`.
- `Auth::routes()` is also registered (Laravel's login, registration, and password routes).
- Separate forgot-password screens exist for the site and for `/admin/forgot-password`.

API: `POST /api/register`, `POST /api/login`, `POST /api/forgot_password`, `GET /api/master_data` are outside the JWT group.

Password minimum length is 8 (`password_min_length`). Identity types are "SA ID No" and "Passport No".

Provenance: **Implemented** routes and config. The exact validation rules inside `RegisterController` were not line-reviewed in this pass.

### WF-2 — Sign in and open the dashboard

`GET /` and `GET /dashboard` sit inside middleware `user` and call `HomeController@index`. A guest is sent to `/login`, and the current URL is stored in the session unless the request is Ajax.

`GET /web_login` calls `Auth\LoginController@webLogin` with no `user` middleware. Provenance: **Implemented**. What token it accepts is the controller method; it was not line-reviewed.

JWT sessions: `CheckJwt` parses the bearer token, rejects invalid and expired tokens (`INVALID_TOKEN`, `TOKEN_EXPIRED`), deletes the matching FCM device token on expiry, and drops the login row if it is older than 4 hours, or 2 days when `keep_signedin` is set (`keep_signedIn_second`).

### WF-3 — Browse the marketplace

Inside `user`:

- `GET /marketplace` lists the market.
- Lot, live-lot, bin-lot, packaging, and product feeds.
- Preference get/update.
- Wishlist and bulk watchlist (limit `bulk_add_to_watchlist_limit` = 100).
- Branded seller search and detail.
- `GET /get_all_market_lots` is outside `user` middleware. The comment says it is for the WordPress site.

Marketplace copy in config states that prices are farmgate and exclude transport, and that buyers arrange transport themselves.

Currency symbols: `1` Rand (`R`), `2` US dollar (`$`). `Lot` queries split bin totals into rand and dollar.

Provenance: **Implemented**.

### WF-4 — Bid on an auction lot

Inside `role:buyer|both`:

- `GET /lot/detail`, `GET /bid_now_save`.
- Proxy bid: `GET /lot/proxy_bid_detail`, `GET /save_proxy_bid`, `GET /lot/cancel_proxybid`.
- `GET /marketplace/live-bidding`.

`config/application.php`:

- Price type `1` is Reserve Price. Bidding starts at that price. Only bids above it become a bid sale (`reserve_price_info_text`). `Lot::RESERVE_PRICE = 1`.
- Price type `2` is Asking Price. Bidding can start lower. Bids above the asking price are a sale. Bids still below it after bidding become an offer to the seller (`asking_price_info_text`). `Lot::ASKING_PRICE = 2`.
- Bid increment options are 5 and 10.
- A proxy bid can be added, amended, or cancelled at any time (`proxy_bid_text`). The proxy button is available `proxy_bid_button_prior` = 3600 seconds before the relevant time.
- Start price may be overridden by `start_price_overridden_percentage` = 10. Call sites were not traced.

The scheduler runs `ProxyBid:proxy_bid` and `PriorProxyBid:prior_proxy_bid` every minute when `APP_ENV` is not `dev`. The same commands are also exposed as unauthenticated `Route::any` URLs.

Provenance: **Implemented** routes, config text, and schedule. The bid-matching algorithm inside `ProxyBid::handle` was not fully traced.

### WF-5 — Buy a bin lot

Inside `role:buyer|both`: bin cart add, remove, detail, save, and `/thank-you`. Bin offer add is on `role:seller|buyer|both`.

Config: `clear_bin_cart_time` is 60 minutes. `BinQuantityRemove:remove` runs every minute. `DeleteLotBinCart:delete_lot_bin_cart` has a public URL and is not on the scheduler.

`is_bin_offers` exists on lots (migration `2021_10_28`). Offer request duration and offer difference percent are settings columns (migration `2021_10_27`).

Sellers accept or decline bin offers from `/bidview`. Buyers do the same from `/dashboard/bin_offer_*`. The API has `bin_offer/save`, `delete`, `accept`, and `decline`.

Provenance: **Implemented**.

### WF-6 — Seller publishes a lot

Inside `role:seller|both`:

- Bulk bid lots: `/bulk_lot/add_new`, edit, save, relist, templates.
- Bin lots: `/bin_bulk_lot/add_new` and the same family.
- Agent loads: `/agent_load/add`.
- POA lots: `/poa_lot/add`.
- Calculator widget.
- Reports: listing summary, bidding, seller transactions, bin sales.

`BulkLot` type constants:

| Value | Constant |
|---|---|
| 0 | `LOT` |
| 1 | `BULK_LOT` |
| 2 | `BIN_BULK_LOT` |
| 3 | `AGENT_LOAD` |
| 4 | `TENDER` |
| 5 | `POA_LOT` |
| 6 | `DONATION_SPECIAL_LOT` |
| 7 | `DONATION_SPECIAL_TEMPLATE` |
| 8 | `BULK_LOT_TEMPLATE` |
| 9 | `BIN_BULK_LOT_TEMPLATE` |

`config/application.php` `lot_type` only lists `1 => BID`, `2 => BIN`, `6 => Donation & Special Offer`. That is a shorter map than `BulkLot`. See the discrepancy in `business-rules.md`.

Identical lots: minimum 1, maximum 10. Images: jpeg/jpg/png, 5 MB, stored under `uploads/lots/` and the bulk/bin/special temp folders. Lot size units: PLT, CRT, BOX, BAG.

Relist is allowed when status text is Sold, Unsold, or Closed (`bulk_lot_relisting`).

Provenance: **Implemented**.

### WF-7 — Special offers and donations

Special lot types in config: `1` Special Offer, `2` Donation.

Routes exist for sellers (and, for several of them, any `user`) under `/donation_special_lot`: create, edit, approve, withdraw, templates, images. Buyers with `role:buyer|both` have `/relief_dashboard` to reserve, reject, and accept.

Relief partner types: `1` General, `2` SA Harvest, `3` Food Forward. Delivery locations in config include kilometre bands, "Provincially", "Country Wide", and named Food Forward and SA Harvest sites.

API mirrors save, list, withdraw, approve, accept under-offer, and decline under-offer. Some special-lot product reads are outside the JWT group.

Provenance: **Implemented**.

### WF-8 — Orders, tenders, and requests

Inside `user` (not limited to buyer or seller): `/order` (grid, edit, approve, reject, messages), `/tender` (view and offer history), `/request` (grid and messages).

Several create/withdraw routes for orders, tenders, and requests are commented out in `routes/web.php`.

Order request statuses: Pending, Approved, Awaiting, Rejected, Withdrawn. Tender statuses: Pending, Approved, Withdrawn.

Provenance: **Implemented** for the live routes. The commented routes are **not registered**.

### WF-9 — Profile, alerts, and account

Inside `user`: profile, password, buyer and seller details, company brand, documents, certificates, my products, trade alerts, listing templates, daily bulletin flags.

Inside `role:seller|buyer|both`: account settings, close account, notification subscription, transport grid.

API: profile, company, password, close account, trade alerts, daily bulletin, device token, settings.

Provenance: **Implemented**.

### WF-10 — Admin console

`/admin/login` is public to guests. Everything under prefix `admin` with middleware `admin` is the catalogue and operations console: products, groups, varieties, sizes, weights, packaging, quantities, colours, flavours, grades, locations, categories, bidding durations and schedules, marketplace setup, product lots, sellers, buyers, guest users, lots of each type, orders, tenders, requests, notifications, user stats, transporters, truck sizes, loading points, municipal markets, settings, platform templates, relief dashboard.

Provenance: **Implemented**. Individual admin actions were not each line-reviewed.

### WF-11 — Scheduled trading maintenance

When `APP_ENV` is not `dev`, `app/Console/Kernel.php` schedules the commands in `technical-documentation.md`. The same commands can be triggered by unauthenticated HTTP routes in `routes/web.php`. `UpdateBinLotStatus` and `DeleteLotBinCart` have HTTP routes and are not on the schedule. `SoftClose` is commented out of the schedule and still has an HTTP route.

Provenance: **Implemented**.

### WF-12 — Daily bulletin and legal PDFs

`SendDailyBulletinMail` runs every minute and builds a PDF with TCPDF. `UserController@dailBulletingPdf` renders `user/bulletin_pdf` through TCPDF on `GET /bulletin/pdf` (no `user` middleware). `CreateMarketplaceFilterPdf` exists; its schedule line is commented out. A public URL still calls it.

`routes/web.php` serves a PDF inline when the first URL segment is one of `buyer_terms`, `marketplace_margin`, `privacy_policy`, `seller_terms`, `general_terms`, `donation_terms`, from `uploads/pdf/`.

Provenance: **Implemented**.

## Functional requirements

### FR-001 — Market home

The signed-in market user can open `/` and receive the dashboard.

- Actor: seller, buyer, guest, or both.
- Trigger: `GET /` or `GET /dashboard`.
- Unauthenticated behaviour: redirect to `/login` and remember the URL for a non-Ajax request.
- Evidence: `routes/web.php`, `AuthUserAccess`.
- Implementation: Implemented.
- Confidence: High.

### FR-002 — Role registration

A visitor can register as buyer, seller, or both, and activate with a token.

- Evidence: `register/*`, `GET /activate/{token}`.
- Implementation: Implemented.
- Confidence: High for the routes. Field-level validation was not fully read.

### FR-003 — Marketplace list

A signed-in user can open `/marketplace` and load lot, bin, packaging, and product data.

- Evidence: `MarketPlaceController` routes inside `user`.
- Implementation: Implemented.
- Confidence: High.

### FR-004 — Auction bid and proxy bid

A buyer or both-role user can place a bid, save a proxy bid, and cancel a proxy bid.

- Evidence: `LotBidController` routes inside `role:buyer|both`. `ProxyBid` and `PriorProxyBid` commands.
- Implementation: Implemented.
- Confidence: High for the routes. Matching rules inside the command need a deeper read before they are treated as a complete specification.

### FR-005 — Bin purchase and offers

A buyer can add bin lots to a cart and check out. Buyers and sellers can accept or decline bin offers on their respective screens. The API exposes the same offer actions.

- Evidence: bin-cart routes, `BidviewController`, `Api\BinLotOfferController`.
- Implementation: Implemented.
- Confidence: High.

### FR-006 — Seller listings

A seller or both-role user can create and edit bulk, bin, agent-load, and POA lots, and can relist a bulk or bin lot.

- Evidence: seller route group.
- Implementation: Implemented.
- Confidence: High.

### FR-007 — Donations and relief

A user can create special-offer and donation lots. A buyer can reserve, accept, or reject them on the relief dashboard. Relief partner type is General, SA Harvest, or Food Forward.

- Evidence: `SpecialLotController`, `DashboardController@releifDashboard`, `relief_partner_type`.
- Implementation: Implemented.
- Confidence: High.

### FR-008 — Admin catalogue

An admin can maintain products and the attribute catalogues listed in WF-10.

- Evidence: `routes/web.php` admin group, `app/Http/Controllers/Admin/`.
- Implementation: Implemented.
- Confidence: High for the route list. Per-screen validation was not fully read.

### FR-009 — Mobile API

A client can log in, register, and call the JWT group in `routes/api.php` for profile, marketplace, lots, bids, bin cart, notifications, and trade alerts.

- Auth: `auth` guard `api` uses driver `jwt`. Group middleware is `check-jwt` and `jwt.auth`.
- Throttle: API group `throttle:1000,1`.
- Implementation: Implemented.
- Confidence: High.

### FR-010 — Live updates and push

Lot, bid, bin quantity, and special-donation status events update Firebase. The API can store a device token and send FCM.

- Evidence: `EventServiceProvider`, `app/Listeners/*Firebase*`, `Api\NotificationController`.
- Implementation: Implemented.
- Confidence: Medium for the exact Firebase payload shape (listeners were not line-reviewed).

### FR-011 — Inactive account

A non-guest user with `account_status` inactive cannot keep a web session or a JWT session.

- Web: logout and flash "Your account has been deactivated".
- API: `CheckJwt` returns `INVALID_TOKEN` and "Your account has been deactivated." when the role id is buyer, seller, or both. The check compares `hasRole` to role **ids**. Whether Spatie's `hasRole` accepts ids in this version was not verified.
- Implementation: Implemented in middleware.
- Confidence: High for the web path. Medium for the API role check.

### FR-012 — Public job triggers

Anyone who can reach the host can call the `Route::any` paths that run Artisan commands (`send_notifications`, `clear_cart`, `update_lot_status`, `start_proxy_bid`, and the others listed in `features.md`).

- Implementation: Implemented.
- This is observed behaviour. It is not confirmed as an approved business rule.
- Confidence: High that the routes exist.

## Constraints

- PHP `>=7.0.0` in Composer. Root `.htaccess` forces cPanel handler `ea-php70`. CodeDeploy targets a Linux EFS path and does not mention cPanel. Which host constraint applies in production is **Unknown**.
- Time zone in config is `+02:00` unless `APP_ENV` is `local`, then `+05:30`. The database connection timezone lines are commented out.
- Dates shown to users use `d-m-Y`. API date validation uses `Y-m-d`.
- Queue driver is `sync`. A `Mailqueue` model and `SendMail:send_mail` every minute imply mail is drained from the database, not from a Laravel queue worker. Call sites of `Mailqueue` were not fully traced.
- Sessions and cache are files. `LOG_PATH` in the production `.env` is the EFS `storage/logs` directory.
- No project test suite covers these workflows. `tests/Feature/ExampleTest.php` requests `GET /` and expects HTTP 200. That route requires `user` middleware, which redirects guests to `/login`.

## Integrations

See the systems table. Failure behaviour:

- JWT errors return JSON `status: error` with `errorType` `INVALID_TOKEN` or `TOKEN_EXPIRED`.
- Web auth failures redirect to `/login` or `/admin/login` with a flash message.
- Scheduler commands write output to files under `LOG_PATH` and use `withoutOverlapping()`. What happens when `LOG_PATH` is missing is **Unknown**.
- CodeDeploy hooks: `install_dependencies` and `start_server` have their `httpd` commands commented out. `stop_server` checks for `httpd` and does not stop it. `remove_archives.sh` deletes old CodeDeploy revision directories except the last successful install.

## Discrepancies that affect requirements

1. `Lot` bid-status constants, `config/application.php` `bid_status_array`, and `marketplace_lot_bid_status_array_new` do not use the same numbers for the same words. A requirement must not pick one map until a human confirms which column uses which map. Detail is in `business-rules.md`.
2. `config('application.lot_type')` is a three-entry display map. `BulkLot` constants are the values the lot queries compare against.
3. `DEV.env` uses Laravel 8-style keys (`MAIL_MAILER`, `LOG_CHANNEL`, `QUEUE_CONNECTION`) and `APP_NAME=Laravel`. The running app is Laravel 5.5 and the production `.env` uses `MAIL_DRIVER` and `QUEUE_DRIVER`.
4. The task scan listed Sonar files that are not in git.
