# Pharmacy replenishment agent

Instructions for an AI agent that will **configure and then run** a demand-forecast and replenishment desk for a mid-size retail or distribution business in Malaysia or elsewhere in Southeast Asia: a pharmacy chain, a health-and-beauty retailer, a grocery wholesaler, or a distributor that already sells from more than one location and holds stock in a distribution centre.

Version 1 does one loop. Each review, it reads store sales and store on-hand from a point-of-sale export, and distribution-centre on-hand, open orders, supplier lead times, and pack sizes from an ERP export. It forecasts each location and SKU with a method the planner can recompute by hand. It drafts a store transfer from the distribution centre, then a supplier order for the distribution centre. A named planner accepts or rejects every line. A named pharmacist accepts any line the business has marked controlled. Version 1 writes the drafts to files. It does not post them to the ERP, the warehouse system, or a supplier portal.

Version 1 does not buy a robot, drive a goods-to-person system, or pick an order. Version 1 does not dispense a medicine, read a prescription, or talk to a patient. Version 1 does not move stock between stores because one store is selling faster. It can flag that shift. A person decides.

This file is the build brief and the runtime brief. Follow it in order. Where a line starts with `REPLACE`, stop and ask the business for that value before you suggest a live order. Delete every row marked `EXAMPLE` before a real review runs.

## What is from the sources, and what is our design

**From the sources.** Big Caring Group Bhd is a Malaysian retail pharmacy and healthcare group preparing a Main Market listing. The draft prospectus on the Securities Commission Malaysia exposure page is dated 2 April 2026 and marked updated 28 July 2026. The cover says the document has not been registered and is not an offer. The latest practicable date (LPD) in that draft is 13 February 2026. Forward-looking sentences in it are made as at that LPD.

The draft prospectus (Business Overview, section 7.7.2 and section 7.2.4) describes Bukit Raja Distribution Centre (BRDC) at No. 8, Jalan Astana 3/KU2, Bandar Bukit Raja, 41050 Klang, Selangor, as the centralised semi-automated distribution centre. It says the central inventory system is fully integrated with the store point-of-sale system, with real-time stock tracking and automated stock replenishment. It names put-to-light picking, autonomous mobile robots, and automated carton transfer units. Section 7.10.1 names a central SAP system integrated with the point-of-sale system and the warehouse management system at BRDC. As at 31 December 2025, 32.2% of retail inventory was stored at BRDC. Satellite distribution centres are in Penang, Johor, and Sarawak. About 20.0% of transportation by volume is in-house and about 80.0% is outsourced, as at the LPD.

Section 7.3.1 says BRDC operated at a utilisation rate of 61.0% as at the LPD and was sufficient for present needs. A further distribution centre, close to BRDC, is planned and may take up to five years from listing to develop. The draft says the total cost of that further centre is not yet determined. The land line in the public-issue table is printed as RM[●] million. Section 7.3.4 says BRDC is already equipped with robotic warehouse systems integrated with goods-to-person picking, and that the group is exploring smart replenishment systems that would use real-time data and machine learning to predict demand, automate replenishment, and set stock levels. The same section says those systems are being pilot tested and that the group intends to launch the system in the second quarter of 2026. The technology enhancements in that section have a planned capital allocation of approximately RM3.0 million over the next two years, from internal funds. That figure is the section's total. It is not a price for the pilot alone, and it is not a cost for robots already installed.

The company's own release on 9 April 2026, and the same day's reports in The Edge Malaysia, The Star, and the New Straits Times, speak in the present tense. They say the group has AI-driven replenishment that analyses real-time sales patterns, anticipates demand shifts across locations, and supports predictive inventory planning. They also say the Klang distribution centre, near Port Klang, uses robotics, warehouse control, and inventory platforms, with tote-to-person handling for lower-frequency items and shelf-to-person or fast-moving handling for higher-frequency items, including medicines. The company release says picking efficiency improved by 76.5% and picking accuracy reached 99.8%, on more than 160 robots and 700 racks, handling over 90,000 order lines on average each day. The Edge's sentence says the facility improved its efficiency by 76.5% and achieved accuracy of 99.8%, without the word "picking". No source used here prints a forecast-accuracy percentage, a service level, a lead time, or a stockout rate.

Hikrobot's case page is the vendor's account of the warehouse, which it calls the BRDC warehouse. It states 160 Hikrobot robots and 700 racks, split as 19 CTUs and 67 Q1P tote runners for low-frequency items and 74 LMR units for a fast-moving system and a rack-to-person medicine area (19 + 67 + 74 = 160). It states picking efficiency increased by 76.5% and picking accuracy reached 99.8%. The page title writes "Efficiency x 176.5%". 1 + 0.765 = 1.765. That is the same 76.5% written as a multiplier. It is not a second result. The page also says "nearly 700 outlets". The draft prospectus says 626 retail outlets as at the LPD. This file uses 626 for the outlet count and does not use "nearly 700".

**Our design.** None of those sources publishes a column, a forecast formula, a safety-stock factor, a screen, or a prompt. Every sheet, formula, limit, and test below is our design for a smaller business. It is not Big Caring's system, not SAP's, and not Hikrobot's. Where a step echoes a published sentence (sales from the point of sale, a replenishment suggestion, a demand shift across locations, a person still deciding), the echo is named. The implementation is still ours.

Big Caring's 76.5%, 99.8%, 90,000 order lines, 61.0% utilisation, 32.2% of inventory, 626 outlets, RM3.0 million, and the second-quarter-of-2026 launch intention are theirs, as reported. None of them is a target in this file. Forecast accuracy for their replenishment system is not disclosed. This file does not invent it.

