# Business rules — shipment_document_system

Evidence date: 2026-10-04.

Source: `main` at `6f61b57`.

Rules are taken from code, views, and SQL constraints. Each rule is labelled **Implemented** or **Inferred**. Nothing here is an approved policy document; the repository does not contain one.

Where two code paths disagree, both are stated. That disagreement is not resolved by this file.

## Identity and access

### BR-001 — Sign-in identity is the host account name

**Implemented.** Login uses `User.Identity.Name` as `mstUser.strDomainLogin`. The screen does not ask for a password. `strPassword` exists on `mstUser` (max length 32) and is not read.

### BR-002 — A missing user still produces an object

**Implemented.** `ValidateUser` returns `new UserModel()` when no row matches. `AccountController` treats any non-null object as authenticated, writes a success log, and redirects to the shipment list. Session role keys are set only for role ids 1, 2, and 3.

**Inferred.** An unknown account can open the list with an empty session. Controllers do not block that.

### BR-003 — Active Directory is not applied

**Implemented** as a comment only. `ValidateUser` says "CHECK USER AGAINST RGBC ACTIVE DIRECTORY" under a TODO. `System.DirectoryServices` is a package reference. No call was found.

### BR-004 — Three roles

**Implemented** in `RoleEnum` and the login switch.

| Id | Session value | What the update view shows |
|---|---|---|
| 1 | `ADMIN` | Save, Submit when the checklist check passes, Cancel when status is not `CANCELLED` and not `COMPLETE`, View and Delete on files |
| 2 | `VIEWER` | View on files. No save, submit, or cancel buttons |
| 3 | `LOGISTICS` | Save when status is not `CANCELLED` and not `COMPLETE`, Submit under the same checklist check, View on files. No Delete. No Cancel |

**Implemented.** The supplier Add button is rendered only for `ADMIN`. Supplier name links, shipment Add / Import / Export, and the controller actions do not check the role.

**Inferred.** The intended rule is that viewers cannot change data and only admins can delete files or cancel. The server does not enforce that. **Requires Confirmation** before treating the view as the rule.

### BR-005 — Logout does not end the session

**Implemented.** `GET account/logout` returns the login view. It does not clear session keys.

### BR-006 — No endpoint authorisation attribute

**Implemented.** No `[Authorize]` was found. `UseAuthorization` is called. `AddAuthentication` is not called. `launchSettings.json` sets IIS Express `windowsAuthentication` to false and `anonymousAuthentication` to true.

## Shipment capture

### BR-007 — Required fields on the screen

**Implemented** in `ShipmentController.ValidateScreen`. Failure text is accumulated into `TempData["ErrorMessage"]`.

| Field | Rule |
|---|---|
| Supplier | id must not be 0. Message: "Please select a Supplier." |
| Indent number | not empty, and must not contain `\ / : * ? " < > |`. Messages: "Please enter a valid Indent Number." and "Please refrain from using invalid characters." |
| Indent uniqueness | on process `SAVE` only, exact match against `indent_number`. Message: "Indent Number entered already exists." |
| Shipshape number | not empty |
| Mother vessel | not empty |
| Bill of entry | not empty, and `int.TryParse` must succeed. Message includes "Numbers Only" |
| Container number | not empty. No numeric check |
| Offloading port | id must not be 0 |
| Number of flatcases | not empty, and `int.TryParse` must succeed |
| Voyage | not empty |
| M/BL number | not empty. The numeric check is commented out |

Dates are not validated in `ValidateScreen`. The add and update views attach a jQuery UI datepicker to ETA, container received, and redirect to SACD.

### BR-008 — Duplicate indent on update is not checked

**Implemented.** `UpdateShipment` sets `Process = "UPDATE"` before `ValidateScreen`. The duplicate query runs only for `SAVE`. Two shipments can be saved with the same indent if one of them is an update that changes its indent onto an existing value.

Git subjects `cd8538b` and `7846c74` (2024-07) say a duplicate-indent update bug was fixed. The current condition is still `Process == "SAVE"` only. **Requires Confirmation** if a later change was supposed to compare the indent while excluding the current row.

### BR-009 — New shipments start as SAVED

**Implemented.** `AddShipment` sets `shipment_status = "SAVED"`, `created_by` from `LoggedInUsername`, `created_at` to `DateTime.Now`, and `user_id` from `LoggedInUserId`.

Supplier choices on create are `IsActive == true` only.

### BR-010 — Ports

**Implemented.** Hard-coded in `PopulatePortsDropdown`, `GetPortName`, and import mapping.

| Id | Name |
|---|---|
| 1 | Cape Town |
| 2 | Coega |
| 3 | Durban |
| 4 | P.E. |

There is no port table. Export prints `N/A` when `port_id` is null or outside 1–4.

### BR-011 — Status words that are actually stored

**Implemented** writers:

| Action | Value written |
|---|---|
| Create, and import insert | `SAVED` |
| `ShipmentController.SubmitShipment` | `SUBMITTED` |
| `ShipmentService.CancelShipment` | `CANCELLED` |

