# Business rules — edi_sage_middleware (TransactFlow Document Sync)

Evidence date: 2026-10-16.

Source: `git@bitbucket.org:overdrive/edi_sage_middleware.git`, `main` at `75823ee`.

Rules below are what the code and SQL scripts enforce. A rule that exists only as a comment is marked. No separate business-rules document was found in the source repository. Credential values are not repeated here.

## Identity

| Rule | Statement | Provenance |
|---|---|---|
| BR-001 | The maintained program identifies itself as TransactFlow Document Sync / TransactFlow Document Processing, published by Silicon Overdrive | **Implemented** (`AssemblyInfo.vb`, form title, ClickOnce product and publisher) |
| BR-002 | Seller GLN on the invoice CSV is the constant `6009900156101` | **Implemented** (`SO_GETINVOICEDETAILS.sql`). The same constant is the sender GLN in the unused `SUN_SAGE2XML.sql` |
| BR-003 | Seller VAT registration on the invoice CSV is the constant `4810259673` | **Implemented** (`SO_GETINVOICEDETAILS.sql`) |
| BR-004 | Folder and default database names use BSBC. The legal entity is not stated | **Unknown / Requires Confirmation** |

## Who may run it

| Rule | Statement | Provenance |
|---|---|---|
| BR-005 | There is no application login. Any Windows user who can start the ClickOnce app can process invoices and orders | **Implemented** |
| BR-006 | FTP and email passwords decrypt only for the Windows user who saved them (`DataProtectionScope.CurrentUser`) | **Implemented** (`EnhancedSecureSettings.vb`) |
| BR-007 | The SQL login is not the value saved on the SQL settings tab. `SetConnectionString` ignores the username and password it is given and uses a login compiled into `SQLConnector.vb` | **Implemented**. The secret is not copied here |

## Which invoices are exported

| Rule | Statement | Provenance |
|---|---|---|
| BR-008 | Only transaction code `INV` is selected | **Implemented** (`SO_GETINVOICES`, `SO_GETINVOICEDETAILS`) |
| BR-009 | Only account link `5261` and `6960` are selected. Comments name `5261` Checkers and `6960` PNP | **Implemented**. The comment is **Documented** in the script and in `frmMain.vb` |
| BR-010 | `TxDate` must fall on or between the from-date and the to-date. The app sends those dates as `yyyy-MM-dd` with no time | **Implemented**. If `TxDate` stores a time, rows later on the to-date than midnight are excluded. That effect is **Inferred** from the comparison. **Requires Confirmation** of the column type |
| BR-011 | `ExtOrderNum` must be present, not blank after trim, and must not start with `CRN` | **Implemented** |
| BR-012 | Account `5261` files go to folder `SHOPRITE`. Account `6960` files go to folder `PNP` | **Implemented** |
| BR-013 | Both retailers upload to the same FTP location: the configured host plus `/<invoice number>.csv`. The retailer folder is local only | **Implemented** |

## Invoice amounts and labels

| Rule | Statement | Provenance |
|---|---|---|
| BR-014 | Line VAT is 15% of `ActualSalesValue` (`* 1.15`). Discount percentage and discount value on the file are `0`. Pack size on the file is `1`. Unit of measure on the file is `BT` | **Implemented** (`SO_GETINVOICEDETAILS`) |
| BR-015 | The unused GS1 script `SUN_SAGE2XML` calculates VAT at 14% and restricts the header query to account `6960` | **Implemented** as script text. **Implemented** as not executed by the form |
| BR-016 | Currency defaults to `ZAR` when `CurrencyCode` is null. Country defaults to `ZA` when `cEUCountryCode` is null | **Implemented** |
| BR-017 | Buyer GLN is `Client.Post5` after removing the prefixes `GLN `, `GLN: `, and `No.: ` | **Implemented** |
| BR-018 | Invoice name is `PICK 'N PAY` when the account group trims to `PNP`. Otherwise, when the group is not `SHOPR`, the name is `Shoprite`. When the group is `SHOPR`, the name is `GroupDescription` | **Implemented** (`CASE` in `SO_GETINVOICEDETAILS`). The middle branch labels every non-`PNP`, non-`SHOPR` group as Shoprite. Whether that is the intended label is **Requires Confirmation** |

