# WWSupport MES API — Move In / Move Out API

Reference for the batch **move-in / move-out** endpoints exposed by the **WWSupport / APIServer**
ServiceStack service. These endpoints hand a container/work-order payload to a machine's
`MesReceiver` object in AVEVA Wonderware (System Platform / Galaxy) over MXAccess, using a
flag-based request/response handshake, and read back the resulting batch id (and, for move-out,
recorded cycle data from SQL).

- **Service host:** `AppHost` (`AppSelfHostBase`, name `"APIServer"`) — `APIServer/APIServer/AppHost.cs`
- **Service implementation:** `MesServices` — `APIServer/APIServer.ServiceInterface/MesServices.cs`
- **Business logic:** `MesNotifier` — `APIServer/APIServer.ServiceInterface/MesNotifier.cs`
- **Framework:** ServiceStack 6.0.2, self-hosted; SQL Server (OrmLite, `SqlServer2016Dialect`)

> Serialization note: `JsConfig.IncludeNullValues = true`, so null fields **are** emitted in JSON
> responses (e.g. `"BatchID": null`).

This document covers `/mes/movein` and `/mes/moveout`. The two alarm endpoints
(`/mes/alarmstatus`, `/mes/simplealarmstatus`) are documented in [`Alarm-API.md`](Alarm-API.md).

---

## Endpoints

| Verb | Route | Request DTO | Response DTO |
|------|-------|-------------|--------------|
| `POST` | `/mes/movein`  | `MoveInRequest`  | `MoveInResponse`  |
| `POST` | `/mes/moveout` | `MoveOutRequest` | `MoveOutResponse` |

Both are dispatched through ServiceStack `Any(...)` handlers in `MesServices`, which resolve the
singleton `MesNotifier` and call `MoveIn(...)` / `MoveOut(...)`.

> The handlers call the async business methods with a blocking `.Result`
> (`mesNotifier.MoveIn(request).Result`), so each request occupies its thread until the
> handshake completes or the 30 s timeout fires.

The service also enables `PostmanFeature` and `OpenApiFeature` (Swagger), so a running instance
exposes a browsable contract and a Postman collection.

---

## Authentication & authorization

Identical to the alarm endpoints — all MES services are decorated with:

```csharp
[Authenticate]
[RequiredRole("MESAPI")]
public class MesServices : Service { ... }
```

The caller must be authenticated **and** hold the `MESAPI` role. Configured in `AppHost.Configure`:

- **API key** — `ApiKeyAuthProvider` (`SessionCacheDuration = 30 min`, `RequireSecureConnection = false`).
  An API key whose `Environment == "test"` is routed to the `TestDb` connection instead of the
  production DB (`AppHost.GetDbConnection`). **Caveat:** that redirect only applies to OrmLite
  lookups — the move-out cycle-data read uses a raw `ConnectionStrings["BatchDB"]` connection and
  is *not* affected by the test-key redirect (see gotchas).
- **LDAP** — `LdapAuthProvider`.
- `AllowGetAuthenticateRequests = true`.

Default API-key transport is the standard `Authorization` header (`Bearer <key>`, or HTTP Basic
with the key as username); `?apikey=<key>` also works since `AllowInHttpParams` defaults on.

---

## `POST /mes/movein`

Hands a container + its work orders to a machine's `MesReceiver` to start/stage a batch.

### Request — `MoveInRequest`

| Field | Type | Notes |
|-------|------|-------|
| `SAPID` | string | SAP identifier of the target machine. Used to resolve the `Machine` row. Required. |
| `OperatorName` | string | Operator initiating the move-in. |
| `JobSequenceNumber` | string | Job sequence number. |
| `ContainerNumber` | string | MES container number. |
| `WorkOrders` | `WorkOrderInfo[]` | Work orders being moved in (default empty). Each item: `WorkOrderNumber` (string), `PartNumber` (string). |

```json
{
  "SAPID": "100012345",
  "OperatorName": "chamalas",
  "JobSequenceNumber": "50",
  "ContainerNumber": "cont-012",
  "WorkOrders": [
    { "WorkOrderNumber": "W111111", "PartNumber": "P111111" }
  ]
}
```

### Response — `MoveInResponse`

| Field | Type | Notes |
|-------|------|-------|
| `WasSuccessful` | bool | `true` only if the machine reported `MoveInSuccessfulFlag = true`. |
| `ErrorText` | string | Failure reason, or the machine's `MoveInErrorText` on a completed-but-failed move. |
| `BatchID` | int? | The machine's `MoveInBatchID`, **only when non-zero**; otherwise null. |

### Behavior