**Implemented** and not on the controller path: `ShipmentService.SubmitShipment` writes `COMPLETED`.

**Implemented** list filter maps dropdown id `"1"` → `SAVED`, `"2"` → `SUBMITTED`, `"3"` → `CANCELLED`. Labels shown in the dropdown are Saved, Submitted, Cancelled.

**Implemented** UI guards compare to `SUBMITTED`, `CANCELLED`, and `COMPLETE`. They do not compare to `COMPLETED` or `SAVED`. A row in `SUBMITTED` still fails the test `status != "COMPLETE"`, so the admin Cancel button stays enabled after submit. Logistics Save stays enabled after submit for the same reason.

`ShipmentStatusEnum` (`Saved`, `Complete`, `Cancelled`) is not used when writing status. **Requires Confirmation** which word the business considers final: `SUBMITTED`, `COMPLETE`, or `COMPLETED`.

### BR-012 — Submit needs every checklist file, with one exception

**Implemented** in `CheckShipmentSubmitStatus`. All of these filename columns must be non-empty:

- clamping request, supplier invoice, bill of lading
- certificate of origin, packing list, DRO to transport
- certificate of removal, extended detention, VOC
- department of health, SARS documents
- GRV complete RGBC, GRV Edward Snell, ACCPAC receipts

Arrival invoices: `arrival_invoice_file` **or** `arrival_invoice_file2` is enough. The other may be empty.

The update view shows Submit only when this returns true and status is not `SUBMITTED`. The add view always renders Submit disabled.

Submit does not send mail or call another system. It only updates the row and redirects to the list with "Shipment Submitted Successfully".

### BR-013 — Cancel requires a reason

**Implemented.** Empty `CancelReason` redisplays the update view with "Please enter a Cancel Reason before Cancelling Shipment." A non-empty reason sets status `CANCELLED`, stores the reason, and sets `updated_by` / `updated_at`.

The add view's cancel button is enabled only when status is already `CANCELLED` or `COMPLETE` (the condition is the inverse of the update view). A new form therefore shows a disabled cancel button.

### BR-014 — Indent change renames SharePoint objects

**Implemented.** If the stored indent differs from the posted indent, `RenameFoldersFiles` is called with the old indent, root folder name `Shipment Checklist`, the new indent, and the list of stored file names. The database indent is updated in the same request.

### BR-015 — Document replace and delete

**Implemented.** Choosing a new file stores the uploaded name on the column and sends the file to Graph as `{indent}_{file name}`. When the column is already non-empty, the service deletes `{indent}_{old file name}` first.

Delete posts `documentType`, clears that column, and deletes the Graph item. Unknown `documentType` values hit `default` and do nothing.

The update and add views post `CertificateOfOrigin`. The controller case is `CerificateOfOrigin`. The strings differ, so certificate-of-origin delete takes the `default` branch.

### BR-016 — Search is indent-only

**Implemented.** Shipment filter uses `indent_number.Contains`. It does not search supplier, container, or vessel. Supplier filter uses `name.Contains`.

## Suppliers

### BR-017 — Supplier name is required

**Implemented.** Empty name on create or update sets "Please enter a valid Supplier Name." Create does not check for a duplicate name.

### BR-018 — Delete deactivates

**Implemented.** `DeleteSupplier` loads the row only when `is_active` is true, then sets `is_active` false and records the session user as `updated_by`. The row remains. The success message says "Supplier Deleted Successfully."

`sdsSupplier.is_active` defaults to 1 in the EF model (`HasDefaultValueSql("((1))")`).

### BR-019 — Inactive suppliers stay on existing shipments

**Implemented.** The update screen loads the full supplier list, not the active subset. Shipments keep `supplier_id`. No rule prevents deactivating a supplier that still has shipments. **Inferred** from the absence of a check, not from a stated policy.

## Import

### BR-020 — File type and size

**Implemented** in `ImportService.ValidateFileAsync` and `ImportConfiguration`.

- Extension must be `.xlsx` or `.xls` (compared after lower-casing).
- Size must be less than or equal to 10 MB (`10 * 1024 * 1024`).

EPPlus licence context is set to `NonCommercial` in `ImportService` and in `ShipmentController`'s static constructor.

### BR-021 — Workbook shape

**Implemented.** The first worksheet is read. Row 1 is headers. Data starts at row 2. Empty rows are skipped (`HasData`). Headers must match the names read by `CreateStagingRecordsAsync`:

Supplier, Indent Number, Shipshape Number, Mother Vessel, E.T.A, Bill of Entry Number, Container Number, Offloading Port, Container Received, No of Flatcases, Voyage, M/BL Number, Redirect to SACD.

A missing header yields an empty field, which then fails required-field validation.

### BR-022 — Every import column is required

**Implemented** in `ValidateStagingRecordsAsync`. Blank values add a "{field} is required" error for all thirteen columns, including the three dates.