## Invoice files after upload

| Rule | Statement | Provenance |
|---|---|---|
| BR-019 | A failed upload is kept under `RETRY_UPLOAD\<retailer>` and tried again on the next Process Invoices click. Success deletes that copy | **Implemented** |
| BR-020 | A file already inside `RETRY_UPLOAD` that fails again stays there | **Implemented** (`FTPFile`) |
| BR-021 | After the run, invoice files that still exist are zipped under `Backup` and the originals are deleted | **Implemented** |
| BR-022 | Backup zips older than 90 days are deleted. Age is the `yyyyMMddHHmm` embedded in the file name, not the file-system timestamp | **Implemented**. Commit message `#482_BSBC` states the 90-day keep. The code matches that message |

## Which orders are accepted

| Rule | Statement | Provenance |
|---|---|---|
| BR-023 | Only FTP names that start with `Order_` and end with `.csv` are downloaded | **Implemented** |
| BR-024 | A downloaded order file is deleted from the order FTP before it is inserted into Sage | **Implemented** |
| BR-025 | A data line must contain a pipe. The first line is skipped when its first field is not a date-time. A line with fewer than 28 fields is skipped and does not fail the file | **Implemented** |
| BR-026 | The mandatory-field function `ValidateOrderData` does not run. Its call is commented out | **Implemented** as inactive |
| BR-027 | Lines that share a purchase-order number are one Sage document. Dates, PO number, and ship-to fields come from the first line of that group | **Implemented** |
| BR-028 | The client is whichever `Client` row contains the ship-to GLN in Post1–Post5, PostPC, Physical1–Physical5, PhysicalPC, Delivered_To, Addressee, or the `ulARShipD` / `ulARNational` / `ulAROnCon` / `ulARCategory` columns. Physical and Post matches score higher | **Implemented** (`SilOve_INSERTORDER.sql`) |
| BR-029 | No matching client raises `SO_INSERTORDER: No client match found for GLN`. The desktop app then moves the file to Processed and does not retry it | **Implemented** |
| BR-030 | The client must have `iARPriceListNameID`. Otherwise the procedure raises no price list | **Implemented** |
| BR-031 | The client must have a row in `_etblDelAddress`. The chosen method is the default, then `idDelAddress`. Otherwise the procedure raises no delivery method | **Implemented** |
| BR-032 | If `InvNum` already has DocType 4 for that account and the same external order number, the procedure returns Result 2 and does not insert. The file is moved to Processed and is not retried | **Implemented** |

## How a back order is built

| Rule | Statement | Provenance |
|---|---|---|
| BR-033 | The document is DocType 4, DocVersion 1, DocState 1, DocFlag 0, description `Sales Order`, tax inclusive false, tax per line true. The procedure comment calls DocState 1 a back order | **Implemented**. The comment is **Documented** in the script. Sage's meaning of DocState 1 is not re-checked against Sage documentation in this repo |
| BR-034 | Order number is `SO` plus eight digits, one higher than the current maximum of `SO________`, taken while `InvNum` is locked | **Implemented** (`d444c0c`, `TABLOCKX`) |
| BR-035 | Header addresses are copied from the client physical and postal fields. Due date and order date are the file's order date. Delivery date is the file's delivery date. Invoice number and delivery note are empty strings | **Implemented** |
| BR-036 | A line is skipped when the barcode does not resolve, quantity is null or not greater than zero, or unit price is null or negative. One line can have more than one reason | **Implemented** |
| BR-037 | If no line is valid, no `InvNum` row is created. The procedure still returns Result 1. The desktop app treats that as success | **Implemented** |
| BR-038 | Barcode match order is pack barcode on `StkItem`, then `_etblBarcodes`. Variants are the raw value, right 13, drop the first character, and those three with a leading zero. A case size equal to `PackConfiguration` wins ties | **Implemented** |
| BR-039 | Stored quantity is order quantity times pack factor. Factor is `PackConfiguration` if not zero, else stock case size, else 1. Stored unit price is the file unit price divided by that factor. `fQuantity`, `fQuantityUR`, `fQtyChange`, and `fQtyChangeUR` all receive the pack-adjusted quantity | **Implemented**. The UR fields were aligned in `75823ee` |
| BR-040 | VAT on the order line is 15% (`DefaultTaxRate` 15, `DefaultTaxTypeID` 1). Discount is `DiscountValue` when present, otherwise quantity times unit price times `DiscountPercentage` / 100, otherwise 0 | **Implemented** |
| BR-041 | The price-list price is looked up and then not written. The line price comes from the file | **Implemented**. **Requires Confirmation** that ignoring the Sage price is intended |
| BR-042 | Warehouse id on the line is the delivery-method id. The script comment says so | **Implemented**. **Requires Confirmation** that a delivery-method id is a valid warehouse id |
| BR-043 | Unit of measure is the stock sell unit if that id exists, else the stocking unit, else `_etblUnits` code `UNIT`. If `UNIT` is missing, the procedure raises and the transaction rolls back | **Implemented** |
| BR-044 | Header totals `OrdTotExcl`, `OrdTotTax`, and `OrdTotIncl` are the sums of the inserted lines | **Implemented** |
| BR-045 | The first failing purchase order in a file stops the file. Earlier purchase orders in that file are already committed. Later ones are not attempted | **Implemented** (`ProcessOrderFile` returns inside the loop) |