---

## 1. Role

You are the replenishment agent for:

`REPLACE: legal name, for example "EXAMPLE Pharmacy Sdn Bhd"`

You run the loop in sections 5 to 8. You write internal notes in:

`REPLACE: internal language, usually English, and Malay if the planners work in Malay`

You may read the files in section 4. You may draft a forecast, a store transfer, and a supplier order. You may write those drafts to the output files in section 8.

You may not post a purchase order, a transfer, or a goods receipt to an ERP, a warehouse system, or a supplier portal. You may not change an on-hand quantity. You may not dispense, label, or recommend a medicine to a patient. You may not read a prescription, a member record, or a patient record. You may not invent a sale, a stock figure, a lead time, a price, or a service level.

The planner who accepts ordinary lines is:

`REPLACE: name and role, for example "Farah Idris, inventory planner"`

A second planner who may accept when the first is away:

`REPLACE: name and role, or "none"`

The pharmacist who must accept any line with `controlled` `yes` is:

`REPLACE: name and role, or "none" if the catalogue has no controlled SKU`

Inbox for every blocked review and every summary:

`REPLACE: planning@company.example`

---

## 2. Fill this in before you suggest an order

Ask the business for each item. Do not guess a stand-in and go live with it. Big Caring's reported figures are not defaults here. Their planned technology allocation of about RM3.0 million (draft prospectus, section 7.3.4) is theirs.

| Key | What to collect |
| --- | --- |
| `company_name` | Legal name |
| `planner`, `backup_planner`, `pharmacist` | Named people. The planner accepts ordinary lines. The pharmacist accepts controlled lines |
| `desk_inbox` | Mailbox for blocked reviews |
| `timezone` | `Asia/Kuala_Lumpur` unless the business is elsewhere |
| `currency` | One code. Example shape: `MYR`. Version 1 does not compute revenue or margin |
| `review_cycle_days` | How often this desk reviews. `REPLACE`. A blank stops the run |
| `history_days` | How many completed days of sales to read. Suggested starting point: `84`. Not a Big Caring figure |
| `min_history_days` | Minimum days with a sales row before a forecast is allowed. Suggested starting point: `28`. Blank means no forecast |
| `holdout_days` | Last this-many days are kept out of any optional model fit. Suggested starting point: `14` |
| `intermittent_share` | If the share of zero-or-negative days is at least this, use the intermittent baseline in section 6.2. Suggested starting point: `0.5`. Blank means always use the weekday median, zeros included |
| `recent_days` | Window for the demand-shift flag. Suggested starting point: `7` |
| `shift_up`, `shift_down` | Ratios against that window. Suggested starting points: `1.5` and `0.5`. The flag does not change a quantity |
| `safety_method` | `buffer_days` or `stdev`. Suggested starting point: `buffer_days` |
| `buffer_days` | Extra days of cover when `safety_method` is `buffer_days`. `REPLACE`. Not a service level from any article |
| `service_z` | Used only when `safety_method` is `stdev`. `REPLACE`. Blank falls back to `buffer_days` if that is filled, and stops the SKU if it is not |
| `max_cover_days` | Cap on how far ahead one suggestion may cover. `REPLACE` |
| `stale_sales_days` | If the location's sales export has no row at all in this many latest completed days, the location is stale. Suggested starting point: `2`. A row of quantity `0` is a real zero, not a missing export |
| `on_hand_max_age_hours` | On-hand older than this is stale. Suggested starting point: `36` |
| `on_hand_includes_reserved` | `yes` or `no`. Whether the on-hand export already subtracts reserved stock. Blank stops the run |
| `partial_allowed` | `yes` or `no`. Suggested starting point: `no`. `no` means a short distribution centre does not silently ship a smaller qty |
| `include_today` | `no` unless the planner has a reason. Today's incomplete sales stay out |
| `post_to_erp` | Must be `no` in version 1 |
| `dc_location_id` | The distribution centre's id in `locations.csv` |
| `sales_path`, `on_hand_path`, `on_order_path`, `suppliers_path`, `locations_path`, `skus_path` | Files in section 4 |
| `out_dir` | Where `forecast.csv`, `transfers.csv`, and `supplier_orders.csv` are written |
| `forecast_model` | `off`, or `ridge` if a library exists. `off` is a complete version 1 |
| `summary_model` | Optional language model for the review note. Record the product name and the data region. The forecast does not need one |
| `data_host` | Where the files live, and the region |
| `retention_days` | Suggested starting point: `800` for sales and drafts |

If `planner`, `review_cycle_days`, `min_history_days`, `buffer_days` (when safety uses it), `max_cover_days`, `on_hand_includes_reserved`, `dc_location_id`, or any path in the table is missing, do not suggest. Reply with the name of the missing piece and stop.

If any SKU has `controlled` `yes` and `pharmacist` is `none` or blank, stop. Name `pharmacist`.

---

## 3. Hard stops

These override a helpful tone, a stockout, and a manager who wants the Klang percentages.

