# Technical documentation — promotions

Evidence date: 2026-10-06.

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

One other remote branch exists: `#112_Promo-Planner-bugfixes` at `6b06854` (2026-09-14), not contained in `master`.

## Repository layout

```text
promotions/
  .gitignore                          ignores RGBC Promotions/.env
  BuildProcessTemplates/              TFS 11 workflow XAML (four files)
  DB_Scripts/
    Database_Migration_ActivityDetails_Tags.sql
  RGBC Promotions/
    .env.example
    Database_Migration_Item3_Phase1.sql
    Database_Migration_Item3_Phase2.sql
    RGBC Promotions.sln
    RGBC Promotions/                  web project (VB.NET Web Forms)
```

The web project root contains the `.aspx` pages, `Admin.Master`, `Web.config`, `db.generic.vb`, `Branding.vb`, `SecureConfig.vb`, `PagedGridEdit.vb`, `App_Code/VB/`, `Scripts/`, `Styles/`, `content/`, and `Account/`.

`bin/`, `obj/`, and `.vs/` are in the cloned tree. They are build outputs and Visual Studio state. `.gitignore` does not list them. Whether they should stay in Git is **Unknown / Requires Confirmation**.

The 2026-09-28 onboarding scan listed `sonar-project.properties` and `sonarqube_scan.bat` at the repository root. They are not in Git history.

## Application structure

ASP.NET Web Forms, code-behind, VB.NET. Project file `RGBC Promotions.vbproj`. Solution `RGBC Promotions.sln`.

| Page | Master | Role |
|---|---|---|
| `Index.aspx` | none | Login |
| `Home.aspx` | `Admin.Master` | Empty home |
| `Activity.aspx` | `Admin.Master` | Activity search |
| `ActivityDetails.aspx` | `Admin.Master` | Activity editor |
| `Planning.aspx` | `Admin.Master` | Monthly counts |
| `Promotions.aspx` | `Admin.Master` | Row editor |
| `Import.aspx` | `Admin.Master` | Excel upload |
| `AuditLog.aspx` | `Admin.Master` | `logPromo` search |
| `MyBrands.aspx` | `Admin.Master` | User brand assignment |
| `About.aspx` | not in the menu | Short page, not linked from `Admin.Master` |
| `Account/*.aspx` | template | Not linked from `Admin.Master` |
| `Site.Master` | template | Title "My ASP.NET Application" |

`Global.asax.vb` handlers are empty.

The default document is forced to `Index.aspx`. The `Web.config` comment says IIS would otherwise return 403.14 for `/promo_module/`.

## Language, framework, and libraries

| Item | Value | Evidence |
|---|---|---|
| Language | VB.NET | `.vb` code-behind, `Language="VB"` |
| Framework | .NET Framework 4.6.2 | `Web.config` `targetFramework="4.6.2"`, `vbproj` `TargetFrameworkVersion` |
| UI | ASP.NET Web Forms, `controlRenderingCompatibilityVersion="4.0"` | `Web.config` |
| Excel | ClosedXML 0.105.0, DocumentFormat.OpenXml 3.1.1, ClosedXML.Parser 2.0.0, ExcelNumberFormat 1.1.0, SixLabors.Fonts 1.0.0, RBush.Signed 4.0.0, Microsoft.Bcl.HashCode 1.1.1 | `packages.config` |
| BCL shims | System.Buffers 4.5.1, System.Memory 4.5.5, System.Numerics.Vectors 4.5.0, System.Runtime.CompilerServices.Unsafe 4.7.0 | `packages.config`, binding redirects in `Web.config` |
| Script | jQuery 1.4.1, jQuery UI 1.8.6 (Smoothness) | `Scripts/`, `Styles/smoothness/` |
| Data | `System.Data.SqlClient` | `db.generic.vb` |

NuGet restore on a clean machine was not executed.

## Configuration

`Web.config` `connectionStrings` is empty.

`SecureConfig.GetConnectionString` accepts only the name `SuperbowlConnectionString`.

| Build | Environment variable |
|---|---|
| DEBUG | `RGBC_PROMOTIONS_CONNECTION_STRING_DEBUG` |
| otherwise | `RGBC_PROMOTIONS_CONNECTION_STRING_RELEASE` |

`SecureConfig` reads `.env` from the parent of the web application physical path (one directory above the folder that contains `Web.config`). The comment in `.env.example` says the same. When the file exists, each key is written onto the process environment. IIS-level variables work when no `.env` file is present. A missing variable throws `ConfigurationErrorsException`.

`.env` is gitignored. `.env.example` points both variables at catalog `Superbowl` on localhost with integrated security and a 60-second timeout. That host is an example, not production.

