Files
ScadaBridge/docs/plans/2026-06-16-ipsen-mes-movein-design.md
T
Joseph Doherty d8ccad6f54 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.
2026-06-16 22:00:10 -04:00

11 KiB

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):

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:
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).