# CMMS database design

Prisma 7.2 / MySQL. Schema follows the current React routes and their forms. Pilot Plant uses the API with Category, Equipment, and Item models. The migration history preserves the earlier baseline and upgrades existing data.

`schema.prisma` defines the generator and datasource; table definitions live in `models/*.prisma`. `prisma.config.ts` points to the `prisma` directory so both are loaded together. This document describes the current schema files, not whether migrations have been applied to a particular database.

## Maintenance persistence (implemented)

The multi-file schema includes the following approved Maintenance migrations:

- `20261014000200_maintenance_work_orders`: unsigned `version` defaults to 0 on WorkOrder and MaintenancePlan; MaintenanceRequest stores the unique hashed request key, actor, operation, payload hash and committed JSON response. User.maintenanceRequests is its reverse relation. Business writes, audit and stored response commit together; changed versions or reused keys with different actors/payloads are rejected.
- `20261014000300_maintenance_system_actor`: WorkOrderStatusHistory.changedById and changedBy are optional. Null represents an automatic scheduler event displayed as “Sistem”; manual operations retain the authenticated user. Historical human references and the foreign key are preserved.

WorkOrder.technicianId and MaintenancePlan.technicianId remain nullable legacy columns to preserve historical data; current Maintenance has no assignment input, display, filter or authorization dependency on them. Active technicians share execution and actual actors are recorded in history.

Warehouse corrective work is created manually from a QuarantineCase, with at most one active WO per case enforced transactionally. Completion does not release or scrap stock. Usage derives from posted WO-linked stock movements and reversals, without checkout associations or financial calculations. CalibrationRecord and its relations have been removed. Calibration result submission is disabled; the legacy work-order type remains for compatibility. WorkOrderComment remains a schema capability; current execution notes use WorkOrder.resolution and transition history.

See [Maintenance behavior](../src/services/maintenance.md) and [deployment instructions](maintenance-deployment.md).

## Photo storage

Category, Equipment, Item, and Part store the uploaded filename in `photoKey`.
The API computes response `photoUrl` from that key and the record ID; it is not a persisted URL.
Migration `20261014000500_remove_legacy_photo_urls` removes the unused `photoUrl`
columns from equipment and items. Their `photoKey` values and uploaded files remain unchanged.
Earlier migrations retain the legacy columns as part of migration history.

## Page mapping

| Page | Models |
| --- | --- |
| Login / Settings | User, SystemSetting, NotificationPreference |
| Pilot Plant | Category, Equipment, Item, PartAssignment, DamageReport |
| Master Stock | PartGroup, PartKind, Part, StockLocation, StockBalance, QuarantineCase |
| Stock Movement / Checkout Approval | StockMovement, StockBalance, Part, User; InventoryRequest for authenticated inventory mutations |
| Public QR Checkout | PartGroup, Part, StockBalance, StockMovement (requesterName; no login or WorkOrder link) |
| Maintenance / PM Calendar | MaintenancePlan, WorkOrder, WorkOrderStatusHistory, MaintenanceRequest; posted StockMovement for sparepart usage |
| Inventory / Quarantine Review | QuarantineCase, WorkOrder, StockMovement, StockBalance |
| Notifications | Notification, NotificationRecipient |
| Home / Reports | Aggregations of WorkOrder, FailureEvent, OperatingLog, StockMovement, StockoutEvent, MaintenancePlan |
| Cross-module change history | AuditLog |

## Entity relationship diagram

The diagram is derived from every model in [models](models). It shows primary
keys, foreign-key columns, and selected checkout identity/lifecycle fields; other scalar fields and composite unique indexes
remain defined in the model files. Relation labels name the owning foreign key,
so separate roles such as reporter, reviewer, and technician are explicit.

Cardinality reflects the schema: `||` means exactly one, `o|` means zero or one,
and `o{` means zero or many. Solid lines identify relations whose foreign keys
form part of the child's primary key; dashed lines show other relations.
`nullable` follows the optional Prisma field. `PK`, `FK`, and `UK` denote
primary, foreign, and single-column unique keys.

The diagram uses Prisma model names. Physical table mappings are:

| Prisma model | Database table |
| --- | --- |
| Category | `category` |
| Equipment | `equipment` |
| Item | `items` |
| OperatingLog | `operating_log` |
| PartAssignment | `part_assignment` |

