# Business rules — contacts (FTI Consulting Intranet Contacts v2.2)

Evidence date: 2026-10-18.

Source: `git@bitbucket.org:overdrive/contacts.git`, `master` at `c1b9e91`.

No business-rules document exists in the source repository. Every rule below is taken from a branch in the ASP pages. Where the code implies a rule that the pages never state in words, the row is marked **Inferred**.

Passwords and connection strings are not copied here.

## Identity and access

### BR-001 — Two stored user types

**Implemented.** `admin_user.asp` maps form value 1 to `lngType` 47 and form value 2 to `lngType` 33. The visible labels are Admin and Viewer.

No other type can be chosen on the form. `topmenu.asp` treats only 47 and 33 as allowed. Any other stored type is sent to `logout.asp`.

### BR-002 — A session is the cookie `ckyUserID`

**Implemented.** Successful login sets the cookie to `UserID|lngType`. Pages that guard access redirect to `index.asp` when the cookie is null or empty. Logout sets the cookie to an empty string.

The cookie is not given an expiry, path, `HttpOnly`, or `Secure` flag in `index.asp`.

### BR-003 — Password check

**Implemented.**

- The browser requires a username and a password.
- The value stored in `tblUser.strPW` is an MD5 digest of the password typed in user admin (`md5(strPW)` in `admin_user.asp`).
- Login accepts the user when `MD5(stored digest + SeedVal)` equals the submitted `combin`, and the stored digest is not empty.
- `SeedVal` is `md5(time)` when the login page is opened without `btnLogin=Login`.
- A wrong password does not show an error alert. The login form is rendered again, after the page has written the username digest, `combin`, and the server digest.

### BR-004 — Username is unique

**Implemented.** Add and edit in `admin_user.asp` select `tblUser` by `strUserName`. A clash alerts "This User already exists!" or "This User already exists and cannot be added!" and does not write the row.

### BR-005 — Password placeholder

**Implemented.** The edit form displays `***************`. On save, that exact value leaves `strPW` unchanged. Any other value is MD5-hashed and stored. The browser asks for confirmation before changing a password on an existing user, and if the user declines it puts the placeholder back.

The password input is `type='text'`, not `type='password'`.

### BR-006 — The last Admin cannot be demoted

**Implemented.** When an existing user would be saved as Viewer (`lngType` 33), the page counts other users with `lngType` 47. If that count is 0, it forces 47 and alerts "The user type was changed back to type admin because there are no other users with admin rights!"

This check does not run when creating a user. The first user can be created as Viewer. Provenance: the count query is inside the `UserID>0` branch only.

### BR-007 — A person cannot delete their own user row

**Implemented.** `admin_user.asp` compares each selected id with `getUserID` of the cookie. A match increments a counter and skips the delete. The alert is "Your own user account cannot deleted!"

### BR-008 — Deleting a user deletes that user's queries

**Implemented.** If `tblQuery.refUserID` matches the user, those query rows are deleted and then the user is deleted. The alert says the user and their queries have been deleted.

An alert that would refuse the delete while queries exist is still in the file. The counter that would trigger it is never incremented (the increment is commented out). The refuse-path is **not in force**.

### BR-009 — Viewer and Admin do not have the same write rights

**Implemented.**

| Action | Admin 47 | Viewer 33 |
|---|---|---|
| Home, Search, Admin links | Yes | Yes |
| Save or delete lookups, publications, shows, contacts, beats, lists | `blnAdmin` true | `blnAdmin` false; the SQL does not run |
| Users, System Settings, Tools | Menu links, and the pages redirect others to `home.asp` | Not in the menu |
| My Details (signature only) | `admin_user_lite.asp` redirects type 47 to `admin_lookup.asp` | Allowed for the id on the query string |

`admin_user_lite.asp` updates `tblUser.strSignature` for the `lngID` on the query string. The menu link passes the current user's id. The page does not check that `lngID` equals the cookie user id.

### BR-010 — Admin area is open to Viewers

**Implemented.** The redirect that would have limited `admin.asp` to type 47 is commented out. The same commented redirect appears on the lookup pages. Write rules in BR-009 still apply.

**Unknown / Requires Confirmation:** whether Viewers are supposed to see the maintenance screens.

## Directory rules

### BR-011 — A category is a unique description

**Implemented.** `admin_category.asp` rejects a duplicate `strDescription` on add and on edit. The browser requires a non-empty description.

The same uniqueness pattern, on `strDescription`, is used for beat, city, country, designation, distribution list, language, suburb, and title.

Show and publication uniqueness use `strName`, not `strDescription`.

### BR-012 — A contact is unique by name within a publication

**Implemented.** `admin_contact.asp` treats a row as a duplicate when `tblContact.strName` and `refPublicationID` both match another row. The alert includes the publication name: "This Contact already exists for <publication>!" The same name on a different publication is allowed by this query.

### BR-013 — Contact required fields