`EnvConnectionStringsExpressionBuilder` lets markup use `<%$ EnvConnectionStrings:SuperbowlConnectionString %>`. The `SqlDataSource` controls use that expression.

Other settings:

| Key | Value | Where used |
|---|---|---|
| `aspnet:MaxHttpCollectionKeys` | 2000 | `Web.config`. Paging exists so a large grid does not exceed the form-key limit. |
| `intDecimals` | 1 | `MySettings` |
| `intColumnWidthDefault` | 56 | `MySettings` |
| `intColumnWidthRegion` | 105 | `MySettings` |

`compilation debug="true"` is set in the base `Web.config`. `Web.Release.config` removes the `debug` attribute. `customErrors mode="Off"`. `trace localOnly="false" pageOutput="true"`.

Authentication: `mode="Windows"`. Membership, profile, and role providers are the ASP.NET SQL providers. `roleManager enabled="false"`. Connection name `ApplicationServices` is referenced and not defined.

## Authentication in the page

1. IIS Windows authentication supplies `Request.LogonUserIdentity`.
2. `LogginUser` selects `mstUser` joined to `mstUserGroupPromotion` on `strDomainLogin`.
3. The `DataRow` is stored at session key `USER` (also read as `User`; ASP.NET session keys are case-insensitive). `USERID` is also set.
4. `Admin.Master` redirects to `Index.aspx?M=1` when that session entry is missing, except a DEBUG compile which first tries user `RGBC\MAANIE`.
5. `blnView` must be true or the content placeholder is cleared.

An insert into `mstUsageLog` uses `strSystem = 'Forecast'` and sleeps 13 milliseconds before the insert. Provenance: **Implemented** in `SystemFunctions.LogginUser`.

## Data access

`db` in `db.generic.vb`:

- `doQuery` — if the SQL starts with `SELECT`, fill a `DataSet`; otherwise `ExecuteNonQuery`. On failure, write SQL and the exception to the HTTP response when a response exists, and return `Nothing`.
- `doQuery(sql, connection, transaction)` — non-query on an open transaction.
- `InsertReturnId` — used by activity create and planning insert (`OUTPUT INSERTED`).
- `CleanString` — null to empty, double quote to single quote, single quote doubled, trim.
- `getRow` — first row helper.
- `nz` — null, empty, or numeric zero becomes the default. Numeric zero becoming the default can hide a real zero. **Implemented.**

There is no ORM and no parameter API. Queries are concatenated.

`WriteLogPromo` inserts `logPromo`. When a connection and transaction are passed, the audit insert joins that transaction. Field values are cut to 262 characters.

## Database objects this application touches

Catalog name in scripts and in `.env.example`: `Superbowl`.

### Created or altered by scripts in this repository

| Object | Script | Notes |
|---|---|---|
| `promoActivity` | Phase 1 | `ActivityID` identity, `strActivityName`, `strActivityType`, `refBrandID` FK to `mstBrand.BrandID`, `strChannel`, `blnActive` default 1, `strLastUser`, `dtStamp`, `dtCreated`. Indexes on active, brand, name. No unique constraint on name. |
| `promoActivity.strTags` | Tags script, 2026-02-11 | `varchar(500)` null |
| `dwfPromo.refActivityID` | Phase 1 | nullable int. Index `IX_dwfPromo_refActivityID`. Foreign key to `promoActivity` is commented out. |
| Backfill | Phase 2 | Distinct 2025 `dwfPromo` activity names into `promoActivity`, then set `refActivityID`. Script says to back up first. |

`promoActivityType` is queried (`ActivityTypeID`, `strActivityType`) and is not created by these scripts. Its origin is **Unknown / Requires Confirmation**.

Whether the scripts have been applied is **Unknown / Requires Confirmation**. The activity pages query `promoActivity` and `promoActivityType` directly. Tags are optional at runtime: Activity Details catches a missing `strTags` and shows none.

### Read or written, not created here

`dwfPromo`, `logPromo`, `mstUser`, `mstUserGroupPromotion`, `mstUserBrand`, `mstBrand`, `mstBranch`, `mstRepManager`, `mstPeriod`, `mstCompany`, `mstChannelSegment`, `dwdCustomer`, `mstUsageLog`, `dwdProduct`, `dwfSales`, `dwfQFAdjustment`, `mstPrincipal`, `sysCronJob`.

`Branding.vb` is the main reader of sales, product, and quantity-forecast adjustment tables. It is about 2,400 lines and builds fiscal-year quantity grids. Home no longer calls that report. The constructor still updates `dwdCustomer` region columns from `mstBranch` whenever Planning or Promotions constructs a `Branding` instance.

`mstCompany.CompanyID = 1` is the company fiscal calendar. `mstPeriod.strPeriod` is `yyyyMM`. `intFiscalYear` filters the year dropdowns (`> 2011`).

