# MRP Migration + Part Level Planning — Implementation Plan
## v3 — Added Scoped MRP Runs, Priority System, inline review comments
## Based on /Downloads/vvmmblldal/ + all 50 stored procedures + product review

---

## Context

Legacy MRP runs on NHibernate + WCF (.NET Framework). `/Downloads/vvmmblldal/` is the authoritative
latest version. Only `UndoMRP` and `GetSelectListMRP` exist in GB5 today.

This plan migrates the full MRP module to GB5 standards and adds two new product capabilities:
**Part Level Planning & Tracking** (execution-unit level) and **Scoped + Priority-driven MRP**
(run at AllocationId / Sales Order / FG Item level with 1-2-3 demand priority).

MRP Run → async Dapr job + SignalR progress.  
Document release → through existing MMHeadBLL / IndentBLL pipelines.

---

## Architecture Decisions

| Decision | Choice | Rationale |
|---|---|---|
| MRP Run execution | Async + SignalR progress | Long-running; real-time feedback |
| MRP Run scope | Macro / Allocation / SalesOrder / FGItem | New: granular re-planning without full run |
| Priority system | Integer 1–N on demand + allocation | New: drives pegging order + part allocation order |
| Document release | Through existing BLL pipelines | Inherits workflow, OutBox, auto-number, cache |
| MRPACTION2PEGGING | Migrate to C# set-based | Testable; priority sort injected at pegging step |
| ALLOCATION2RESERVATION | Keep as stored proc via ExecuteAsync | Complex reservation math; no BOM context |
| Transaction ownership | BLL only | DAL receives `DbTransaction tx` |
| Large dataset inserts | BulkInsertAsync | MRP creates 100k+ records per run |
| Tenant safety | TENANTID in every query | Mandatory per CLAUDE.md |
| Return types | DAL → typed DTO / Result\<string\> | Never serialize in DAL |

---

## What Exists in GB5 Already — Do Not Re-Create

- `MRPRunBLL.cs` / `MRPRunDAL.cs` — UndoMRP + GetSelectListMRP only
- `MRPRunQB.cs` — UndoMRP cascade deletes + GET_SELECTLIST_MRP
- `MMSL/EndPoints/MRPRun/UndoMRP.cs`, `GetSelectListMRP.cs`
- `MMSL/EndPoints/MRPPartyType/` — full CRUD (4 endpoints), fully migrated
- `MMBLL/MRPPartyType/`, `MMDAL/MRPPartyType/` — fully migrated
- All existing DTOs under `MMDAL/DTO/MRPRun/`, `MMDAL/DTO/MRP/`

---

## Canonical Reference Files

| Layer | File |
|---|---|
| DAL | `GB5Solution/MM/MMDAL/CustomCode/SupplyGroup/SupplyGroupDAL.cs` |
| BLL | `GB5Solution/MM/MMBLL/SupplyGroup/SupplyGroupBLL.cs` |
| SL | `GB5Framework/FrameworkSL/Endpoints/GOP/TargetOperation/GetGopTargetOperation.cs` |
| QB | `GB5Solution/MM/MMDAL/Query/SupplyGroup/SupplyGroupQB.cs` |
| Existing MRP QB | `GB5Solution/MM/MMDAL/Query/MRPRun/MRPRunQB.cs` |

---

## Gap Analysis — What Changed from v1 → v2 → v3

### v2 gaps (from stored procedure + latest legacy analysis)

| Gap | Impact | Where Fixed |
|---|---|---|
| MRPACTION2PEGGING stored proc — complex pegging algorithm | Core MRP correctness | Phase 3 Step 10 |
| Two release paths (direct vs allocation-based) | Release pipeline completeness | Phase 5b |
| ALLOCATION2RESERVATION proc — creates stock reservations | Release side-effect | Phase 5b |
| DocumentPegging module (TDOCUMENTPEGGING) — separate from TPEGGING | Data integrity | Phase 5c |
| StockReservation module (TRESERVATION) — MRP-driven reservations | Stock accuracy | Phase 5d |
| MasterSchedule full module — demand source for MRP | MRP cannot run without it | Phase 0 |
| ClearingOldSchedule endpoint | Operational housekeeping | Phase 5e |
| CombinedMRPRunProcess (MPS → MRP sequential) | Run type completeness | Phase 3 |
| MinMax (Type=2) and ROL (Type=3) run types | Run type completeness | Phase 3 |
| BOMMRPDTO, BOMDetailForMRPDTO, MRPWithDimensionDTO | DTO completeness | Phase 3 |
| ReleaseType correction: 3=Work Indent (not 4) | Correctness | Phase 5 |
| INDENTDETAILPOST → Part Detail WIP status sync | Part Level integration | Phase 7c |
| GetMRPWithDimensionReport | Dimension-level report | Phase 6 |

### v3 gaps (from product review — new features)

| Gap | Impact | Where Added |
|---|---|---|
| Scoped MRP run (AllocationId / SalesOrder / FG Item level) | Targeted replanning; avoids full macro run | Phase 3a |
| Priority system on demand + allocation (1,2,3…) | Stock allocation fairness; serve highest priority first | Phase 3b |
| Priority-aware pegging algorithm | Pegging order respects demand priority | Phase 3 Step 10 |
| Priority-aware Part Level allocation engine | AllocateParts serves highest priority TPARTDETAIL first | Phase 7d |
| SetDemandPriority + SetAllocationPriority endpoints | Allow user to assign priority | Phase 3b |
| TMRPRUN.RUNSCOPE + SCOPEOBJECTID — scope tracking | Audit which scope was used for each run | DB + Phase 2 |
| MMASTERSCHEDULEDETAIL.PRIORITY + MALLOCATION.PRIORITY columns | Store priority values | DB + Phase 0 |

> **Review:** The scoped run feature and priority system are product differentiators but they share
> a common mechanism: both filter and sort the demand set before planning begins. Design them as
> a single `DemandFilterSpec` object that the trigger DTO carries, rather than adding ad-hoc
> fields for each scope type. This keeps the processing pipeline's demand-loading step clean —
> it receives one spec and applies it, regardless of scope type or priority combination.

---

## Phase 0 — Master Schedule (MRP Demand Source) — PREREQUISITE

MRP cannot run without demand data in MMASTERSCHEDULE / MMASTERSCHEDULEDETAIL.
Must be migrated before MRP Run Process (Phase 3) is testable.

> **Review:** `LoadMasterSchedule` aggregates customer orders and independent demand into
> MMASTERSCHEDULEDETAIL. This is effectively a snapshot / materialized view of demand.
> If customer orders are already in a separate sales order table, verify whether LoadMasterSchedule
> is still needed or whether Phase 3a (SalesOrder-scoped run) renders it partially redundant.
> The answer affects whether MMASTERSCHEDULEDETAIL is the single source of truth or just one input.

### Legacy source
`/Downloads/vvmmblldal/MMBLL/MasterSchedule/MasterScheduleBLL.cs`

### Key features to migrate

| Feature | Method | Priority |
|---|---|---|
| CRUD | GetMasterSchedule, SaveMasterSchedule, DeleteMasterSchedule | P0 |
| Detail management | SaveMasterScheduleDetail, DeleteMasterScheduleDetails | P0 |
| Demand loading | LoadMasterSchedule, LoadMasterScheduleBasedonItemType | P0 |
| Pending list | GetMasterSchedulePending | P1 |
| Register view | GetMasterScheduleRegister | P1 |
| Requirement report | GetMasterScheduleRequirement | P1 |
| Shortage report | GetMasterScheduleShortageReport | P1 |

### Schema change (NEW — Priority support)

```sql
-- Add PRIORITY column to MMASTERSCHEDULEDETAIL
ALTER TABLE MMASTERSCHEDULEDETAIL
    ADD PRIORITY SMALLINT NOT NULL DEFAULT 0;
-- 0 = unset (treated as lowest); 1 = highest priority, 2, 3 … N = lower
-- Index: MMASTERSCHEDULEDETAIL (MASTERSCHEDULEID, PRIORITY, ITEMID, TENANTID)
```

### Files to Create

```
MMDAL/DTO/MasterSchedule/MasterScheduleDTO.cs
MMDAL/DTO/MasterSchedule/MasterScheduleDetailDTO.cs     ← add Priority field
MMDAL/DTO/MasterSchedule/MasterScheduleFilterDTO.cs
MMDAL/DTO/MasterSchedule/MasterScheduleListDTO.cs
MMDAL/Query/MasterSchedule/MasterScheduleQB.cs
MMDAL/Interfaces/MasterSchedule/IMasterScheduleDAL.cs
MMDAL/CustomCode/MasterSchedule/MasterScheduleDAL.cs
MMBLL/Interfaces/IMasterScheduleBLL.cs
MMBLL/MasterSchedule/MasterScheduleBLL.cs               ← ExecuteSaveAsync pattern
MMSL/EndPoints/MasterSchedule/GetMasterSchedule.cs
MMSL/EndPoints/MasterSchedule/SaveMasterSchedule.cs
MMSL/EndPoints/MasterSchedule/DeleteMasterSchedule.cs
MMSL/EndPoints/MasterSchedule/LoadMasterSchedule.cs
MMSL/EndPoints/MasterSchedule/GetMasterScheduleList.cs
MMSL/EndPoints/MasterSchedule/GetMasterScheduleReq.cs
MMSL/EndPoints/MasterSchedule/GetMasterScheduleShortage.cs
MMSL/EndPoints/MasterSchedule/SetDemandPriority.cs      ← POST (see Phase 3b)
```

---

## Phase 1 — MRP Master CRUD (MMRP table)

### Legacy source
`/Downloads/vvmmblldal/MMBLL/MRP/MRPBLL.cs`, `/Downloads/vvmmblldal/MMDAL/Poco/MRP.cs`

### POCO fields (all must be in MRPDTO)

```
Id, Code, Name
IsAutoRelease, IsTrackConversion, OverWriteType (byte)
OrganizationType (byte): 0=OU, 1=OUGroup
OUID, OUGroupId, MasterScheduleId
IncludeLeadTime, IncludeLotSize, IncludeStockInTransit, IncludeStock (byte flags)
IsPending, Status, Version, SortOrder, TenantId, CreatedById, CreatedOn, ModifiedById, ModifiedOn
```

> **Review:** `OrganizationType` controls whether the MRP runs for a single OU or an OU group.
> The scoped run feature (Phase 3a) adds a second dimension — scope at demand object level.
> These are orthogonal: OrganizationType = OU/OUGroup is the *organisational boundary*;
> RunScope = Allocation/SalesOrder/FGItem is the *demand filter*. Do not conflate them.

### Files to Create

```
MMDAL/DTO/MRP/MRPDTO.cs
MMDAL/Query/MRP/MRPQB.cs
MMDAL/Interfaces/MRP/IMRPDAL.cs
MMDAL/CustomCode/MRP/MRPDAL.cs
MMBLL/Interfaces/IMRPBLL.cs
MMBLL/MRP/MRPBLL.cs
MMSL/EndPoints/MRP/GetMRP.cs
MMSL/EndPoints/MRP/SaveMRP.cs
MMSL/EndPoints/MRP/DeleteMRP.cs
MMSL/EndPoints/MRP/GetSelectListMRP.cs
MMSL/EndPoints/MRP/GetMRPList.cs
```

---

## Phase 2 — MRP Run Header CRUD (TMRPRUN)

### Schema change (NEW — scope tracking)

```sql
-- Track which scope this run used
ALTER TABLE TMRPRUN
    ADD RUNSCOPE     TINYINT NOT NULL DEFAULT 0,  -- 0=Macro,1=Allocation,2=SalesOrder,3=FGItem
        SCOPEOBJECTID INT NULL;                   -- AllocationId / SalesOrderId / ItemId (for scope 1-3)
-- CacheKeyLevel.NOT_REQUIRED
```

> **Review:** Storing RUNSCOPE on TMRPRUN enables undo logic (UndoMRP) to be scope-aware.
> A scoped run for Allocation X should only undo actions related to Allocation X, not the entire
> TMRPRUNDETAILS set. Update UNDO_MRP_DELETE_MRPACTION in MRPRunQB.cs to filter by
> ALLOCATIONID / SALESORDERID when RUNSCOPE != 0.

### Files to Create / Extend

```
MMDAL/DTO/MRPRun/MRPRunDTO.cs          ← add RunScope, ScopeObjectId fields
MMDAL/Query/MRPRun/MRPRunQB.cs         ← extend: GET_MRPRUN, SAVE_MRPRUN, GET_MRPRUN_LIST
MMDAL/Interfaces/MRPRun/IMRPRunDAL.cs  ← extend
MMDAL/CustomCode/MRPRun/MRPRunDAL.cs   ← extend
MMBLL/Interfaces/IMRPRunBLL.cs         ← extend
MMBLL/MRPRun/MRPRunBLL.cs             ← extend
MMSL/EndPoints/MRPRun/GetMRPRun.cs
MMSL/EndPoints/MRPRun/SaveMRPRun.cs
MMSL/EndPoints/MRPRun/GetMRPRunList.cs
```

---

## Phase 3 — MRP Run Process Engine (Async + SignalR)

### Run Types

| TypeofRun | Name | Notes |
|---|---|---|
| 0 | MPS | Master schedule demand only |
| 1 | MRP | Uses MPS results as input; must run after MPS |
| 2 | MinMax | Uses MITEMOU min/max levels; no BOM explosion |
| 3 | ROL | Reorder level; compares SOH vs reorder point |

