# Business rules — laravel

Evidence date: 2026-10-10.

Source: `git@bitbucket.org:overdrive/laravel.git`, `master` at `10c0b9fa`.

Rules below are taken from `config/application.php`, middleware, route groups, model constants, and the scheduler. Where a comment and a value disagree, both are recorded. Rules are not inferred beyond what the code states. "Inferred" is used only when behaviour is a reasonable reading of a comparison or a comment and is not spelled out as a rule.

Committed secrets are not copied.

## BR-001 — Who may use the market

**Implemented.**

| Role id | Role name | Rule |
|---|---|---|
| 1 | admin | May open `/admin` after admin login. A signed-in non-admin is sent to `/` |
| 2 | seller | May open market pages. Selling routes also require `role:seller\|both` |
| 3 | buyer | May open market pages. Buying routes also require `role:buyer\|both` |
| 4 | guest_user | May open market pages. Inactive account status does not log them out. `ROLE_ID` is `selected_role_id` |
| 5 | both | May open selling routes and buying routes |

A signed-in user with none of seller, buyer, guest, or both is redirected to `/admin`.

Evidence: `config/application.php` role keys; `AuthUserAccess`; `AuthAdminAccess`; route groups in `routes/web.php`.

## BR-002 — Account active flag

**Implemented.**

`account_status` `0` is inactive and `1` is active.

On the web, an inactive user who is not a guest is logged out with "Your account has been deactivated".

On the API, `CheckJwt` treats an inactive buyer, seller, or both account as an invalid token with the same sentence.

If the session's `role_id` differs from the user's current role, the web user is logged out with "Your account info is changed."

Evidence: `app/Models/User.php`; `AuthUserAccess`; `CheckJwt`.

## BR-003 — Registration roles

**Implemented.**

Web forms exist for buyer, seller, and both. The API register role list is the same three ids (2, 3, 5). Guest and admin are not in `api_register_roles`.

Password minimum length is 8.

Identity document types offered: "SA ID No" and "Passport No". A third option, "Foreign Passport No", is commented out.

Name and surname max length 20. Identity number and contact number max length 15. Company name max 150. Company description max 191.

Evidence: `config/application.php` `api_register_roles`, `password_min_length`, `identity_type`, `user_max_length_arr`.

## BR-004 — Lot kind

**Implemented, with a conflict.**

Queries compare `lot_type` to `BulkLot` constants:

| Value | Constant | Meaning in the constant name |
|---|---|---|
| 0 | `LOT` | Single lot |
| 1 | `BULK_LOT` | Bulk lot |
| 2 | `BIN_BULK_LOT` | Bin lot |
| 3 | `AGENT_LOAD` | Agent load |
| 4 | `TENDER` | Tender |
| 5 | `POA_LOT` | Price on application |
| 6 | `DONATION_SPECIAL_LOT` | Donation or special offer |
| 7 | `DONATION_SPECIAL_TEMPLATE` | Template |
| 8 | `BULK_LOT_TEMPLATE` | Template |
| 9 | `BIN_BULK_LOT_TEMPLATE` | Template |

`config/application.php` `lot_type` is only:

| Value | Label |
|---|---|
| 1 | BID |
| 2 | BIN |
| 6 | Donation & Special Offer |

**Requires confirmation.** The config label for `1` is BID. The constant name for `1` is `BULK_LOT`. Lot queries use the constants, not the config map. Which label a screen shows for each integer was not traced per view.

Special-offer versus donation, inside type 6, is a second field: `special_lot_types` `1` Special Offer, `2` Donation.

Evidence: `app/Models/BulkLot.php`; `config/application.php` `lot_type` and `special_lot_types`.

## BR-005 — Bid status numbers disagree

**Implemented constants. The maps conflict. Do not treat one table as the business rule until confirmed.**

`app/Models/Lot.php`:

| Value | Constant |
|---|---|
| 0 | `BID_STATUS_PENDING` |
| 1 | `BID_STATUS_LIVE` |
| 2 | `BID_STATUS_SOLD` |
| 3 | `BID_STATUS_UNSOLD` |
| 4 | `BID_STATUS_EXPIRED` |
| 5 | `BID_STATUS_CLOSED` |
| 6 | `BID_STATUS_HOLD` |
| 7 | `BID_STATUS_DELETED` |
| 8 | `BID_STATUS_SUSPENDED` |