### BR-023 — Port must be one of the four names

**Implemented.** `ValidPorts.AllPorts`: `Cape Town`, `Coega`, `Durban`, `P.E.`. Match is ordinal via `Contains` on the string array (case-sensitive). The error lists the allowed names. A second check maps those strings to ids 1–4.

The screen uses the same four names. The Excel template sample row uses "Sample Port", which does not pass this rule.

### BR-024 — Dates must parse

**Implemented.** ETA, container received, and redirect to SACD use `DateTime.TryParse` with `CultureInfo.InvariantCulture`. Failure messages: "Invalid ETA date format", "Invalid Container Received date format", "Invalid Redirect to SACD date format".

### BR-025 — Supplier must already exist

**Implemented.** Supplier name is compared case-insensitively to `sdsSupplier.name`. Missing supplier: "Supplier '{name}' not found in system". The import does not create suppliers. It does not require `is_active` to be true.

### BR-026 — Duplicate indent inside one file

**Implemented.** Case-insensitive. If another row in the same session has the same indent, the error lists the row numbers.

### BR-027 — Existing indent in the database is an update, not an error

**Implemented** in `ExecuteImportAsync`. A matching indent updates the shipment. No match inserts a row with status `SAVED`, the session's `CreatedBy`, and the supplier id resolved by name.

**Implemented** and unused during execute: session `ImportMode`. The view model says `UpdateExisting` skips rows that are not already in the database, and `AddNewOnly` skips rows that are. The loop does neither. It always upserts.

SQL check constraint on `sdsImportSessions.ImportMode` allows `UpdateExisting`, `AddNewOnly`, and `ReplaceAll`. The UI does not offer `ReplaceAll`.

**Requires Confirmation** which of these is the business rule.

### BR-028 — A session with some bad rows can still run

**Implemented.** Preview sets `CanExecute` when status is `Pending` and `ValidRows > 0`. Execute processes staging rows in status `Validated` only. Error rows are left behind.

Execute is refused when session status is not `Pending`.

### BR-029 — Cancel is only for a pending session

**Implemented.** `CancelImportSessionAsync` returns false unless status is `Pending`. On success, status becomes `Cancelled` and `Notes` records who cancelled and a UTC timestamp.

### BR-030 — History is the last 20 sessions for that display name

**Implemented.** `GetRecentImportSessionsAsync` filters `CreatedBy` and takes 20, ordered by `CreatedAt` descending. The controller passes that list into a view model that also counts completed and failed rows and a success rate. The service method's own default of 10 is overridden by the controller's 20.

### BR-031 — Import row counts in SQL

**Implemented** in `sql files/1. CreateImportTables.sql`, if that script has been applied. Whether it has been applied is **Unknown**.

- `TotalRows`, `ValidRows`, `ErrorRows`, `ProcessingTime`, `FileSize` must be >= 0 where those checks exist.
- `ValidRows + ErrorRows <= TotalRows`.
- `ImportMode` in (`UpdateExisting`, `AddNewOnly`, `ReplaceAll`) or null.
- Staging `Status` in (`Pending`, `Validated`, `Imported`, `Error`).
- Error type in (`File`, `Validation`, `Import`, `System`).
- Severity in (`Info`, `Warning`, `Error`, `Critical`).
- Deleting a session cascades to staging rows. Error logs set `SessionId` to null.

The C# path can still insert sessions if the tables exist and the constraints agree with the values EF sends. No migration runner is in the repository.

## SharePoint

### BR-032 — One folder per indent

**Implemented.** Path `/Shipment Checklist/{indent number}` on the configured site drive.

### BR-033 — Graph uses an application token

**Implemented.** Confidential client, client secret, scope from configuration. `.env.example` shows the scope example `https://graph.microsoft.com/.default`. The site id is configuration, not hard-coded.

### BR-034 — Preview failure is returned as text

**Implemented** in `GetSharepointPreviewURL`. On HTTP 404 the method returns the string `File not Found`. Controller comments that would replace that string with empty are commented out, so the detail page can use that text as the link href.

## Other constraints

### BR-035 — CORS policy is registered and not obviously applied

**Implemented.** Policy name `CorsPolicy`, any origin, any method, any header. No `UseCors` was found in `Program.cs`. Effect at runtime is **Unknown / Requires Confirmation**.

### BR-036 — Non-development errors use the exception handler page

**Implemented.** When the environment is not Development, `UseExceptionHandler("/Home/Error")` and HSTS are enabled. Development uses the developer exception page. `HomeController.Error` has no `[Route]`, and the conventional route is commented out. Whether `/Home/Error` resolves is **Unknown / Requires Confirmation**.

### BR-037 — Timestamps use the server local clock for shipments

**Implemented.** Shipment `created_at` and `updated_at` use `DateTime.Now`. Import session notes and SQL defaults use UTC (`DateTime.UtcNow`, `GETUTCDATE()`). The two clocks are not the same. **Inferred** only as a fact of the code, not as a chosen business calendar.