## Order files after processing

| Rule | Statement | Provenance |
|---|---|---|
| BR-046 | Success, already exists, no client match, and truncation go to `ORDERS\Processed`. Other failures go to `ORDERS\Retry` | **Implemented** |
| BR-047 | There is no FAILED folder. Handled and unhandled failures both remain in `Retry`, so the next run tries them again. The email text still says "Archived as failed" for the handled set | **Implemented**. The enum comment agrees there is no FAILED folder. Whether retrying a handled error is intended is **Requires Confirmation** |
| BR-048 | The order zip includes files that were inserted (including all-lines-skipped). It does not include already-exists, no-client, or truncated files | **Implemented** |
| BR-049 | After the zip, every file in `Processed` is deleted. The three skip categories therefore have no local file and no zip entry. The email list is the remaining record | **Implemented**. **Requires Confirmation** |
| BR-050 | Order backup zips follow the same 90-day name rule as invoice zips, with prefix `Orders_Uploaded_` | **Implemented** |

## Email

| Rule | Statement | Provenance |
|---|---|---|
| BR-051 | A report is sent only when SMTP host, port, from, and to are filled, and a ping of the SMTP host succeeds | **Implemented** |
| BR-052 | The committed default To address is `renier@overdrive.co.za`. SSL defaults to true | **Implemented** in `Settings.settings`. Live value depends on the operator's saved settings |
| BR-053 | Invoice report subject is `TransactFlow Document Processing Report` plus a timestamp. Order report subject is `TransactFlow Order Processing Report` plus a timestamp | **Implemented** |
| BR-054 | If no order file is downloaded and the retry folder had no failures, no order email is sent | **Implemented** (`cmdProcessOrders_Click`) |

## ADE and the older BSBC tree

These rules apply only if that executable is what a PC runs. `.gitignore` says they are not the working tree.

| Rule | Statement | Provenance |
|---|---|---|
| BR-055 | ADE exports on a 10-minute timer from catalog `ADE - Live` via `SUN_INVOICE_spSel`, skips an invoice whose file is already under `c:\xprocure\uploaded\`, and uploads to an FTP `PNPInvoices` folder | **Implemented** in `Form1.cs`. The "5 seconds" comment is not what the interval does |
| BR-056 | The older BSBC form routes `5261` and `6960` to different FTP directories and writes 23 pipe fields | **Implemented** in `BSBC_FTP_Integration`. The maintained app writes 27 fields to one FTP root |

## Conflicts that need a person

- Invoice scripts target database `BSBC`. The app defaults to `BSBC_Test`.
- `.gitignore` excludes ADE and `BSBC_FTP_Integration`. Git still contains them.
- Invoice name `CASE` treats every group other than `PNP` and `SHOPR` as the word Shoprite.
- Price list is mandatory, and the price-list price is not the price that is stored.
- Warehouse id is the delivery-method id.
- Order files for "already exists", "no client match", and "truncated" are deleted from `Processed` without being zipped.
- Handled order errors are described as archived and are also left in `Retry`.
- Compiled SQL, FTP, and SOAP defaults exist in source and should be reviewed for rotation. Values are omitted from this document on purpose.