Combined run: MPS (Type=0) → MRP (Type=1) sequentially; separate `TriggerCombinedMRPRun` endpoint.

> **Review:** MinMax and ROL skip BOM explosion entirely. Ensure the processing pipeline has
> an early branch that routes Type=2/3 to a lightweight path rather than running the full
> BOM explosion + net requirement calculation. Mixing the code paths will cause subtle bugs.

### MRPRunTriggerDTO — Extended for Scope + Priority

```csharp
public class MRPRunTriggerDTO
{
    public int      MrpId           { get; set; }
    public DateTime FromDate        { get; set; }
    public DateTime ToDate          { get; set; }
    public int      TypeofRun       { get; set; }    // 0=MPS,1=MRP,2=MinMax,3=ROL
    public int      IncludeException      { get; set; }
    public int      IncludeSafetyStock    { get; set; }
    public int      IsPeggingRequired     { get; set; }
    public int      IsStockPostRequired   { get; set; }
    public bool     IsCombined            { get; set; }  // true = MPS then MRP

    // ── NEW: Scoped run ───────────────────────────────────────────
    public MrpRunScope RunScope     { get; set; } = MrpRunScope.Macro;
    public int?     AllocationId    { get; set; }   // set when RunScope = Allocation
    public int?     SalesOrderId    { get; set; }   // set when RunScope = SalesOrder
    public List<int>? FGItemIds     { get; set; }   // set when RunScope = FGItem (1–N items)

    // ── NEW: Priority filter ──────────────────────────────────────
    // If set, only demand with Priority <= MaxPriority is planned in this run.
    // Useful to run a "priority-1 only" plan first, then fill remaining capacity.
    public int?     MaxPriorityToInclude { get; set; }  // null = include all
}

public enum MrpRunScope { Macro = 0, Allocation = 1, SalesOrder = 2, FGItem = 3 }
```

> **Review:** `MaxPriorityToInclude` is a powerful but easy-to-misuse parameter.
> If a user runs with MaxPriority=1 and later runs MaxPriority=2, the second run must not
> overwrite the TMRPRUNDETAILS from the first. Consider whether scoped runs write to a separate
> RUNSCOPE partition of TMRPRUNDETAILS, or whether they append with a tag. Decision needed
> before implementation: recommend that each scoped run creates its own TMRPRUNID and
> TMRPRUNDETAILS rows tagged with that run's RUNSCOPE — same model as today, but scoped.

### Processing Pipeline (MRPRunBLL.ProcessMRPRunAsync)

```
Step 1  (5%)  : Load MRP config (MMRP flags)
Step 2  (8%)  : Validate prerequisite (Type=1 MRP → MPS run exists)
Step 3  (12%) : ── SCOPE FILTER ──
                Build DemandFilterSpec from trigger DTO:
                  Macro:       no additional filter
                  Allocation:  AND msd.ALLOCATIONID = @AllocationId
                  SalesOrder:  AND msd.SALESORDERID = @SalesOrderId
                  FGItem:      AND msd.ITEMID IN @FGItemIds
                Apply MaxPriorityToInclude if set:
                  AND msd.PRIORITY <= @MaxPriorityToInclude (or PRIORITY = 0)
Step 4  (15%) : Load demand from MMASTERSCHEDULEDETAIL using DemandFilterSpec
                — StreamAsync; include PRIORITY column in result
Step 5  (20%) : Load opening stock per item (GET_OPENING_STOCK_MRP_PROCESS)
                  For scope=Allocation: optionally filter stock to that AllocationId
Step 6  (25%) : Load scheduled receipts (open POs/PRs/confirmed WOs)
Step 7  (35%) : BOM Explosion — recursive CTE
                  Skip for MinMax/ROL (TypeofRun=2/3)
Step 8  (50%) : Net Requirement Calculation
                  NetReq = GrossReq − OpeningStock − ScheduledReceipts − SafetyStock
                  Apply LotSizing; apply LeadTime offset
Step 9  (65%) : BulkInsertAsync TMRPRUNDETAILS
Step 10 (75%) : Generate TMRPACTION records
                  Assign PRIORITY from source demand PRIORITY field
                  Assign ALLOCATIONID / SALESORDERID from scope (for scoped runs)
                  BulkInsertAsync TMRPACTION
Step 11 (82%) : ── PRIORITY-AWARE PEGGING (C# migration of MRPACTION2PEGGING) ──
                  Sort demand by PRIORITY ASC (1 first), then by Period ASC
                  For each BOM level: run cumulative pegging on priority-sorted demand
                  Higher priority demands are pegged first; lower priority gets remainder
                  BulkInsertAsync TPEGGING
Step 12 (88%) : Update MMASTERSCHEDULEDETAIL (set MRPRUNID, POSTEDMRPRUNID)
Step 13 (92%) : Insert TPARTDETAIL rows for each TMRPACTION (Part Level integration)
                  Carry PRIORITY and ALLOCATIONID from MRPAction into TPARTDETAIL
Step 14 (100%): Update TMRPRUNSTATUS COMPLETED; push ReceiveComplete

Error: catch → ReceiveError → TMRPRUNSTATUS FAILED → rethrow
```

> **Review:** Step 11 is the most algorithmically sensitive change. The original
> MRPACTION2PEGGING proc processes by BOM level without priority sorting. Injecting
> priority sort before the cumulative pegging loop changes which demand lines consume
> supply first. Test this with a scenario where priority-1 and priority-2 demand compete
> for the same planned order — priority-1 should fully absorb available supply before
> priority-2 receives any allocation.

### Priority-Aware Pegging (C# logic)

```csharp
// Before the level loop, sort all demand lines:
var demandLines = masterScheduleDetails
    .OrderBy(d => d.Priority == 0 ? int.MaxValue : d.Priority)  // 0 = unset → treat as lowest
    .ThenBy(d => d.PeriodDate)
    .ToList();

// CalcCumulativePeg — unchanged from MRPACTION2PEGGING proc:
static decimal CalcCumulativePeg(decimal cumPlan, decimal cumSched,
                                  decimal lineQty, decimal totalPlan)
{
    if (cumPlan >= cumSched)                              return lineQty;
    if (cumPlan >= cumSched - lineQty)                   return cumPlan - (cumSched - lineQty);
    if ((totalPlan - cumPlan) > (cumSched - lineQty))    return cumSched - (totalPlan - cumPlan);
    return lineQty;
}
```

### Fire-and-Forget Architecture

```
POST /MRPRun/TriggerMRPRun              ← standard macro run
POST /MRPRun/TriggerCombinedMRPRun      ← MPS then MRP sequential
POST /MRPRun/TriggerScopedMRPRun        ← NEW: scoped run (Allocation/SalesOrder/FGItem)
GET  /MRPRun/GetMRPRunStatus?MrpRunId=x ← polls TMRPRUNSTATUS

SignalR /hubs/mrprun  Group: "mrprun:ou:{workOUId}"
  ReceiveProgress(int percent, string step, string currentItem)
  ReceiveComplete(int mrpRunId, MRPRunSummaryDTO summary)
  ReceiveError(string message)
```

> **Review:** `TriggerScopedMRPRun` and `TriggerMRPRun` differ only in which fields of
> `MRPRunTriggerDTO` are populated. Consider a single endpoint that accepts the full DTO —
> the processing service branches internally based on `RunScope`. This avoids routing logic
> duplication at the SL layer. Two endpoints are acceptable only if the frontend has materially
> different UX flows for scoped vs macro runs (which it likely does). Confirm with product team.

### New Files (Phase 3)

```
MMDAL/DTO/MRPRun/MRPRunTriggerDTO.cs       ← extended (scope + priority fields)
MMDAL/DTO/MRPRun/MRPRunStatusDTO.cs
MMDAL/DTO/MRPRun/MRPRunSummaryDTO.cs
MMDAL/DTO/MRPRun/BOMMRPDTO.cs
MMDAL/DTO/MRPRun/BOMDetailForMRPDTO.cs
MMDAL/DTO/MRPRun/MRPOpeningStockDTO.cs
MMDAL/DTO/MRPRun/DemandFilterSpecDTO.cs    ← NEW: encapsulates scope + priority filter
MMDAL/Query/MRPRun/MRPRunQB.cs            ← extend
MMSL/EndPoints/MRPRun/TriggerMRPRun.cs
MMSL/EndPoints/MRPRun/TriggerCombinedMRPRun.cs
MMSL/EndPoints/MRPRun/TriggerScopedMRPRun.cs  ← NEW
MMSL/EndPoints/MRPRun/GetMRPRunStatus.cs
MMSL/Hubs/MRPRunHub.cs
MMSL/Hubs/IMRPRunClient.cs
MMSL/Services/MRPRunProcessingService.cs
DB/Migrations/YYYYMMDD_MRPRunStatus.sql
```

### Key SQL additions (MRPRunQB.cs)

```sql
-- GET_OPENING_STOCK_MRP_PROCESS (unchanged from v2)
-- GET_SCHEDULED_RECEIPTS (unchanged from v2)
-- GET_BOM_EXPLOSION recursive CTE (unchanged from v2)

-- GET_DEMAND_FOR_MRP (NEW — replaces hardcoded MMASTERSCHEDULEDETAIL query; accepts filter spec)
SELECT msd.ITEMID, msd.SKUID, msd.PERIODDATE, msd.DAILYQUANTITY,
       msd.ALLOCATIONID, msd.SALESORDERID,
       ISNULL(msd.PRIORITY, 0)   AS Priority    -- ← NEW column
FROM   MMASTERSCHEDULEDETAIL msd
WHERE  msd.MASTERSCHEDULEID = @MasterScheduleId
AND    msd.TENANTID         = @TenantId
-- Dynamic WHERE clauses added by DemandFilterSpec (parameterised, never concatenated):
-- AND msd.ALLOCATIONID = @AllocationId          (scope=Allocation)
-- AND msd.SALESORDERID = @SalesOrderId          (scope=SalesOrder)
-- AND msd.ITEMID IN @FGItemIds                  (scope=FGItem)
-- AND (msd.PRIORITY = 0 OR msd.PRIORITY <= @MaxPriority)  (priority filter)
ORDER  BY ISNULL(msd.PRIORITY, 0) ASC, msd.PERIODDATE ASC;
-- Index required: MMASTERSCHEDULEDETAIL (MASTERSCHEDULEID, PRIORITY, ITEMID, TENANTID)
```

> **Review:** The dynamic WHERE approach using `DemandFilterSpec` is safe as long as the
> DAL builds parameters via `DynamicParameters` and never concatenates strings. The `IN @FGItemIds`
> list must use Dapper's built-in list expansion (pass `new { FGItemIds = ids }`) — Dapper handles
> the parameterisation correctly. Never build `IN (1,2,3)` via string concat.

---

## Phase 3a — Scoped MRP Run (NEW FEATURE)

This phase details the three new run scopes. The processing pipeline (Phase 3) already handles
them via `DemandFilterSpec`. This phase covers the additional supporting features.

### Scope: AllocationId Level

Run MRP for a specific project/allocation. Useful when a customer project has its own
BOM/demand and needs to be re-planned independently without affecting the full OU plan.

**Demand filter:** `MMASTERSCHEDULEDETAIL.ALLOCATIONID = @AllocationId`  
**Stock consideration:** Option to plan against ring-fenced stock for that allocation, OR
against shared OU stock. Expose as `UseRingFencedStock` boolean in trigger DTO.

```sql
-- Ring-fenced stock: stock already reserved/allocated to this AllocationId
-- Shared stock: full OU opening stock (default, backward compatible)
-- For ring-fenced:
SELECT sl.ITEMID, SUM(...) AS OpeningStock
FROM   TSTOCKLEDGER sl
WHERE  sl.ALLOCATIONID = @AllocationId AND sl.OUID = @OuId AND sl.TENANTID = @TenantId
GROUP  BY sl.ITEMID;
```

**Undo scope-awareness:** When UndoMRP is called for a scoped run:
```sql
-- Add to existing UNDO_MRP_DELETE_MRPACTION:
AND (MRPRUNID = @mrprunid)   -- already scoped by run ID; no change needed
-- TMRPRUNDETAILS is already filtered by MRPRUNID — scoped run creates its own rows
```

> **Review:** A scoped run creates a NEW TMRPRUNID. The existing UndoMRP cascade-delete
> already operates on MRPRUNID, so undo is naturally scoped. No special handling needed
> as long as scoped runs always create their own TMRPRUN row with the correct RUNSCOPE.

### Scope: Sales Order Level

Run MRP driven directly by a specific sales order. Demand comes from sales order lines,
not from MMASTERSCHEDULEDETAIL (bypasses the schedule loading step).

**Demand source:** Query sales order detail lines for the given SalesOrderId as the demand set.  
**MMASTERSCHEDULEDETAIL:** For sales-order scoped run, the demand load step is skipped — sales
order lines feed directly into the net requirement calculation.

```sql
-- GET_DEMAND_FROM_SALESORDER (new query in MRPRunQB.cs)
SELECT sod.ITEMID, sod.SKUID, sod.DELIVERYDATE AS PeriodDate,
       sod.PENDINGQUANTITY AS DailyQuantity,
       sod.ALLOCATIONID,
       @SalesOrderId AS SalesOrderId,
       ISNULL(sod.PRIORITY, 0) AS Priority
FROM   TSALESORDERDETAIL sod
JOIN   TSALESORDERHEADER soh ON soh.SALESORDERID = sod.SALESORDERID
WHERE  sod.SALESORDERID = @SalesOrderId AND soh.TENANTID = @TenantId
AND    sod.STATUS IN (1, 2)   -- open/partial
ORDER  BY ISNULL(sod.PRIORITY, 0) ASC, sod.DELIVERYDATE ASC;
```

