diff --git a/src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/Repositories/NotificationRepository.cs b/src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/Repositories/NotificationRepository.cs
index fde0bc97..cca96c99 100644
--- a/src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/Repositories/NotificationRepository.cs
+++ b/src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/Repositories/NotificationRepository.cs
@@ -17,7 +17,12 @@ public class NotificationRepository : INotificationRepository
///
public async Task GetNotificationListByIdAsync(int id, CancellationToken cancellationToken = default)
- => await _context.Set().FindAsync(new object[] { id }, cancellationToken);
+ // Eager-load Recipients: single-list selection feeds the Transport exporter,
+ // which serializes the recipient collection — a FindAsync without the Include
+ // exported a recipient-less list (silent data loss).
+ => await _context.Set()
+ .Include(n => n.Recipients)
+ .FirstOrDefaultAsync(n => n.Id == id, cancellationToken);
///
public async Task> GetAllNotificationListsAsync(CancellationToken cancellationToken = default)
diff --git a/src/ZB.MOM.WW.ScadaBridge.Transport/Import/BundleImporter.cs b/src/ZB.MOM.WW.ScadaBridge.Transport/Import/BundleImporter.cs
index 1bab567a..4b864a0c 100644
--- a/src/ZB.MOM.WW.ScadaBridge.Transport/Import/BundleImporter.cs
+++ b/src/ZB.MOM.WW.ScadaBridge.Transport/Import/BundleImporter.cs
@@ -1050,6 +1050,16 @@ public sealed class BundleImporter : IBundleImporter
await ResolveAlarmScriptLinksAsync(content.Templates, resolutionMap, user, ct).ConfigureAwait(false);
await ResolveCompositionEdgesAsync(content.Templates, resolutionMap, user, ct).ConfigureAwait(false);
await ResolveInheritanceEdgesAsync(content.Templates, resolutionMap, user, ct).ConfigureAwait(false);
+ // Folder parent edges are name-keyed too — resolve them AFTER the flush
+ // above so every imported folder has a materialised id to point at
+ // (the Add pass created the folders parent-less). Mirrors the template
+ // inheritance/composition rewires.
+ await ResolveFolderParentEdgesAsync(content.TemplateFolders, resolutionMap, user, ct).ConfigureAwait(false);
+ // External-system methods have no navigation on the parent definition,
+ // so an Add can't cascade them — persist them here, once the parent
+ // system has a real id from the flush above. (Overwrite already syncs
+ // its methods inline against the existing parent id.)
+ await PersistAddedExternalSystemMethodsAsync(content.ExternalSystems, resolutionMap, user, ct).ConfigureAwait(false);
// ---- Import-time acyclicity guard over the merged template graph ----
// The two rewire passes above have STAGED (not yet saved) the bundle's
@@ -2544,6 +2554,8 @@ public sealed class BundleImporter : IBundleImporter
existing.EndpointUrl = dto.BaseUrl;
existing.AuthType = dto.AuthType;
existing.AuthConfiguration = dto.Secrets?.Values.TryGetValue("AuthConfiguration", out var auth) == true ? auth : null;
+ existing.MaxRetries = dto.MaxRetries;
+ existing.RetryDelay = dto.RetryDelay;
await _externalRepo.UpdateExternalSystemAsync(existing, ct).ConfigureAwait(false);
await _auditService.LogAsync(user, "Update", "ExternalSystem", existing.Id.ToString(), existing.Name,
new { existing.Name, existing.EndpointUrl }, ct).ConfigureAwait(false);
@@ -2575,6 +2587,8 @@ public sealed class BundleImporter : IBundleImporter
var sys = new ExternalSystemDefinition(overrideName ?? dto.Name, dto.BaseUrl, dto.AuthType)
{
AuthConfiguration = dto.Secrets?.Values.TryGetValue("AuthConfiguration", out var auth) == true ? auth : null,
+ MaxRetries = dto.MaxRetries,
+ RetryDelay = dto.RetryDelay,
};
return sys;
}
@@ -2687,6 +2701,116 @@ public sealed class BundleImporter : IBundleImporter
}
}
+ ///
+ /// Second-pass rewire of the name-keyed
+ /// edge. The Add pass creates folders parent-less (parent may be later in the
+ /// bundle and have no id yet); after the folder flush, resolve each folder's
+ /// bundle ParentName (through the rename resolution map) to a persisted
+ /// folder id and set the FK. Mirrors .
+ ///
+ private async Task ResolveFolderParentEdgesAsync(
+ IReadOnlyList dtos,
+ Dictionary<(string, string), ImportResolution> map,
+ string user,
+ CancellationToken ct)
+ {
+ if (dtos.Count == 0) return;
+ var allFolders = await _templateRepo.GetAllFoldersAsync(ct).ConfigureAwait(false);
+ var byName = allFolders.ToDictionary(f => f.Name, f => f, StringComparer.Ordinal);
+
+ foreach (var dto in dtos)
+ {
+ var resolution = ResolveOrDefault(map, "TemplateFolder", dto.Name);
+ if (resolution.Action == ResolutionAction.Skip) continue;
+
+ var childName = resolution.Action == ResolutionAction.Rename
+ ? resolution.RenameTo ?? dto.Name
+ : dto.Name;
+ if (!byName.TryGetValue(childName, out var child)) continue;
+
+ int? parentId = null;
+ if (dto.ParentName is not null)
+ {
+ // A renamed parent is persisted under its RenameTo name.
+ var parentResolution = ResolveOrDefault(map, "TemplateFolder", dto.ParentName);
+ var parentName = parentResolution.Action == ResolutionAction.Rename
+ ? parentResolution.RenameTo ?? dto.ParentName
+ : dto.ParentName;
+ if (byName.TryGetValue(parentName, out var parent))
+ {
+ parentId = parent.Id;
+ }
+ }
+
+ if (child.ParentFolderId != parentId)
+ {
+ child.ParentFolderId = parentId;
+ await _templateRepo.UpdateFolderAsync(child, ct).ConfigureAwait(false);
+ }
+ }
+ }
+
+ ///
+ /// Persists an added external system's method rows.
+ /// has no navigation on its parent definition, so an Add can't cascade its
+ /// methods the way template children do — they are added here after the parent
+ /// system has a materialised id from the central-config flush. Idempotent: a
+ /// method already present (e.g. a genuine Overwrite that synced its methods
+ /// inline) is skipped, so re-processing every non-Skip system is safe.
+ ///
+ private async Task PersistAddedExternalSystemMethodsAsync(
+ IReadOnlyList dtos,
+ Dictionary<(string, string), ImportResolution> map,
+ string user,
+ CancellationToken ct)
+ {
+ if (dtos.Count == 0) return;
+
+ foreach (var dto in dtos)
+ {
+ if (dto.Methods.Count == 0) continue;
+ var resolution = ResolveOrDefault(map, "ExternalSystem", dto.Name);
+ if (resolution.Action == ResolutionAction.Skip) continue;
+
+ var persistedName = resolution.Action == ResolutionAction.Rename
+ ? resolution.RenameTo ?? dto.Name
+ : dto.Name;
+ var sys = await _externalRepo.GetExternalSystemByNameAsync(persistedName, ct).ConfigureAwait(false);
+ if (sys is null) continue;
+
+ var existing = await _externalRepo.GetMethodsByExternalSystemIdAsync(sys.Id, ct).ConfigureAwait(false);
+ var existingNames = existing.Select(m => m.Name).ToHashSet(StringComparer.Ordinal);
+
+ foreach (var methodDto in dto.Methods)
+ {
+ if (existingNames.Contains(methodDto.Name)) continue; // already synced (Overwrite) or already added.
+ var method = new ExternalSystemMethod(methodDto.Name, methodDto.HttpMethod, methodDto.Path)
+ {
+ ExternalSystemDefinitionId = sys.Id,
+ ParameterDefinitions = methodDto.ParameterDefinitions,
+ ReturnDefinition = methodDto.ReturnDefinition,
+ };
+ await _externalRepo.AddExternalSystemMethodAsync(method, ct).ConfigureAwait(false);
+ await _auditService.LogAsync(
+ user,
+ "ExternalSystemMethodAdded",
+ "ExternalSystemMethod",
+ "0",
+ $"{sys.Name}.{method.Name}",
+ new
+ {
+ ExternalSystemName = sys.Name,
+ MethodName = method.Name,
+ method.HttpMethod,
+ method.Path,
+ method.ParameterDefinitions,
+ method.ReturnDefinition,
+ },
+ ct).ConfigureAwait(false);
+ }
+ }
+ }
+
private async Task ApplyDatabaseConnectionsAsync(
IReadOnlyList dtos,
Dictionary<(string, string), ImportResolution> map,