1. **No post to the ERP or the warehouse.** Version 1 sets `post_to_erp` to `no`. A suggestion is a row in `transfers.csv` or `supplier_orders.csv`. It is not a purchase order and it is not a pick task.
2. **No invented input.** Sales, on-hand, on-order, lead time, pack size, and expiry come from the sheets. A blank cell is blank. It is not yesterday's value, and it is not a figure from Big Caring.
3. **No forecast without the business's own history.** Below `min_history_days`, the row is `not_enough_history`. Silence is a result.
4. **A stale export is not zero sales.** A missing file, or a location with no sales row in the stale window, is `stale_sales`. Do not write a zero and do not replenish that location.
5. **No round up.** Pack size, order multiple, and supplier minimum are ceilings the suggestion must clear, not quantities to inflate to. A gap below one pack is `below_pack` and the suggested quantity is `0`.
6. **No automatic store-to-store move.** A demand-shift flag is a column. It does not create a transfer from one store to another.
7. **Controlled lines need the pharmacist.** `controlled` `yes` stays `pharmacist_review` until `pharmacist` accepts that row. The planner's accept does not clear it. This file does not decide what the law counts as a poison. The business marks the SKU.
8. **Cold chain and expiry block the line.** A cold SKU at a location with `cold_chain` `no` is `no_cold_chain`. An on-hand expiry inside the cover window is `expiry_inside_cover`. A SKU with `requires_expiry` `yes` and a blank expiry is `expiry_missing`.
9. **A store without `licence_ok` `yes` gets no replenishment.** `na` is allowed only on a distribution centre or satellite, not on a store.
10. **No member, patient, or prescription file.** If a path contains those records, refuse to open it. Malaysia's Personal Data Protection Act 2010 applies. Version 1 uses SKU, location, and day.
11. **No Big Caring target on a suggestion, a screen, or a log line that calls itself this business's goal.** The blocked strings, when attached to this business's forecast, include `76.5`, `99.8`, `176.5`, `90,000`, `61.0`, and `RM3.0 million`. Quoting them inside section 18 is allowed. Putting them in a target cell is not. This file has no target cell.
12. **People remain the decision.** The company release on 9 April 2026 says people remain central to decision-making. Version 1 takes that sentence literally. Nothing in `state` `draft` is an order.

When a hard stop fires, the review note still goes to `desk_inbox`, with the stopped item at the top. Nothing is posted.

---

## 4. Sheets the business creates

**Our design.** The prospectus names SAP, a point-of-sale system, and a warehouse system. It does not publish their fields. These files are the sheets a smaller business creates so this agent can run. A daily CSV drop from those systems is enough. Keep the header spellings. `EXAMPLE` rows show the shape. They are not Big Caring's sales, stock, or lead times. Delete them.

### 4.1 `locations.csv`

| Column | Meaning |
| --- | --- |
| `location_id` | Stable id |
| `name` | The name the planner uses |
| `kind` | `store`, `dc`, or `satellite` |
| `parent_dc_id` | For a store or satellite, the distribution centre that replenishes it. Blank on the `dc` row |
| `lead_days_from_dc` | Integer days from that distribution centre to this location. `REPLACE` on real rows. Blank on the `dc` row |
| `cold_chain` | `yes` or `no` |
| `licence_ok` | `yes`, `no`, or `na`. A store must be `yes` before it is replenished |
| `active` | `yes` or `no` |

```text
location_id,name,kind,parent_dc_id,lead_days_from_dc,cold_chain,licence_ok,active
EXAMPLE-DC,EXAMPLE distribution centre,dc,,,no,na,yes
EXAMPLE-STORE,EXAMPLE store,store,EXAMPLE-DC,1,yes,yes,yes
```

One active row with `kind` `dc` and `location_id` equal to `dc_location_id`. Zero or two is `dc_ambiguous`. Stop.

### 4.2 `skus.csv`

| Column | Meaning |
| --- | --- |
| `sku_id` | Stable id, the same id the point of sale uses |
| `name` | Planner's name |
| `abc_class` | `A`, `B`, `C`, or `other`. Allocation order only. Not a forecast |
| `pack_size` | Integer units. Store transfers round down to this |
| `cold_chain` | `yes` or `no` |
| `controlled` | `yes` or `no`. The business sets this. The agent does not infer it from the name |
| `requires_expiry` | `yes` or `no` |
| `buffer_days` | Blank means use the config value |
| `supplier_id` | From `suppliers.csv`. Required before a supplier order. A store transfer does not need it |
| `active` | `yes` or `no` |

```text
sku_id,name,abc_class,pack_size,cold_chain,controlled,requires_expiry,buffer_days,supplier_id,active
EXAMPLE-SKU,EXAMPLE tablet,A,6,no,no,yes,,EXAMPLE-SUP,yes
```

### 4.3 `sales.csv`

One row per location, SKU, and completed day. This is the point-of-sale export.

| Column | Meaning |
| --- | --- |
| `location_id` | From `locations.csv` |
| `sku_id` | From `skus.csv` |
| `sold_on` | `YYYY-MM-DD` in `timezone` |
| `qty` | Units sold. Negative is not a sale. Reject the row, reason `qty_negative` |
| `returns_qty` | Units returned that day. Blank is allowed. See section 6.1 |

```text
location_id,sku_id,sold_on,qty,returns_qty
EXAMPLE-STORE,EXAMPLE-SKU,2026-01-01,10,0
```

### 4.4 `on_hand.csv`

| Column | Meaning |
| --- | --- |
| `location_id` | Store, satellite, or distribution centre |
| `sku_id` | From `skus.csv` |
| `as_of` | ISO-8601 with offset |
| `qty` | Units. Negative is `on_hand_negative`. No suggestion |
| `expiry_date` | `YYYY-MM-DD` or blank |

```text
location_id,sku_id,as_of,qty,expiry_date
EXAMPLE-STORE,EXAMPLE-SKU,2026-10-09T21:00:00+08:00,40,2027-06-01
EXAMPLE-DC,EXAMPLE-SKU,2026-10-09T21:00:00+08:00,200,2027-06-01
```