> **Review:** This requires confirming the exact table and column names for the sales order module
> in GB5 (TSALESORDERHEADER / TSALESORDERDETAIL). If the sales order module is not yet in GB5,
> this scope type should be flagged as PENDING until that module is migrated. Do not implement
> a sales-order-scoped run against legacy WCF sales order data — it will create a cross-system
> dependency.

### Scope: FG Item Level

Run MRP for one or more specific finished goods items. Most useful for replanning a single
product after a demand change without running the full OU plan.

**Demand filter:** `MMASTERSCHEDULEDETAIL.ITEMID IN @FGItemIds`  
**BOM explosion:** Only explodes BOMs for the specified FG items (massive performance gain
for large item catalogues — avoids processing 10k items when only 3 need replanning).

> **Review:** The FGItem scope must also carry the planned orders (TMRPACTION) for the
> component items in the BOM — not just the FG item. A user running FGItem scope for
> Item A still needs POs/WOs generated for Item A's components B, C, D. The filter
> applies only to the TOP-LEVEL demand; BOM explosion proceeds fully for each filtered item.
> Clarify this in the UI so users understand what "FG Item level run" means.

### Endpoint

```
POST /MRPRun/TriggerScopedMRPRun
Body: MRPRunTriggerDTO with RunScope set + appropriate scope field populated

Validation in BLL (before queuing):
  Scope=Allocation → AllocationId required; verify AllocationId exists for this TenantId
  Scope=SalesOrder → SalesOrderId required; verify order is open
  Scope=FGItem     → FGItemIds required (1 to 50 items max per run); verify items exist
```

---

## Phase 3b — Priority System (NEW FEATURE)

Priority is an integer (1 = highest, 2, 3 … N = lower, 0 = unset treated as lowest).
It operates at two levels that cascade to form the effective priority of each demand line.

### Priority levels

| Level | Where stored | Effect |
|---|---|---|
| Allocation priority | MALLOCATION.PRIORITY | All demand lines under that allocation inherit this priority |
| Demand-line priority | MMASTERSCHEDULEDETAIL.PRIORITY | Overrides allocation priority if set explicitly |
| Effective priority | Computed: COALESCE(msd.PRIORITY, ma.PRIORITY, 0) | Used in sorting |

> **Review:** Avoid a dual-priority system that causes confusion about which takes precedence.
> Recommend: demand-line priority always wins; allocation priority is the fallback default
> that gets applied when SaveMasterScheduleDetail runs (copy allocation priority into detail row).
> This keeps the query simple — always read from MMASTERSCHEDULEDETAIL.PRIORITY; never
> need a runtime JOIN to MALLOCATION for priority resolution.

### Schema Changes

```sql
-- MALLOCATION: add priority if not present (verify legacy schema first)
ALTER TABLE MALLOCATION ADD PRIORITY SMALLINT NOT NULL DEFAULT 0;

-- MMASTERSCHEDULEDETAIL: already added in Phase 0
-- TMRPACTION: carry priority from demand
ALTER TABLE TMRPACTION ADD PRIORITY SMALLINT NOT NULL DEFAULT 0;
-- TPARTDETAIL: carry priority from MRPAction
ALTER TABLE TPARTDETAIL ADD PRIORITY SMALLINT NOT NULL DEFAULT 0;
```

### New Endpoints

```
POST /MRPRun/SetDemandPriority
  Body: { MasterScheduleId, List<{DetailId, Priority}> }
  → BLL validates priorities are positive integers
  → Batch UPDATE MMASTERSCHEDULEDETAIL SET PRIORITY = @Priority WHERE ...
  → Invalidate master schedule cache

POST /MRPRun/SetAllocationPriority
  Body: { AllocationId, Priority }
  → UPDATE MALLOCATION SET PRIORITY = @Priority WHERE ALLOCATIONID = @AllocationId
  → Optionally cascade: UPDATE MMASTERSCHEDULEDETAIL SET PRIORITY = @Priority
                        WHERE ALLOCATIONID = @AllocationId AND PRIORITY = 0 (only unset rows)

POST /MRPRun/GetDemandPriorityView          ← paged list; shows demand with effective priority
```

### New Files

```
MMDAL/DTO/MRPRun/SetDemandPriorityDTO.cs
MMDAL/DTO/MRPRun/SetAllocationPriorityDTO.cs
MMDAL/DTO/MRPRun/DemandPriorityViewDTO.cs
MMDAL/Query/MRPRun/MRPPriorityQB.cs        ← SET_DEMAND_PRIORITY, SET_ALLOCATION_PRIORITY, GET_PRIORITY_VIEW
MMDAL/Interfaces/MRPRun/IMRPPriorityDAL.cs
MMDAL/CustomCode/MRPRun/MRPPriorityDAL.cs
MMBLL/Interfaces/IMRPPriorityBLL.cs
MMBLL/MRPRun/MRPPriorityBLL.cs
MMSL/EndPoints/MRPRun/SetDemandPriority.cs
MMSL/EndPoints/MRPRun/SetAllocationPriority.cs
MMSL/EndPoints/MRPRun/GetDemandPriorityView.cs
```

### Priority in Workbench (Phase 4 impact)

`WorkBenchDTO` gains a `Priority` field. Workbench filter criteria gains:
- `FilterByPriority` (nullable int) — show only demand at or above this priority
- Workbench default sort: Priority ASC, Period ASC

---

## Phase 4 — MRP Workbench API

> **Review:** The workbench query (GET_MRPRUN_DETAILS_AND_ACTION_FULL from legacy) joins
> TMRPRUNDETAILS + TMRPACTION + several reference tables. For scoped runs, the workbench
> must accept a ScopeFilter (AllocationId or SalesOrderId) as an optional filter criterion so
> users can view just the scoped run's actions without scrolling through thousands of rows.
> Add `AllocationId`, `SalesOrderId`, `Priority` to WorkBenchCriteriaDTO.

### Files to Create / Extend

```
MMDAL/DTO/MRPRun/WorkBenchDTO.cs             ← add Priority field
MMDAL/DTO/MRPRun/WorkBenchCriteriaDTO.cs     ← add AllocationId, SalesOrderId, Priority filters
MMDAL/Query/MRPRun/MRPRunQB.cs
MMDAL/Interfaces/MRPRun/IMRPRunDAL.cs
MMDAL/CustomCode/MRPRun/MRPRunDAL.cs
MMBLL/MRPRun/MRPRunBLL.cs
MMSL/EndPoints/MRPRun/GetMRPWorkbench.cs
MMSL/EndPoints/MRPRun/GetMRPWorkbenchItemWise.cs
```

### WorkBenchDTO fields (from legacy GET_MRPRUN_DETAILS_AND_ACTION_FULL)

```
PlanItemId/Code/Name/ShortName, PlanSKUId/Name/Code
SlNo, Level, LowLevelCode, RequiredPeriod, LeadTime
TotalGrossRequirement, IndependentPlanDemand, IndependentCustomerOrder,
DependentPlanDemand, DependentCustomerOrder
Allocation, ScheduledReceipts, ProjectedSOH, SOH, WIP
NetRequirement, PlannedOrderReceipts, PlannedOrderReleases
MrpActionId, PlanStatus, PlannedQuantity, FirmQuantity, ImplementedQuantity,
OriginalPlannedQuantity, InProcessQuantity
PlannedStartDate, PlannedOrderDate, PlannedDueDate, FirmDate, IsFirmPlan
CompressionDays, NestingPlanId
ReleaseType, ReleaseBizTransactionTypeId, ReleaseId
PartyId/Name, PartyBranchId/Name
StoreId/Name, AlternateBOMId, AlternateRoutingId
Priority             ← NEW
AllocationId         ← NEW (for scoped run filtering)
SalesOrderId         ← NEW (for scoped run filtering)
RunScope             ← NEW (which scope generated this action)
```

CacheKeyLevel.NOT_REQUIRED (live transactional planning data)

---

## Phase 5 — MRP Action Management + Release

### 5a. MRP Action CRUD

> **Review:** `SaveMRPAction` allows the user to update PlanStatus, FirmQuantity, FirmDate,
> AlternateBOM, Remarks. Make sure SaveMRPAction does NOT allow changing Priority directly —
> priority is set via SetDemandPriority (Phase 3b) and flows into actions at run time.
> Post-run priority changes should trigger a re-run, not a manual action edit.

```
MMDAL/DTO/MRPRun/MRPActionDTO.cs              ← add Priority, AllocationId, SalesOrderId, RunScope
MMDAL/DTO/MRPRun/MRPActionDetailReportDTO.cs
MMDAL/DTO/MRPRun/MRPRunWithActionReportDTO.cs
MMDAL/DTO/MRPRun/ClearScheduleDTO.cs
MMDAL/Query/MRPRun/MRPActionQB.cs
MMDAL/Interfaces/MRPRun/IMRPActionDAL.cs
MMDAL/CustomCode/MRPRun/MRPActionDAL.cs
MMBLL/Interfaces/IMRPActionBLL.cs
MMBLL/MRPRun/MRPActionBLL.cs
MMSL/EndPoints/MRPRun/SaveMRPAction.cs
MMSL/EndPoints/MRPRun/GetMRPActionList.cs
MMSL/EndPoints/MRPRun/GetMRPRunWithActions.cs
```

### MRPAction POCO fields — corrected and complete

```
MrpActionId, MrpRunId, SlNo
PlanItemId, PlanSKUId, PlanType, ObjectId
PlannedStartDate, PlannedOrderDate, PlannedDueDate
PlannedQuantity, OriginalPlannedQuantity, InProcessQuantity, ImplementedQuantity
CompressionDays, IsFirmPlan, FirmDate, FirmQuantity
PlannedAction, Remarks
PlanStatus (byte): 0=UNRELEASED, 1=TOBERELEASE, 2=RELEASED, 3=ONHOLD
StoreId, AllocationId, TaskId
ReleaseType (byte): 0=None, 1=PO, 2=PR, 3=WorkIndent   ← 3 NOT 4
ReleaseBizTransactionTypeId, ReleaseId
PartyId, PartyBranchId
AlternateBOMId, AlternateRoutingId
MRPRunDetailId, NestingPlanId
IsAllocationBasedRelease (byte)
Priority        ← NEW — carried from demand PRIORITY at run time
AllocationId    ← NEW — from scope (Allocation-scoped run) or demand line
SalesOrderId    ← NEW — from scope (SalesOrder-scoped run) or demand line
RunScope        ← NEW — tinyint; which scope generated this action
```

### 5b. Release to Documents — Two Paths

> **Review:** Both release paths (direct and allocation-based) build documents from MRPAction data.
> The shared logic is: map MRPActionDTO → document DTO, call document BLL, update TMRPACTION,
> insert TPEGGING. Extract this into a private `BuildReleaseContext()` method in MRPActionBLL
> to avoid duplication. The two paths diverge only at: (a) whether TALLOCATION is created first,
> and (b) whether ALLOCATION2RESERVATION is triggered. Everything else is common.

```
POST /MRPRun/ReleaseMRPActions
Body: ReleaseMRPActionDTO (list of MrpActionIds, IsAllocationBasedRelease)
```

**Path A — Direct Release (IsAllocationBasedRelease = 0):**

```
For each RELEASED action:
  If ReleaseType IN (1=PO, 2=PR):
    → BuildReleaseContext() → MMHeadDTO
    → await _mmHeadBLL.SaveMMHead(mmHeadDTO, loginDTO, ct)
    → UpdateMRPActionRelease + InsertTPegging

  If ReleaseType = 3 (WorkIndent):
    → AssignIndentResourceDuringSave → IndentDTO with routing + dates
    → AssignResourceDate: offset dates by LeadTime per routing step
    → await _indentBLL.SaveWorkOrder([indentDTO], loginDTO, ct)
    → UpdateMRPActionRelease + InsertTPegging
```

**Path B — Allocation-Based Release (IsAllocationBasedRelease = 1):**

```
For each RELEASED action:
  → BuildReleaseContext() → AllocationDTO
  → await _allocationBLL.AllocationSave([allocationDTO], ...)
       AllocationSave calls:
         EXEC ALLOCATION2RESERVATION @ObjectHeaderTypeId, @ObjectHeaderId
         EXEC UPDATE_PREALLOTEDALLOCATIONID_IN_TPENDINGALLOCATION ...
  → After allocation saved:
    If ReleaseType = 3:
      → Build IndentDTO linked to AllocationId
      → await _indentBLL.SaveWorkOrder([indentDTO], ...)
      → UPDATE TALLOCATION.ALLOTEDALLOCATIONID = IndentId
  → UpdateMRPActionRelease + InsertTPegging
```

> **Review:** For priority-based release scenarios, users may want to release only actions
> with Priority=1 first (fill the highest priority orders) before releasing Priority=2.
> Add an optional `MaxPriorityToRelease` filter to `ReleaseMRPActionDTO` so the BLL can
> filter the action list before processing. This makes priority actionable at release time,
> not just at planning time.

### 5c. Document Pegging (TDOCUMENTPEGGING) — Separate Module

