diff --git a/eFormAPI/Plugins/Workflow.Pn/Workflow.Pn.Test/ShiftedLookupIdsTests.cs b/eFormAPI/Plugins/Workflow.Pn/Workflow.Pn.Test/ShiftedLookupIdsTests.cs
new file mode 100644
index 00000000..91007f94
--- /dev/null
+++ b/eFormAPI/Plugins/Workflow.Pn/Workflow.Pn.Test/ShiftedLookupIdsTests.cs
@@ -0,0 +1,220 @@
+using System;
+using System.Linq;
+using System.Threading.Tasks;
+using eFormCore;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Logging.Abstractions;
+using Microting.eForm.Infrastructure;
+using Microting.eForm.Infrastructure.Constants;
+using Microting.eForm.Infrastructure.Data.Entities;
+using Microting.eForm.Infrastructure.Helpers;
+using Microting.eFormApi.BasePn.Abstractions;
+using Microting.eFormApi.BasePn.Infrastructure.Database.Entities;
+using Microting.eFormApi.BasePn.Infrastructure.Helpers.PluginDbOptions;
+using Microting.eFormWorkflowBase.Infrastructure.Data;
+using Microting.eFormWorkflowBase.Infrastructure.Data.Entities;
+using NSubstitute;
+using NUnit.Framework;
+using Testcontainers.MariaDb;
+using Workflow.Pn.Infrastructure.Models.Settings;
+using Workflow.Pn.Services.WorkflowCasesService;
+using Workflow.Pn.Services.WorkflowLocalizationService;
+
+#nullable enable
+namespace Workflow.Pn.Test;
+
+///
+/// microting/eform-angular-frontend#8141: on a MariaDB Galera cluster
+/// (auto_increment_increment=3) the SDK lookup tables get non-contiguous ids
+/// (Picture=15, SingleSelect=24, Dansk=3, ...). The workflow plugin must
+/// resolve field types by FieldType.Type, never by a hardcoded numeric id.
+///
+[TestFixture]
+public class ShiftedLookupIdsTests
+{
+ // Mimic a Galera node: every AUTO_INCREMENT step is 3, starting at 3,
+ // so the SDK field types seed as Text=3 .. Picture=15 .. SingleSelect=24.
+ private readonly MariaDbContainer _container = new MariaDbBuilder("mariadb:11.2")
+ .WithDatabase("myDb").WithUsername("bla").WithPassword("secretpassword")
+ .WithCommand("--auto-increment-increment=3", "--auto-increment-offset=3")
+ .Build();
+
+ private string _sdkConnection = null!;
+ private MicrotingDbContext _sdkDbContext = null!;
+ private WorkflowPnDbContext _workflowDbContext = null!;
+ private Language _danish = null!;
+
+ // SDK seeding order (SqlController.SettingCheckAll)
+ private static readonly string[] SdkFieldTypeOrder =
+ {
+ Constants.FieldTypes.Text, Constants.FieldTypes.Number, Constants.FieldTypes.None,
+ Constants.FieldTypes.CheckBox, Constants.FieldTypes.Picture, Constants.FieldTypes.Audio,
+ Constants.FieldTypes.Movie, Constants.FieldTypes.SingleSelect, Constants.FieldTypes.Comment,
+ Constants.FieldTypes.MultiSelect, Constants.FieldTypes.Date, Constants.FieldTypes.Signature,
+ Constants.FieldTypes.Timer, Constants.FieldTypes.EntitySearch, Constants.FieldTypes.EntitySelect,
+ Constants.FieldTypes.ShowPdf, Constants.FieldTypes.FieldGroup, Constants.FieldTypes.SaveButton,
+ Constants.FieldTypes.NumberStepper
+ };
+
+ [OneTimeSetUp]
+ public async Task OneTimeSetUp()
+ {
+ await _container.StartAsync();
+ var conn = _container.GetConnectionString();
+ _sdkConnection = conn.Replace("myDb", "420_SDK").Replace("bla", "root");
+ var pluginConnection = conn.Replace("myDb", "420_eform-angular-workflow-plugin").Replace("bla", "root");
+
+ _sdkDbContext = new DbContextHelper(_sdkConnection).GetDbContext();
+ await _sdkDbContext.Database.MigrateAsync();
+
+ foreach (var type in SdkFieldTypeOrder)
+ {
+ if (!await _sdkDbContext.FieldTypes.AnyAsync(x => x.Type == type))
+ {
+ await new FieldType { Type = type, Description = type }.Create(_sdkDbContext);
+ }
+ }
+
+ _danish = await _sdkDbContext.Languages.FirstOrDefaultAsync(x => x.LanguageCode == "da");
+ if (_danish == null)
+ {
+ _danish = new Language { Name = "Dansk", LanguageCode = "da" };
+ await _danish.Create(_sdkDbContext);
+ }
+
+ var optionsBuilder = new DbContextOptionsBuilder();
+ optionsBuilder.UseMySql(pluginConnection,
+ new MariaDbServerVersion(ServerVersion.AutoDetect(pluginConnection)));
+ _workflowDbContext = new WorkflowPnDbContext(optionsBuilder.Options);
+ await _workflowDbContext.Database.MigrateAsync();
+ }
+
+ [OneTimeTearDown]
+ public async Task OneTimeTearDown()
+ {
+ await _container.DisposeAsync();
+ if (_workflowDbContext != null) await _workflowDbContext.DisposeAsync();
+ if (_sdkDbContext != null) await _sdkDbContext.DisposeAsync();
+ }
+
+ [SetUp]
+ public async Task SetUp()
+ {
+ _workflowDbContext.PluginConfigurationValues.RemoveRange(_workflowDbContext.PluginConfigurationValues);
+ await _workflowDbContext.SaveChangesAsync();
+ }
+
+ [Test]
+ public async Task FieldTypeIds_AreShiftedLikeAGaleraInstall()
+ {
+ // Guard: the fixture really does produce shifted ids, otherwise the
+ // tests below would pass for the wrong reason.
+ Assert.That(await FieldTypeId(Constants.FieldTypes.Picture), Is.EqualTo(15));
+ Assert.That(await FieldTypeId(Constants.FieldTypes.SingleSelect), Is.EqualTo(24));
+ }
+
+ [Test]
+ public async Task Read_ResolvesPictureFieldByType_WhenPictureIdIsShifted()
+ {
+ var secondEform = await CreateCheckList("second eform", parentId: null);
+ var child = await CreateCheckList("second eform child", parentId: secondEform.Id);
+ await CreateField(child.Id, Constants.FieldTypes.Text, "Text");
+ await CreateField(child.Id, Constants.FieldTypes.Picture, "Pictures of task done");
+
+ await AddConfig($"WorkflowBaseSettings:{nameof(WorkflowBaseSettings.SecondEformId)}", secondEform.Id.ToString());
+ await AddConfig($"WorkflowBaseSettings:{nameof(WorkflowBaseSettings.IncidentPlaceListId)}", "1");
+ await AddConfig($"WorkflowBaseSettings:{nameof(WorkflowBaseSettings.IncidentTypeListId)}", "2");
+
+ var workflowCase = new WorkflowCase
+ {
+ Description = "desc",
+ DateOfIncident = DateTime.UtcNow,
+ IncidentPlace = "place",
+ IncidentType = "type",
+ };
+ await workflowCase.Create(_workflowDbContext);
+
+ var result = await CreateService(new WorkflowBaseSettings()).Read(workflowCase.Id);
+
+ Assert.That(result.Success, Is.True, result.Message);
+ Assert.That(result.Model.Id, Is.EqualTo(workflowCase.Id));
+ }
+
+ [Test]
+ public async Task GetPlaces_ResolvesSingleSelectFieldsByType_WhenSingleSelectIdIsShifted()
+ {
+ var firstEform = await CreateCheckList("first eform", parentId: null);
+ await CreateField(firstEform.Id, Constants.FieldTypes.Text, "Text");
+ await CreateField(firstEform.Id, Constants.FieldTypes.SingleSelect, "Incident type");
+ var placesField = await CreateField(firstEform.Id, Constants.FieldTypes.SingleSelect, "Incident place");
+ await CreateOption(placesField.Id, "Kontoret");
+ await CreateOption(placesField.Id, "Lageret");
+
+ var result = await CreateService(new WorkflowBaseSettings { FirstEformId = firstEform.Id }).GetPlaces();
+
+ Assert.That(result.Success, Is.True, result.Message);
+ Assert.That(result.Model.Select(x => x.Name), Is.EquivalentTo(new[] { "Kontoret", "Lageret" }));
+ }
+
+ private WorkflowCasesService CreateService(WorkflowBaseSettings settings)
+ {
+ var core = new Core { DbContextHelper = new DbContextHelper(_sdkConnection) };
+ var coreService = Substitute.For();
+ coreService.GetCore().Returns(core);
+
+ var userService = Substitute.For();
+ userService.GetCurrentUserLanguage().Returns(_danish);
+
+ var options = Substitute.For>();
+ options.Value.Returns(settings);
+
+ return new WorkflowCasesService(
+ coreService,
+ userService,
+ _workflowDbContext,
+ Substitute.For(),
+ options,
+ NullLogger.Instance,
+ null!);
+ }
+
+ private async Task FieldTypeId(string type) =>
+ (await _sdkDbContext.FieldTypes.SingleAsync(x => x.Type == type)).Id;
+
+ private async Task CreateCheckList(string label, int? parentId)
+ {
+ var checkList = new CheckList { Label = label, ParentId = parentId };
+ await checkList.Create(_sdkDbContext);
+ return checkList;
+ }
+
+ private async Task CreateField(int checkListId, string type, string label)
+ {
+ var field = new Field
+ {
+ CheckListId = checkListId,
+ FieldTypeId = await FieldTypeId(type),
+ Label = label
+ };
+ await field.Create(_sdkDbContext);
+ return field;
+ }
+
+ private async Task CreateOption(int fieldId, string text)
+ {
+ var option = new FieldOption { FieldId = fieldId, Key = text };
+ await option.Create(_sdkDbContext);
+ await new FieldOptionTranslation
+ {
+ FieldOptionId = option.Id,
+ LanguageId = _danish.Id,
+ Text = text
+ }.Create(_sdkDbContext);
+ }
+
+ private async Task AddConfig(string name, string value)
+ {
+ _workflowDbContext.PluginConfigurationValues.Add(new PluginConfigurationValue { Name = name, Value = value });
+ await _workflowDbContext.SaveChangesAsync();
+ }
+}
diff --git a/eFormAPI/Plugins/Workflow.Pn/Workflow.Pn.Test/Workflow.Pn.Test.csproj b/eFormAPI/Plugins/Workflow.Pn/Workflow.Pn.Test/Workflow.Pn.Test.csproj
index c65d09dd..1a5069d5 100644
--- a/eFormAPI/Plugins/Workflow.Pn/Workflow.Pn.Test/Workflow.Pn.Test.csproj
+++ b/eFormAPI/Plugins/Workflow.Pn/Workflow.Pn.Test/Workflow.Pn.Test.csproj
@@ -20,4 +20,8 @@
+
+
+
+
diff --git a/eFormAPI/Plugins/Workflow.Pn/Workflow.Pn/Services/WorkflowCasesService/WorkflowCasesService.cs b/eFormAPI/Plugins/Workflow.Pn/Workflow.Pn/Services/WorkflowCasesService/WorkflowCasesService.cs
index 5e0b9dc2..71d10117 100644
--- a/eFormAPI/Plugins/Workflow.Pn/Workflow.Pn/Services/WorkflowCasesService/WorkflowCasesService.cs
+++ b/eFormAPI/Plugins/Workflow.Pn/Workflow.Pn/Services/WorkflowCasesService/WorkflowCasesService.cs
@@ -181,7 +181,8 @@ public async Task> Read(int id)
var secondEformId = int.Parse(secondEformIdValue!);
var childCheckList = await sdkDbContext.CheckLists.FirstAsync(x => x.ParentId == secondEformId);
var fieldIdOfTaskCompleted =
- await sdkDbContext.Fields.FirstAsync(x => x.CheckListId == childCheckList.Id && x.FieldTypeId == 5);
+ await sdkDbContext.Fields.FirstAsync(x =>
+ x.CheckListId == childCheckList.Id && x.FieldType.Type == Constants.FieldTypes.Picture);
// get query
var workflowDbCase = await workflowPnDbContext.WorkflowCases
@@ -627,7 +628,7 @@ public async Task>> GetPlaces()
var fieldWithPlaces = await sdkDbContext.Fields
.Where(x => x.CheckListId == options.Value.FirstEformId)
- .Where(x => x.FieldTypeId == 8) // fieldType.id == 8; fieldType.type -> SingleSelect
+ .Where(x => x.FieldType.Type == Constants.FieldTypes.SingleSelect)
.Select(x => x.Id)
.Skip(1)
.FirstOrDefaultAsync();