All other tables use their model names.

```mermaid
erDiagram
  MaintenanceRequest {
    String id PK
    String key UK
    String actorId FK
    String operation
    String payloadHash
    Json result
    DateTime createdAt
  }
  InventoryRequest {
    String id PK
    String key UK
    String actorId FK
  }
  AuditLog {
    String id PK
    String actorId FK "nullable"
  }
  Category {
    String id PK
  }
  DamageReport {
    String id PK
    String equipmentId FK "nullable"
    String itemId FK "nullable"
    String reporterId FK
    String reviewedById FK "nullable"
  }
  Equipment {
    String id PK
    String categoryId FK
  }
  FailureEvent {
    String id PK
    String equipmentId FK "nullable"
    String itemId FK "nullable"
    String workOrderId FK, UK
  }
  Item {
    String id PK
    String equipmentId FK
  }
  MaintenancePlan {
    String id PK
    Int version "unsigned; default 0"
    String equipmentId FK "nullable"
    String itemId FK "nullable"
    String technicianId FK "nullable"
    String createdById FK
    MaintenanceCycle cycle
    DateTime startDate "date only; recurrence anchor"
    DateTime endDate "nullable; date only"
    DateTime nextDueAt "nullable; generation cursor"
    Boolean isActive
  }
  Notification {
    String eventKey UK "nullable SHA-256 event identity; legacy rows remain null"
    String id PK
    String workOrderId FK "nullable"
    String equipmentId FK "nullable"
    String itemId FK "nullable"
    String partId FK "nullable"
    String movementId FK "nullable"
  }
  NotificationPreference {
    String userId PK, FK
  }
  NotificationRecipient {
    String notificationId PK, FK
    String userId PK, FK
  }
  OperatingLog {
    String id PK
    String equipmentId FK "nullable"
    String itemId FK "nullable"
  }
  Part {
    String id PK
    String groupId FK
    String kindId FK
  }
  PartAssignment {
    String equipmentId FK "nullable"
    String partId FK
    String id PK
  }
  PartGroup {
    String id PK
  }
  PartKind {
    String id PK
  }
  QuarantineCase {
    String id PK
    String balanceId FK
    String reporterId FK
  }
  StockBalance {
    String id PK
    String partId FK
    String locationId FK
  }
  StockLocation {
    String id PK
  }
  StockMovement {
    String id PK
    String number UK
    String idempotencyKey UK "hashed retry and status key for public checkout"
    String balanceId FK
    MovementType type
    MovementSource source
    ApprovalStatus approval
    Int quantity
    Int physicalDelta
    Int quarantineDelta
    String actorId FK "nullable"
    String requesterName "nullable in DB; required for new public checkout"
    String approvedById FK "nullable"
    DateTime approvedAt "nullable"
    DateTime postedAt "nullable; pending does not affect stock"
    String workOrderId FK "nullable; unused by checkout"
    String quarantineId FK "nullable"
    String reversalOfId FK, UK "nullable"
  }
  StockoutEvent {
    String id PK
    String partId FK
    String requestedById FK
    String workOrderId FK "nullable"
  }
  SystemSetting {
    Int id PK
  }
  User {
    String id PK
  }
  WorkOrder {
    String id PK
    Int version "unsigned; default 0"
    String equipmentId FK "nullable"
    String itemId FK "nullable"
    String quarantineId FK "nullable"
    String damageReportId FK, UK "nullable"
    String reporterId FK
    String technicianId FK "nullable"
    String deletedById FK "nullable; deletion actor"
    DateTime deletedAt "nullable; soft deletion"
    String planId FK "nullable"
    WorkOrderStatus status
    DateTime scheduledFor "nullable; unique with planId"
    DateTime releaseAt
    DateTime dueAt "nullable"
  }
  WorkOrderComment {
    String id PK
    String workOrderId FK
    String authorId FK
  }
  WorkOrderStatusHistory {
    String id PK
    String workOrderId FK
    String changedById FK "nullable; automatic scheduler event"
    WorkOrderStatus fromStatus "nullable"
    WorkOrderStatus toStatus
    DateTime changedAt
  }

  User o|..o{ AuditLog : "actorId"
  User ||..o{ InventoryRequest : "actorId"
  User ||..o{ MaintenanceRequest : "actorId"
  Equipment o|..o{ DamageReport : "equipmentId"
  Item o|..o{ DamageReport : "itemId"
  User ||..o{ DamageReport : "reporterId"
  User o|..o{ DamageReport : "reviewedById"
  Category ||..o{ Equipment : "categoryId"
  Equipment o|..o{ FailureEvent : "equipmentId"
  Item o|..o{ FailureEvent : "itemId"
  WorkOrder ||..o| FailureEvent : "workOrderId"
  Equipment ||..o{ Item : "equipmentId"
  Equipment o|..o{ MaintenancePlan : "equipmentId"
  Item o|..o{ MaintenancePlan : "itemId"
  User o|..o{ MaintenancePlan : "technicianId"
  User ||..o{ MaintenancePlan : "createdById"
  WorkOrder o|..o{ Notification : "workOrderId"
  Equipment o|..o{ Notification : "equipmentId"
  Item o|..o{ Notification : "itemId"
  Part o|..o{ Notification : "partId"
  StockMovement o|..o{ Notification : "movementId"
  User ||--o| NotificationPreference : "userId"
  Notification ||--o{ NotificationRecipient : "notificationId"
  User ||--o{ NotificationRecipient : "userId"
  Equipment o|..o{ OperatingLog : "equipmentId"
  Item o|..o{ OperatingLog : "itemId"
  PartGroup ||..o{ Part : "groupId"
  PartKind ||..o{ Part : "kindId"
  Equipment o|..o{ PartAssignment : "equipmentId"
  Part ||..o{ PartAssignment : "partId"
  StockBalance ||..o{ QuarantineCase : "balanceId"
  User ||..o{ QuarantineCase : "reporterId"
  Part ||..o{ StockBalance : "partId"
  StockLocation ||..o{ StockBalance : "locationId"
  StockBalance ||..o{ StockMovement : "balanceId"
  User o|..o{ StockMovement : "actorId"
  User o|..o{ StockMovement : "approvedById"
  WorkOrder o|..o{ StockMovement : "workOrderId"
  QuarantineCase o|..o{ StockMovement : "quarantineId"
  StockMovement o|..o| StockMovement : "reversalOfId"
  Part ||..o{ StockoutEvent : "partId"
  User ||..o{ StockoutEvent : "requestedById"
  WorkOrder o|..o{ StockoutEvent : "workOrderId"
  Equipment o|..o{ WorkOrder : "equipmentId"
  Item o|..o{ WorkOrder : "itemId"
  QuarantineCase o|..o{ WorkOrder : "quarantineId"
  DamageReport o|..o| WorkOrder : "damageReportId"
  User ||..o{ WorkOrder : "reporterId"
  User o|..o{ WorkOrder : "technicianId"
  User o|..o{ WorkOrder : "deletedById"
  MaintenancePlan o|..o{ WorkOrder : "planId"
  WorkOrder ||..o{ WorkOrderComment : "workOrderId"
  User ||..o{ WorkOrderComment : "authorId"
  WorkOrder ||..o{ WorkOrderStatusHistory : "workOrderId"
  User o|..o{ WorkOrderStatusHistory : "changedById"
```