1. **30 s budget** (`CancellationTokenSource(30000)`).
2. **Resolve machine:** `db.Single<Machine>(x => x.SAPID == request.SAPID)`. If not found →
   `WasSuccessful = false`, `ErrorText = "Failed to find machine with SAPID '<id>'"` (early return).
3. **Subscribe** to the machine's move-in tagset (`MesMoveInTagset`, all under
   `{Machine.Code}.MesReceiver.*`) via `AdviseSupervisory`; wait for the first value of each.
   If any subscription fails → `"Failed to connect to machine"`.
4. **Ready gate:** require `MoveInReadyFlag == true`, else `"Machine move in ready flag not set to true"`.
5. If still successful, perform the **handshake**:
   - Arm a watcher for `MoveInCompleteFlag → true` (`OnValue`).
   - Write the payload tags (see table) **and** set `MoveInFlag = true`. If any write is not
     acknowledged → `"Failed to write move in information to machine"`.
   - Wait for `MoveInCompleteFlag`:
     - **Completed:** `WasSuccessful = MoveInSuccessfulFlag`; `ErrorText = MoveInErrorText`;
       `BatchID = MoveInBatchID` (only if ≠ 0).
     - **Timed out:** `"Timeout waiting for move in information to be processed"`.
6. **Unsubscribe** all tags.

### Tags written / read (`{Machine.Code}.MesReceiver.*`)

| Property | MXAccess tag suffix | Type | Direction | Source / meaning |
|----------|---------------------|------|-----------|------------------|
| `MoveInReadyFlag` | `MoveInReadyFlag` | bool | read (gate) | Machine must be ready to accept a move-in. |
| `MoveInMesContainerNumber` | `MoveInMesContainerNum` | string | write | `request.ContainerNumber`. |
| `MoveInOperatorName` | `MoveInOperatorName` | string | write | `request.OperatorName`. |
| `MoveInJobSequenceNumber` | `MoveInJobSequenceNumber` | string | write | `request.JobSequenceNumber`. |
| `MoveInNumberWorkOrders` | `MoveInNumberWorkOrders` | int | write | `request.WorkOrders.Count`. |
| `MoveInPartNumbers` | `MoveInPartNumbers[]` | string[] | write | `WorkOrders[*].PartNumber`, **fixed length 50**. |
| `MoveInWorkOrderNumbers` | `MoveInWorkOrderNumbers[]` | string[] | write | `WorkOrders[*].WorkOrderNumber`, **fixed length 50**. |
| `MoveInFlag` | `MoveInFlag` | bool | write (trigger) | Set `true` to start processing. |
| `MoveInCompleteFlag` | `MoveInCompleteFlag` | bool | watch | Machine sets `true` when done. |
| `MoveInSuccessfulFlag` | `MoveInSuccessfulFlag` | bool | read (result) | Machine's success verdict. |
| `MoveInErrorText` | `MoveInErrorText` | string | read (result) | Machine's error message. |
| `MoveInBatchID` | `MoveInBatchID` | int | read (result) | Created batch id (0 = none). |

---

## `POST /mes/moveout`

Closes out a container's work orders on a machine's `MesReceiver`, and (when the machine is
configured for it) returns the recorded cycle data for the resulting batch.

### Request — `MoveOutRequest`

Same as `MoveInRequest` **minus `JobSequenceNumber`**:

| Field | Type | Notes |
|-------|------|-------|
| `SAPID` | string | SAP identifier of the target machine. Required. |
| `OperatorName` | string | Operator initiating the move-out. |
| `ContainerNumber` | string | MES container number. |
| `WorkOrders` | `WorkOrderInfo[]` | Work orders being moved out. Each: `WorkOrderNumber`, `PartNumber`. |

```json
{
  "SAPID": "100012345",
  "OperatorName": "chamalas",
  "ContainerNumber": "cont-012",
  "WorkOrders": [
    { "WorkOrderNumber": "W111111", "PartNumber": "P111111" }
  ]
}
```

### Response — `MoveOutResponse`

| Field | Type | Notes |
|-------|------|-------|
| `WasSuccessful` | bool | `true` only if the machine reported `MoveOutSuccessfulFlag = true`. |
| `ErrorText` | string | Failure reason, or the machine's `MoveOutErrorText`. |
| `BatchID` | int? | The machine's `MoveOutBatchID` (non-zero), **and** only when cycle storage is enabled (below). |
| `Data` | `MoveOutData[]` | Recorded cycle values (empty unless cycle storage is enabled). Defaults to `[]`. |

#### `MoveOutData`

| Field | Type | Source |
|-------|------|--------|
| `BatchId` | int | `MachineCycle.MachineBatchId`. |
| `CycleId` | int | `MachineCycle.MachineCycleId`. |
| `ValueName` | string | One of the cycle-data keys (below). |
| `Value` | object | The value for that key (`null` becomes empty string in SQL via `COALESCE`). |