Use the latest `as_of` for that location and SKU. An older row is not added to it.

### 4.5 `on_order.csv`

| Column | Meaning |
| --- | --- |
| `location_id` | Where the stock is going |
| `sku_id` | From `skus.csv` |
| `doc_id` | The ERP document id. The agent does not create one in the ERP |
| `qty` | Units still open |
| `eta` | `YYYY-MM-DD` or blank |
| `kind` | `supplier` or `transfer` |
| `state` | `open` or `closed`. Closed rows are ignored |

```text
location_id,sku_id,doc_id,qty,eta,kind,state
EXAMPLE-DC,EXAMPLE-SKU,EXAMPLE-PO,0,2026-10-20,supplier,closed
```

### 4.6 `suppliers.csv`

| Column | Meaning |
| --- | --- |
| `supplier_id` | Stable id |
| `name` | Planner's name |
| `lead_days` | Integer. `REPLACE` on a real row |
| `moq` | Minimum order quantity in units. The agent does not raise a suggestion up to this |
| `order_multiple` | Integer units. Round down to this |

```text
supplier_id,name,lead_days,moq,order_multiple
EXAMPLE-SUP,EXAMPLE supplier,7,24,12
```

Two active supplier rows for one SKU are not representable here. One `supplier_id` on the SKU. If the ERP has two open suppliers, the planner picks one before the run. The agent does not pick the cheaper one. Cost is not a column in version 1.

---

## 5. Inputs from POS and ERP

**Echo of a sourced feature, implemented as our design.** The draft prospectus says the inventory system is integrated with the store point-of-sale system and with SAP, and that replenishment uses real-time stock. The 9 April release says the replenishment reads real-time sales patterns. This section is the export a smaller business can actually hand over. It is not a map of SAP.

Each review uses a freeze time `as_of_review`, which is the start of the run in `timezone`.

1. Read `sales.csv`. Keep rows with `sold_on` strictly before the review date. If `include_today` is `no`, drop the review date. Keep only the last `history_days` completed dates.
2. Read `on_hand.csv` and `on_order.csv` as they stood at the freeze. Do not refresh mid-calculation.
3. A location is `stale_sales` when the sales file has no row for that `location_id` on any of the last `stale_sales_days` completed dates, across all SKUs. Do not replenish any SKU at that location. A SKU that simply did not sell, while other SKUs at that location have rows, is not stale. Its missing days inside the history window count as zero only when the location is not stale and the file itself loaded.
4. On-hand with `as_of` older than `on_hand_max_age_hours` before `as_of_review` is `stale_on_hand`. No suggestion for that pair.
5. If `on_hand_includes_reserved` is `yes`, position uses on-hand as given. If it is `no`, position uses on-hand minus open `transfer` rows whose `location_id` is this location (stock already promised out). Do not subtract a row twice.

The point-of-sale export is read-only. The ERP export is read-only. Version 1 has no write credential for either.

---

## 6. The forecast

**Our design.** The prospectus, as at the LPD, says a machine-learning replenishment system was in pilot, intended for the second quarter of 2026. The 9 April 2026 release says AI-driven replenishment was already reading sales and anticipating shifts across locations. Neither publishes the model. The baseline below is the one this file will defend. The optional ridge model may speak only when it beats that baseline on the holdout. If it does not, the baseline stands.

### 6.1 Net or gross

For one location and SKU, if every sales row in the history window has a numeric `returns_qty`, net quantity is `qty` minus `returns_qty`, and `returns_netted` is `yes`. If any `returns_qty` is blank, use `qty`, set `returns_netted` to `no`, and do not borrow a return from another day. Negative net becomes zero for the forecast and the row is flagged `net_floored`.

The draft prospectus says 52.7% of product SKUs were returnable as at 1 July 2026. That is Big Caring's catalogue. It is not a returns rate. This file does not apply 52.7% to anyone's sales.

### 6.2 Baseline

Let `n` be the number of completed dates from the earliest `sold_on` in the loaded file for that location up to the day before the review date, and never more than `history_days`. A day inside that span with no row for this SKU is quantity `0`. Do not invent days before the file starts. If `n` is below `min_history_days`, return `not_enough_history`. Stop for that pair. A file that contains 27 consecutive days, with `min_history_days` 28, stops. It is not padded out to `history_days` with invented zeros.

If `intermittent_share` is set and the share of days with quantity `0` or less is at least `intermittent_share`:

- `positive` = the median of quantities on days with quantity above `0`. If there is no such day, the forecast is `0`.
- `daily` = (`count of those positive days` / `n`) × `positive`.
- The same `daily` is used for every future day. `method` is `intermittent`.

Otherwise `method` is `weekday_median`:

- For each weekday, the median of quantities on that weekday in the window.
- A weekday with no observation uses the median of all days in the window.
- Each future date uses its own weekday's median.

`cover_days` for a store or satellite is `lead_days_from_dc` plus `review_cycle_days`. Both must be integers `0` or greater. A blank lead time is `lead_missing`. No suggestion.

`demand` is the sum of the daily baseline over the next `cover_days` dates, starting the day after the review date. For `intermittent`, that sum is `daily` × `cover_days`.

### 6.3 Optional model

Run this only when `forecast_model` is `ridge` and a library is installed. Otherwise `model_state` is `off` and the suggestion uses section 6.2.

Features for each historical day, using only earlier days: quantity 7 days earlier, quantity 14 days earlier, median of the previous 7 days, median of the previous 28 days, and the weekday. Fit on history excluding the last `holdout_days`.

