# Business rules — forecast (RGBC Forecast Control)

Evidence date: 2026-10-14.

Source: `git@bitbucket.org:overdrive/forecast.git`, `master` at `6aac1d2`.

Rules below are taken from code and from comments that state a rule. Nothing here is a product decision. Where the code behaves a certain way and no comment states the rule, the rule is marked **Inferred**.

Secrets and connection strings are not copied.

## Identity

| ID | Rule | Provenance |
|---|---|---|
| BR-001 | The operator-facing name is RGBC Forecast Control. The solution and assembly are RGBC Forecast / `RGBC_Forecast`. The repository is `forecast`. | **Implemented** |
| BR-002 | What "RGBC" stands for is not stated. | **Unknown / Requires Confirmation** |
| BR-003 | The SQL connection is named Superbowl. What that name means in the business is not stated. | **Unknown / Requires Confirmation** |
| BR-004 | Forecast quantities on screen are 9L. Report captions say "Adjusted Forecast 9L" and "ACTUALS & QF 9L". | **Implemented** labels. The commercial definition of 9L is **Inferred** from the `9000 / size / pack` stock update and is **Requires Confirmation** |

## Who may use the system

| ID | Rule | Provenance |
|---|---|---|
| BR-010 | A person is recognised by the Windows logon name stored in `mstUser.strDomainLogin`. The login page says access is controlled through Active Directory. | **Implemented** message and lookup. Directory group policy itself is not in this repo |
| BR-011 | A matched user is logged in `mstUsageLog` with `strSystem = 'Forecast'`. | **Implemented** |
| BR-012 | Without `blnView`, the user is told they need view rights. | **Implemented** |
| BR-013 | `blnSpecial` is required for Planning, Open Season, Apply, and for editing another user's brands. | **Implemented** |
| BR-014 | `blnEdit` is required to enable Save on Adjustments, and only while a season is open. | **Implemented** |
| BR-015 | User group `Dev` sees the Sandbox menu. Other groups do not. | **Implemented** string compare |
| BR-016 | User group `RSM` may adjust only brands in `mstUserBrand` and only the region stored on `mstUser.strRegion`, and may not use Calculate Regions. | **Implemented** |
| BR-017 | A non-special user on My Brands can see and save only their own user id. | **Implemented** |
| BR-018 | Other forecast group names and the meaning of each flag are data. They are not listed in code. | **Unknown / Requires Confirmation** |
| BR-019 | There is no self-registration and no in-app user create. | **Implemented** absence |

## Fiscal calendar

| ID | Rule | Provenance |
|---|---|---|
| BR-030 | Company 1 in `mstCompany` supplies `strFYStart` and `strFYEnd`. | **Implemented** |
| BR-031 | If `strFYStart` is `01`, fiscal year Y runs from January Y through December Y. Otherwise it starts in calendar year Y−1 at `strFYStart` and ends in year Y at `strFYEnd`. The same rule builds the three prior years. | **Implemented** in `Branding.New` and `QFGen` |
| BR-032 | "Last completed fiscal" is the previous fiscal year when the start month is January, otherwise it ends in fiscal year − 1 at `strFYEnd` and starts two years back at `strFYStart`. | **Implemented** in `getLastCompletedFiscalStart` / `End`. Used as the sales base for QF generation |
| BR-033 | Period keys are `yyyyMM`. Display of a 6-character key is `MMM yyyy`. | **Implemented** `DisplayDate` |
| BR-034 | Empty planning and adjustment templates label months July through June and quarters Q1–Q4 in that order. | **Implemented** SQL in `Planning.aspx`. Whether company 1 uses that calendar is **Unknown / Requires Confirmation** |
| BR-035 | Report and planning year dropdowns start after 2009 (reports) or 2010 (planning). | **Implemented** `SqlDataSource` filters |

## Planning