### Behavior

Steps 1–5 mirror move-in (resolve `Machine` by `SAPID`; subscribe `MesMoveOutTagset`; require
`MoveOutReadyFlag == true`; write payload + `MoveOutFlag = true`; wait for `MoveOutCompleteFlag`).
On completion: `WasSuccessful = MoveOutSuccessfulFlag`, `ErrorText = MoveOutErrorText`.

**Cycle-data read (move-out only).** If `MoveOutBatchID != 0` **and** the machine's `OtherData`
contains the literal `"StoreCycleDataForMES"`:
- `BatchID = MoveOutBatchID`.
- Open a **raw** `SqlConnection` on `ConnectionStrings["BatchDB"]` and run a parameterized query
  (`@machineBatchID = MoveOutBatchID`) against `BT.dbo.MachineCycle`, expanding each cycle's
  `OtherData` JSON (via `OPENJSON … CROSS APPLY (VALUES …)`) into one `MoveOutData` row per
  key/value pair. Only rows where `ISJSON(mc.OtherData) = 1` are processed.

```sql
-- Shape of the cycle-data query (BT.dbo.MachineCycle, WHERE MachineBatchId = @machineBatchID)
SELECT mc.MachineBatchId, mc.MachineCycleId, v.ValueName, COALESCE(v.Value, N'') AS Value
FROM BT.dbo.MachineCycle mc
CROSS APPLY OPENJSON(mc.OtherData) WITH ( /* one column per key below */ ) od
CROSS APPLY (VALUES (N'ProgramNum', od.ProgramNum), /* … one row per key … */ ) AS v(ValueName, Value)
WHERE mc.MachineBatchId = @machineBatchID AND ISJSON(mc.OtherData) = 1;
```

**Cycle-data keys extracted** (19 per cycle): `ProgramNum`, `DewPointStart`, `SegmentStart2`,
`HighVacEndSeg1`, `SegmentStart3`, `SegmentStart4`, `SoakStartTime`, `SegmentStart5`,
`SoakEndTime`, `DurationFinalSoak`, `MaxSoakTemp`, `MinSoakTemp`, `MaxSoakPressure`,
`MinSoakPressure`, `SegmentStart6`, `QuenchTemp`, `DewPointMax`, `StartTimestamp`, `EndTimestamp`.

### Tags written / read (`{Machine.Code}.MesReceiver.*`)

Same set as move-in with the `MoveOut` prefix, **minus `JobSequenceNumber`**:
`MoveOutReadyFlag` (gate), `MoveOutMesContainerNum` / `MoveOutOperatorName` /
`MoveOutNumberWorkOrders` / `MoveOutPartNumbers[]` / `MoveOutWorkOrderNumbers[]` (write),
`MoveOutFlag` (trigger), `MoveOutCompleteFlag` (watch), `MoveOutSuccessfulFlag` /
`MoveOutErrorText` / `MoveOutBatchID` (result).

---

## How the handshake works (MXAccess)

`MesNotifier` holds a single process-wide `LMXProxyServerClass` (handle registered as
`"MesNotifier"`, wired to `OnDataChange` + `OnWriteComplete`). For each request it:

1. **Advise** — `AddItem(path)` then `AdviseSupervisory`; the first `OnDataChange` per tag
   resolves that tag's read task (success = quality `192` / OPC-Good). Values are coerced to the
   tag's CLR type via `Convert.ChangeType`.
2. **Write** — `LMXProxyServerClass.Write`; `OnWriteComplete` resolves the write task with the
   driver's success flag.
3. **Watch** — `Tag.OnValue(target)` completes when a subsequent `OnDataChange` reports a value
   equal to the target (used for the `…CompleteFlag → true` step).
4. **Unadvise** — every tag is unsubscribed and removed at the end of the request.

Everything is bounded by the per-request **30 s** `CancellationTokenSource`; on expiry, all
pending reads/writes/watches resolve as `false`.

The move-in/move-out contract is therefore a classic flag protocol on the machine's `MesReceiver`:
**read ready → write payload + set request flag → wait for complete flag → read success/error/batch**.

---

## Underlying data model — `Machine`

SQL table backing machine lookup (`APIServer.ServiceModel/DTO/Machine.cs`); `Code` is also the
MXAccess tag prefix and `SAPID` is the move-in/move-out selector:

| Column | Type | Constraints |
|--------|------|-------------|
| `MachineID` | int | PK, auto-increment |
| `Code` | string | Required, ≤ 50 — **MXAccess tag prefix** (`{Code}.MesReceiver.*`) |
| `Name` | string | Required, ≤ 50 |
| `ZTag` | string | ≤ 10 |
| `SAPID` | string | ≤ 10 — **move-in/out selector** |
| `Description` | string | ≤ 256 |
| `TimeZone` | string | Required, ≤ 128 |
| `MultipleBatch` / `MultipleCycle` / `Active` | bool | |
| `LastUpdate` | DateTime | |
| `LastUpdateBy` | string | Required, ≤ 128 |
| `OtherData` | string | Max-length text — **gates cycle storage** when it contains `"StoreCycleDataForMES"` |

---

## Behavior notes & gotchas

- **Work-order arrays are fixed length 50.** `ToFixedLength(50)` always writes exactly 50-element
  string arrays (padded with `null`); a 51st+ work order is **silently dropped**. `PartNumbers`
  and `WorkOrderNumbers` are written as two parallel arrays — index *i* of one corresponds to
  index *i* of the other (positional pairing from the same `WorkOrders` list). `NumberWorkOrders`
  carries the true count.
- **Connect-failure message gets clobbered.** The "connect" check and the "ready flag" check are
  sequential `if`s with no early return; if the subscribe fails, `MoveInReadyFlag.Value` is its
  default (`false`), so the ready-flag check also fires and **overwrites** `ErrorText` with
  `"…ready flag not set to true"`. A connection problem can surface as a ready-flag error.
- **`BatchID` is null unless the machine reports a non-zero batch id.** Move-out additionally
  requires cycle storage to be enabled before it sets `BatchID`.
- **Cycle data is opt-in per machine.** `Data` is empty unless `Machine.OtherData` contains
  `"StoreCycleDataForMES"` *and* `MoveOutBatchID != 0`.
- **Cycle-data read bypasses the test-DB redirect.** It uses a raw `ConnectionStrings["BatchDB"]`
  connection against hard-coded `BT.dbo.MachineCycle`, so a `test`-environment API key (which
  redirects *OrmLite* to `TestDb`) still reads cycle data from `BatchDB`.
- **`WasSuccessful` reflects the machine, not just the transport.** Even with a clean handshake,
  `WasSuccessful` is whatever the machine wrote to `…SuccessfulFlag`, and `ErrorText` is the
  machine's `…ErrorText`.
- **Synchronous blocking.** `MesServices` calls `.Result` on the async methods; combined with the
  single shared MXAccess proxy, throughput is effectively serialized per request thread.
- **Machine lookup uses `db.Single`** on `SAPID`; returns null when unmatched (handled), and the
  first match if `SAPID` is non-unique.
- The route attributes carry placeholder Swagger text (`Summary = "POST Summary"`,
  `Notes = "Notes"`); cosmetic only.

---

## Quick reference (curl)

```bash
# Move in a container + work order
curl -X POST http://<host>:<port>/mes/movein \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer <api-key>" \
  -d '{
        "SAPID":"100012345",
        "OperatorName":"chamalas",
        "JobSequenceNumber":"50",
        "ContainerNumber":"cont-012",
        "WorkOrders":[{"WorkOrderNumber":"W111111","PartNumber":"P111111"}]
      }'

# Move out the same container (returns cycle Data when the machine stores it for MES)
curl -X POST http://<host>:<port>/mes/moveout \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer <api-key>" \
  -d '{
        "SAPID":"100012345",
        "OperatorName":"chamalas",
        "ContainerNumber":"cont-012",
        "WorkOrders":[{"WorkOrderNumber":"W111111","PartNumber":"P111111"}]
      }'
```

> Exact auth header format depends on how `ApiKeyAuthProvider` is configured for the deployment
> (bearer token vs. HTTP Basic with the key as username). Confirm against the live Swagger/Postman
> metadata for the target server.

---

## ScadaBridge equivalent (porting note)

ScadaBridge re-implements these flows as **Inbound API methods** (`POST /api/{method}`,
`X-API-Key` header — *not* the ServiceStack `Authorization`/`apikey` scheme) that route to a
site's `MesReceiver` instance script via `Route.To(<instanceCode>).Call("MesMoveIn"/"MesMoveOut", …)`:

- `IpsenMESMoveIn` / `MesMoveIn` ↔ `/mes/movein`; `MesMoveOut` ↔ `/mes/moveout`.
- The ready/trigger/complete **flag handshake moves into the site instance script** (Site Runtime),
  rather than the central API driving MXAccess tags directly.
- Machine resolution by SAP id is a `Database.QuerySingleAsync<string>("BTDB", "SELECT … Machine WHERE SAPID=@s")`
  inside the inbound script (see `docs/plans/2026-06-16-ipsen-mes-movein-design.md`).

This file documents the **legacy** WWSupport contract for reference/parity during that migration.