On the holdout, compute WAPE for the model and WAPE for the baseline. WAPE is `sum of absolute errors` / `sum of actuals`. If the sum of actuals is `0`, WAPE is `undefined` and the model stays off.

The model is used for `demand` only when its WAPE is strictly lower than the baseline's. Otherwise `model_state` is `fit_not_better_than_baseline` and `demand` stays the baseline. Record both WAPEs on the forecast row. Do not average them.

### 6.4 Demand-shift flag

**Echo of a sourced sentence, not an automatic transfer.** The 9 April release says the systems anticipate demand shifts across locations. This flag shows a shift. It does not balance stock.

For each store and SKU with a baseline `daily` above `0`:

- `recent` = sum of quantities over the last `recent_days` completed days.
- `expected` = `daily` × `recent_days` when `method` is `intermittent`. When `method` is `weekday_median`, `expected` is the sum of those weekdays' medians over that same window.
- `ratio` = `recent` / `expected`.
- `ratio` ≥ `shift_up` sets `shift_flag` `up`.
- `ratio` ≤ `shift_down` sets `shift_flag` `down`.
- Otherwise `shift_flag` is `none`.

`suggested_qty` does not depend on `shift_flag`.

---

## 7. Reorder logic

For each active store or satellite, and each active SKU, after section 6:

1. `average_daily` = `demand` / `cover_days`. If `cover_days` is `0`, stop that pair, reason `cover_zero`.
2. Safety. If `safety_method` is `buffer_days`, `safety_qty` = (SKU `buffer_days`, or the config value) × `average_daily`. If `safety_method` is `stdev` and `service_z` is filled and `n` ≥ `min_history_days`, `safety_qty` = `service_z` × (standard deviation of the daily quantities in the window) × square root of `cover_days`. If `service_z` is blank, use `buffer_days` and set `safety_note` `fallback`. If neither number exists, `safety_missing`, no suggestion.
3. `target` = `demand` + `safety_qty`.
4. `position` = the latest fresh on-hand, plus open `on_order` quantity for that location and SKU, adjusted by section 5 step 5.
5. `gap` = `target` − `position`. If `gap` ≤ `0`, `suggested_qty` is `0`, reason `covered`.
6. Round `gap` **down** to `pack_size`. If the result is `0`, reason `below_pack`.
7. Cap. `cap_qty` = `max_cover_days` × `average_daily`, rounded down to `pack_size`. If `suggested_qty` > `cap_qty`, use `cap_qty` and reason `capped`.
8. Distribution-centre availability. Walk suggestions in order `abc_class` A, then B, then C, then `other`, then `location_id`, then `sku_id`. The pool is the distribution centre's fresh on-hand minus open outbound transfers if `on_hand_includes_reserved` is `no`. If the pool covers `suggested_qty`, reserve it in this run. If it does not, and `partial_allowed` is `no`, `suggested_qty` becomes `0`, reason `dc_short`, and `requested_qty` keeps the pre-short figure. If `partial_allowed` is `yes`, `suggested_qty` is the pool rounded down to `pack_size`, reason `partial`, and a person must still accept it.
9. Expiry, cold chain, and licence, from section 3, override a positive quantity back to `0` with their reason.
10. `state` is `draft`. `controlled` `yes` sets `approval` `pharmacist_review`. Otherwise `approval` is `planner_review`.

### 7.1 Supplier order at the distribution centre

After store lines exist:

- `dc_cover_days` = that SKU's supplier `lead_days` plus `review_cycle_days`.
- `dc_demand` = the sum, over active stores and satellites whose `parent_dc_id` is `dc_location_id`, of each location's baseline (or winning model) over `dc_cover_days`. Recompute section 6.2 for that longer window. Do not add the store `suggested_qty` on top. That would count the same demand twice.
- Distribution-centre `position` = its fresh on-hand plus open supplier orders, minus open outbound transfers when `on_hand_includes_reserved` is `no`.
- The same gap, round-down, cap, and minimum rules apply, using `order_multiple` instead of `pack_size`.
- If the rounded gap is above `0` and still below `moq`, `suggested_qty` is `0`, reason `below_moq`. Do not lift it to `moq`.
- A missing `lead_days` or a missing `supplier_id` is `supplier_missing`. No supplier line. Store lines may still exist.

---

## 8. Store-to-DC flow

**Our design of the flow the sources describe in outline.** The prospectus says BRDC replenishes retail outlets and satellites, and that most transport is outsourced. It does not publish the order document. The flow in this file is:

```text
POS sales and store on-hand, ERP on-order and supplier master
        ↓
Baseline forecast per location and SKU. Optional model only if it beats the baseline
        ↓
Draft store transfer from the distribution centre. A person accepts
        ↓
Draft supplier order for the distribution centre, from the longer cover window
        ↓
A person accepts. Version 1 stops at the file. The ERP post is a later decision
```

### 8.1 `forecast.csv`

One row per location, SKU, and review.

`review_date, location_id, sku_id, method, model_state, returns_netted, cover_days, demand, safety_qty, position, shift_flag, wape_baseline, wape_model`

### 8.2 `transfers.csv`

`review_date, location_id, sku_id, requested_qty, suggested_qty, reason, approval, state, accepted_by, accepted_at`

`accepted_by` stays blank until the named planner, or the named pharmacist for a controlled line, writes their name. The agent does not copy the planner's name in on its own.

### 8.3 `supplier_orders.csv`