**Implemented** in the browser on `admin_contact.asp`: Title, Contact Name, Designation, Publication. If those pass and `cntBeat` is 0, the browser asks "No beats have been added to this contact. Do you want to add beats now?" Choosing No still returns true and the save proceeds. Choosing Yes opens the beat editor and cancels the save.

The server does not repeat this required-field check. It relies on the browser and on `blnAdmin`.

### BR-014 — Publication required fields

**Implemented** in the browser on `admin_publication.asp`: Publication Name, Category, Language, Suburb, City, Country. Reach may be empty and is then set to 0. A reach that contains a character other than digits, `.`, or `-` is rejected.

If the URL starts with `http://`, those seven characters are removed before submit. A `https://` prefix is not removed.

### BR-015 — Show required fields

**Implemented** in the browser on `admin_show.asp`: Show Name. Reach uses the same empty-becomes-0 and numeric rule as publications.

### BR-016 — Delete is blocked while children exist

**Implemented.** The page counts (by selecting rows) and skips the delete when a child exists, then alerts with the number that were kept.

| Parent | Child check | Alert text |
|---|---|---|
| Category | `tblPublication.refCategoryID` | "linked to Publications" |
| Language | `tblPublication.refLanguageID` | "linked to Publications" |
| City | `tblPublication.refPhysCityID` | "linked to Publications" |
| Country | `tblPublication.refPhyscountryID` | "linked to Publications" |
| Suburb | `tblPublication.refPhysSuburbID` | "linked to Contacts" |
| Publication | `tblContact.refPublicationID` | "linked Contacts" |
| Show | `tblContact.refShowID` | "linked Contacts" |
| Title | `tblContact.refTitleID` | "linked to Contacts" |
| Designation | `tblContact.refDesignationID` | "linked to Contacts" |
| Beat | `tblContactBeat.refBeatID` | "linked to Contacts" |
| Distribution list | `tblContactDistributionList.refDistributionListID` | "linked to Contacts" |

**Discrepancy:** the suburb check reads publications, and the alert says Contacts. The country filter column is `refPhyscountryID` (lowercase `c` in `country`). The insert and update use `refPhysCountryID`. SQL Server collations are often case-insensitive; whether this database is, is **Unknown**.

### BR-017 — Deleting a contact deletes its links

**Implemented.** `admin_contact.asp` deletes the `tblContact` row, then `tblContactBeat` rows for that contact, then `tblContactDistributionList` rows for that contact. There is no child-exists block.

### BR-018 — A beat or distribution list is linked at most once per contact

**Implemented.** `admin_contact_beat.asp` rejects a `refBeatID` that is already stored for that `refContactID`. `admin_contact_distributionlist.asp` does the same for `refDistributionListID`.

### BR-019 — Contact and publication edits record who and when

**Implemented.**

- Contact save sets `strDateEdited` to `DD/MM/YYYY` and `strLastUser` to `getUserName` of the cookie user id.
- Publication save sets `strLastEdit` to `DD/MM/YYYY` and `strLastUser` the same way.
- Query run sets `tblQuery.strDateLastRun` to `YYYY/MM/DD`.

The three dates do not use the same format.

### BR-020 — Twitter Handle is the column `strCarReg`

**Implemented.** The contact form and both card layouts label the field "Twitter Handle" and read and write `tblContact.strCarReg`. A comment dated 20130128 says the label was changed from "Car Reg".

**Inferred:** older data in that column may be vehicle registrations. The application does not convert or validate the value as a Twitter handle.

### BR-021 — Switchboard and fax on the card come from the publication

**Implemented** on the Journalist tab (`lngTab=1`): `tblPublication.strSwitchboard` and `tblPublication.strFax`. Direct fax is `tblContact.strFaxAlternate`.

**Implemented** on the printer layout (`lngTab=0`): `tblContact.strSwitchboard` and `tblContact.strFax`.

**Implemented** on save: assignments to contact switchboard and fax are commented out, so new edits do not write those contact columns.

### BR-022 — Interests are stored and not shown on the card

**Implemented.** `admin_contact.asp` saves `memInterests`. `contactdetail.asp` selects the column and does not write it into the HTML.

### BR-023 — Notes are shown as stored

**Implemented.** The card writes `memNotes` into the cell without replacing newlines. The replacements that would turn `vbCr` into `<br>` are commented out.

## Search rules

### BR-024 — A query name is unique for its owner

**Implemented.** `rpt_custom_1.asp` rejects a save when another `tblQuery` row has the same `strDescription` and the same `refUserID`. The alert is "A query with this name already exists, please enter a different description". Two different users may use the same description.

The browser also requires a non-empty description and at least one selected field.

### BR-025 — Private is a flag, not a hide rule

**Implemented.** `blnPrivate` is stored as 1 when the checkbox is `on`, otherwise 0. The query list loads every query for every user. Private rows are red.