```
MMDAL/DTO/DocumentPegging/DocumentPeggingDTO.cs
MMDAL/Query/DocumentPegging/DocumentPeggingQB.cs
MMDAL/Interfaces/DocumentPegging/IDocumentPeggingDAL.cs
MMDAL/CustomCode/DocumentPegging/DocumentPeggingDAL.cs
MMBLL/Interfaces/IDocumentPeggingBLL.cs
MMBLL/DocumentPegging/DocumentPeggingBLL.cs
MMSL/EndPoints/DocumentPegging/GetDocumentPegging.cs
MMSL/EndPoints/DocumentPegging/SaveDocumentPegging.cs
```

TDOCUMENTPEGGING fields: DocumentPeggingId, OuId, TenantId, PeggedQuantity,
Generated side (7 fields), Source side (8 fields) — see v2 plan for full column list.

Key query: bidirectional match on either source or generated side (see v2 for SQL).

### 5d. Stock Reservation Module (TRESERVATION)

```
MMDAL/DTO/StockReservation/{5 DTO variants}
MMDAL/Query/StockReservation/StockReservationQB.cs
MMDAL/Interfaces/StockReservation/IStockReservationDAL.cs
MMDAL/CustomCode/StockReservation/StockReservationDAL.cs
MMBLL/Interfaces/IStockReservationBLL.cs
MMBLL/StockReservation/StockReservationBLL.cs
MMSL/EndPoints/StockReservation/{7 endpoints}
```

Auto-creation path: AllocationBLL → EXEC ALLOCATION2RESERVATION → TRESERVATION rows.  
Manual path: StockReservationBLL (SaveStockReservation, DeleteStockReservation, DeReservation).

### 5e. ClearOldSchedule

```
MMSL/EndPoints/MRPRun/ClearOldSchedule.cs   ← POST /MRPRun/ClearOldSchedule
MMDAL/DTO/MRPRun/ClearScheduleDTO.cs
```

MRPRunBLL.ClearingOldSchedule: Type=0 deletes MMASTERSCHEDULEDETAIL; Type=1 resets BALANCEQUANTITY.

---

## Phase 6 — MRP Reporting Endpoints

All reports: QueryPagedAsync; CacheKeyLevel.NOT_REQUIRED.

> **Review:** Reports that show shortage data (GetShortageReport, GetMasterScheduleShortageReport,
> GetPartShortage) should all surface the Priority field so operations staff can triage
> which shortages to address first. Make Priority a sortable/filterable column in all
> shortage-type reports.

### Endpoints

```
POST /MRPRun/GetShortageReport                ← add Priority column + filter
POST /MRPRun/GetStageWiseShortageReport       ← add Priority column
POST /MRPRun/GetMRPWorksheet
POST /MRPRun/GetPurchaseRequirement           ← optionally group by Priority
POST /MRPRun/GetScheduleReceipt
POST /MRPRun/GetMRPRunWithActions
POST /MRPRun/GetMRPWithDimensionReport        ← bridges MRP + Part Level
POST /MRPRun/GetScopedRunComparison           ← NEW: compare scoped run vs macro run
```

**GetScopedRunComparison (NEW):**  
Compare planned quantities and actions between a scoped run and the last macro run for
the same MRP config. Shows the delta — what changed when the scoped replan ran.

```sql
-- Compares two TMRPRUN records side-by-side by item
SELECT a.PLANITEMID, a.PLANNEDQUANTITY AS MacroQty, b.PLANNEDQUANTITY AS ScopedQty,
       (b.PLANNEDQUANTITY - a.PLANNEDQUANTITY) AS Delta
FROM   TMRPACTION a
JOIN   TMRPACTION b ON b.PLANITEMID = a.PLANITEMID AND b.MRPRUNID = @ScopedRunId
WHERE  a.MRPRUNID = @MacroRunId AND a.TENANTID = @TenantId;
```

### Files to Create

```
MMDAL/DTO/MRPRun/MrpRunShortageReportDTO.cs      ← add Priority field
MMDAL/DTO/MRPRun/PurchaseRequirementCalcReportDTO.cs
MMDAL/DTO/MRPRun/MRPRunDetailReportDTO.cs
MMDAL/DTO/MRPRun/MRPScheduleReceiptDTO.cs
MMDAL/DTO/MRPRun/MRPWithDimensionDTO.cs
MMDAL/DTO/MRPRun/ScopedRunComparisonDTO.cs        ← NEW
MMDAL/Query/MRPRun/MRPReportQB.cs
MMDAL/Interfaces/MRPRun/IMRPReportDAL.cs
MMDAL/CustomCode/MRPRun/MRPReportDAL.cs
MMBLL/Interfaces/IMRPReportBLL.cs
MMBLL/MRPRun/MRPReportBLL.cs
MMSL/EndPoints/MRPRun/{8 endpoints}
```

---

## Phase 7 — Part Level Planning (New Feature)

### 7a. Database Migration

```sql
-- MPARTDIMENSION: dimension master (GetOrCreate pattern)
CREATE TABLE MPARTDIMENSION (
    PARTDIMID  INT NOT NULL PRIMARY KEY, D1..D5 NVARCHAR(100) NULL,
    TENANTID   INT NOT NULL, ROWVERSION ROWVERSION NOT NULL,
    CONSTRAINT UQ_PARTDIM UNIQUE (D1, D2, D3, D4, D5, TENANTID)
);

-- TPARTDETAIL: requirement + execution unit + lineage
CREATE TABLE TPARTDETAIL (
    PARTDETAILID        INT           NOT NULL PRIMARY KEY,
    ENTITYTYPEID        INT           NOT NULL,  -- 1=MRPAction,2=Indent,3=PR,4=PO,5=WO
    ENTITYID            INT           NOT NULL,
    ENTITYDETAILID      INT           NOT NULL,
    PARENTPARTDETAILID  INT           NULL,
    ROOTPARTDETAILID    INT           NULL,
    ITEMID              INT           NOT NULL,
    SKUID               INT           NULL,
    PARTDIMID           INT           NULL,        -- NULL = non-dimension item
    REQUIREDQTY         DECIMAL(18,4) NOT NULL DEFAULT 0,
    REQUIREDNUMBERS     INT           NOT NULL DEFAULT 0,
    ALLOCATIONID        INT           NULL,
    PEGGINGGROUPID      INT           NULL,
    SALESORDERID        INT           NULL,        -- NEW: traceability to source order
    PRIORITY            SMALLINT      NOT NULL DEFAULT 0,  -- NEW: from MRPAction
    PLANNEDSTARTDATE    DATE          NULL,
    PLANNEDDUEDATE      DATE          NULL,
    WIPSTOCK            DECIMAL(18,4) NOT NULL DEFAULT 0,
    AVAILABLETOWORK     DECIMAL(18,4) NOT NULL DEFAULT 0,
    STATUS              SMALLINT      NOT NULL DEFAULT 0,
    TENANTID            INT           NOT NULL,
    ROWVERSION          ROWVERSION    NOT NULL,
    CREATEDBYID         INT NOT NULL, CREATEDON DATETIME2 NOT NULL DEFAULT GETUTCDATE(),
    MODIFIEDBYID        INT NOT NULL, MODIFIEDON DATETIME2 NOT NULL DEFAULT GETUTCDATE()
);
CREATE INDEX IX_TPARTDETAIL_ENTITY   ON TPARTDETAIL (ENTITYTYPEID, ENTITYID, TENANTID);
CREATE INDEX IX_TPARTDETAIL_PARENT   ON TPARTDETAIL (PARENTPARTDETAILID, TENANTID);
CREATE INDEX IX_TPARTDETAIL_ROOT     ON TPARTDETAIL (ROOTPARTDETAILID, TENANTID);
CREATE INDEX IX_TPARTDETAIL_ITEM     ON TPARTDETAIL (ITEMID, TENANTID, STATUS);
CREATE INDEX IX_TPARTDETAIL_PRIORITY ON TPARTDETAIL (PRIORITY, STATUS, TENANTID);  -- NEW: for priority-based allocation

-- TPARTALLOCATION: part detail → lot mapping
CREATE TABLE TPARTALLOCATION (
    PARTALLOCATIONID INT NOT NULL PRIMARY KEY, PARTDETAILID INT NOT NULL,
    LOTID INT NULL, LOTDETAILID INT NULL,
    ALLOCATEDQTY DECIMAL(18,4) NOT NULL DEFAULT 0,
    ISSUEDQTY    DECIMAL(18,4) NOT NULL DEFAULT 0,
    CONSUMEDQTY  DECIMAL(18,4) NOT NULL DEFAULT 0,
    STATUS       SMALLINT      NOT NULL DEFAULT 0,  -- 0=Reserved,1=Issued,2=Consumed,3=Released
    TENANTID     INT           NOT NULL,
    ROWVERSION   ROWVERSION    NOT NULL,
    CREATEDBYID  INT NOT NULL, CREATEDON DATETIME2 NOT NULL DEFAULT GETUTCDATE(),
    MODIFIEDBYID INT NOT NULL, MODIFIEDON DATETIME2 NOT NULL DEFAULT GETUTCDATE()
);
CREATE INDEX IX_TPARTALLOC_DETAIL ON TPARTALLOCATION (PARTDETAILID, TENANTID);
CREATE INDEX IX_TPARTALLOC_LOT    ON TPARTALLOCATION (LOTID, TENANTID, STATUS);

-- TPARTPOSITION: current stock state snapshot
CREATE TABLE TPARTPOSITION (
    PARTPOSITIONID INT NOT NULL PRIMARY KEY,
    ITEMID INT NOT NULL, PARTDIMID INT NULL, STOREID INT NOT NULL, LOTID INT NULL,
    AVAILABLE      DECIMAL(18,4) NOT NULL DEFAULT 0,
    RESERVED       DECIMAL(18,4) NOT NULL DEFAULT 0,
    ISSUED         DECIMAL(18,4) NOT NULL DEFAULT 0,
    TENANTID       INT           NOT NULL,
    ROWVERSION     ROWVERSION    NOT NULL
);
CREATE UNIQUE INDEX IX_TPARTPOS_KEY ON TPARTPOSITION (ITEMID, PARTDIMID, STOREID, LOTID, TENANTID);

-- TPARTSUMMARY: optional aggregated reporting (async update)
CREATE TABLE TPARTSUMMARY (
    PARTSUMMARYID INT NOT NULL PRIMARY KEY, ITEMID INT NOT NULL, OUID INT NOT NULL,
    TOTALREQUIRED DECIMAL(18,4) NOT NULL DEFAULT 0,
    TOTALAVAILABLE DECIMAL(18,4) NOT NULL DEFAULT 0,
    TOTALSHORTAGE DECIMAL(18,4) NOT NULL DEFAULT 0,
    ASOFDATE DATE NOT NULL, TENANTID INT NOT NULL
);
```

### 7b. Part Dimension CRUD (GetOrCreate pattern)

```
MMDAL/DTO/PartLevel/PartDimensionDTO.cs
MMDAL/Query/PartLevel/PartDimensionQB.cs
MMDAL/Interfaces/PartLevel/IPartDimensionDAL.cs
MMDAL/CustomCode/PartLevel/PartDimensionDAL.cs
MMBLL/Interfaces/IPartDimensionBLL.cs
MMBLL/PartLevel/PartDimensionBLL.cs
MMSL/EndPoints/PartLevel/GetPartDimension.cs
MMSL/EndPoints/PartLevel/SavePartDimension.cs
MMSL/EndPoints/PartLevel/GetSelectListPartDimension.cs
```

### 7c. Part Detail + WIP Sync

> **Review:** TPARTDETAIL.PRIORITY is inherited from TMRPACTION at creation time (Phase 3 Step 13).
> If a user later changes demand priority (via SetDemandPriority) and re-runs MRP, the
> new MRP run creates new TMRPACTION rows with the updated priority, which creates new
> TPARTDETAIL rows. Old TPARTDETAIL rows from the previous run are NOT automatically updated.
> This is correct behaviour — old part details are historical. Document this clearly in
> the UI: "Priority on open part details reflects the priority at the time the MRP was run."

```
MMDAL/DTO/PartLevel/PartDetailDTO.cs          ← add Priority, SalesOrderId fields
MMDAL/DTO/PartLevel/PartDetailListDTO.cs
MMDAL/DTO/PartLevel/PartDetailCriteriaDTO.cs  ← add Priority, AllocationId, SalesOrderId filters
MMDAL/Query/PartLevel/PartDetailQB.cs
MMDAL/Interfaces/PartLevel/IPartDetailDAL.cs
MMDAL/CustomCode/PartLevel/PartDetailDAL.cs
MMBLL/Interfaces/IPartDetailBLL.cs
MMBLL/PartLevel/PartDetailBLL.cs
MMSL/EndPoints/PartLevel/GetPartDetail.cs
MMSL/EndPoints/PartLevel/GetPartDetailLineage.cs
MMSL/EndPoints/PartLevel/GetPartDetailList.cs
MMSL/EndPoints/PartLevel/UpdatePartDetailStatus.cs
MMSL/EndPoints/PartLevel/SyncPartDetailWIP.cs
```

### 7d. Part Allocation — Priority-Aware Engine