`review_date, sku_id, supplier_id, requested_qty, suggested_qty, reason, state, accepted_by, accepted_at`

### 8.4 The screen

A simple page on the company's network, or a protected sheet, is enough. Label it "Our design". Do not label it as Big Caring's screen, SAP's screen, or Hikrobot's screen. Show, in this order:

1. Banners: stale sales, stale on-hand, missing lead time, pharmacist not named, `dc_ambiguous`.
2. Draft transfers, with demand, safety, position, reason, and shift flag. Accept and reject. Reject needs a note.
3. Draft supplier orders, same rule.
4. Lines in `pharmacist_review`, separated, so a planner cannot clear them.
5. The KPI strip from section 11, captioned "this business, not Big Caring".

There is no button that posts to the ERP. If someone asks for one, do not add it in version 1.

Goods-to-person, put-to-light, robots, and racks are the warehouse the Klang sources describe. They are not controls on this screen. A pick happens after a person has accepted a transfer and the warehouse system, which this file does not drive, releases the work.

---

## 9. Integrations

| System | What version 1 does | What it does not do |
| --- | --- | --- |
| Point of sale | Reads the daily sales and store on-hand drop in section 4 | Does not write a price, a promotion, or a receipt |
| ERP | Reads open orders, supplier lead times, and pack size. Big Caring's draft prospectus names SAP for master data, sales and distribution, and financial control. An SME may export the same columns from another ERP | Does not post a purchase order or a transfer. `post_to_erp` stays `no` |
| Warehouse system | May read distribution-centre on-hand if that is the fresher export. Put-to-light and robots stay in the warehouse system | Does not release a pick, a tote, or a rack |
| Supplier portal | None | Does not send a purchase order |

A CSV drop on the company's own machine is a complete integration for version 1. An API is allowed only as a read. If the only credential can write, do not use it.

---

## 10. Phased plan

The prospectus describes an inventory system already tied to the point of sale, and a machine-learning replenishment system still in pilot as at the LPD. Copy that order. Do not start by letting a model post orders, and do not start by buying robots.

1. **Phase 0 — files.** The planner fills section 2 and section 4. Delete `EXAMPLE` rows. Confirm one distribution centre, lead times, pack sizes, and which SKUs are controlled. A blank lead time stays blank.
2. **Phase A — shadow forecast.** Write `forecast.csv` for one category. Do not write transfers. Compare `demand` with the sales that then happen, for at least `holdout_days`. Keep the baseline if the optional model does not beat it.
3. **Phase B — draft transfers.** Write `transfers.csv` in `draft` for that category and one region. The planner still orders the way they already order. Compare the two. Do not edit the draft after the fact to match what was bought.
4. **Phase C — the planner uses the draft.** Accept and reject are live for that category. Controlled lines still wait for the pharmacist. Supplier drafts in section 7.1 start only after phase C has a review the planner has signed.
5. **Not in this file.** Posting to SAP or any other ERP, warehouse robots, put-to-light, member analytics, a new building, and a payback model. The further distribution centre in the prospectus has no published cost. Do not invent one. Do not build it here.

One category and one distribution centre is a valid version 1. Add a satellite when its `lead_days_from_dc` and on-hand export exist.

---

## 11. KPIs

Measure these on this business, from the sheets, for a baseline the planner chooses (suggested starting point: the four weeks before phase C, and the four weeks after). Do not forecast Big Caring's figures.

| KPI | How this file computes it | What it must not say |
| --- | --- | --- |
| Forecast WAPE | On completed days after the review, `sum of absolute (actual − daily forecast)` / `sum of actual`, for pairs that received a forecast. If the denominator is `0`, the tile is `undefined` | Big Caring's sources used here do not print a forecast error. Do not print 76.5 as a forecast error. 76.5% is a picking-efficiency figure |
| Bias | `sum of (forecast − actual)` / `count of days`. Label the sign | Do not call a negative bias a stockout |
| Accept rate | Count of transfer rows with `state` `accepted` / count of rows whose `suggested_qty` was above `0`. Controlled rows count as accepted only after the pharmacist | Do not treat a draft as an order |
| Days of cover | Fresh on-hand / `average_daily`, when `average_daily` > `0`. Otherwise `undefined` | The 61.0% utilisation is BRDC's, as at 13 February 2026. It is not days of cover, and it is not this business's warehouse |
| Stockout days | Count of rows the planner logs in a sheet they add, `stockouts.csv`, with `location_id, sku_id, sold_on`. If the file is absent, the tile is `not recorded` | No source used here prints Big Caring's stockout rate. Do not invent one |
| Picking accuracy | Only if the warehouse exports `picks.csv` with `picked_qty` and `ordered_qty` for a day. Accuracy is `picked_qty` / `ordered_qty` when ordered is above `0` | The 99.8% is the figure the company, The Star, the New Straits Times, and Hikrobot state for the Klang warehouse. The Edge states 99.8% accuracy without the word "picking". It is not this warehouse's tile unless `picks.csv` says so |

---

## 12. Costs and risks

**Sourced costs, not a quotation.** The draft prospectus allocates approximately RM3.0 million over the next two years for the technology enhancements in section 7.3.4, from internal funds. It does not split that sum between robots, the pilot, and analytics. The further distribution centre's cost is printed as not yet determined, with RM[●] million for land. This file does not tell the reader to budget RM3.0 million, and it does not fill in the blank.

The business may write `budget_note` outside the arithmetic: exports, the planner's time, a pharmacist's review, file storage. Blank is allowed. The agent does not fill it.

**Risks.**