SystemSetting has no foreign-key relationships and is shown independently.
DamageReport–WorkOrder, and the StockMovement
reversal link are optional one-to-one relationships because their foreign keys
are nullable and unique. WorkOrder–FailureEvent and User–NotificationPreference
allow at most one child, but each child requires its parent.

Equipment/item references are nullable on the dependent models. The diagram
therefore shows both as optional; choosing exactly one target is a service
invariant, not an exclusive-or constraint represented by Prisma. AuditLog's
entityType/entityId pair is a historical pointer, not a foreign-key relation.
User deactivation preserves historical attribution.

For public checkout, `StockMovement.requesterName` stores the self-reported name
directly on the transaction and `actorId` is null; no guest table or guest User
is created. The optional User relations represent authenticated actors/reviewers,
not a login requirement for checkout. `approvedById` records the ADMIN/SUPERVISOR
who approves the request. The optional WorkOrder relation belongs to other
maintenance-related stock flows; checkout leaves `workOrderId` null.

## Pilot Plant persistence

See [split migration guide](split-pilot-hierarchy.md) for deployment, preserved history, and historical HTTP compatibility. See [current Pilot Plant contract](pilot-plant-naming.md). Item has no inventory Part relation; PartAssignment only references Equipment and Part. Other dependent records use equipmentId/itemId foreign keys: exactly one for required targets, at most one for optional notification targets. Services must enforce this exclusivity; Prisma cannot represent an exclusive union of relations.