For a Viewer, Edit and Delete are disabled in the browser when `blnPrivate` is not 0 and `refUserID` is not the Viewer. An Admin does not get that browser lock.

Server-side delete allows the delete only when `refUserID` equals the current user, for every type. The alert names the owner.

**Inferred:** private was meant to stop other people editing the query. It does not stop them running it. `runQuery.asp` loads any `QID` and does not check `blnPrivate` or `refUserID`.

### BR-026 — Running a query updates the last-run date before showing rows

**Implemented.** `runQuery.asp` updates `strDateLastRun` as soon as `QID > 0`, including when the user only opens the result.

### BR-027 — Beat and distribution-list criteria are substring matches

**Implemented.** When the saved field label is `Beat` or `Distribution List`, `runQuery.asp` turns the criteria into `LIKE '%value%'` predicates combined with `OR` when several values are present. Other fields are selected by the `strName` expression stored in `tblFields`. The catalogue of those expressions is data, not source. **Unknown** until `tblFields` is read from the database.

## Mail rules

### BR-028 — One compose, three kinds of send

**Implemented** in `contact_email.asp` when `btnSend=Send`:

1. A summary to the sender (From and To are the username plus the read-only My Email Address). Bcc is `fdsouthafrica@fd.com`. The body lists Cc, Bcc, and each selected contact's name and email, then the message.
2. One message per selected contact, To `strName <strEmail>`, no Cc and no Bcc.
3. One message to the sender with the form's Cc and Bcc.

Each body is the FCKeditor HTML plus `<br><br><p><hr>` plus `tblSystem.strDisclaimer`.

Contacts with no row, or a failed lookup, are skipped for the per-contact send. The summary list is built from the same query.

### BR-029 — The sender address is the user's email, and it is not editable

**Implemented.** The My Email Address input is `readonly` and is posted as `strTo`. From is `getUserName(...) & " <" & strTo & ">"`.

### BR-030 — The signature is not added to the message

**Implemented.** `strSignature` is loaded into `mySignature` and is not concatenated into the body or the editor. Admins edit it on the user form. Viewers edit it on My Details. Neither path feeds the mail page.

**Unknown / Requires Confirmation:** whether the signature was supposed to be appended.

### BR-031 — Attachments are per-user files in `tmp_upload`

**Implemented.** Uploaded names are `<UserID>_<original filename>`. The attachment page lists only files with that prefix. After send, those files are deleted. There is no type or size check in `fAttach.cls.asp`.

### BR-032 — Disclaimer is a single system field

**Implemented.** `admin_system.asp` reads `tblSystem`. If a row exists it sets `lngID` to 1 and updates every row (`Update tblSystem Set strDisclaimer = ...` with no `WHERE`). If no row exists it inserts one.

Only an Admin can open the page (`lngType` must be 47, otherwise redirect to `home.asp`).

## Tooling rule

### BR-033 — The 2011 database button is conditional

**Implemented.** `admin_tools.asp` shows "Update database 2011/09/12" when the `tblFields` row whose `strLabel` is like `Notes` has a `strName` other than `CAST(tblContact.memNotes as varchar) AS conNotes`.

The button's server action does not update that Notes expression. It adds `strLastUser` on `tblContact` and on `tblPublication`, and `strLastEdit` on `tblPublication`, when the column count is 0.

A handler for `btnNotesUpdate=Update Notes Field` would update the Notes expression. The page does not render that button.

## Rules that are not product rules

These pages run without the cookie gate. They are not linked from the menus. They are recorded so they are not mistaken for current business behaviour.

| Page | Behaviour |
|---|---|
| `default.asp` | Writes `Hello` |
| `db_connect.asp` | Opens `Contacts` and writes a user id or `failed` |
| `new.asp` | Opens a local database named `bmsa` |
| `email.asp` | Sends one fixed test message |
| `testemail.asp` | Sends whatever from, to, subject, and body are posted |
| `contact_emailBCC.asp` | Same send shape as the live mailer, plus an extra Bcc added in a 2009 testing comment |

## Inferred rules (not stated as policy)

- The directory is a media-contacts list: publications contain contacts, contacts have beats and distribution lists, and mail is how a list is used.
- Type 33 is a reader who may still compose mail and run queries, and who may not change the directory.
- `fixquote` is the intended protection against a single quote breaking a SQL string. It is not applied to every value, and it is not a parameterised query.

## Unknown / Requires Confirmation

- Whether blocking the last Admin demotion, the self-delete block, and the child-delete blocks are approved policy or only defensive code.
- Whether private queries should be hidden from other users.
- Whether the suburb alert text or the suburb SQL is the intended rule.
- Whether `fdsouthafrica@fd.com` is still the required copy recipient.
- Whether empty beats are allowed. The browser asks; the server accepts a save with none.
- Database-level unique keys, foreign keys, and cascades. They are not in the repository. The rules above are the application checks only.