Domestic scope is `mstBranch.strRegion = 'Domestic'` and `strRegionOrder <> '0'`.

## Paging and session

`PagedGridEdit` keeps editable grids in session because `EnableViewState` is turned off and because a full grid would exceed the form-key limit.

| Grid | Page size | Session key |
|---|---|---|
| Planning | 80 | `PlanningGridData`, `PlanningGridPageIndex` |
| Promotions | 120 | `PromotionsGridData`, `PromotionsGridPageIndex` |
| Audit | 100 | `AuditLogGridData`, `AuditLogFilterDesc` |

Promotions also keeps `EditRefRepManagerID` on the session table so a manager id is not confused with the manager name. The unmerged commit adds `PromotionsGridPageKeys` for the same grid. That key is not on `master`.

`Page_LoadComplete` copies the current page's controls back into the session table unless the post was Load, or the pager already synced. This exists because the grid is rebound with view state off.

## Excel

Export uses ClosedXML. First row is a caption, second row is headers, data starts at row 3. Content type is the Open XML spreadsheet type.

Import uses ClosedXML on worksheet 1, rows 3 onward, 15 columns. The legacy Jet `Microsoft.Jet.OLEDB.4.0` / Excel 8.0 function `getExcelDataSet` is still compiled and is not called by the upload button.

Uploaded files go to `{app base}\Upload\{original name}{yyyyMMddHHmmss}.xlsx`.

## Background work, queues, storage, monitoring

No Windows service, Hangfire, or message queue is in the repository.

`sysCronJob` inserts of type `BrandQF` exist as helpers and are not called by the pages.

No application log file, health endpoint, or metrics library was found. Failures are written into the page. `lblOutput` on the master page receives some planning insert errors.

SonarQube project files are not in Git. The onboarding scan saw scanner files only on the scan host.

## Deployment and infrastructure

| Artefact | What it says |
|---|---|
| `Web.config` comment | Site path `/promo_module/` |
| `Profile1.pubxml` | File-system publish, Release, Any CPU, `publishUrl` `C:\Users\PJ\Desktop\pm`, `DeleteExistingFiles` false |
| `BuildProcessTemplates/*.xaml` | Visual Studio / TFS 11 default, lab, upgrade, and Azure continuous deployment templates |
| Bitbucket | Remote `git@bitbucket.org:overdrive/promotions.git`. No `bitbucket-pipelines.yml`. |

No Dockerfile, no transform that sets a server name, and no release notes. How `/promo_module/` is deployed today is **Unknown / Requires Confirmation**.

Footer version constants (`1.1.0`, `17 June 2026`) are not the assembly version (`1.0.0.0`).

## Security notes from the tree

Documented so the next reader does not miss them. They are observations, not a penetration test.

- Debug impersonation of `RGBC\MAANIE`.
- Errors and trace can be rendered to the browser, including SQL text from `db.doQuery`.
- SQL concatenation with quote doubling only.
- Uploaded Excel stored under the web root's `Upload` folder.
- `bin/` build outputs are in the cloned tree.
- jQuery 1.4.1 and jQuery UI 1.8.6 are old vendored copies.

## Development and testing

Open `RGBC Promotions.sln` in Visual Studio. The project targets .NET Framework 4.6.2. Copy `.env.example` to `.env` beside the web project folder (parent of the folder that contains `Web.config`) and point it at a Superbowl database that already has the shared tables plus `promoActivity` and `promoActivityType`.

No test project and no CI file exist. Nothing in this repository was executed against a database while writing this document.

`Duodecimal.vb` converts compact period strings such as `yyyyMM` into dates and month labels. Planning uses `CultureInfo.InvariantCulture` so month names stay `Jan` through `Dec` and match the grid `Bind` expressions.

## Source map for the next change

| Concern | File |
|---|---|
| Menu, session, version | `Admin.Master`, `Admin.Master.vb` |
| Login | `Index.aspx.vb`, `App_Code/VB/SystemFunctions.vb` |
| Activity rules | `Activity.aspx.vb`, `ActivityDetails.aspx.vb` |
| Planning counts | `Planning.aspx.vb` |
| Row edit, export, delete | `Promotions.aspx.vb` |
| Import | `Import.aspx.vb` |
| Audit | `AuditLog.aspx.vb` |
| Brands | `MyBrands.aspx.vb` |
| SQL execution | `db.generic.vb` |
| Connection string | `SecureConfig.vb` |
| Grid session | `PagedGridEdit.vb` |
| Schema | `RGBC Promotions/Database_Migration_Item3_Phase1.sql`, `Phase2.sql`, `DB_Scripts/Database_Migration_ActivityDetails_Tags.sql` |