| ID | Rule | Provenance |
|---|---|---|
| BR-040 | A plan can be saved only for a fiscal year greater than the fiscal year that contains today's month. The current fiscal year is changed on Adjustments. | **Documented** comment `r1` in `Planning.aspx.vb`. **Implemented** by disabling Save |
| BR-041 | Saving a plan overrides adjustments for that brand and fiscal window: both tables are deleted and adjustments are reinserted from the plan as unapplied. | **Documented** comment `r2`. **Implemented** in `btnSave_Click` |
| BR-042 | The plan also deletes generated QF rows for that brand's forecast product codes (`blnQF = 1` and product code containing `F`) in the same window. | **Implemented** |
| BR-043 | Projected sales are year-to-date 9L divided by the average completion ratio of earlier years. Growth labels compare successive fiscal totals and the projection. Missing years display `n/a`. | **Implemented**. The completion formula is **Inferred** from the arithmetic in `LoadSalesRhythm` |
| BR-044 | A copy brand supplies the sales rhythm. The saved plan is still stored against the selected brand, and the cron parameter records `CopyBrandID`. | **Implemented** save. The copy execution inside this site is commented out |
| BR-045 | The save dialog says installation may take up to an hour. The page then says the brand will be processed shortly and does not generate QF itself. | **Implemented** text versus **Implemented** queue-only behaviour. Whether the hour still applies is **Unknown / Requires Confirmation** |

## Adjustments and open season

| ID | Rule | Provenance |
|---|---|---|
| BR-050 | Adjustments Save is available only when an open season covers today for `refBrandID` 0 or the selected brand, and the user has edit rights. The season with the higher `refBrandID` wins the message (`ORDER BY refBrandID DESC`). | **Implemented** |
| BR-051 | New seasons created in the UI are for all brands (`refBrandID = 0`). | **Implemented**. Comment says this is hardcoded until they know whether seasons should be per brand |
| BR-052 | The UI does not reject an overlapping or duplicate season. | **Implemented** (`If False`) |
| BR-053 | Outside a season the model is labelled locked and Save is off. Apply can still be on for a special user. | **Implemented** |
| BR-054 | A changed adjustment cell is stored at 4 decimal places, marked unapplied, and prefixed to `strLog` with new value, old value, timestamp, and user name. A missing row is inserted. | **Implemented** |
| BR-055 | Equal values after rounding half away from zero to 0 decimals are not saved. | **Implemented** compare in `Save` |
| BR-056 | Calculate Regions reapplies a changed Global total onto other groups using `%C QF LY`. It does not write the database until Save. RSM cannot run it. | **Implemented** |
| BR-057 | Past months were locked when the period key was before today's `yyyyMM`. The condition is combined with `And False`, so that lock does not run. A 2015-02-16 comment calls the `FALSE` a temporary workaround. A 2015-05-29 comment says all QFs were unlocked. | **Implemented** behaviour is unlocked. The earlier lock is **Documented** in comments and is not effective |
| BR-058 | A cell that is unapplied, and has a tooltip or a non-zero value, stays editable even before the applied-style branch. | **Implemented** |
| BR-059 | Notes do not change `dblQF`. They update `strNotes`, last user, and timestamp on the matching adjustment row. | **Implemented** |
| BR-060 | Each group is represented by one customer number matching `HIST%`. If two such customers share a group, the later one is dropped. | **Implemented**. Comment cites `HISTJ1` kept and `HISTJ2` ignored. **Inferred** that these rows are rollup customers rather than outlets |

## Quantity forecast