`config/application.php` `bid_status_array` uses the same numbers through 5, then `6` Under Offer, `7` Withdrawn, `8` Suspended.

`marketplace_lot_bid_status_array_new` is a different scale (0 Upcoming/Future through 15 Expired), including Reserved, Declined, Requested, and Unavailable.

`special_donation_lot_bid_status_array` is a third scale (0 Upcoming through 6 Withdrawn), and the word Donated is used for both value 2 and value 5.

Seller edit is allowed when status is in `seller_allow_edit_lot_status` = `[0, 4]`. The comment says "0-pending 4-Unsold". On `Lot` constants, 4 is Expired, and Unsold is 3. On `bid_status_array`, 4 is also Expired.

Delete is allowed when status is in `lot_delete_when_status` = `[2, 3]`. The comment says "3-Sold, 4-Unsold". On `Lot` constants, 2 is Sold and 3 is Unsold. The comment does not match the array.

Relist text list `bulk_lot_relisting` is the words Sold, Unsold, and Closed. That list is strings, not the integers above.

Evidence: `app/Models/Lot.php`; `config/application.php`.

## BR-006 — Listing status numbers disagree

**Implemented. Conflict.**

`Lot` listing constants: 0 pending, 1 approved, 2 awaiting, 3 rejected, 4 expired, 5 live, 7 suspended, 8 deleted, 9 inactive. There is no constant for 6.

`listing_status_array` matches 0–5 and 7, omits 8, and labels 9 "In Progress". The model constant for 9 is `LISTING_STATUS_INACTIVE`.

Evidence: `app/Models/Lot.php`; `config/application.php` `listing_status_array`.

## BR-007 — Reserve price and asking price

**Implemented** as config text and as `Lot::RESERVE_PRICE = 1`, `Lot::ASKING_PRICE = 2`.

Reserve price: bidding starts at that price, and only bids above it result in a bid sale.

Asking price: bidding starts at a lower start price. Bids above the asking price result in a bid sale. Bids that remain below the asking price become an offer to the seller. If the seller accepts the offer, a bid sale is concluded.

`arr_price_type` uses the same two labels.

`start_price_overridden_percentage` is 10. No comment states what is overridden. Usage was not traced. **Unknown** as a business rule beyond the constant.

`supplier_invoice_unit_bid_price` is 0.95. No comment. Usage was not traced.

Evidence: `config/application.php` `reserve_price_info_text`, `asking_price_info_text`; `app/Models/Lot.php`.

## BR-008 — Proxy bids

**Implemented.**

Config text: a user can add, amend, or cancel a proxy bid at any time.

`proxy_bid_button_prior` is 3600 seconds.

The commands `ProxyBid:proxy_bid` and `PriorProxyBid:prior_proxy_bid` run every minute when `APP_ENV` is not `dev`, and each has an unauthenticated URL.

Top bidder count in config is 5.

Bid increment options are 5 and 10.

Evidence: `config/application.php`; `app/Console/Kernel.php`.

## BR-009 — Bin cart lifetime

**Implemented.**

`clear_bin_cart_time` is 60 (the comment says minutes).

`BinQuantityRemove:remove` is scheduled every minute.

`DeleteLotBinCart` is not scheduled. A public URL calls it.

`bin_bulk_auto_approve` is 300 seconds. Usage was not traced.

`suspend_button_prior` is 300 seconds. Usage was not traced.

Evidence: `config/application.php`; `Kernel.php`; `routes/web.php`.

## BR-010 — Under-offer timing

**Implemented** in `UpdateLotStatus::handle`, which reads `under_offer_duration` and `offer_request_duration` from the latest `Setting` row. Empty offer-request duration becomes `00:00:00`.

The duration values themselves live in the `setting` table, not in git. **Unknown** without that row.

`admin_approved_lot_edit_before` is 5 minutes. `rejected_lot_hide_time` is 10 minutes for special and donation lots.

Evidence: `app/Console/Commands/UpdateLotStatus.php`; `config/application.php`.

## BR-011 — Bidding mandates

**Implemented** as data on the user, not as a fixed amount in code, except one default.

`AuthUserAccess` defines `DAILY_BIDDING_MANDATE`, `WEEKLY_BIDDING_MANDATE`, and `MONTHLY_BIDDING_MANDATE` from `userInformation`.

`daily_mandate_limit` in config is the string `10000`. Whether that caps the daily mandate was not traced.

