# Business rules — promotions

Evidence date: 2026-10-06.

Source: `git@bitbucket.org:overdrive/promotions.git`, `master` at `f06ff39` (2026-08-28).

There is no business-rules document in the source repository. Every rule below is taken from code, SQL comments, or page text. Rules marked **Inferred** are a reading of that implementation, not a signed-off policy.

## Identity and access

**BR-001 — Windows name must match a user row.** Login uses `Request.LogonUserIdentity.Name` (upper case) as `mstUser.strDomainLogin`, joined to `mstUserGroupPromotion`. No row means no session. **Implemented.** The Index message says Active Directory controls access. The application does not call a directory API. **Inferred:** Active Directory is only the source of the Windows logon name; authorisation is the Superbowl user row.

**BR-002 — View right gates the page body.** If `blnView` is not true, the master page replaces the content with "You need View rights to access this page." **Implemented.**

**BR-003 — Logout clears the ASP.NET session only.** The menu navigates to `Index.aspx`, which calls `Session.Clear()` and `FormsAuthentication.SignOut()`. Windows authentication is unchanged, so the same person can click Login again. **Implemented.**

**BR-004 — Numeric group ids control writes.**

| Action | `refUserGroupPromotionID` |
|---|---|
| Save, delete, or import promotion rows | 4, 5, 6, or 7 |
| Change activity Active flag, or Mark Inactive | 4, 5, 6, or 7 |
| Save planning counts (and therefore Set Campaign Duration, which saves first) | 5, 6, or 7 |
| Create or rename an activity, change its type, brand, channel, or tags | No group check. Any session user who can open the page. |

**Implemented.** The split that excludes group 4 from planning save is **Requires Confirmation** as a business rule. Comments that call these ids Admin or RSM disagree with each other (see the PRD). Treat the numbers as the rule the code enforces. Treat the names as **Inferred** and unconfirmed.

**BR-005 — RSM region limit is a string, not the numeric id.** When `strUserGroup = "RSM"`, the Promotions region list keeps only `mstUser.strRegion`, then adds `- All -`. An RSM with an empty region is told to set it on My Brands. **Implemented.** Whether every person with group id 5, 6, or 7 also has `strUserGroup = "RSM"` is **Unknown / Requires Confirmation**.

**BR-006 — Dev sees a sandbox slot.** `strUserGroup <> "Dev"` removes the second-to-last menu item (the sandbox). The item is `Selectable="False"` and `zSandbox.aspx` is not in the repository. **Implemented.**

**BR-007 — My Brands special flag.** `blnSpecial` may edit any user's brands and region. Everyone else may edit only their own user id, and the user dropdown is reduced to that person. **Implemented.**

**BR-008 — Brand lists follow `mstUserBrand`.** Activity, Activity Details, Planning, and Promotions brand dropdowns join `mstUserBrand` for the session user and `mstBrand.blnActive = 1`. **Implemented.** A user with no brand rows sees no brands. **Inferred** from the SQL, not from an empty-state message.

**BR-009 — Debug impersonation.** A debug compile, and only a debug compile, fills an empty session with domain login `RGBC\MAANIE` before the redirect. **Implemented.** This is a development convenience in source, not a documented production role.

## Activities

**BR-010 — An activity is name, type, brand, channel, active flag, and tags.** Stored in `promoActivity`. Type comes from `promoActivityType`. Channel is `strSegmentPrimary + '\' + strSegment` from `mstChannelSegment`. **Implemented.**

**BR-011 — Some channel families are not offered.** Activity and Activity Details exclude segment primaries `Export`, `House`, `Not Allocated`, `Unknown`, and `- Old -`. **Implemented** in the `SqlDataSource`. Promotions channel filter is different: it lists distinct `dwfPromo.strSegments`, so historical rows can show channels the activity form would not offer.

**BR-012 — New activities are always active.** The checkbox is disabled, and the insert writes `blnActive = 1`. **Implemented.**

