# Business rules — thegsa

Evidence date: 2026-10-01.

Source: `git@bitbucket.org:overdrive/thegsa.git`, `master` at `522705535dc8a80d3676a76d4b3a189dd7a924b4`.

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`. It lasts 3600000 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`, and the `arefCompanyID` branch of `contract.php` do not call `checkLogin`. `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.

| Check | Rule |
|---|---|
| Menu item id 1, level above 1 | `Advertiser` sets `blnEditAll`. 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 `blnSalesPersonAccess` |

The labels for item ids 1 and 3 are in the database. They were not in git.

### 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 "You have insufficient access levels to change sales person."

A user with no sales-person links is warned in red 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 with "An advertiser with the name … already exists!"

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

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

Filter value 1 (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. Value 0 (labelled Prospective) means it does not. This filter does not read `tblStatus`.

`sql_company_update` does not write `refStatusID` or `blnAgency`. The agency flag is still a list filter against column `blnAgency`. How that column is maintained now is **Unknown / Requires Confirmation**.

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

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

Contracts are checked first, then contacts. Either match blocks the delete.

### BR-009 — Chosen positions are stored separately from contracts

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

On every successful save the code deletes `tblAdvertiserPosition` for the company and inserts the posted position ids.

Copying positions from the company's contracts onto that table, inside `Contract::Save`, is commented out. The comment says it was disabled because GSA did not want to pay for it. That sentence is **Documented** in the comment. The disabled behaviour is not a current rule.

## Contacts

### BR-010 — A contact name is unique within one advertiser

**Inferred** from `Contact::Save` and `sql_contact_check_unique`.

The same name on a different company is not blocked by this query.

### BR-011 — Each advertiser has one default contact

**Inferred** from `Contact::Save`.

Marking a contact default clears `blnDefault` on the other contacts of that company. If the company then has no default contact, the contact just saved is set as the default.

### BR-012 — A new contact form starts at subscribe status 2

**Inferred** from `Contact::AddEdit`.

Before a row is loaded, `refSubscribeStatusID` is set to 2. An empty posted value becomes 0 on save. The words for these ids are rows in `tblSubscribeStatus`, not constants in PHP.

### BR-013 — The first contract for a client sets every contact to subscribe status 3

**Inferred** from `Contract::Save`. The nearby comment says this is for a prospective client.

When the client has no contract yet, insert sets `tblContact.refSubscribeStatusID = 3` for every contact of that company. The label for id 3 is **Unknown**. Later contracts do not run this update.

### BR-014 — A contact with contact logs cannot be deleted

**Inferred** from `Contact::Delete` and `sql_contact_check_delete`.

The query reads `tblContactLog`. The message shown to the user says the contact is associated with a Fee Structure. The message does not describe the query.

## Contact logs

### BR-015 — A contact-log update stores the follow-up and does not rewrite the call date

**Inferred** from `sql_contactlog_update`.

Update writes sales person, contact, type, status, contract, follow-up date, discussion, and action. It does not set `strDate`. Follow-up dates posted as `dd-mm-YYYY` are stored via `makeShortDateToODBCDate`.

### BR-016 — Magazines on a contact log are replaced as a set

**Inferred** from `ContactLog::Save`.

After insert or update, all `tblContactLogMagazine` rows for that log are deleted and the posted ids are inserted.

### BR-017 — Contact logs can be deleted with no child check

**Inferred** from `ContactLog::Delete`.

`sql_contactlog_delete` runs whenever page access is above 1.

## Contracts

### BR-018 — A contract header requires the booking fields in the browser

**Inferred** from `validateContractForm` in `include/contract.cls.php`.

Required before post: advertiser, sales person, contract status, publication, position, size, a positive integer item count, payment type, a numeric amount, a numeric discount, and a start date. The server save does not repeat this list. A blank discount becomes 0. A missing free flag becomes 0.

### BR-019 — Item count drives how many monthly rows exist

**Inferred** from `ContractItem::Save`.

The number of `tblContractItem` rows is increased or reduced until it matches `intItems`. New rows step forward one month at a time from the start month, or from the month after the latest existing item. Extra rows are deleted from the end of the date order.

### BR-020 — Payment type interval decides which months carry the amount

**Inferred** from `ContractItem::Save`.

`intInterval` comes from `tblPaymentType`. Walking items in date order, an item whose index modulo the interval is 0 can receive `dblAmount`. The others can be set to 0. That write happens when the item's `dblAmountEdit` is null or when `blnAmount` is posted as 1.

### BR-021 — Per-item edits stand unless an overwrite box is ticked

**Inferred** from `ContractItem::Save`.

Each item can store its own position, size, colour, shape, edited amount, update label, CI number, skipped flag, and cancelled flag. If `blnPosition`, `blnSize`, `blnColour`, or `blnShape` is 1, that header value is written onto every item of the contract.

### BR-022 — A contract with contact logs cannot be deleted

**Inferred** from `Contract::Delete`.

`sql_contract_check_delete_contactlog` must return no rows.

### BR-023 — Duplicate copies the header and the items

**Inferred** from `Contract::Duplicate`.

The new header keeps agency, shape, client, sales person, status, magazine, position, size, colour, payment type, amount, notes, date, item count, and start date. Each item is copied, including cancelled state. The insert column list in that function does not set `refContractID` on the new header.

### BR-024 — Free is a flag, not a forced zero amount

**Inferred** from `Contract::Save` and the STAR query.

`blnFree` is stored on `tblContract`. The STAR mail, if it ran, would print `Free` instead of a rand amount when the flag is 1. `Contract::Save` does not zero the amount when the flag is set.

## Reports and mail

### BR-025 — Publication groups are hardcoded ids

**Inferred** from `salespersonreport.cls.php` and `Contract::ShowList`.

Filter value `-1` means magazine ids 3 or 12. Filter value `-2` means magazine ids 14 or 19. The publication names for those ids are **Unknown**.

### BR-026 — Advert Updates names an exclusion list on the console

**Documented** on the report console: "Prints only for: A (Excludes: SU, SD, NN, NB)".

The SQL inside `include/reports/advertchange.cls.php` was not line-traced for this onboarding. Confirm the exclusion in that file before writing a test that depends on it.

### BR-027 — Commissions are a separate permission

**Inferred** from `ReportConsole::Show` and `SalesPersonReport::ShowList`.

A user who can open the report console still cannot select or render Commissions unless security item id 3 has level above 0.

### BR-028 — The STAR newsletter does not send

**Documented** and **Implemented** in `cron.daily.php`.

The file comment describes a send on day 1 of the month, only for magazines with `blnSTAR = 1`, only for non-cancelled items in the current year-month, with a "Not ordered" row when that company has no booking in a STAR magazine. The running code stops with `site inactive 20180108` before that query.

### BR-029 — Import scripts are not behind the login cookie

**Inferred** from `ximport.php` and `zimport.php`.

`checkLogin` is commented out. Both scripts include the live database connection and insert core rows. Whether the web server still maps these URLs is **Unknown**.

## Rules that look like leftovers

These are in the source and should not be treated as current GSA policy without confirmation.

- `User::Delete` does not check `tblFeeStructure`. The check is commented out. `sql_user_check_delete` still selects that table.
- `Contact::Delete` mentions a Fee Structure while querying contact logs (BR-014).
- `settings.php` calls `checkSecurity` and `NotSufficientSecurityLevel`. Neither function is defined in `include/system.inc.php`, and `include/general/settings.cls.php` is not in the tree.
- Several SQL helpers are commented "Check for constraints later" above a delete that does not look for children. Company, contract, and contact deletes have their checks in the class. Branch delete does check `tblSalesPerson`.