> **Review:** The priority-based allocation is the most critical behaviour in the Part Level
> system. When stock is limited and multiple TPARTDETAIL records compete for the same lot,
> the engine MUST serve Priority=1 first. If Priority=1 demand is fully satisfied and stock
> remains, Priority=2 is served, and so on. If Priority=1 demand cannot be fully satisfied,
> it is partially allocated — do NOT skip to Priority=2 to fill the lot. Partial allocation
> of higher priority is correct; full allocation of lower priority at the expense of
> higher priority is wrong.

```
MMDAL/DTO/PartLevel/PartAllocationDTO.cs
MMDAL/Query/PartLevel/PartAllocationQB.cs
MMDAL/Interfaces/PartLevel/IPartAllocationDAL.cs
MMDAL/CustomCode/PartLevel/PartAllocationDAL.cs
MMBLL/Interfaces/IPartAllocationBLL.cs
MMBLL/PartLevel/PartAllocationBLL.cs
MMSL/EndPoints/PartLevel/AllocateParts.cs
MMSL/EndPoints/PartLevel/IssueParts.cs
MMSL/EndPoints/PartLevel/ConsumeParts.cs
MMSL/EndPoints/PartLevel/ReleaseParts.cs
MMSL/EndPoints/PartLevel/GetPartAllocationList.cs
MMSL/EndPoints/PartLevel/AutoAllocateByPriority.cs  ← NEW: batch allocate against all open demands sorted by priority
```

**AutoAllocateByPriority (NEW):**  
Batch allocation engine — scans all open TPARTDETAIL records, sorts by PRIORITY ASC,
and automatically allocates available TPARTPOSITION stock to highest-priority demands first.

```csharp
// PartAllocationBLL.AutoAllocateByPriorityAsync(AllocationScopeDTO, loginDTO, ct)
// 1. Load open TPARTDETAIL (STATUS=0) ordered by PRIORITY ASC, PLANNEDDUEDATE ASC
//    — optionally filter by AllocationId or FGItemIds (matches scoped MRP run)
// 2. For each part detail:
//    a. Query TPARTPOSITION for available stock (ITEMID + PARTDIMID match)
//    b. Allocate MIN(RequiredQty - already allocated, Available) qty
//    c. INSERT TPARTALLOCATION (STATUS=0 Reserved)
//    d. UPDATE TPARTPOSITION (ROWVERSION optimistic lock; retry x3)
//    e. If fully allocated → TPARTDETAIL.STATUS = 1 (InProgress)
// 3. Return AllocationResultDTO (totalAllocated, shortages, partiallyAllocated)
```

```sql
-- GET_OPEN_PARTDETAILS_PRIORITIZED (for auto-allocate engine)
SELECT pd.PARTDETAILID, pd.ITEMID, pd.SKUID, pd.PARTDIMID,
       pd.REQUIREDQTY, pd.PRIORITY, pd.ALLOCATIONID, pd.SALESORDERID,
       ISNULL(SUM(pa.ALLOCATEDQTY), 0) AS AlreadyAllocated,
       pd.REQUIREDQTY - ISNULL(SUM(pa.ALLOCATEDQTY), 0) AS RemainingQty
FROM   TPARTDETAIL pd
LEFT   JOIN TPARTALLOCATION pa ON pa.PARTDETAILID = pd.PARTDETAILID
                               AND pa.STATUS IN (0, 1)  -- Reserved or Issued
WHERE  pd.STATUS     IN (0, 1)   -- Open or InProgress
AND    pd.TENANTID   = @TenantId
GROUP  BY pd.PARTDETAILID, pd.ITEMID, pd.SKUID, pd.PARTDIMID,
          pd.REQUIREDQTY, pd.PRIORITY, pd.ALLOCATIONID, pd.SALESORDERID
HAVING pd.REQUIREDQTY - ISNULL(SUM(pa.ALLOCATEDQTY), 0) > 0
ORDER  BY CASE WHEN pd.PRIORITY = 0 THEN 999 ELSE pd.PRIORITY END ASC,
          pd.PLANNEDDUEDATE ASC;
-- Index: IX_TPARTDETAIL_PRIORITY already defined above
```

**Allocation lifecycle (sync):**

```
AllocateParts (manual or auto):
  → Validate TPARTPOSITION.AVAILABLE >= requested qty
  → INSERT TPARTALLOCATION STATUS=0 Reserved
  → UPDATE TPARTPOSITION: AVAILABLE -= qty, RESERVED += qty (ROWVERSION lock; retry x3)
  → If fully allocated: TPARTDETAIL.STATUS = 1 InProgress

IssueParts: TPARTALLOCATION.ISSUEDQTY += qty, STATUS=1; TPARTPOSITION: RESERVED -= qty, ISSUED += qty
ConsumeParts: TPARTALLOCATION.CONSUMEDQTY += qty, STATUS=2; TPARTPOSITION: ISSUED -= qty
              If fully consumed: TPARTDETAIL.STATUS = 2 Complete
ReleaseParts: TPARTALLOCATION.STATUS=3; TPARTPOSITION: RESERVED -= remainingQty, AVAILABLE += qty
              If no remaining allocations: TPARTDETAIL.STATUS = 0 Open
```

### 7e. Part Position Read API

```
MMSL/EndPoints/PartLevel/GetPartPosition.cs
MMSL/EndPoints/PartLevel/GetPartPositionList.cs
MMSL/EndPoints/PartLevel/GetPartShortage.cs         ← add Priority sort/filter
MMSL/EndPoints/PartLevel/InitialisePartPosition.cs
```

**GetPartShortage (updated):** Returns items sorted by Priority ASC so highest-priority
shortages appear first. Accepts optional `MaxPriority` filter to show only critical shortages.

### 7f. Part Level Workbench

```
MMSL/EndPoints/PartLevel/GetPartLevelWorkbench.cs   ← add Priority, AllocationId, SalesOrderId columns
MMSL/EndPoints/PartLevel/GetPartLevelWIPSummary.cs
MMSL/EndPoints/PartLevel/GetAllocationPriorityView.cs ← NEW: shows all allocations with their priority + open part details
```

> **Review:** `GetAllocationPriorityView` is a high-value operational screen: it shows all
> project allocations ranked by priority alongside their open part requirements and available
> stock. This is the key screen for production planners to answer "which project can we make
> next with available material?" Build this as a single efficient query — avoid multiple
> round-trips from the client.

---

## Phase 8 — Constants & Registration

### Constants to Add (GB5Shared/GB5Constant/Constant.cs)

```csharp
// EntityConstant — verify existing OBJECTMRP, OBJECTMRPRUN; add new
public static int OBJECTMRPACTION        = /* new */;
public static int OBJECTMASTERSCHEDULE   = /* new */;
public static int OBJECTPARTDIMENSION    = /* new */;
public static int OBJECTPARTDETAIL       = /* new */;
public static int OBJECTPARTALLOCATION   = /* new */;
public static int OBJECTDOCUMENTPEGGING  = /* new */;
public static int OBJECTSTOCKRESERVATION = /* new */;

// AUTONUMBERCONSTANT
public static int MRPRUN             = /* new */;
public static int MRPACTION          = /* new */;
public static int MASTERSCHEDULE     = /* check existing */;
public static int PARTDIMENSION      = /* new */;
public static int PARTDETAIL         = /* new */;
public static int PARTALLOCATION     = /* new */;

// EventTypeConstant
public static int SAVEMASTERSCHEDULEEVENTTYPEID     = /* new */;
public static int SAVEMRPRUNEVENTTYPEID             = /* new */;
public static int SAVEMRPACTIONEVENTTYPEID          = /* new */;
public static int SETDEMANDPRIORITYEVENTTYPEID      = /* new */;
public static int SETALLOCATIONPRIORITYEVENTTYPEID  = /* new */;
public static int SAVEPARTDETAILEVENTTYPEID         = /* new */;
public static int SAVEPARTALLOCATIONEVENTTYPEID     = /* new */;
public static int SAVEDOCUMENTPEGGINGEVENTTYPEID    = /* new */;
```

### SignalR + Dapr Registration

```csharp
builder.Services.AddSignalR(options =>
{
    options.EnableDetailedErrors = builder.Environment.IsDevelopment();
    options.MaximumReceiveMessageSize = 32 * 1024;
    options.ClientTimeoutInterval = TimeSpan.FromSeconds(60);
    options.KeepAliveInterval     = TimeSpan.FromSeconds(15);
});
app.MapHub<MRPRunHub>("/hubs/mrprun");

app.MapPost("/mrprun/trigger",
    [Topic("pubsub", "mrprun.trigger")]
    async (MRPRunTriggerDTO dto, MRPRunProcessingService svc)
        => await svc.ProcessAsync(dto));
```

---

## Implementation Order (Updated)

| # | Phase | Key deliverable | Depends on |
|---|---|---|---|
| 0 | MasterSchedule | MMASTERSCHEDULE CRUD + Load + PRIORITY column | — |
| 1 | MRP Master | MMRP CRUD endpoints | — |
| 2 | MRP Run header | TMRPRUN + RUNSCOPE column | Ph 0,1 |
| 3 | MRP Run Process | Async + SignalR + BOM explosion + priority-aware pegging | Ph 0,1,2 |
| 3a | Scoped MRP Run | DemandFilterSpec; Allocation/SalesOrder/FGItem trigger endpoints | Ph 3 |
| 3b | Priority System | SetDemandPriority, SetAllocationPriority, priority view | Ph 0,1 |
| 4 | MRP Workbench | Planning grid with Priority + scope filter columns | Ph 3 |
| 5a | MRP Action CRUD | Action list with Priority; SaveMRPAction | Ph 3 |
| 5b | Release to documents | Direct + allocation-based; priority release filter | Ph 5a |
| 5c | Document Pegging | TDOCUMENTPEGGING module | Ph 5b |
| 5d | Stock Reservation | TRESERVATION module | Ph 5b |
| 5e | ClearOldSchedule | Schedule housekeeping | Ph 0 |
| 6 | MRP Reports | 8 reports inc. Priority sort + ScopedRunComparison | Ph 3 |
| 7a | DB Migration | 4 new tables + PRIORITY + SALESORDERID columns + indexes | — |
| 7b | Part Dimension | GetOrCreate | Ph 7a |
| 7c | Part Detail | CRUD + lineage + Priority + WIP sync | Ph 3, 7b |
| 7d | Part Allocation | Manual + AutoAllocateByPriority engine | Ph 7c |
| 7e | Part Position | Availability + shortage (priority-sorted) + Initialise | Ph 7a |
| 7f | Part Workbench | AllocationPriorityView + WIP summary | Ph 7c,d,e |
| 8 | Constants + registration | SignalR, Dapr, auto-number, event type constants | All |

---

## Verification

1. **MasterSchedule PRIORITY:** SaveMasterScheduleDetail with Priority=2 → GET returns Priority=2; SetDemandPriority changes it to 1.
2. **MRP trigger (macro):** POST TriggerMRPRun → TMRPRUNSTATUS STATUS=0; SignalR ReceiveComplete with ActionsCreated > 0.
3. **Priority pegging:** Two demand lines for same item — Priority=1 qty=100, Priority=2 qty=50; planned order qty=120. After run, TPEGGING shows Priority=1 demand pegged 100 and Priority=2 pegged 20.
4. **BOM explosion depth:** TMRPRUNDETAILS has component rows at level > 0 (multi-level BOM).
5. **Scheduled receipts reduce NetReq:** Open PO for an item → after MRP run, that item's NetRequirement is lower by PO quantity.
6. **Combined run:** TriggerCombinedMRPRun → MPS run (Type=0) appears first in TMRPRUN, then MRP (Type=1) referencing it.
7. **Scoped run — Allocation:** TriggerScopedMRPRun (RunScope=1, AllocationId=X) → TMRPACTION rows have ALLOCATIONID=X; no TMRPACTION rows for items not in that allocation's demand.
8. **Scoped run — FGItem:** TriggerScopedMRPRun (RunScope=3, FGItemIds=[A,B]) → TMRPRUNDETAILS has rows only for items in A's and B's BOM; no other items.
9. **Scoped undo:** UndoMRP on scoped run deletes only that run's TMRPACTION/TMRPRUNDETAILS; sibling runs unaffected.
10. **Priority release:** ReleaseMRPActions with MaxPriorityToRelease=1 → only Priority=1 actions are released; Priority=2 actions remain UNRELEASED.
11. **Direct release to PO:** ReleaseMRPActions (IsAllocationBasedRelease=0, ReleaseType=1) → MMHead created; TMRPACTION.RELEASEID updated; TPEGGING row inserted.
12. **Allocation-based release:** IsAllocationBasedRelease=1, ReleaseType=3 → TALLOCATION created; ALLOCATION2RESERVATION fires; TRESERVATION row exists; Indent created.
13. **DocumentPegging chain:** Release creates Indent → PR → TDOCUMENTPEGGING has Indent→PR link.
14. **Part Dimension GetOrCreate:** Two AllocateParts calls with same D1-D5 values → same PARTDIMID returned; only one MPARTDIMENSION row created.
15. **Priority-based auto-allocate:** AutoAllocateByPriority with limited stock (covers Priority=1 fully, Priority=2 partially) → Priority=1 TPARTDETAIL fully allocated; Priority=2 partially; Priority=3 untouched.
16. **Concurrency:** Two concurrent AllocateParts for same lot → ROWVERSION guard triggers retry; eventual consistency; no double-allocation.
17. **WIP sync:** IndentDetailPost fires → SyncPartDetailWIP updates TPARTDETAIL.WIPSTOCK and AVAILABLETOWORK.
18. **AllocationPriorityView:** GET returns all allocations ranked by priority with their TPARTDETAIL open counts and TPARTPOSITION available qty — single query, no N+1.
19. **Tenant isolation:** All queries include TENANTID; two different logins confirm zero cross-tenant leakage.
20. **Performance:** Macro MRP run for 1,000 items < 60s; TMRPRUNDETAILS insert uses BulkInsertAsync (single batch confirmed by query profiler); AutoAllocateByPriority for 10,000 open TPARTDETAIL rows < 30s.

