feat(sms): complete SmsConfig bundle export/import wiring + GetSmsConfigurationByIdAsync (S10b)
This commit is contained in:
@@ -252,6 +252,40 @@ public sealed class ArtifactDiff
|
||||
return BuildItem("SmtpConfiguration", incoming.Host, changes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compares an incoming SMS provider configuration against the existing one in
|
||||
/// the database (S10b). Mirrors <see cref="CompareSmtpConfiguration"/>: keyed by
|
||||
/// the natural key <c>AccountSid</c>, with the auth token diffed presence-only
|
||||
/// (it lives in <see cref="SecretsBlock"/>, never compared by value).
|
||||
/// </summary>
|
||||
/// <param name="incoming">The incoming SMS configuration from the bundle.</param>
|
||||
/// <param name="existing">The existing SMS configuration in the database, or null if new.</param>
|
||||
/// <returns>An import preview item describing the conflict type and differences.</returns>
|
||||
public ImportPreviewItem CompareSmsConfiguration(SmsConfigDto incoming, SmsConfiguration? existing)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(incoming);
|
||||
if (existing is null) return New("SmsConfiguration", incoming.AccountSid);
|
||||
|
||||
var changes = new List<FieldChange>();
|
||||
AddIfDifferent(changes, "FromNumber", existing.FromNumber, incoming.FromNumber);
|
||||
AddIfDifferent(changes, "MessagingServiceSid", existing.MessagingServiceSid, incoming.MessagingServiceSid);
|
||||
AddIfDifferent(changes, "ApiBaseUrl", existing.ApiBaseUrl, incoming.ApiBaseUrl);
|
||||
AddIfDifferent(changes, "ConnectionTimeoutSeconds", existing.ConnectionTimeoutSeconds, incoming.ConnectionTimeoutSeconds);
|
||||
AddIfDifferent(changes, "MaxRetries", existing.MaxRetries, incoming.MaxRetries);
|
||||
AddIfDifferent(changes, "RetryDelay", existing.RetryDelay.ToString(), incoming.RetryDelay.ToString());
|
||||
|
||||
var existingHasSecret = !string.IsNullOrEmpty(existing.AuthToken);
|
||||
var incomingHasSecret = incoming.Secrets is not null && incoming.Secrets.Values.ContainsKey("AuthToken");
|
||||
if (existingHasSecret != incomingHasSecret)
|
||||
{
|
||||
changes.Add(new FieldChange("Secrets.AuthToken",
|
||||
existingHasSecret ? "<present>" : null,
|
||||
incomingHasSecret ? "<present>" : null));
|
||||
}
|
||||
|
||||
return BuildItem("SmsConfiguration", incoming.AccountSid, changes);
|
||||
}
|
||||
|
||||
// CompareApiKey was removed in re-arch C4: inbound API keys are not transported
|
||||
// between environments, so the import preview never diffs keys.
|
||||
|
||||
|
||||
@@ -429,6 +429,15 @@ public sealed class BundleImporter : IBundleImporter
|
||||
items.Add(_diff.CompareSmtpConfiguration(sm, existing));
|
||||
}
|
||||
|
||||
// ---- SmsConfigurations (S10b; no by-AccountSid lookup — scan GetAll) ----
|
||||
var allSms = await _notificationRepo.GetAllSmsConfigurationsAsync(ct).ConfigureAwait(false);
|
||||
var smsBySid = allSms.ToDictionary(s => s.AccountSid, s => s, StringComparer.Ordinal);
|
||||
foreach (var sms in content.SmsConfigs)
|
||||
{
|
||||
smsBySid.TryGetValue(sms.AccountSid, out var existing);
|
||||
items.Add(_diff.CompareSmsConfiguration(sms, existing));
|
||||
}
|
||||
|
||||
// ---- ApiKeys ----
|
||||
// Inbound API keys are not transported between environments (re-arch C4).
|
||||
// New bundles never carry a keys section. A pre-C4 bundle may still contain
|
||||
@@ -1010,6 +1019,7 @@ public sealed class BundleImporter : IBundleImporter
|
||||
await ApplyDatabaseConnectionsAsync(content.DatabaseConnections, resolutionMap, user, summary, ct).ConfigureAwait(false);
|
||||
await ApplyNotificationListsAsync(content.NotificationLists, resolutionMap, user, summary, ct).ConfigureAwait(false);
|
||||
await ApplySmtpConfigsAsync(content.SmtpConfigs, resolutionMap, user, summary, ct).ConfigureAwait(false);
|
||||
await ApplySmsConfigsAsync(content.SmsConfigs, resolutionMap, user, summary, ct).ConfigureAwait(false);
|
||||
// Inbound API keys are NOT applied from a bundle (re-arch C4) — any keys
|
||||
// in a legacy bundle were counted above (apiKeysIgnored) and are skipped.
|
||||
await ApplyApiMethodsAsync(content.ApiMethods, resolutionMap, user, summary, ct).ConfigureAwait(false);
|
||||
@@ -2534,6 +2544,80 @@ public sealed class BundleImporter : IBundleImporter
|
||||
target.Credentials = dto.Secrets?.Values.TryGetValue("Credentials", out var cred) == true ? cred : null;
|
||||
}
|
||||
|
||||
// SMS (S10b): mirrors ApplySmtpConfigsAsync exactly. SmsConfiguration is keyed by
|
||||
// AccountSid (the diff engine's natural key — analogous to SMTP's Host), so a
|
||||
// Rename targets AccountSid and Overwrite matches an existing config by AccountSid.
|
||||
// The provider auth token is decrypted out of the SecretsBlock via the same path
|
||||
// SMTP uses for its Credentials secret.
|
||||
private async Task ApplySmsConfigsAsync(
|
||||
IReadOnlyList<SmsConfigDto> dtos,
|
||||
Dictionary<(string, string), ImportResolution> map,
|
||||
string user,
|
||||
ImportSummary summary,
|
||||
CancellationToken ct)
|
||||
{
|
||||
if (dtos.Count == 0) return;
|
||||
var all = await _notificationRepo.GetAllSmsConfigurationsAsync(ct).ConfigureAwait(false);
|
||||
var bySid = all.ToDictionary(s => s.AccountSid, s => s, StringComparer.Ordinal);
|
||||
|
||||
foreach (var dto in dtos)
|
||||
{
|
||||
var resolution = ResolveOrDefault(map, "SmsConfiguration", dto.AccountSid);
|
||||
switch (resolution.Action)
|
||||
{
|
||||
case ResolutionAction.Skip:
|
||||
summary.Skipped++;
|
||||
break;
|
||||
case ResolutionAction.Rename:
|
||||
{
|
||||
var sid = resolution.RenameTo ?? dto.AccountSid;
|
||||
var sms = BuildSms(dto, overrideAccountSid: sid);
|
||||
await _notificationRepo.AddSmsConfigurationAsync(sms, ct).ConfigureAwait(false);
|
||||
await _auditService.LogAsync(user, "Create", "SmsConfiguration", "0", sid,
|
||||
new { sms.AccountSid, RenamedFrom = dto.AccountSid }, ct).ConfigureAwait(false);
|
||||
summary.Renamed++;
|
||||
break;
|
||||
}
|
||||
case ResolutionAction.Overwrite when bySid.TryGetValue(dto.AccountSid, out var ex):
|
||||
ApplySmsFields(ex, dto);
|
||||
await _notificationRepo.UpdateSmsConfigurationAsync(ex, ct).ConfigureAwait(false);
|
||||
await _auditService.LogAsync(user, "Update", "SmsConfiguration", ex.Id.ToString(), ex.AccountSid,
|
||||
new { ex.AccountSid }, ct).ConfigureAwait(false);
|
||||
summary.Overwritten++;
|
||||
break;
|
||||
case ResolutionAction.Add:
|
||||
case ResolutionAction.Overwrite:
|
||||
default:
|
||||
{
|
||||
var sms = BuildSms(dto, overrideAccountSid: null);
|
||||
await _notificationRepo.AddSmsConfigurationAsync(sms, ct).ConfigureAwait(false);
|
||||
await _auditService.LogAsync(user, "Create", "SmsConfiguration", "0", sms.AccountSid,
|
||||
new { sms.AccountSid }, ct).ConfigureAwait(false);
|
||||
summary.Added++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static SmsConfiguration BuildSms(SmsConfigDto dto, string? overrideAccountSid)
|
||||
{
|
||||
var sms = new SmsConfiguration(overrideAccountSid ?? dto.AccountSid, dto.FromNumber);
|
||||
ApplySmsFields(sms, dto);
|
||||
return sms;
|
||||
}
|
||||
|
||||
private static void ApplySmsFields(SmsConfiguration target, SmsConfigDto dto)
|
||||
{
|
||||
target.FromNumber = dto.FromNumber;
|
||||
target.MessagingServiceSid = dto.MessagingServiceSid;
|
||||
target.ApiBaseUrl = dto.ApiBaseUrl;
|
||||
target.ConnectionTimeoutSeconds = dto.ConnectionTimeoutSeconds;
|
||||
target.MaxRetries = dto.MaxRetries;
|
||||
target.RetryDelay = dto.RetryDelay;
|
||||
target.AuthToken = dto.Secrets?.Values.TryGetValue("AuthToken", out var token) == true ? token : null;
|
||||
}
|
||||
|
||||
// ApplyApiKeysAsync was removed in re-arch C4: inbound API keys are not
|
||||
// transported between environments, so a bundle never re-creates keys. Any keys
|
||||
// present in a legacy (pre-C4) bundle are counted and ignored in ApplyAsync.
|
||||
|
||||
Reference in New Issue
Block a user