Settlement options in config: the only uncommented option is "Direct EFT via your bank's Pay and Clear Now, or Immediate Interbank Payment (IPP) facility" at key `1`. Credit Card and IPAY are commented out.

Evidence: `AuthUserAccess`; `config/application.php` `daily_mandate_limit`, `bidding_settlement_options`.

## BR-012 — Prices exclude transport

**Implemented** as display text, not as a price calculation that was verified.

`bin_marketplace_text` and `bid_marketplace_text` say prices are farmgate, exclude transport, and buyers arrange transport.

Currency `1` is Rand and `2` is US dollar.

Evidence: `config/application.php`.

## BR-013 — Product groups

**Implemented** as environment ids, not as names stored in code.

| Config key | Production `.env` value |
|---|---|
| `MEAT_PRODUCT_GROUP_ID` | 17 |
| `BEVERAGES_PRODUCT_GROUP_ID` | 19 |
| `NUT_DRIED_FRUIT_PRODUCT_GROUP_ID` | 11 |
| `GRAINS_BEANS_PRODUCT_GROUP_ID` | 5 |
| `NURSERY_PRODUCT_GROUP_ID` | 21 |
| `MAINLINE_VEG_PRODUCT_GROUP_ID` | 1 |
| `FRUIT_PRODUCT_GROUP_ID` | 2 |

`beans_product_group_id` is hardcoded `7` when `APP_ENV` is `local` or `dev`, and otherwise uses the grains/beans env value (5 in the production file). A comment in the same file says "Live Meat Product group ID: 17" and "Live Beverages Product group ID: 19", which matches the production `.env`.

Evidence: `config/application.php`; production `.env` keys only.

## BR-014 — Catalogue attributes

**Implemented** as the attribute index in config: Variety, VAT, Packaging, Packaging Units, Count, Pallet/Crate/Box, Pricing, Colour, Flavour, Minimum Order Quantity, Grade, Size, Weight (Est), Lot Size. Inventory Category and Bulk Discount are in `attributes_index` and commented out of the display array.

Lot size type labels: 1 PLT, 2 CRT, 3 BOX, 4 BAG.

Product description max length 450. Identical lots minimum 1 and maximum 10. Images jpeg/jpg/png, 5 MB. Up to 4 logos on a lot, resized toward 200 by 200.

Grade initial string is `Grd`. Variety code length is 2.

GlobalG.A.P., LocalG.A.P., and Solidaridad member numbers have max length 15.

Evidence: `config/application.php`.

## BR-015 — Seller business classification

**Implemented** as option lists.

Seller types: Mega Farmer, Commercial Farmer, Smallholder/Emerging Farmer, Packers/Food Processors, Agent/Trader.

Buyer types: National Retailer/Wholesaler, Franchise Retailer, Independent Retailer/Wholesaler, Packer/Food Processor, Farmer/Exporter, Agent/Trader.

Business size: Small, Medium, Large, Mega.

Seasons: One Month, Spring, Summer, Autumn, Winter, All Year, Multiple Months. Month sets: autumn Mar–May, winter Jun–Aug, spring Sep–Nov, summer Dec–Feb.

Evidence: `config/application.php` `type_of_business`, `business_size`, `season_type`.

## BR-016 — Relief partners and delivery reach

**Implemented.**

Partner types: 1 General, 2 SA Harvest, 3 Food Forward.

Delivery location options: 20 km, 50 km, 100 km, 200 km, Provincially, Country Wide, and named Food Forward and SA Harvest sites (Port Elizabeth, Johannesburg, Pinetown, Rustenburg, Cape Town, Polokwane, and the three SA Harvest cities).

`reflief_dashboard_accepted_visible_time` is `23:59:59`. The key is spelled that way in config.

Evidence: `config/application.php` `relief_partner_type`, `delivery_location`.

## BR-017 — Guest and branded display names

**Implemented.**

Guest default name is `Guest`. Guest company code is `GUE`.

Anonymous seller label prefix is `Seller-`. Anonymous buyer label prefix is `Bidder-`.

Branded is permission id 2. Branded seller documents are PDF, max 5 files, 5 MB, under `uploads/seller_document/`.

Evidence: `config/application.php`.

## BR-018 — Notifications and mail categories

**Implemented.**

Notification category: 1 Message, 2 Update, 3 Both MSG & Update, 4 Urgent.

Seller audience flags: 1 Global Gap Sellers, 2 Bee Empowered Sellers.