---

## Appendix A — Implementation Scope (What Is Built)

This implementation is NOT limited to the MRP run trigger. Every phase from MRP Master CRUD through
Part Level workbench has been built. The table below is the authoritative inventory.

### A1. MRP Configuration (MMRP) — 5 endpoints

| Route | Verb | Description |
|---|---|---|
| `GET  /MRP/GetMRP?MRPId=x` | GET | Single MRP config record |
| `GET  /MRP/GetMRPList?PageOffset=0&PageSize=50` | GET | Paged list of MRP configs |
| `GET  /MRP/GetSelectListMRP` | GET | Dropdown picklist |
| `POST /MRP/SaveMRP` | POST | Create or update MRP config (body: `MRPDTO`) |
| `DELETE /MRP/DeleteMRP?MRPId=x` | DELETE | Soft-delete MRP config |

### A2. Master Schedule — 8 endpoints

| Route | Verb | Description |
|---|---|---|
| `GET  /MasterSchedule/GetMasterSchedule?MasterScheduleId=x` | GET | Single schedule with detail lines |
| `GET  /MasterSchedule/GetMasterScheduleList?PageOffset=0&PageSize=50` | GET | Paged list |
| `GET  /MasterSchedule/GetSelectListMasterSchedule` | GET | Dropdown picklist |
| `POST /MasterSchedule/SaveMasterSchedule` | POST | Create/update schedule (body: `MasterScheduleDTO`) |
| `DELETE /MasterSchedule/DeleteMasterSchedule?MasterScheduleId=x` | DELETE | Delete |
| `POST /MasterSchedule/LoadMasterSchedule?MasterScheduleId=x&SourceType=0` | POST | Load demand from source data |
| `POST /MasterSchedule/SetDemandPriority` | POST | Set priority on detail lines (body: `SetDemandPriorityDTO`) |
| `POST /MasterSchedule/SetAllocationPriority` | POST | Set priority on allocation (body: `SetAllocationPriorityDTO`) |

### A3. MRP Run — 11 endpoints (trigger + status + workbench + actions + release + undo)

| Route | Verb | Description |
|---|---|---|
| `GET  /MRPRun/GetMRPRun?MRPRunId=x` | GET | Single run header |
| `GET  /MRPRun/GetMRPRunList?PageOffset=0&PageSize=50` | GET | Paged list of runs |
| `POST /MRPRun/SaveMRPRun` | POST | Create/update run header (body: `MRPRunDTO`) |
| `POST /MRPRun/TriggerMRPRun` | POST | Fire-and-forget macro run (body: `MRPRunTriggerDTO`) |
| `POST /MRPRun/TriggerCombinedMRPRun` | POST | Fire-and-forget MPS→MRP sequential (body: `MRPRunTriggerDTO`) |
| `POST /MRPRun/TriggerScopedMRPRun` | POST | Fire-and-forget scoped run (body: `MRPRunTriggerDTO` with RunScope set) |
| `GET  /MRPRun/GetMRPRunStatus?MrpRunId=x` | GET | Poll run status (PENDING/RUNNING/COMPLETED/FAILED) |
| `POST /MRPRun/UndoMRP` | POST | Cascade delete run data (body: `UndoMRPDTO`) |
| `POST /MRPRun/ClearOldSchedule` | POST | Clear/reset master schedule demand (body: `ClearScheduleDTO`) |
| `GET  /MRPRun/GetSelectListMRP` | GET | Dropdown picklist for completed runs |
| `POST /MRPRun/GetMRPRunWithActions` | POST | Report: run summary + all actions (body: `MRPReportCriteriaDTO`) |

### A4. MRP Workbench — 2 endpoints

| Route | Verb | Description |
|---|---|---|
| `POST /MRPRun/GetMRPWorkbench` | POST | Paged planning grid (body: `WorkBenchCriteriaDTO`) |
| `GET  /MRPRun/GetMRPWorkbenchItemWise?MrpRunId=x` | GET | Item-level summary for a run |

### A5. MRP Actions — 3 endpoints

| Route | Verb | Description |
|---|---|---|
| `GET  /MRPRun/GetMRPAction?MrpActionId=x` | GET | Single action record |
| `GET  /MRPRun/GetMRPActionList?MrpRunId=x` | GET | All actions for a run |
| `POST /MRPRun/SaveMRPAction` | POST | Update plan status / firm plan / remarks (body: `MRPActionDTO`) |

### A6. MRP Release — 1 endpoint

| Route | Verb | Description |
|---|---|---|
| `POST /MRPRun/ReleaseMRPActions` | POST | Release actions to PO/PR/WorkIndent, direct or allocation-based (body: `ReleaseMRPActionDTO`) |

### A7. MRP Reports — 8 endpoints

| Route | Verb | Description |
|---|---|---|
| `POST /MRPRun/GetShortageReport` | POST | Shortage by item with Priority column (body: `MRPReportCriteriaDTO`) |
| `POST /MRPRun/GetStageWiseShortageReport` | POST | Shortage by BOM level + Priority (body: `MRPReportCriteriaDTO`) |
| `POST /MRPRun/GetMRPWorksheet` | POST | Net requirement calculation worksheet (body: `MRPReportCriteriaDTO`) |
| `POST /MRPRun/GetPurchaseRequirement` | POST | Purchase requirement summary (body: `MRPReportCriteriaDTO`) |
| `POST /MRPRun/GetScheduleReceipt` | POST | Scheduled receipts view (body: `MRPReportCriteriaDTO`) |
| `POST /MRPRun/GetMRPWithDimensionReport` | POST | MRP + part dimensions bridge (body: `MRPReportCriteriaDTO`) |
| `POST /MRPRun/GetScopedRunComparison` | POST | Delta: macro run vs scoped run (body: `ScopedRunComparisonCriteriaDTO`) |

### A8. Part Dimension — 3 endpoints

| Route | Verb | Description |
|---|---|---|
| `GET  /PartLevel/GetPartDimension?PartDimId=x` | GET | Single dimension record |
| `GET  /PartLevel/GetSelectListPartDimension` | GET | All dimensions picklist |
| `POST /PartLevel/SavePartDimension` | POST | Create dimension (body: `PartDimensionDTO`) |

### A9. Part Detail — 5 endpoints

| Route | Verb | Description |
|---|---|---|
| `GET  /PartLevel/GetPartDetail?PartDetailId=x` | GET | Single part requirement record |
| `GET  /PartLevel/GetPartDetailLineage?RootPartDetailId=x` | GET | Full parent→child chain |
| `POST /PartLevel/GetPartDetailList` | POST | Paged list with filters (body: `PartDetailCriteriaDTO`) |
| `POST /PartLevel/UpdatePartDetailStatus` | POST | Change status (body: `UpdatePartDetailStatusDTO`) |
| `POST /PartLevel/SyncPartDetailWIP?PartDetailId=x&WipStock=y&AvailableToWork=z` | POST | Sync WIP quantities from production |

### A10. Part Allocation — 6 endpoints

| Route | Verb | Description |
|---|---|---|
| `GET  /PartLevel/GetPartAllocationList?PartDetailId=x` | GET | All allocations for a part detail |
| `POST /PartLevel/AllocateParts` | POST | Reserve stock for a part detail (body: `AllocatePartsDTO`) |
| `POST /PartLevel/IssueParts` | POST | Issue reserved stock to production (body: `IssuePartsDTO`) |
| `POST /PartLevel/ConsumeParts` | POST | Record consumption (body: `ConsumePartsDTO`) |
| `POST /PartLevel/ReleaseParts` | POST | Cancel/release reservation (body: `ReleasePartsDTO`) |
| `POST /PartLevel/AutoAllocateByPriority` | POST | Batch priority-aware allocation (body: `AllocationScopeDTO`) |

### A11. Part Position — 4 endpoints

| Route | Verb | Description |
|---|---|---|
| `GET  /PartLevel/GetPartPosition?PartPositionId=x` | GET | Single stock position |
| `GET  /PartLevel/GetPartPositionList?StoreId=x&PageOffset=0&PageSize=50` | GET | Paged list by store |
| `GET  /PartLevel/GetPartShortage?MaxPriority=1` | GET | Items with shortage, Priority-sorted (MaxPriority optional) |
| `POST /PartLevel/InitialisePartPosition` | POST | Seed TPARTPOSITION from TSTOCKLEDGER (body: `InitialisePartPositionDTO`) |

### A12. Part Level Workbench — 3 endpoints

| Route | Verb | Description |
|---|---|---|
| `POST /PartLevel/GetPartLevelWorkbench` | POST | Full planning grid (body: `PartLevelWorkbenchCriteriaDTO`) |
| `GET  /PartLevel/GetPartLevelWIPSummary?AllocationId=x` | GET | WIP aggregation by item (AllocationId optional) |
| `GET  /PartLevel/GetAllocationPriorityView` | GET | All allocations ranked by priority + open demand + available stock |

### A13. SignalR — Real-Time Progress

```
Hub:  /hubs/mrprun
Group: mrprun:ou:{workOUId}
Events pushed by server:
  ReceiveProgress(int percent, string step, string currentItem)
  ReceiveComplete(int mrpRunId, MRPRunSummaryDTO summary)
  ReceiveError(string message)
```

---

## Appendix B — Frontend API Reference

All requests require the `Login` header (base64-encoded `LoginDTO` from auth service).
All responses are wrapped in `ResponseStandardDTO<T>`.

### B1. Authentication header format

```
Login: <base64-encoded LoginDTO JSON>
Content-Type: application/json
```

### B2. Priority values (used across all modules)

```
0 = Unset (treated as lowest priority in all queries)
1 = Highest priority
2, 3, ... N = Lower priority
```

### B3. Enumerations

**MrpRunScope (MRPRunTriggerDTO.RunScope)**
```
0 = Macro (full OU plan)
1 = Allocation (single project/allocation)
2 = SalesOrder (single sales order)
3 = FGItem (one or more finished goods items)
```

**TypeofRun (MRPRunTriggerDTO.TypeofRun)**
```
0 = MPS (Master Production Schedule)
1 = MRP (uses MPS output as input; run after MPS)
2 = MinMax (min/max reorder — no BOM explosion)
3 = ROL (reorder level — no BOM explosion)
```

**PlanStatus (MRPActionDTO.MRPActionPlanStatus)**
```
0 = UNRELEASED
1 = TO BE RELEASED
2 = RELEASED
3 = ON HOLD
```

**ReleaseType (MRPActionDTO.MRPActionReleaseType)**
```
0 = None
1 = Purchase Order
2 = Purchase Requisition
3 = Work Indent (production order)
```

**PartDetail.Status**
```
0 = Open
1 = InProgress (partially or fully allocated)
2 = Complete (fully consumed)
3 = OnHold
```

**PartAllocation.Status**
```
0 = Reserved
1 = Issued
2 = Consumed
3 = Released
```

**MRPRunStatus (TMRPRUNSTATUS)**
```
0 = PENDING / RUNNING
1 = COMPLETED
2 = FAILED
```

---

### B4. Typical Frontend Workflows

#### Workflow 1 — Setup & Configure

```
1. GET  /MRP/GetSelectListMRP                    → populate MRP config dropdown
2. POST /MRP/SaveMRP                             → create MRP config if needed
   Body: { MrpId: 0, MrpCode: "MRP01", MrpName: "Main Plant MRP",
           IncludeLeadTime: 1, IncludeLotSize: 1, IncludeStock: 1, ... }

3. POST /MasterSchedule/SaveMasterSchedule       → create schedule
   Body: { MasterScheduleId: 0, MasterScheduleCode: "MS2026",
           MasterScheduleName: "2026 Plan", OUId: 5, ... }

4. POST /MasterSchedule/LoadMasterSchedule
   ?MasterScheduleId=101&SourceType=0            → load demand from sales orders

5. POST /MasterSchedule/SetDemandPriority        → assign priority to demand lines
   Body: { MasterScheduleId: 101,
           Lines: [ { MasterScheduleDetailId: 1001, Priority: 1 },
                    { MasterScheduleDetailId: 1002, Priority: 2 } ] }
```

#### Workflow 2 — Run MRP (Macro)

```
1. POST /MRPRun/SaveMRPRun                       → create run header
   Body: { MrpRunId: 0, MrpId: 5, ... }

2. POST /MRPRun/TriggerMRPRun                    → fire async run
   Body: { MrpId: 5, FromDate: "2026-01-01", ToDate: "2026-12-31",
           TypeofRun: 0, WorkOUId: 10, IsCombined: false,
           RunScope: 0 }

3. Connect SignalR: /hubs/mrprun                 → join group "mrprun:ou:10"
   Listen: ReceiveProgress → update progress bar
   Listen: ReceiveComplete → { MrpRunId: 123, ActionsCreated: 450 }

4. GET  /MRPRun/GetMRPRunStatus?MrpRunId=123    → fallback polling if SignalR unavailable

5. POST /MRPRun/GetMRPWorkbench                  → view planning grid
   Body: { MrpRunId: 123, PageOffset: 0, PageSize: 50,
           SortBy: "RequiredPeriod", SortDesc: false }
```