| ID | Rule | Provenance |
|---|---|---|
| BR-070 | Product QF for a period, SKU, and customer is customer-in-group sales share times SKU share of applicable sales times the group adjustment. Sales come from the last completed fiscal. | **Documented** formula comment in `QFGenerator.vb`. **Implemented** in `Generate` |
| BR-071 | `blnQF = 2` products are left out of the customer and brand sales share. `blnQF = 1` products form the applicable SKU share. | **Implemented**. Business names for those flags are **Inferred** as not-applicable versus applicable. **Requires Confirmation** |
| BR-072 | The forecast SKU code is the first five characters of the product code plus `F`. Stock rows are updated on that five-character code. | **Implemented** |
| BR-073 | `stkProductPeriod.dblQF` is `Round(summed 9L budget * 9000 / intSize * intPack, 4)`. SQL `*` and `/` associate left to right, so pack multiplies after the division. | **Implemented** expression. Whether that precedence is the intended pack conversion is **Requires Confirmation** |
| BR-074 | An adjustment is marked applied when the generated 9L for that group and period rounds to the same integer as the adjustment, half away from zero. The log is cleared on that update. | **Implemented** |
| BR-075 | Generated rows with `percRegion = 0` are deleted asynchronously. | **Implemented** |
| BR-076 | If a group/period adjustment is missing during generation, QF rows for that period and the region's customers are deleted. | **Implemented** in the `Generate` catch block |
| BR-077 | Planning and Apply do not run this generation. They insert `sysCronJob` (`Type = 'BrandQF'`, `Status = 'New'`) and first mark an identical `New` job `Removed`. | **Implemented** |
| BR-078 | The job parameter string is `BrandID`, `Finyear`, `CopyBrandID`, and `BrandName` joined by `|`. | **Implemented** |

## Customer groups

| ID | Rule | Provenance |
|---|---|---|
| BR-080 | Region on the forecast screens is `dwdCustomer.strGroupName`, ordered by `strRegionOrder`. | **Implemented** since `b4c83ce` (2026-08-28) |
| BR-081 | When a `Branding` object is constructed, `strGroupName` is set from `mstCustomer.strCustomerGroup` and `strRegionOrder` from `mstCustomer.refCustomerGroupID` (or `99` when null) for customers whose dimension values differ. | **Implemented**. Comment calls this a sync and also refers back to a 2012 data-bug compensation |
| BR-082 | Empty group names are excluded from region dropdowns. | **Implemented** `ISNULL(strGroupName,'') <> ''` |
| BR-083 | The previous axis was `strRegionDesc2`. It remains only in comments. | **Documented** history in comments. **Not** the current query path |

## Reports

| ID | Rule | Provenance |
|---|---|---|
| BR-090 | A report will not run until a financial year is selected. | **Implemented** |
| BR-091 | Brand `-1` means the session user's `mstUserBrand` list. Brand `0` means no brand filter. | **Implemented** |
| BR-092 | The RSM report includes only `blnActive = 1` brands. The forecast report does not apply `blnActive`. | **Implemented** difference. Whether the forecast report should exclude inactive brands is **Unknown / Requires Confirmation** |
| BR-093 | Daily grouping `-1` means all. Home's unused focus-brands path used grouping id `3` and commented it as "Focus Brands". | **Implemented** filter. The name of id 3 is **Documented** only in that comment |
| BR-094 | The region dropdown applies to the RSM report. The forecast report constructs `Branding` without the region argument. | **Implemented**. The caption says "RSM Report Only" |

## Numbers and display

| ID | Rule | Provenance |
|---|---|---|
| BR-100 | Positive percentages are shown with a leading `+`. Format failures become `0.0`. | **Implemented** `FormatPercent` |
| BR-101 | Displayed numbers replace character 160 with a comma before the user sees them. | **Implemented**. Comment attributes this to the RGBC server number format |
| BR-102 | Default decimal display uses one decimal (`intDecimals`). Quarter and total columns are highlighted. The current calendar month column is highlighted. | **Implemented** |

## Conflicts to confirm

| ID | Conflict |
|---|---|
| BR-C1 | Comments from 2015 say past QFs were unlocked as a temporary workaround. The `And False` is still in `master` in 2026. Confirm whether past periods must stay editable. |
| BR-C2 | Apply and Planning tell the user generation may take an hour and will be processed shortly. This repository only inserts `sysCronJob`. Confirm the worker still runs. |
| BR-C3 | Open Season's own comment doubts per-brand seasons, and the insert forces brand 0. The grid still supports a brand name when `refBrandID` is not 0. Confirm that new seasons must remain global. |
| BR-C4 | Footer version `1.0.3` / 4 April 2012 does not match the 2026 customer-group behaviour. The version label is not a reliable rule for what the site does. |