**BR-013 — There is no unique name.** Phase 1 SQL comment: the same activity name may exist more than once; `ActivityID` is the identifier. Phase 2 migration, by contrast, inserts at most one `promoActivity` row per `strActivityName` (the most common type/brand/channel among 2025 rows). **Implemented** in the two scripts. They do not say the same thing. **Requires Confirmation** which rule operations follows after migration.

**BR-014 — Promotion ID on the activity screens is the activity id.** Format `PR` plus four digits (`PR0001`). It is not `dwfPromo.PromoID`. **Implemented.**

**BR-015 — Required activity fields.** Name, type, brand (not `0`), and channel. Tags optional, stored length at most 500. **Implemented.**

**BR-016 — Tag characters.** The browser rejects a tag that contains `|`, rejects a case-insensitive duplicate, and blocks a joined string over 500 characters, with a warning at 400. The server checks only the 500-character length. **Implemented.** A client that posts the hidden field directly can bypass the pipe and duplicate checks. **Inferred** from the split between script and server.

**BR-017 — Inactive is one-way from the list.** Mark Inactive sets `blnActive = 0`. The list has no control to turn it back on. Activity Details can set Active again when the user is in groups 4–7. **Implemented.**

**BR-018 — Editing an activity rewrites linked promotions.** Rows with that `refActivityID`, or with a null `refActivityID` and the previous name, brand, and channel, receive the new name, type, brand, channel, and id. Legacy matching ignores activity type. **Implemented.**

**BR-019 — Activity delete is not available.** Comment: "Delete functionality removed per requirements." **Implemented** absence. The requirement text is not in the repo, so the business reason is **Unknown / Requires Confirmation**.

## Planning and promotion rows

**BR-020 — A planning cell is a count of promotion rows.** For one activity, year, month, and rep manager, the cell is `COUNT` of matching `dwfPromo` rows. The Total row counts without the rep-manager split. Regions are domestic branches with `strRegionOrder <> '0'`. **Implemented.**

**BR-021 — Raising a count creates bare rows.** Each new row stores period, brand, rep manager, region, activity type, activity name, channel, `refActivityID`, a "New Record from Planning" log line, user, and stamp. Customer number, date, and rep are not set on insert. **Implemented.**

**BR-022 — Lowering a count deletes only unallocated rows.** A row is eligible when `strCustomerNo` is null or empty, for that period, brand, region, rep manager, and activity match. The oldest `PromoID` is deleted first. If fewer such rows exist than the reduction, the page says the remainder could not be removed because they have customer allocations. **Implemented.**

**BR-023 — Activity match includes legacy rows.** `refActivityID` equals the activity, or `refActivityID` is null and name, brand, and channel (`strSegments`) match. **Implemented.**

**BR-024 — Campaign duration overwrites the year.** Set Campaign Duration writes `txtNewCD` onto `strDuration` for every matching promotion in that calendar year prefix (`LEFT(strPeriod, 4)`). It does not write `logPromo`. **Implemented.** No validation of the duration text was found.

**BR-025 — Promotions load needs a year and both months.** Start month and end month must be greater than `00`. The period filter is `strPeriod` between `year+start` and `year+end`. **Implemented.** Years offered are fiscal years after 2011 that appear on `dwfPromo` (promotions page) or any fiscal year after 2011 (planning page). **Implemented** in the two `SelectCommand`s.

**BR-026 — Editable promotion fields.** Save writes segment to `strChannelPrimary`, account to `strCustomerNo`, date to `strDate` with `-` removed, duration, rep, and `refRepManagerID`. **Implemented.**

**BR-027 — Account number maximum is 7 characters.** Longer values are rejected and the stored account is kept. The page message names the PromoID. **Implemented.** The business reason for 7 is **Unknown / Requires Confirmation**. It matches the import rule.

**BR-028 — Date format.** A changed date must be length 10 or the stored date is kept and `logPromo` records `INVALID DATE FORMAT`. Empty handling differs by branch: on `master`, a blank can be saved; on `6b06854` (not merged) a blank keeps the stored value. **Implemented** on `master`. The unmerged behaviour is **not** the rule on `master`.

