# Business rules — gsa

Evidence date: 2026-10-13.

Source: `git@bitbucket.org:overdrive/gsa.git`, `master` at `ebb9cca93051bcd23a569d2b0a243a8d465bb740`.

Rules below are what the code enforces. They are **Inferred** unless a comment states them. There is no business-rules document in the source repository. Nothing here was confirmed with a product owner.

Database passwords are not copied into this file.

## Access

### BR-001 — Login matches the username and an MD5 password

**Inferred** from `login.php`.

`tblUser.strUser` must match. `md5` of the posted password must equal `strPassword`. `blnActive` must be 1. A missing user and a bad password share one message. An inactive user gets a different message that tells them to contact a systems administrator.

### BR-002 — The session is the cookie ckyGSA

**Inferred** from `login.php` and `checkLogin`.

The cookie value is `UserID|username`. In `system.inc.php` it lasts 3600000 seconds; in `systemLive.inc.php` it lasts 3600 seconds. Protected pages refresh it. There is no server-side session record in this code, and the cookie is not signed.

`renewalpdf.php`, `ximport.php`, `zimport.php`, many report wrappers, and the `arefCompanyID` branch of `contract.php` do not call `checkLogin` before returning data or running. `cron.daily.php` includes the system file and then dies.

### BR-003 — Page access is None, View, or Full

**Inferred** from `user.cls.php` and `getAccess`.

`tblSecurity.intLevel` is 0, 1, or 2. The user form labels them None, View, and Full. The menu shows an item only when the level is above 0. List methods run when the level is above 0. Save and delete run when the level is above 1. Below 2, form controls are marked disabled.

Levels are stored per user and per `tblmenulevel1` row. They are not role names in code.

### BR-004 — Some screens have a second permission

**Inferred** from constructors and report console.

| Check | Rule |
|---|---|
| Menu item id 1, level above 1 | `Advertiser` sets edit-all. Required to change sales person on an advertiser the user does not own. Contact logs use the same flag |
| Menu item id 3, level above 0 | Commissions report is enabled |
| Menu item label `Default`, level above 0 | Contact form treats the user as allowed to set the default flag |
| Menu item label `Sales Person`, level above 0 | `Search` sets sales-person access |

The labels for item ids 1 and 3 are in the database. They were not hard-coded as display names in the permission check for ids 1 and 3.

### BR-005 — Advertiser and contact-log edits are limited to linked sales people

**Inferred** from `Advertiser::Save`, `ContactLog::Save`, and `UserOwnsSalesPerson`.

Save proceeds only when page access is above 1 and at least one of these is true:

- `tblUserSalesPerson` has a row for this user and the posted sales person
- the user has edit-all (BR-004)
- the sales person id is not greater than 0

Otherwise the page says the user has insufficient access to change sales person.

A user with no sales-person links is warned on the user form. New users cannot be linked until after the first save, because the checkbox list is rendered only when `UserID` is already set.

## Advertisers

### BR-006 — Advertiser names are unique

**Inferred** from `Advertiser::Save` and `sql_company_check_unique`.

A second company with the same name is rejected.

### BR-007 — Active and prospective are computed from future bookings

**Inferred** from `Advertiser::ShowList`.

Filter value labelled Active means the company has at least one non-cancelled contract item, as client or as agency, with `strDate` after the current year-month. Prospective means it does not. This filter does not read `tblStatus` alone.

How `blnAgency` and related status columns are maintained in all paths is **Unknown / Requires Confirmation** where update SQL omits a column.

### BR-008 — An advertiser cannot be deleted while it has contracts or contacts

**Inferred** from `Advertiser::Delete`.

Delete is refused when related contracts or contacts exist.

### BR-009 — Advertiser name is required on the client

**Inferred** from `validateAdvertiserForm` (JavaScript).

## Contacts

### BR-010 — Default contact permission

**Inferred** from contact UI and security item label `Default`.

Setting the default contact flag is limited to users with that item permission (level above 0).

### BR-011 — Subscribe status is a lookup

**Inferred** from `tblSubscribeStatus` and contact forms.

## Contracts

### BR-012 — Contract form requires core booking fields

**Inferred** from `validateContractForm`.

Required: Advertiser, Sales Person, Status, Publication, Position, Size, Items (unsigned int), Payment Type, Amount (numeric), Discount (numeric), Start Date. Per-item amount boxes must pass `checkAmountTexts()` or save is blocked.

### BR-013 — Payment interval spreads amounts across items

**Inferred** from `ContractItem::Save` and `tblPaymentType.intInterval`.

Seed/lookup data uses intervals such as Monthly, Bimonthly, Quarterly, Biannually, Annually.

### BR-014 — Free contracts display as Free in STAR mail

**Inferred** from `cron.daily.php` handling of `blnFree` (unreachable after die).

### BR-015 — Discount reduces STAR totals

**Inferred** from cron SQL using `amount * (1 - discount/100)` (unreachable after die).

### BR-016 — Cancelled items are excluded from STAR month query

**Inferred** from cron SQL `blnCanceled = 0` (unreachable after die).

### BR-017 — Open contracts for XML exclude renewed parents

**Inferred** from `contract.php` XML branch: contracts where `refContractID = 0` for client or agency.

## Contact logs

### BR-018 — Contact log save uses sales-person ownership

**Inferred** from `ContactLog::Save` and BR-005.

## Reports

### BR-019 — Commissions report is optional per user

**Inferred** from `ReportConsole::Show` reading `tblSecurityItem` for `refMenuLevel1ItemID = 3`.

### BR-020 — Advert Updates report filters update labels

**Inferred** from report console copy: prints only for A, excludes SU, SD, NN, NB.

Exact filter implementation lives in `include/reports/advertchange.cls.php`.

## STAR newsletter

### BR-021 — STAR only includes magazines flagged blnSTAR

**Inferred** from `cron.daily.php` comments and SQL (after the hard stop).

### BR-022 — STAR is intended for the first of the month

**Inferred** from date checks in `cron.daily.php` (after the hard stop).

### BR-023 — STAR send is aborted

**Implemented** comment/behaviour: `die("site inactive 20180108")` runs before send logic.

## Users and passwords

### BR-024 — User passwords stored as MD5

**Inferred** from `login.php` and user save SQL in `sql.inc.php`.

### BR-025 — Inactive users cannot log in

**Inferred** from `login.php` and `blnActive`.

## Edge cases and failure behaviour

| Case | Observed behaviour | Provenance |
|---|---|---|
| Wrong password | Generic login failure message | **Inferred** |
| Inactive user | Distinct disabled message | **Inferred** |
| Missing security row for page | Code still reads `$row->intLevel` | **Implemented** — runtime effect **Unknown** |
| Duplicate advertiser name | Reject with exists message | **Inferred** |
| Delete advertiser with children | Blocked | **Inferred** |
| Insufficient sales-person access | Save refused with access message | **Inferred** |
| STAR cron invoked | Immediate die with site inactive message | **Implemented** |
| Unauthenticated report URL | Many wrappers skip `checkLogin` | **Implemented** |
| Unauthenticated renewal PDF | Skips `checkLogin` | **Implemented** |
| Unauthenticated import | Skips `checkLogin` | **Implemented** |

## Rules not found

No written SLA, pricing rules, commission percentage formulae beyond report code, or GDPR/retention policy appear in this repository. Do not invent them.