## Required service / migration invariants

### Inventory implementation scope (approved October 2026)

Inventory is technical-only: price entry, valuation and maintenance costs are excluded. The existing monetary columns remain for compatibility; their historical values are not rewritten and their defaults are not presented as measured costs. The financial reporting notes below are future design context, not requirements for this feature.

`20261013000200_inventory_checkout` adds Part.photoKey/version, InventoryRequest with a globally unique request key and atomic result record, and QuarantineStatus.CLOSED_MIXED. The migration is additive and must be deployed explicitly; generation/testing does not apply it to the application database. Photo keys reference private media files, not inline data URLs.

All new kiosk checkout requests are public and start PENDING, including requests submitted while logged in as ADMIN/SUPERVISOR. The API requires requesterName, a positive whole quantity and purpose/notes; actorId and workOrderId remain null. Only an authenticated ADMIN/SUPERVISOR can approve or reject. Pending does not reserve or deduct stock; approval rechecks availability and posts the movement atomically. This overrides priority-based auto-approval for checkout regardless of the legacy SystemSetting value. Immediate damage holds use a separate documented IMMEDIATE_DAMAGE_HOLD policy with attributed AUTO_APPROVED movement for technicians; they do not grant checkout privileges. Quarantine final outcomes derive from net posted release/scrap quantities, with CLOSED_MIXED when both occurred. Reversals restore case quantities/state atomically.

`20261014000100_public_checkout` adds only nullable StockMovement.requesterName (VARCHAR(191)). Existing records retain null; new public requests require a name at the API boundary. No separate guest/request table is added. The client UUID retry/status key is hashed into the existing unique StockMovement.idempotencyKey. Matching retries return the same transaction with its current status; a changed payload conflicts. Individual public status reads require the matching token; the approval queue requires ADMIN/SUPERVISOR. Stock Movement history lists posted transactions, while its checkout approval tab lists requests from the same table.

One internal INVENTORY_ROOM is selected by the service; no client place/rack entry or new rack field exists. Legacy balances outside the room require reviewed reconciliation. InventoryRequest stores request fingerprint and response for authenticated inventory mutations with the same commit as domain writes and is not TTL-purged in this MVP; public checkout uses StockMovement.idempotencyKey instead. Stockout analytics remain deferred rather than synthesizing events from zero balances.

See [Inventory API behavior](../src/services/inventory.md) and [preflight](inventory-preflight.md) for operation and deployment details.

Prisma validates foreign keys, enums, indexes and unique constraints; it does not express all cross-field checks below. Enforce these in the API transaction layer, and add SQL CHECK constraints in a reviewed MySQL migration where supported.