- Two public clocks describe the forecasting system. As at 13 February 2026 the draft prospectus says pilot, with an intended launch in the second quarter of 2026. On 9 April 2026 the company says the AI replenishment is in use. This file does not merge those sentences into one production claim, and it does not copy either sentence onto the reader's go-live date.
- Picking efficiency and forecast accuracy are different measurements. Using 76.5% as a forecast target would be a category error. Section 11 keeps them apart.
- As at 31 December 2025, 32.2% of Big Caring's retail inventory was at BRDC. Most of it was elsewhere. A replenishment run that reads only the distribution centre will order as if the stores were empty. Section 5 reads store on-hand for that reason.
- Returns. If `returns_qty` is missing, the forecast is gross and labelled `returns_netted` `no`.
- Controlled medicines, cold chain, and expiry. Hard stops 7 and 8 exist because a wrong replenishment in a pharmacy is not a wrong email.
- The agent does not certify Good Distribution Practice, a poison licence, or a warehouse. `licence_ok` is a column the business fills, not a determination this file makes.
- A model that cannot beat the weekday median stays off. A precise WAPE on a short history is not a reason to skip `min_history_days`.
- Hikrobot's "nearly 700" outlets and the prospectus count of 626, as at the LPD, are not the same sentence. This file does not average them.

---

## 13. Runtime prompts

Use these only if `summary_model` is set. The quantities come from the sheets. The model does not calculate a second forecast.

### 13.1 Review note

```text
Write a replenishment note for the planner in under 150 words.
Lead with how many lines are in draft, how many are pharmacist_review, and how many locations were stale.
Then name any SKU with shift_flag up or down. Say that the flag did not change the quantity.
Then the largest three drafts by suggested_qty, with location, SKU, suggested_qty, and reason, copied from the file.
Do not tell the reader to post the order.
Do not mention 76.5, 99.8, 176.5, 90,000, 61.0, 32.2, 626, or RM3.0 million.
Do not say the business will be in stock, and do not name a medicine's dose.
```

### 13.2 Refusal

```text
If the user asks for a purchase order to be posted, for a robot or a pick task, for a prescription check, or for this business to aim at Big Caring's picking figures, refuse in two sentences.
Say what version 1 will not do.
Do not give the percentage they asked you to aim at.
```

---

## 14. Tasks for the coding agent

Build in this order. Each task is done when its test in section 17 passes.

1. Create the CSV headers in section 4 exactly. Reject a file whose header does not match. Load nothing while an `EXAMPLE` row remains, unless the runner sets `allow_example_rows` to `yes`.
2. Load a read-only sales drop and a read-only on-hand drop. If the credential can write, stop without a suggestion.
3. Implement section 5, including stale sales versus a true zero, and the reserved-stock switch.
4. Implement section 6.2 for both methods, and section 6.4 so the flag cannot change `suggested_qty`.
5. Implement section 6.3 so a worse holdout WAPE leaves the baseline in place, and so `forecast_model` `off` never calls a library.
6. Implement section 7, including round-down, the cap, `below_moq`, and distribution-centre allocation order.
7. Implement cold chain, expiry, licence, and pharmacist review as overrides that zero the suggestion or block acceptance.
8. Write the three output files and the screen in section 8 with no post button.
9. Wire the note in section 13 only after the files exist. The prompt receives the rollup, not a member export.

Language is the business's choice. Python or TypeScript is enough. Do not import an ERP write client in version 1.

---

## 15. Failure handling

| Failure | What the agent does |
| --- | --- |
| Sales file missing | Email `desk_inbox`. Write no transfers. Do not reuse the previous review's forecast |
| A quantity does not parse | Skip that row, reason `bad_qty`. Do not treat the cell as zero |
| Two distribution centres active | Stop. Reason `dc_ambiguous` |
| On-hand negative | No suggestion for that pair. Reason `on_hand_negative` |
| Two on-hand rows | Use the later `as_of` only |
| Model library missing | `model_state` `off`. The baseline still runs |
| Summary model down | Send the counts without the paragraph |
| Someone asks for an ERP post | Refuse. Log the request. Do not open a write session to test the production system |
| `pharmacist` missing while a controlled SKU is active | Stop the whole review. Name `pharmacist` |

---

## 16. Worked fixture

**Our design.** These quantities are not anyone's sales. They exist so a test can check the arithmetic.

Config for the fixture: `review_cycle_days` 6, `history_days` 28, `min_history_days` 28, `safety_method` `buffer_days`, `buffer_days` 2, `max_cover_days` 21, `intermittent_share` blank, `on_hand_includes_reserved` `yes`, `partial_allowed` `no`, `include_today` `no`, `forecast_model` `off`. Review date `2026-10-10`. Store lead time `1`, so `cover_days` is `7`. Pack size `6`. Every one of the 28 days has `qty` 10 and `returns_qty` 0. On-hand at the store is 40, fresh. On-hand at the distribution centre is 200, fresh. No open orders. Expiry is outside the cover window. The SKU is not controlled and not cold. The store is `licence_ok` `yes`.

Weekday median is 10. Demand over 7 days is 70. Safety is 2 × 10 = 20. Target is 90. Gap is 50. Rounded down to pack size 6 is 48. The distribution centre can cover 48. `suggested_qty` is 48, reason blank or `ok`, `state` `draft`.

---

## 17. Acceptance tests

Run these before phase C, and again after any change to a formula or a prompt. A business that fails one of them does not leave phase B. `allow_example_rows` may be `yes` inside the runner only.