Notification type: 1 normal, 2 trade alert.

API notification message max 120 characters. List page size 10. Subject max 191. Detail max 175.

`notification_sandbox` is true only when `APP_ENV` is `local`.

iOS category string is `CustomSamplePush`. `old_notification_payload` is false.

Evidence: `config/application.php`.

## BR-019 — Orders and tenders

**Implemented** as status lists.

Order request: 0 Pending, 1 Approved, 2 Awaiting, 3 Rejected, 4 Withdrawn.

Tender: 0 Pending, 1 Approved, 2 Withdrawn.

Quantity or price change message template: `{company_name} amended quantity or price`.

Evidence: `config/application.php` `order_request_status_array`, `tender_status_array`, `order_quantity_price_updated_message`.

## BR-020 — Scheduler does not run in dev

**Implemented.**

`Kernel::schedule` registers commands only when `env('APP_ENV') != 'dev'`. The production `.env` is `production`, so those commands are registered for that file. `local` would also register them. Only the literal `dev` skips them.

`SoftClose` is commented out of the schedule. A public route still runs it.

Evidence: `app/Console/Kernel.php`.

## BR-021 — JWT lifetime

**Implemented.**

Default signed-in window is 4 hours. If `keep_signedin` is set, the window is 2 days. An older `user_login_tokens` row is deleted and the API returns an invalid token.

Expired JWT deletes the FCM row for that user, device platform, and device token when those headers are present.

Missing `device-platform` or `device-token` does not fail the request. The error string for that case is commented out. The download-attachment segment is excluded from the empty check.

Evidence: `app/Http/Middleware/CheckJwt.php`; `keep_signedIn_second`.

## BR-022 — Uploads and CSRF

**Implemented.**

CSRF is on the `web` group. The only exception is `lot/upload_lot_image`.

Many state-changing routes are registered as `GET` (bid save, proxy bid, deletes, offer accept). CSRF does not apply to GET. That is observed routing, not a documented product rule.

Image and PDF limits in config are 5 MB (`1024*1024*5` or `1024*5` where the comment says the server value is kilobytes).

`lot_note_allowed_file_size` is `1024*1024*5` while its comment says "1 MB". The value is 5 MB. **Discrepancy.**

Evidence: `app/Http/Middleware/VerifyCsrfToken.php`; `config/application.php`.

## BR-023 — Legal documents

**Implemented.**

A first URL segment in `buyer_terms`, `marketplace_margin`, `privacy_policy`, `seller_terms`, `general_terms`, `donation_terms` is answered with the PDF of that name from `uploads/pdf/`, then `exit`. No auth check surrounds that block.

Evidence: `routes/web.php`; `config/application.php` `pdf_files`, `pdf_path`.

## BR-024 — Company and inventory codes

**Implemented** as constants used for CSV or inventory export. Call sites were not each traced.

Company code: 3-character initial, number length 2, number start 4, number end 5.

Customer CSV: tax type 3, discount type 3, with-VAT type 15, without-VAT type 2, store `001`, address field length 30. Product code prefixes `BID-`, `BIN-`, `RELIEF-`.

Inventory: purchase and selling tax type 3, discount type 3, group `001`, store `001`, category `000`.

`financial_year_month` maps calendar month 1→11, 2→12, 3→1, and so on through 12→10. No comment explains the shift. **Unknown** as a business meaning.

Evidence: `config/application.php`.

## BR-025 — Time zone

**Implemented.**

`time_zone` is `+05:30` when `APP_ENV` is `local`, otherwise `+02:00`.

`UpdateLotStatus` calls `convertTimeZone()` before status updates.

User-facing PHP date format is `d-m-Y`. API validation format is `Y-m-d`.

Evidence: `config/application.php`; `UpdateLotStatus.php`.

## BR-026 — Transporters

**Implemented.**

Transporter status 0 inactive, 1 active. Empty route message: "No trucks in the selected size found for your route".

Evidence: `config/application.php` `transporter_status`, `transport_route_empty_msg`.

## Rules that are not confirmed

- Whether the unauthenticated Artisan URLs are an approved operations interface.
- The numeric values of under-offer duration in the live `setting` row.
- Which status map a given screen or column uses when BR-005 and BR-006 conflict.
- Whether `daily_mandate_limit` of 10000 is enforced.
- Whether Mailgun or SMTP sends a given message. Both are configured.