**BR-029 — Every accepted field change is audited.** `logPromo` stores PromoID, field name, old value, new value, user, and a shared `refStamp`. Values are truncated to 262 characters after `CleanString`. The promotion row's own `strLog` is prefixed with the change text, timestamp, and user. **Implemented.**

**BR-030 — Direct delete is allowed for groups 4–7.** The user confirms in the browser. The row is deleted even if it has a customer number. This is stricter than planning deletes, which refuse allocated rows. **Implemented.** Whether both delete paths are intended is **Requires Confirmation**.

**BR-031 — Import updates three fields only.** PromoID must already exist. Rep, account number, and date are updated. Account and date use BR-027 and BR-028. A row with no difference is skipped. The log prefix is `IMPORT::` plus the file name. **Implemented.**

**BR-032 — Import file shape.** `.xlsx` only. Data starts at row 3. Headers on row 2 may be `PROMOID`, `REP`, `ACCOUNTNO`, `DATE`. Otherwise columns 1, 11, 13, and 15 are used (indexes 0, 10, 12, 14). A 2021 comment says added columns will shift the indexes. Header detection reduces that risk when the header row is present. **Implemented.**

## Audit and usage

**BR-033 — Audit search defaults to the current month.** Start defaults to the first of the month, end to today. Both are required unless Ignore Dates is checked. **Implemented.**

**BR-034 — Audit results stop at 5,000.** The user is told to narrow filters. Page size is 100. "Updates only" hides `Promo added` and `Promo deleted`. **Implemented.**

**BR-035 — Login is written as a Forecast usage event.** `mstUsageLog.strSystem` is the literal `Forecast`. **Implemented.** **Requires Confirmation** that promotions traffic should be labelled Forecast.

## Calendar, company, and shared data

**BR-036 — One company row drives the fiscal calendar.** `mstCompany` where `CompanyID = 1` supplies `strFYStart` and `strFYEnd`. `getLastCompletedFiscalStart` and `getLastCompletedFiscalEnd` use `mstPeriod` for the current `yyyyMM`. **Implemented** in `SystemFunctions`. Those two functions were not found on the promotions pages that were read. `Branding` uses the company row when it is constructed.

**BR-037 — Constructing `Branding` rewrites customer regions.** The constructor runs an `UPDATE dwdCustomer` so region order and description match `mstBranch`. Promotions and Planning construct `Branding` when a brand is selected on postback. **Implemented.** Whether that update is still an approved side effect of opening a promotions page is **Requires Confirmation**.

**BR-038 — Number display compensates for a non-breaking space.** `FormatNumber` replaces character 160 with a comma because a comment says the RGBC server uses `999 999.99`. **Implemented** helper. **Inferred** that the live SQL Server locale still does this. Not confirmed against a server.

## Security behaviour that is visible in the product

These are facts about the build, not approved policies.

- `customErrors mode="Off"` and trace `pageOutput="true"` `localOnly="false"`. **Implemented.**
- SQL is string-built. `CleanString` escapes quotes. **Implemented.**
- Uploaded workbooks are stored under the site `Upload\` directory using the client's file name plus a timestamp. **Implemented.**
- Membership and role providers are configured and unused by the menu. The membership connection name is missing. **Implemented** configuration.

## Rules that must not be treated as policy yet

- Names of group ids 4, 5, 6, and 7.
- Whether group 4 should be unable to save planning.
- Whether a viewer who is not in those groups should be able to edit activity master data.
- Whether blank Excel cells should clear account, date, and rep (`master`) or keep them (`6b06854`).
- Whether Phase 2's "one row per activity name, 2025 only" migration has been applied, and whether older promotions were left without `refActivityID` on purpose.
- Whether the commented foreign key from `dwfPromo.refActivityID` to `promoActivity` should exist. The script says to add it after migration. The application does not require it.