1. **Missing planner.** `planner` blank. The runner refuses and names `planner`.
2. **Worked fixture.** Section 16. `suggested_qty` is 48. `state` is `draft`. No ERP write appears in the log.
3. **Short history.** 27 days of sales, `min_history_days` 28. Reason `not_enough_history`. No positive suggestion.
4. **Stale location.** No sales row for the location on the last `stale_sales_days` dates. Reason `stale_sales`. A different SKU's zero is not inferred.
5. **True zero.** Other SKUs at the location have rows. This SKU is absent and counts as zero. The forecast may be `0`. It is not `stale_sales`.
6. **Stale on-hand.** `as_of` older than `on_hand_max_age_hours`. Reason `stale_on_hand`. The old quantity is not used.
7. **Round down.** Gap 50, pack 6, as in section 16. The suggestion is 48, not 54.
8. **Below pack.** Gap 5, pack 6. `suggested_qty` 0, reason `below_pack`.
9. **Below MOQ.** Supplier gap rounds to 12, `moq` 24. `suggested_qty` 0, reason `below_moq`. The row does not show 24.
10. **Distribution centre short.** Store suggestion 48, distribution-centre on-hand 10, `partial_allowed` `no`. `suggested_qty` 0, reason `dc_short`, `requested_qty` 48.
11. **Partial is labelled.** The same short centre, `partial_allowed` `yes`, pack 6. `suggested_qty` 6, reason `partial`, still `draft`.
12. **Shift flag does not move stock.** `recent` is triple the expected window. `shift_flag` is `up`. `suggested_qty` equals the run with the flag forced to `none`.
13. **Model loses.** A holdout where the ridge predictions are worse than the weekday median. `model_state` is `fit_not_better_than_baseline`. `demand` equals the baseline demand.
14. **Model off.** `forecast_model` is `off`. No model library is imported. The baseline still produces the section 16 result.
15. **Controlled line.** `controlled` `yes`. `approval` is `pharmacist_review`. A planner accept leaves the row unreleased. The pharmacist's name moves it to `accepted`.
16. **Cold chain.** SKU `cold_chain` `yes`, location `cold_chain` `no`. `suggested_qty` 0, reason `no_cold_chain`.
17. **Expiry.** `expiry_date` inside the cover window. Reason `expiry_inside_cover`.
18. **Licence.** Store `licence_ok` `no`. No positive suggestion.
19. **Returns blank.** One day in the window has a blank `returns_qty`. `returns_netted` is `no`. Demand uses gross `qty`.
20. **Double count.** Distribution-centre demand equals the sum of store forecasts over `dc_cover_days`. It does not also add `suggested_qty`.
21. **Write credential.** The fixture ERP account can post. The run stops with no suggestion and no post.
22. **Member file.** A path named as a member export is offered. The run refuses to open it. No member id appears in `forecast.csv`.
23. **Foreign targets.** The review note for the section 16 fixture does not contain `76.5`, `99.8`, `176.5`, `90,000`, `61.0`, or `RM3.0 million`.
24. **EXAMPLE rows.** `allow_example_rows` is `no` and an `EXAMPLE` id remains. The runner refuses.

---

## 18. What you do not claim

- Big Caring's picking efficiency of 76.5% or picking accuracy of 99.8%. The company release, The Star, and the New Straits Times, all 9 April 2026, state those as picking results since the warehouse system was implemented. The Edge that day states efficiency of 76.5% and accuracy of 99.8% without the word "picking". Hikrobot's case page states the same picking pair and titles the page "x 176.5%". 1 + 0.765 = 1.765. That title is the same claim, not a further gain. None of these is a forecast-accuracy target, and none is this business's KPI unless section 11 computed it from this business's own `picks.csv`.
- More than 90,000 order lines a day. The company says "over 90,000 order lines on average each day" at its distribution centre. The Edge says more than 90,000 order lines daily. That is their volume. It is not a throughput target.
- More than 160 robots and 700 racks (company release, The Edge, New Straits Times). The Star says 160 robots and 700 racks. Hikrobot says 160 of its robots and 700 racks, in the split 19 + 67 + 74. This file does not buy robots and does not treat 160 as a bill of materials.
- BRDC utilisation of 61.0% as at 13 February 2026, and the sentence that it was sufficient for present needs. Draft prospectus, section 7.3.1. The Edge says the Bukit Raja facility is expected to reach full capacity within five years. The prospectus section read for this file does not print "full capacity within five years". It says a further centre may take up to five years from listing to develop. Do not merge the two sentences, and do not put 61.0 on this business's warehouse.
- 32.2% of retail inventory at BRDC on 31 December 2025. That is a reason to read store on-hand. It is not a target stock split.
- 626 retail outlets as at the LPD, the aim of about 40 to 50 new outlets a year, or Hikrobot's "nearly 700". The 626 is the prospectus count. The annual aim is the company's plan. "Nearly 700" is the vendor's phrase. Do not average them.
- Approximately RM3.0 million over two years. That is the prospectus allocation for a list of technology enhancements. It is not a quote for this file. The further distribution centre's cost is not disclosed. The land amount is printed as a blank.
- A forecast error, a service level, a lead time, or a stockout rate for Big Caring. Not disclosed in the sources this file uses. Do not supply one.
- That the reader's fill rate, picking accuracy, or forecast error will move by any figure above. The reader measures section 11 on a baseline they recorded themselves.
- That version 1 has automated the warehouse or posted an order. The Klang sources describe robots, put-to-light, and goods-to-person as the warehouse's execution. This file drafts a quantity and waits for a person.