#### Workflow 3 — Scoped Re-Plan (Allocation Level)

```
1. POST /MasterSchedule/SetAllocationPriority    → update priority for an allocation
   Body: { AllocationId: 77, Priority: 1, CascadeToDetails: true }

2. POST /MRPRun/TriggerScopedMRPRun             → re-plan just one allocation
   Body: { MrpId: 5, FromDate: "2026-01-01", ToDate: "2026-12-31",
           TypeofRun: 1, WorkOUId: 10,
           RunScope: 1, AllocationId: 77 }

3. Listen SignalR ReceiveComplete → { MrpRunId: 124 }

4. POST /MRPRun/GetScopedRunComparison           → compare vs last macro run
   Body: { MacroRunId: 123, ScopedRunId: 124, PageOffset: 0, PageSize: 50 }
```

#### Workflow 4 — Review Actions & Release

```
1. GET  /MRPRun/GetMRPActionList?MrpRunId=123   → list all planned actions

2. POST /MRPRun/SaveMRPAction                    → firm a planned order
   Body: { MRPActionId: 501, MRPActionPlanStatus: 1,
           MRPActionIsFirmPlan: 1, MRPActionFirmQuantity: 100,
           MRPActionFirmDate: "2026-02-15" }

3. POST /MRPRun/ReleaseMRPActions                → release to documents
   Body: { MrpActionIds: [501, 502, 503],
           IsAllocationBasedRelease: 0,
           MaxPriorityToRelease: 1 }          ← optional: only Priority=1

   — or allocation-based release:
   Body: { MrpActionIds: [501],
           IsAllocationBasedRelease: 1,
           MaxPriorityToRelease: null }
```

#### Workflow 5 — Reports

```
POST /MRPRun/GetShortageReport
Body: { MrpRunId: 123, MaxPriority: 2,         ← show Priority 1+2 only
        AllocationId: null, PageOffset: 0, PageSize: 100 }
Response: PagedResult<MrpRunShortageReportDTO>  ← sorted Priority ASC

POST /MRPRun/GetMRPWorksheet
Body: { MrpRunId: 123, ItemId: null, PageOffset: 0, PageSize: 50 }

POST /MRPRun/GetPurchaseRequirement
Body: { MrpRunId: 123, FromDate: "2026-01-01", ToDate: "2026-03-31" }

POST /MRPRun/GetMRPWithDimensionReport         ← bridges MRP + Part Level
Body: { MrpRunId: 123, AllocationId: 77, PageOffset: 0, PageSize: 50 }
```

#### Workflow 6 — Part Level: Initialise & Allocate

```
1. POST /PartLevel/InitialisePartPosition       → seed stock snapshot from TSTOCKLEDGER
   Body: { StoreId: 3, ItemId: null, ResetToZero: false }
                                                ← ItemId null = all items in store

2. GET  /PartLevel/GetPartShortage?MaxPriority=1 → see critical shortages first

3. GET  /PartLevel/GetAllocationPriorityView    → ranked list of projects + available stock

4. POST /PartLevel/AutoAllocateByPriority       → run priority-based batch allocation
   Body: { AllocationId: 77,                   ← optional scope filter
           FGItemIds: null,
           SalesOrderId: null }
   Response: { TotalAllocated: 12, TotalPartiallyAllocated: 3,
               TotalShortages: 5, TotalAllocatedQty: 850.0,
               TotalShortageQty: 120.0,
               Shortages: [ { ItemCode: "RM001", RequiredQty: 50, ... } ] }
```

#### Workflow 7 — Part Level: Manual Allocation Lifecycle

```
1. POST /PartLevel/GetPartDetailList            → find the requirement
   Body: { AllocationId: 77, Status: 0, Priority: 1, PageOffset: 0, PageSize: 50 }

2. POST /PartLevel/AllocateParts                → reserve stock
   Body: { PartDetailId: 201, LotId: 55, Quantity: 10.0 }

3. POST /PartLevel/IssueParts                   → issue to production floor
   Body: { PartAllocationId: 301, Quantity: 8.0 }

4. POST /PartLevel/ConsumeParts                 → record actual consumption
   Body: { PartAllocationId: 301, Quantity: 8.0 }

5. POST /PartLevel/ReleaseParts                 → cancel remaining reservation
   Body: { PartAllocationId: 302 }
   ← releases any un-issued reserved qty back to Available

6. POST /PartLevel/SyncPartDetailWIP
   ?PartDetailId=201&WipStock=5.0&AvailableToWork=3.0
                                                ← called by production system on WO progress
```

#### Workflow 8 — Part Level Workbench

```
1. GET  /PartLevel/GetAllocationPriorityView    → dashboard: all projects ranked by priority
   Response: [ { AllocationId: 77, AllocationName: "Order A",
                 Priority: 1, OpenPartDetailCount: 12,
                 TotalRemainingQty: 245.0, TotalAvailableQty: 180.0 } ]

2. POST /PartLevel/GetPartLevelWorkbench        → drill into a project
   Body: { AllocationId: 77, Status: 0,
           MaxPriority: null, PageOffset: 0, PageSize: 50 }

3. GET  /PartLevel/GetPartLevelWIPSummary?AllocationId=77
                                                → WIP aggregation by item for a project

4. GET  /PartLevel/GetPartDetailLineage?RootPartDetailId=201
                                                → parent→child chain for a requirement
```

---

### B5. Error Handling

All responses follow `ResponseStandardDTO<T>`. Check `IsSuccess` flag before reading `Data`.

| HTTP Status | Meaning | Action |
|---|---|---|
| 200 + IsSuccess=true | OK | Read `Data` |
| 200 + IsSuccess=false | Business validation error | Show `Message` to user |
| 400 | Invalid request structure | Fix DTO fields |
| 500 | Server error | Show generic message; log `TraceId` |

MRP trigger endpoints (`TriggerMRPRun`, `TriggerScopedMRPRun`) return immediately with `MrpRunId`.
The actual run result arrives via SignalR `ReceiveComplete` or `ReceiveError`.

---

## Appendix C — Detailed Test Scenarios

### C1. Setup Matrix (run before all tests)

| Data | Value |
|---|---|
| Tenant | TenantId=1 |
| OU | OUId=10 |
| MRP Config | MrpId=5, IncludeLeadTime=1, IncludeStock=1 |
| MasterSchedule | MasterScheduleId=101 |
| Items | FG-A (ItemId=1001), FG-B (ItemId=1002), RM-X (ItemId=2001), RM-Y (ItemId=2002) |
| BOM | FG-A → RM-X qty=2; FG-B → RM-Y qty=3 |
| Allocation P1 | AllocationId=77, Priority=1 (FG-A demand) |
| Allocation P2 | AllocationId=78, Priority=2 (FG-B demand) |
| Store | StoreId=3 |
| Opening stock | RM-X: 80 units available, RM-Y: 50 units available |

---

### C2. MRP Configuration Tests

**TC-MRP-01: Create and retrieve MRP config**
```
1. POST /MRP/SaveMRP  Body: { MrpId: 0, MrpCode: "TEST", MrpName: "Test MRP",
         IncludeLeadTime:1, IncludeLotSize:1, IncludeStock:1,
         OrganizationType:0, OUId:10 }
   Expected: SaveSuccess + new MrpId returned
2. GET  /MRP/GetMRP?MRPId=<new id>
   Expected: all fields match submitted values
3. GET  /MRP/GetSelectListMRP
   Expected: new config appears in list
4. DELETE /MRP/DeleteMRP?MRPId=<new id>
   Expected: DeleteSuccess
5. GET  /MRP/GetMRP?MRPId=<deleted id>
   Expected: null or 404
```

**TC-MRP-02: MasterSchedule demand load and priority**
```
1. POST /MasterSchedule/SaveMasterSchedule → create schedule
2. POST /MasterSchedule/LoadMasterSchedule?MasterScheduleId=101&SourceType=0
   Expected: demand lines loaded from sales orders
3. POST /MasterSchedule/SetDemandPriority
   Body: { MasterScheduleId: 101,
           Lines: [{ MasterScheduleDetailId: <line1>, Priority: 1 }] }
4. GET  /MasterSchedule/GetMasterSchedule?MasterScheduleId=101
   Expected: detail line Priority = 1
5. POST /MasterSchedule/SetAllocationPriority
   Body: { AllocationId: 77, Priority: 1, CascadeToDetails: true }
   Expected: all detail lines under AllocationId=77 have Priority=1
```

---

### C3. MRP Run Process Tests

**TC-RUN-01: Macro MRP run — full flow**
```
Pre: FG-A demand qty=50, FG-B demand qty=20 in MMASTERSCHEDULEDETAIL
     Opening stock: RM-X=80 (FG-A uses 2 per unit → needs 100, short by 20)
                    RM-Y=50 (FG-B uses 3 per unit → needs 60, short by 10)

1. POST /MRPRun/TriggerMRPRun
   Body: { MrpId:5, FromDate:"2026-01-01", ToDate:"2026-12-31",
           TypeofRun:1, WorkOUId:10, RunScope:0, IsCombined:false }
2. Listen SignalR ReceiveProgress → percent goes 0→100
3. Listen SignalR ReceiveComplete → MrpRunId assigned
4. GET  /MRPRun/GetMRPRunStatus?MrpRunId=<id>
   Expected: Status=1 (COMPLETED)
5. POST /MRPRun/GetMRPWorkbench
   Body: { MrpRunId:<id>, PageOffset:0, PageSize:100 }
   Expected: RM-X row: NetRequirement=20 (50 demand × 2 - 80 stock)
             RM-Y row: NetRequirement=10 (20 demand × 3 - 50 stock)
6. GET  /MRPRun/GetMRPActionList?MrpRunId=<id>
   Expected: TMRPACTION rows for RM-X qty=20 and RM-Y qty=10
             MRPActionPlanStatus=0 (UNRELEASED)
```

**TC-RUN-02: Priority pegging correctness**
```
Pre: Both Priority=1 (AllocationId=77) and Priority=2 (AllocationId=78) demand for RM-X
     Priority=1 demand: qty=60; Priority=2 demand: qty=40
     Opening stock RM-X: 0 (all planned)
     Planned order (TMRPACTION): qty=80

1. After macro run, POST /MRPRun/GetMRPWorkbench
   Expected: TPEGGING shows Priority=1 demand pegged 60 (fully served)
             TPEGGING shows Priority=2 demand pegged 20 (partially served; 40-20=20 shortage)

Failure mode to verify against: Priority=2 demand MUST NOT be fully served at the expense
of Priority=1, even if Priority=2 could be fully satisfied with remaining stock.
```

**TC-RUN-03: Scoped run — Allocation level**
```
1. POST /MRPRun/TriggerScopedMRPRun
   Body: { MrpId:5, FromDate:"2026-01-01", ToDate:"2026-12-31",
           TypeofRun:1, WorkOUId:10, RunScope:1, AllocationId:77 }
2. After ReceiveComplete → MrpRunId=<scoped>
3. GET  /MRPRun/GetMRPActionList?MrpRunId=<scoped>
   Expected: ALL TMRPACTION rows have AllocationId=77
             NO rows exist for AllocationId=78 (other project not re-planned)
4. UndoMRP on <scoped>
   Expected: Only <scoped> run's TMRPACTION deleted; macro run's actions untouched
```

**TC-RUN-04: Scoped run — FGItem level**
```
1. POST /MRPRun/TriggerScopedMRPRun
   Body: { MrpId:5, ..., RunScope:3, FGItemIds:[1001] }  ← FG-A only
2. After ReceiveComplete
3. POST /MRPRun/GetMRPWorkbench Body: { MrpRunId:<scoped> }
   Expected: rows for RM-X present (component of FG-A)
             NO rows for RM-Y (component of FG-B, not in scope)
   Critical: RM-X rows must exist despite filter being on FG-A — components are NOT filtered
```

**TC-RUN-05: Combined run (MPS → MRP sequential)**
```
1. POST /MRPRun/TriggerCombinedMRPRun
   Body: { MrpId:5, ..., TypeofRun:1, IsCombined:true }
2. GET  /MRPRun/GetMRPRunList?PageOffset=0&PageSize=10
   Expected: Two runs visible — Type=0 (MPS) completed before Type=1 (MRP)
             MRP run references MPS run's MrpRunId
```

**TC-RUN-06: Scheduled receipts reduce net requirement**
```
Pre: Open PO for RM-X qty=15 (scheduled receipt)
     FG-A demand qty=50 → RM-X gross req=100, stock=80
     Without receipt: NetReq=20
     With receipt: NetReq=5 (100 - 80 - 15)
1. Run MRP
2. POST /MRPRun/GetMRPWorksheet
   Expected: RM-X ScheduledReceipts=15, NetRequirement=5
```

**TC-RUN-07: MinMax run (TypeofRun=2)**
```
Pre: MITEMOU MinStock=50, MaxStock=200 for RM-X, current SOH=30
1. POST /MRPRun/TriggerMRPRun Body: { TypeofRun:2, ... }
2. GET  /MRPRun/GetMRPActionList
   Expected: Action for RM-X qty=170 (MaxStock - SOH = 200 - 30)
             NO BOM explosion rows (MinMax skips BOM)
```

---

### C4. MRP Action & Release Tests