- SystemSetting is a single row with id=1. This schema is for one facility, not multi-tenant authorization.
- Physical tables are category, equipment, and items. Equipment requires categoryId; Item requires equipmentId. No recursive hierarchy or level/nameScopeId fields remain. Category is derived through equipment for an item. Archive master records rather than deleting history.
- WorkOrder source PILOT_PLANT requires exactly one of equipmentId/itemId and no quarantineId. WAREHOUSE_QUARANTINE requires quarantineId and neither equipmentId nor itemId. Damage reports and PM plans must match the WO target. Failed equipment or items must match their WO.
- Plans support PREVENTIVE/CALIBRATION only. Preserve startDate as recurrence anchor; clamp end-of-month dates and leap years, do not drift from the anchor. Use the configured IANA timezone. Store timestamps as UTC and date-only schedule fields as local dates.
- Generate one WO per plan occurrence in a transaction, using @@unique([planId, scheduledFor]) for idempotency. Planned WOs require scheduledFor, dueAt, releaseAt; list only releaseAt <= now. Future occurrences are projected from plans. Calendar completion history comes from WOs. ONCE plans stop after the first release; nextDueAt advances for repeating plans. API must preserve missed occurrences for compliance reporting.
- Overdue is a derived presentation state (dueAt < now and not completed/cancelled), not a stored status that hides OPEN/IN_PROGRESS. Record transitions with actor and timestamp. Completion requires completedAt; validate chronological started/completed/cancelled values.
- All quantities are whole units matching the current UI. Require quantity > 0 and monetary amounts >= 0. physical >= quarantined >= 0. Low stock is derived from sum(physical-quarantined) vs Part.minimumStock. Part numbers can repeat (including missing/N/A); productId and qrToken are unique.
- StockMovement is the source audit ledger; StockBalance is its transactional materialized balance. Post ledger + update balance/version + quarantine disposition atomically, under a row lock or compare-and-swap. Reject insufficient available quantity and concurrent double posting. Initial stock must be a posted RECEIPT/ADJUSTMENT, not an unexplained balance edit.
- PENDING/REJECTED movements never affect balances. APPROVED requires an authorized ADMIN/SUPERVISOR and approvedAt; AUTO_APPROVED must satisfy the owning feature's explicit policy and record the decision; SYSTEM requires a trusted system operation. Authenticated non-system operations require actorId; public QR_CHECKOUT instead requires requesterName and leaves actorId null. postedAt is set once only when posting an authorized movement. The client creates and retains one idempotencyKey before the first attempt; the request boundary validates that key and retries reuse it. A freshly generated server key per retry is not idempotent. The owning feature defines key scope, payload matching and replay semantics; retries must not duplicate stock effects, and a different payload with that key returns a conflict. Public checkout replays the existing transaction with its current status.
- Normal deltas: RECEIPT/RETURN = (+quantity,0), ISSUE = (-quantity,0), QUARANTINE = (0,+quantity), RELEASE = (0,-quantity), SCRAP from quarantine = (-quantity,-quantity). ADJUSTMENT requires a reason. A reversal must negate the original deltas, reference a posted transaction, and retain the same balance/WO; reverse any quarantine disposition atomically. One full reversal per original is enforced; do not reverse reversals.
- Quarantine-linked movements must reference the same balance as QuarantineCase. Cumulative release/scrap cannot exceed held quantity. Quarantine creation, holding movement, and balance update are one transaction. Do not close a case until its remaining held quantity is zero.
- WO sparepart usage derives from its explicitly linked posted ISSUE minus RETURN/reversal movements; public checkout has no WO link and must not be assigned to an asset or WO implicitly. No second independent usage quantity table is introduced. Financial valuation and maintenance-cost calculations are outside this feature; existing monetary columns remain for compatibility only.
- FailureEvent is one breakdown per corrective WO. Require restoredAt >= failedAt; downtime = interval intersection with reporting window, merge overlaps for plant totals. Maintenance duration is a different metric (startedAt to completedAt). Validate operating periods do not overlap per asset and operatingMinutes <= period duration.
- MTBF = operating time / failure count in the same scope/period; zero failures yields undefined/N/A, not zero hours. PM compliance = due occurrences completed by their dueAt / eligible due occurrences, including unreleased/missed occurrences from plans. Report technician completion count by completedAt, pending backlog as of the cutoff, consumption from posted ledger rows, and stockouts from explicit failed demand events.
- NotificationRecipient.readAt/deletedAt are per-user. Hapus Log hides the recipient row via deletedAt; it does not destroy shared system history. Restrict notification reads/updates to the authenticated recipient. Notifications may have multiple related context references, all validated for consistency.
- Authentication uses signed JWTs without a Session model; User stores only passwordHash. Never include passwords/tokens/hashes in AuditLog JSON. AuditLog is append-only; entityType/entityId is a historical pointer rather than a polymorphic database FK.

## Future transaction acceptance tests

These are requirements for future Inventory/Maintenance feature changes, not
runtime features or database tests supplied by the core API baseline. Use real
transactional MySQL storage in those changes:

- Atomic rollback: inject failure after balance work but before all required
  ledger, quarantine, work-order and audit writes complete. Assert every
  affected record remains unchanged.
- Competing issues: run concurrent requests with stock sufficient for one.
  At most one commits; the other receives a conflict. Available stock never
  becomes negative and no duplicate ledger row is posted.
- Duplicate retries: simulate a lost response after commit, then reuse the
  retained key with the same payload. Return the original result without new
  stock effects. Reusing that key with a different payload is a conflict.
- Preserved history: archive a referenced part/asset and verify posted ledger,
  work-order usage and attribution remain readable. Deriving overdue in a
  read model must not overwrite lifecycle state.
