docs(inbound): design for Ipsen MES MoveIn -> reactor MES receiver
SAPID(+side) -> BTDB Machine.SAPID -> Code -> instance; inbound script does the lookup via a new scoped read-only DB helper, then routes to a new T1 template script that gates on MoveInReadyFlag and writes the MoveIn to the correct Left/Right MES receiver. -LT deferred.
This commit is contained in:
@@ -0,0 +1,188 @@
|
||||
# Design — Ipsen MES MoveIn → reactor MES receiver
|
||||
|
||||
**Date:** 2026-06-16
|
||||
**Status:** Approved (design); implementation pending
|
||||
**Scope:** ScadaBridge — inbound API `POST /api/IpsenMESMoveIn`, a small inbound-script DB
|
||||
capability, and a new template script on the reactor template that writes a MoveIn to the
|
||||
correct MES-receiver child.
|
||||
**Target environment:** production `wonder-app-vd03` (Central + Site, ConfigDb `ScadaBridge`
|
||||
on `wonder-app-vd03,1433`; reactors live on Site 2). BTDB = db-connection id 1 (`dbo.Machine`
|
||||
on `wonder-sql-vd03/BT`).
|
||||
|
||||
## 1. Problem
|
||||
|
||||
The deployed `IpsenMESMoveIn` inbound method is a stub (`Script = Parameters["SAPID"]`). It must
|
||||
become a real MoveIn: take the MES payload, resolve which reactor instance + side it targets,
|
||||
and write the values onto that reactor's correct MES-receiver child — but only when that
|
||||
receiver is ready to accept a MoveIn.
|
||||
|
||||
## 2. Verified facts (current system)
|
||||
|
||||
- **Inbound `/api` script context** (`InboundScriptContext`, `InboundScriptExecutor.cs`)
|
||||
exposes only `Parameters`, `Route`, `CancellationToken`. **No DB access.** It returns a
|
||||
JSON object validated against the method's `ReturnDefinition`; a thrown exception becomes a
|
||||
generic HTTP 500 ("Internal script error"), and a return-shape mismatch becomes a 500
|
||||
("…did not match its return definition"). `Route.To(instanceCode).Call(scriptName, params)`
|
||||
routes to the instance's site and returns that script's result object. RouteHelper can only
|
||||
target an **instance code** (no shared-script entry point).
|
||||
- **Template/site scripts** (`ScriptGlobals` / `ScriptRuntimeContext`) expose
|
||||
`Database.Connection(name)` + `Database.CachedWrite(...)`, `Attributes[...]`,
|
||||
`Children["<comp>"].Attributes/.SetAttribute/.GetAttribute`, `Instance.SetAttribute` (string
|
||||
value) which routes a `SetStaticAttributeCommand` to the Instance Actor → for data-connected
|
||||
attributes the DCL performs the **authorized Galaxy write**, synchronously, throwing on
|
||||
failure.
|
||||
- **`DatabaseGateway`/`IDatabaseGateway`** is registered via `AddExternalSystemGateway()` in
|
||||
**`Host/Program.cs`** (shared root) and `Host/SiteServiceRegistration.cs`. So the **Central**
|
||||
host can also open BTDB. `DatabaseConnectionDefinition` has no `SiteId` (global).
|
||||
- **Reactor template** = `IpsenFurnaceTitan` (TemplateId **1**). Instances **`Z28061`** (id 1)
|
||||
and **`Z28062`** (id 2), both Site 2, Enabled. Children (TemplateCompositions on T1):
|
||||
`LeftMESReceiver` (→T5), `RightMESReceiver` (→T6), `LeftReactorSide` (→T8),
|
||||
`RightReactorSide` (→T9). **No leak-test receiver.** No existing TemplateScripts/SharedScripts.
|
||||
- **MES-receiver attributes** live on base template T3 `MESReceiver` (full set;
|
||||
`MoveInPartNumbers` + `MoveInWorkOrderNumbers` are `List<String>`; also `MoveInType`). The
|
||||
derived T5/T6 still carry stale `String` placeholder rows for those two array attrs — expected
|
||||
to be `IsInherited` placeholders skipped at flatten time (the T3 `List` definition wins);
|
||||
**must be verified** before relying on array writes.
|
||||
- **BTDB `dbo.Machine`** (141 rows): match column **`SAPID`** (nvarchar 10) → result column
|
||||
**`Code`** (nvarchar 50); also `ZTag`, `Name`. The side suffix is baked into `Code`
|
||||
(`Z28061A` = T5A, `Z28061B` = T5B); `ZTag` of the A-row = bare `Z28061`. `SAPID` is null on
|
||||
the present reactor rows and there are no `Z28062*` rows — i.e. these are partial test rows;
|
||||
production must have `SAPID` populated. The ScadaBridge instance UniqueName is the **bare**
|
||||
reactor (`Z28061`/`Z28062`).
|
||||
|
||||
## 3. Decisions
|
||||
|
||||
1. **Lookup:** input value = `<SAP#>-<side>` (e.g. `131453-A`). Strip the suffix → SAP#;
|
||||
`SELECT Code FROM dbo.Machine WHERE SAPID=@sap`; the matched `Code` with a single trailing
|
||||
`A`/`B` removed = the **instance UniqueName**; the suffix maps `A`→Left, `B`→Right.
|
||||
2. **Leak test deferred:** only `-A`/`-B` are handled now. Any other suffix (`-A-LT`,
|
||||
`-B-LT`, missing, unknown) → `WasSuccessful=false` with an "unsupported side/target" message.
|
||||
Leak test can be added later once its MES-receiver child + Galaxy reference exist.
|
||||
3. **Architecture:** the **inbound script does the lookup** via a new scoped, read-only DB helper
|
||||
on the inbound context (named connections only; no raw `DbConnection`; parameterized). Single
|
||||
routed hop to the reactor template script. (Chosen over a config-only resolver-instance +
|
||||
2-hop approach.)
|
||||
4. **Write set (Q1):** the reactor script writes every provided param that maps to a receiver
|
||||
attribute, **except** `MoveInReadyFlag` (read-only gate) and `SAPID` (routing only).
|
||||
`MoveInCompleteFlag` / `MoveInSuccessfulFlag` / `MoveInErrorText` are treated as **PLC-owned
|
||||
outputs and are NOT written** (even though the API accepts the first three as params).
|
||||
Written: `MoveInMesContainerNum`, `MoveInBatchID`, `MoveInJobSequenceNumber`,
|
||||
`MoveInNumberWorkOrders`, `MoveInWorkOrderNumbers[]`, `MoveInPartNumbers[]`,
|
||||
`MoveInOperatorName`, `MoveInFlag`.
|
||||
5. **Return BatchID (Q2):** echo the input `MoveInBatchID` (no read-back).
|
||||
|
||||
## 4. Components
|
||||
|
||||
### A. ScadaBridge source change — inbound read-only DB helper
|
||||
- New `InboundDatabaseHelper` backed by `IDatabaseGateway`, surface:
|
||||
`T? QuerySingle<T>(string conn, string sql, object? args = null)` and
|
||||
`IReadOnlyList<IReadOnlyDictionary<string, object?>> Query(string conn, string sql, object? args = null)`.
|
||||
**No write methods.**
|
||||
- Add `Database` property to `InboundScriptContext`; construct it in `InboundScriptExecutor`
|
||||
from a service scope (gateway is scoped). Add the helper's assembly to the script
|
||||
`ScriptOptions.WithReferences(...)` and confirm `ForbiddenApiChecker` still passes (the script
|
||||
references only `Database.QuerySingle`, never `System.Data`).
|
||||
- Unit tests with a fake `IDatabaseGateway`.
|
||||
|
||||
### B. Rewrite inbound `/api/IpsenMESMoveIn` script (data, via management API)
|
||||
Pseudocode (always returns the 3-field shape; never throws out):
|
||||
```csharp
|
||||
try {
|
||||
var raw = (Parameters["SAPID"] as string ?? "").Trim();
|
||||
var dash = raw.IndexOf('-');
|
||||
if (dash < 0) return new { WasSuccessful = false, ErrorText = "SAPID missing -A/-B side suffix", BatchID = 0 };
|
||||
var sap = raw.Substring(0, dash);
|
||||
var suf = raw.Substring(dash + 1); // "A", "B", "A-LT", ...
|
||||
var side = suf == "A" ? "Left" : suf == "B" ? "Right" : null;
|
||||
if (side == null) return new { WasSuccessful = false, ErrorText = $"Unsupported side '{suf}'", BatchID = 0 };
|
||||
|
||||
var code = Database.QuerySingle<string>("BTDB",
|
||||
"SELECT TOP 1 Code FROM dbo.Machine WHERE SAPID=@s", new { s = sap });
|
||||
if (string.IsNullOrEmpty(code)) return new { WasSuccessful = false, ErrorText = $"No machine for SAP {sap}", BatchID = 0 };
|
||||
|
||||
var instance = (code.EndsWith("A") || code.EndsWith("B")) ? code.Substring(0, code.Length - 1) : code;
|
||||
|
||||
return await Route.To(instance).Call("IpsenMoveIn", new {
|
||||
side,
|
||||
MoveInMesContainerNum = Parameters["MoveInMesContainerNum"],
|
||||
MoveInBatchID = Parameters["MoveInBatchID"],
|
||||
MoveInJobSequenceNumber= Parameters["MoveInJobSequenceNumber"],
|
||||
MoveInNumberWorkOrders = Parameters["MoveInNumberWorkOrders"],
|
||||
MoveInWorkOrderNumbers = Parameters["MoveInWorkOrderNumbers"],
|
||||
MoveInPartNumbers = Parameters["MoveInPartNumbers"],
|
||||
MoveInOperatorName = Parameters["MoveInOperatorName"],
|
||||
MoveInFlag = Parameters["MoveInFlag"],
|
||||
});
|
||||
} catch (Exception ex) {
|
||||
return new { WasSuccessful = false, ErrorText = "MoveIn failed: " + ex.Message, BatchID = 0 };
|
||||
}
|
||||
```
|
||||
`ParameterDefinitions` / `ReturnDefinition` unchanged from today.
|
||||
|
||||
### C. New template script `IpsenMoveIn` on T1 (data, via management API)
|
||||
- On-demand (no trigger). Parameters: `side` + the written MoveIn fields. Return:
|
||||
`{ WasSuccessful, ErrorText, BatchID }`.
|
||||
- Logic:
|
||||
```csharp
|
||||
var recv = Children[ (string)Parameters["side"] == "Right" ? "RightMESReceiver" : "LeftMESReceiver" ];
|
||||
var batchId = Parameters.Get<int>("MoveInBatchID");
|
||||
var ready = recv.Attributes["MoveInReadyFlag"]; // live Galaxy read
|
||||
if (!(ready is bool b && b)) // tolerate bool/"True"/1
|
||||
return new { WasSuccessful = false, ErrorText = "MoveInReadyFlag is not true", BatchID = batchId };
|
||||
|
||||
await recv.SetAttribute("MoveInMesContainerNum", ...);
|
||||
await recv.SetAttribute("MoveInBatchID", ...);
|
||||
await recv.SetAttribute("MoveInJobSequenceNumber", ...);
|
||||
await recv.SetAttribute("MoveInNumberWorkOrders", ...);
|
||||
await recv.SetAttribute("MoveInWorkOrderNumbers", <array-encoded>); // see §5
|
||||
await recv.SetAttribute("MoveInPartNumbers", <array-encoded>);
|
||||
await recv.SetAttribute("MoveInOperatorName", ...);
|
||||
await recv.SetAttribute("MoveInFlag", ...);
|
||||
return new { WasSuccessful = true, ErrorText = "", BatchID = batchId };
|
||||
```
|
||||
- `MoveInReadyFlag` readiness check tolerates the value arriving as `bool`, `"True"`, or `1`.
|
||||
|
||||
## 5. Open implementation items (not blockers)
|
||||
|
||||
- **Array write encoding:** `SetAttribute(name, string)` takes a string. Confirm how the recent
|
||||
multivalue-array support expects a `List<String>` value to be written from a script (JSON
|
||||
array string vs. an `Attributes[name] = list` path). If arrays can't be written from a script
|
||||
today, add a small ScadaBridge item to support it.
|
||||
- **Derived placeholders:** verify T5/T6's stale `String` rows for the two array attrs are
|
||||
`IsInherited=true` (skipped at flatten so the T3 `List` types apply). If they are real
|
||||
overrides, fix them.
|
||||
- **SAPID format:** match is exact-string; live rows store leading zeros ("002797"). Confirm MES
|
||||
sends the SAP in the stored format, or normalize in the lookup.
|
||||
- **Multiple/ambiguous SAP rows:** `SELECT TOP 1`; if a SAP maps to >1 row they must resolve to
|
||||
the same bare instance — otherwise surface a clear error.
|
||||
|
||||
## 6. Error handling / return contract
|
||||
|
||||
Every path returns the validated `{WasSuccessful, ErrorText, BatchID}`:
|
||||
- success → `{true, "", batchId}`
|
||||
- not ready → `{false, "MoveInReadyFlag is not true", batchId}`
|
||||
- bad/missing side, unknown SAP, route/instance-not-found, write failure → `{false, "<reason>", 0}`
|
||||
|
||||
## 7. Testing
|
||||
|
||||
- Unit: `InboundDatabaseHelper` + inbound-script DB path (fake `IDatabaseGateway`); inbound
|
||||
parse/route logic; reactor script gate + write-set (fake instance/children).
|
||||
- On-box dry run: `POST /api/IpsenMESMoveIn` with a known SAP+`-A` → resolves to `Z28061`,
|
||||
gates when `MoveInReadyFlag` false, writes when true (verify values land on the Galaxy
|
||||
receiver via the gateway, as in the earlier enrichment). Negative: bad suffix, unknown SAP,
|
||||
not-ready.
|
||||
|
||||
## 8. Deployment
|
||||
|
||||
1. Implement + test Component A; build ScadaBridge.
|
||||
2. Rebuild + redeploy ScadaBridge Central on `wonder-app-vd03` (preserve `app\data\`;
|
||||
apply any EF migration BEFORE boot — Central is Production, no auto-migrate).
|
||||
3. Apply Component B (inbound script update) + Component C (new T1 template script) via the
|
||||
management API; redeploy config so the reactor instances pick up the new script.
|
||||
4. Verify on-box; commit source change; keep this design doc + the implementation plan.
|
||||
|
||||
## 9. Out of scope
|
||||
|
||||
- Leak-test (`-LT`) routing/receivers.
|
||||
- Writing PLC-output flags (Complete/Successful/ErrorText) from the MoveIn.
|
||||
- `Z28062` BTDB data completeness (operational data fix, not code).
|
||||
Reference in New Issue
Block a user