**TC-ACT-01: Firm a planned order**
```
Pre: MRPActionId=501, PlanStatus=0 (UNRELEASED)
1. POST /MRPRun/SaveMRPAction
   Body: { MRPActionId:501, MRPActionPlanStatus:1,
           MRPActionIsFirmPlan:1, MRPActionFirmQuantity:20,
           MRPActionFirmDate:"2026-02-15", MRPActionRemarks:"Firmed by planner" }
2. GET  /MRPRun/GetMRPAction?MrpActionId=501
   Expected: IsFirmPlan=1, FirmQuantity=20, PlanStatus=1
3. Verify: SaveMRPAction does NOT allow changing Priority directly
           (Priority field is ignored even if present in body)
```

**TC-REL-01: Direct release to Purchase Order**
```
Pre: MRPActionId=501, PlanStatus=1, ReleaseType=1 (PO)
1. POST /MRPRun/ReleaseMRPActions
   Body: { MrpActionIds:[501], IsAllocationBasedRelease:0 }
2. GET  /MRPRun/GetMRPAction?MrpActionId=501
   Expected: MRPActionPlanStatus=2 (RELEASED), ReleaseId > 0
3. Verify: TPEGGING row exists linking action to generated PO
4. Verify: TMMHEAD row created with correct party + quantity
```

**TC-REL-02: Allocation-based release to Work Indent**
```
Pre: MRPActionId=502, PlanStatus=1, ReleaseType=3, IsAllocationBasedRelease=1
1. POST /MRPRun/ReleaseMRPActions
   Body: { MrpActionIds:[502], IsAllocationBasedRelease:1 }
2. Expected:
   - TALLOCATION row created
   - EXEC ALLOCATION2RESERVATION fires → TRESERVATION row exists
   - Work Indent (TINDENT) created + linked to TALLOCATION
   - TMRPACTION.RELEASEID = IndentId
   - TPEGGING row inserted
```

**TC-REL-03: Priority-filtered release**
```
Pre: MRPActionIds=[501 (Priority=1), 502 (Priority=2), 503 (Priority=1)]
     All PlanStatus=1
1. POST /MRPRun/ReleaseMRPActions
   Body: { MrpActionIds:[501,502,503], IsAllocationBasedRelease:0,
           MaxPriorityToRelease:1 }
Expected: Actions 501 and 503 released (Priority=1)
          Action 502 remains PlanStatus=1 UNRELEASED (Priority=2 filtered out)
```

---

### C5. MRP Report Tests

**TC-RPT-01: Shortage report with priority filter**
```
POST /MRPRun/GetShortageReport
Body: { MrpRunId: <id>, MaxPriority: 1, PageOffset: 0, PageSize: 50 }
Expected: Only items with Priority=1 demand shortage appear
          Results sorted: Priority=1 first, then by item code
          Priority column visible in response DTO
```

**TC-RPT-02: Scoped run comparison**
```
Pre: MacroRunId=123 (full plan), ScopedRunId=124 (AllocationId=77 only)
POST /MRPRun/GetScopedRunComparison
Body: { MacroRunId:123, ScopedRunId:124 }
Expected: Rows only for items whose planned qty differs between runs
          Delta = ScopedQty - MacroQty
          Items not re-planned in scope show as no delta
```

**TC-RPT-03: MRP with dimension report (bridge to Part Level)**
```
POST /MRPRun/GetMRPWithDimensionReport
Body: { MrpRunId:<id>, AllocationId:77, PageOffset:0, PageSize:50 }
Expected: Response includes PartDimId, Dimension1-5 columns
          Priority and RunScope columns present
          Only actions for AllocationId=77 shown
```

---

### C6. Part Level Tests

**TC-PL-01: PartDimension GetOrCreate idempotency**
```
1. POST /PartLevel/SavePartDimension
   Body: { PartDimId:0, D1:"RED", D2:"LARGE", D3:null, D4:null, D5:null }
   Expected: SaveSuccess, PartDimId=X assigned
2. POST /PartLevel/SavePartDimension (SAME D1-D5)
   Body: { PartDimId:0, D1:"RED", D2:"LARGE", D3:null, D4:null, D5:null }
   Expected: Returns same PartDimId=X (GetOrCreate did not create duplicate)
3. GET  /PartLevel/GetSelectListPartDimension
   Expected: Only ONE row for RED/LARGE combination
```

**TC-PL-02: Initialise part position from stock ledger**
```
Pre: TSTOCKLEDGER has RM-X: 80 units in StoreId=3
1. POST /PartLevel/InitialisePartPosition
   Body: { StoreId:3, ItemId:null, ResetToZero:false }
2. GET  /PartLevel/GetPartPositionList?StoreId=3&PageOffset=0&PageSize=50
   Expected: RM-X row with Available=80, Reserved=0, Issued=0
3. Run again with ResetToZero:true
   Expected: Available, Reserved, Issued all = 0 (reset for clean re-initialisation)
```

**TC-PL-03: Manual allocation lifecycle**
```
Pre: RM-X Available=80, PartDetailId=201 (RequiredQty=30)
1. POST /PartLevel/AllocateParts
   Body: { PartDetailId:201, LotId:55, Quantity:20 }
   Expected: SaveSuccess
             TPARTPOSITION: Available=60, Reserved=20
             TPARTALLOCATION: Status=0, AllocatedQty=20
2. POST /PartLevel/IssueParts
   Body: { PartAllocationId:301, Quantity:15 }
   Expected: TPARTPOSITION: Reserved=5, Issued=15
             TPARTALLOCATION: IssuedQty=15, Status=1
3. POST /PartLevel/ConsumeParts
   Body: { PartAllocationId:301, Quantity:15 }
   Expected: TPARTPOSITION: Issued=0
             TPARTALLOCATION: ConsumedQty=15, Status=2
4. POST /PartLevel/ReleaseParts  ← release remaining 5 reserved
   Body: { PartAllocationId:301 }
   Expected: TPARTPOSITION: Available=65 (60+5 returned), Reserved=0
             TPARTALLOCATION: Status=3
```

**TC-PL-04: Insufficient stock guard**
```
Pre: RM-X Available=10
POST /PartLevel/AllocateParts
Body: { PartDetailId:201, LotId:55, Quantity:50 }
Expected: InvalidOperationException / error response
          "Insufficient available stock for this allocation"
          TPARTPOSITION unchanged
```

**TC-PL-05: AutoAllocateByPriority — Priority ordering correctness**
```
Pre:
  TPARTDETAIL: PartDetailId=201, Priority=1, RequiredQty=60, ItemId=RM-X
               PartDetailId=202, Priority=2, RequiredQty=40, ItemId=RM-X
               PartDetailId=203, Priority=3, RequiredQty=30, ItemId=RM-X
  TPARTPOSITION: RM-X Available=70

POST /PartLevel/AutoAllocateByPriority
Body: { AllocationId:null, FGItemIds:null, SalesOrderId:null }

Expected AllocationResultDTO:
  TotalAllocated=1 (PartDetail 201 fully allocated)
  TotalPartiallyAllocated=1 (PartDetail 202 partially: 10 of 40)
  TotalShortages=2 (202 short 30, 203 short 30)
  TotalAllocatedQty=70
  Shortages: [{ PartDetailId:202, ShortageQty:30, Priority:2 },
              { PartDetailId:203, ShortageQty:30, Priority:3 }]

Critical violation to detect:
  MUST NOT: allocate 40 to Priority=2 (PartDetail 202) leaving only 30 for Priority=1 (PartDetail 201)
  MUST: serve Priority=1 fully (60) before serving Priority=2 (remaining 10)
```

**TC-PL-06: AutoAllocateByPriority — scoped to AllocationId**
```
Pre: Two allocations: AllocationId=77 (Priority=1) and AllocationId=78 (Priority=2)
     Both have open TPARTDETAIL for RM-X
POST /PartLevel/AutoAllocateByPriority
Body: { AllocationId:77 }    ← scope to project 77 only
Expected: Only PartDetail rows with AllocationId=77 are processed
          AllocationId=78 rows untouched
```

**TC-PL-07: Concurrency — ROWVERSION protection**
```
Pre: RM-X Available=10, Two requests fire simultaneously for same lot
Request A: AllocateParts qty=10
Request B: AllocateParts qty=10  ← concurrent

Expected:
  One request succeeds (Available → 0)
  Other request: ROWVERSION mismatch → BLL retry loop → "Insufficient available stock" on retry
  Net result: Available=0, Reserved=10 (not -10)
  No double-allocation
```

**TC-PL-08: Part shortage with priority filter**
```
Pre: Multiple items with shortages at different priorities
GET  /PartLevel/GetPartShortage?MaxPriority=1
Expected: Only Priority=1 items with shortage appear
          Sorted by Priority ASC (all Priority=1), then ShortageQty DESC

GET  /PartLevel/GetPartShortage   ← no filter
Expected: ALL shortage items, Priority=1 items appear first
```

**TC-PL-09: AllocationPriorityView — single query, no N+1**
```
Pre: 10 allocations with varying priorities and open part details
GET  /PartLevel/GetAllocationPriorityView

Expected:
  Results sorted Priority=1 first, then Priority=2, etc.; Priority=0 (unset) last
  Each row shows: AllocationCode, Priority, OpenPartDetailCount, TotalRemainingQty, TotalAvailableQty
  Single DB round-trip (verify via SQL trace: exactly 1 query executed)
```

**TC-PL-10: WIP sync from production**
```
Pre: PartDetailId=201, WipStock=0, AvailableToWork=0
POST /PartLevel/SyncPartDetailWIP
?PartDetailId=201&WipStock=15&AvailableToWork=10
Expected: TPARTDETAIL.WIPSTOCK=15, AVAILABLETOWORK=10

GET  /PartLevel/GetPartDetail?PartDetailId=201
Expected: WipStock=15, AvailableToWork=10
```

**TC-PL-11: Part detail lineage traversal**
```
Pre: TPARTDETAIL chain: Root(201) → Child(202) → Grandchild(203)
     All linked via ParentPartDetailId and RootPartDetailId
GET  /PartLevel/GetPartDetailLineage?RootPartDetailId=201
Expected: Returns [201, 202, 203] in parent→child order
          Each row includes EntityTypeId showing which document created it
```

---

### C7. Integration / End-to-End Tests

**TC-E2E-01: Full MRP → Part Level flow**
```
1. POST /MasterSchedule/SetDemandPriority → Priority=1 for FG-A demand
2. POST /MRPRun/TriggerMRPRun → macro run
3. SignalR ReceiveComplete
4. GET  /MRPRun/GetMRPActionList → get RM-X action
5. POST /MRPRun/ReleaseMRPActions (ReleaseType=3) → create Work Indent
6. POST /PartLevel/InitialisePartPosition → seed stock
7. POST /PartLevel/AutoAllocateByPriority → allocate Priority=1 requirements
8. GET  /PartLevel/GetAllocationPriorityView → verify AllocationId=77 TotalRemainingQty decreased
9. POST /PartLevel/IssueParts → issue to floor
10. POST /PartLevel/SyncPartDetailWIP → update WIP quantities
11. GET  /PartLevel/GetPartLevelWIPSummary?AllocationId=77 → verify WIP totals
12. POST /PartLevel/ConsumeParts → record consumption
13. GET  /PartLevel/GetPartDetail → verify Status=2 (Complete) for fully consumed requirement
```

**TC-E2E-02: Priority-driven resource allocation across projects**
```
Scenario: Two projects compete for limited RM-X (available=50)
  AllocationId=77 (Priority=1): needs 60 units of RM-X
  AllocationId=78 (Priority=2): needs 40 units of RM-X
  Total demand=100, Total available=50

1. POST /PartLevel/AutoAllocateByPriority Body: { AllocationId:null }
2. Expected:
   AllocationId=77: 50 allocated (limited by stock), 10 short
   AllocationId=78: 0 allocated (all stock consumed by Priority=1)
   TotalShortages: [ {AllocationId=77, Shortage=10}, {AllocationId=78, Shortage=40} ]

3. POST /PartLevel/GetPartLevelWorkbench
   Body: { MaxPriority:1 }  ← show only Priority=1 items
   Expected: Only AllocationId=77 rows visible

4. GET  /PartLevel/GetAllocationPriorityView
   Expected:
     AllocationId=77 row: TotalAvailableQty ≈ 0 (all allocated)
     AllocationId=78 row: TotalAvailableQty = 50 (nothing allocated to it yet)
     Both rows appear, sorted Priority=1 first
```

---

### C8. Security / Tenant Isolation Tests

**TC-SEC-01: Cross-tenant data leak prevention**
```
1. Login with TenantId=1 → POST /MRP/SaveMRP (creates MrpId=100)
2. Login with TenantId=2 → GET /MRP/GetMRP?MRPId=100
   Expected: null or 404 (TenantId=2 cannot see TenantId=1 records)
3. POST /PartLevel/AutoAllocateByPriority (TenantId=2)
   Expected: Only TPARTDETAIL rows for TenantId=2 processed;
             TPARTPOSITION updates only for TenantId=2
```

**TC-SEC-02: Priority field is read-only on SaveMRPAction**
```
1. Create MRPAction with Priority=1 via MRP run
2. POST /MRPRun/SaveMRPAction
   Body: { MRPActionId:<id>, Priority:3, ... }   ← attempt to change priority
3. GET  /MRPRun/GetMRPAction
   Expected: Priority still = 1 (SaveMRPAction ignores Priority field)
```