- Transaction ownership: the orchestrating service supplies one transaction
  client to participants; required audit/ledger writes do not escape through
  independent transactions or the global client. Retry only recognized
  transient conflicts under a bounded policy without duplicate external effects.

This clarification changes documentation only; it does not add endpoints,
schema changes, migrations, scheduling, or stock-posting implementation.

## Cleanup audit

Import graph was built with the installed TypeScript resolver (relative imports, @ alias and barrel exports). All page roots were preserved, including the inactive Calibration page. Five unreachable Pilot Plant components were removed: create-name-dialog, equipment-card, photo-placeholder, spare-part-row, sub-item-dialog. No active page route or shared UI primitive was deleted.

## Deployment

Schema validation does not apply database changes. Review/generate migrations against a development database, implement the invariants above, and generate Prisma Client before API integration. Existing prisma.config.ts keeps DATABASE_URL outside the schema; existing prisma-client generator/output/moduleFormat are preserved.

## Historical Pilot Plant schema extension

Migration `20261012000100_pilot_plant_api` introduced the contracts below on the legacy schema.
Migration `20261013000100_split_pilot_hierarchy` subsequently replaced PlantUnit/Asset
with Category/Equipment/Item and migrated their references. The ERD above describes
the current models; the legacy names below document migration history only.

- `Asset.nameScopeId` equals unitId for EQUIPMENT and parentId for SUB_ITEM.
  The migration first adds it nullable, backfills existing rows, then enforces
  NOT NULL and unique (nameScopeId, name). Name reservation includes archived
  records. Service validation must keep scope, unit, parent and level consistent;
  there is no generic reparent or move operation.
- PlantUnit and Asset have a server-owned photoKey and unsigned version. Updates
  must compare the expected version, increment it atomically, and return conflict
  on stale input. Existing Asset.photoUrl is preserved for explicit legacy-media
  handling. No stored URL is fetched automatically and no photo blob is added to
  the database.
- DamageReport gains ACCEPTED, nullable reviewer identity/time/note, and a
  normalized acceptance-input hash. User has a named reviewer relation distinct
  from the original reporter relation. Never invent attribution for old reports.
- WorkOrder.damageReportId is unique when non-null; reports have an optional
  singular workOrder relation. Multiple unrelated WOs can retain null report IDs.
  The unique index is created before dropping the old nonunique FK-supporting
  index. Existing links and historical work orders are not deleted or merged.
- Accept must create a CORRECTIVE/PILOT_PLANT/OPEN WO, update the OPEN report to
  ACCEPTED, and append audit/status history within the same transaction. Server
  checks and the unique key together prevent duplicate WOs. Identical committed
  retries return the existing WO; different input is a state conflict.
- Future Maintenance execution maps IN_PROGRESS to IN_REPAIR and resolved
  completion to RESOLVED. Cancellation retains the same WO link and leaves the
  report ACCEPTED for review. Accept does not reopen cancelled work or create
  a replacement WO. These transitions are not implemented by the schema alone.

Before deployment, run `npm run prisma:preflight:pilot-plant`; see
[preflight instructions](pilot-plant-preflight.md) for remediation and exit codes.
Resolve findings explicitly, back up data/media, quiesce writers, and rerun before
deploying. MySQL DDL is not an atomic multi-statement transaction: investigate a
partial migration and fix forward rather than blindly rerunning ALTER statements.

`npm run test:migration:pilot-plant` creates randomly named isolated databases on
the configured server, applies all three migrations to clean and
populated fixtures, checks backfill/uniqueness/history, and removes only databases
created by that process. It requires CREATE/DROP DATABASE privileges; it never
migrates or resets the configured application database. Use a development server.

Keep the original migration unchanged. A rollback of routes does not justify
removing ACCEPTED records, review history, or unique report links; use compatible
code or a reviewed forward repair. Regenerate Prisma Client after schema changes.

WO cancellation and deletion: ADMIN/SUPERVISOR may atomically cancel and soft delete an OPEN WO without execution notes, started history, comments, failures, stockouts or movements. WorkOrder.deletedAt/deletedById record deletion; history, report/quarantine relations and audit are retained. Queue, summary, calendar and analytics omit deleted orders. Detail remains readable; mutations reject deleted orders. PM occurrence uniqueness includes deleted orders to prevent regeneration. No stock changes or quarantine release occur.
