diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 1a5b3a71..a8f4d5cc 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -13,7 +13,7 @@ updates: timezone: "Asia/Amman" cooldown: default-days: 3 - semver-patch-days: 1 + semver-patch-days: 5 semver-minor-days: 3 semver-major-days: 7 open-pull-requests-limit: 10 @@ -36,7 +36,7 @@ updates: timezone: "Asia/Amman" cooldown: default-days: 3 - semver-patch-days: 1 + semver-patch-days: 5 semver-minor-days: 3 semver-major-days: 7 open-pull-requests-limit: 5 @@ -54,7 +54,7 @@ updates: timezone: "Asia/Amman" cooldown: default-days: 3 - semver-patch-days: 1 + semver-patch-days: 5 semver-minor-days: 3 semver-major-days: 7 open-pull-requests-limit: 5 diff --git a/SW.Bitween.Api/Controllers/GatewayController.cs b/SW.Bitween.Api/Controllers/GatewayController.cs index 2debf48e..a4372580 100644 --- a/SW.Bitween.Api/Controllers/GatewayController.cs +++ b/SW.Bitween.Api/Controllers/GatewayController.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Net.Mime; using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using SW.Bitween.Domain; @@ -56,6 +57,17 @@ private async Task ProcessAsync([FromRoute] string gatewayApiName if (apiGatewayPartner == null) return Unauthorized(); + // After authorisation on purpose: whether a gateway exists and is switched off is + // something only an attached partner should learn — checking it earlier would + // answer that for anyone who guessed the url. + // + // 503 rather than 404 because the url is right and the partner should keep it: a + // 404 reads as "wrong address" and sends someone hunting for a new one, where this + // is a gateway somebody switched off and will switch back on. + if (apiGateway.Inactive) + return StatusCode(StatusCodes.Status503ServiceUnavailable, + $"The '{apiGateway.Name}' gateway is currently deactivated."); + var subscription = await cache.SubscriptionByIdAsync(apiGatewayPartner.SubscriptionId); if (subscription == null) diff --git a/SW.Bitween.Api/Data/BitweenDbContext.cs b/SW.Bitween.Api/Data/BitweenDbContext.cs index 200e098b..1a3f0eae 100644 --- a/SW.Bitween.Api/Data/BitweenDbContext.cs +++ b/SW.Bitween.Api/Data/BitweenDbContext.cs @@ -229,6 +229,8 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) b.Property(p => p.Id).ValueGeneratedOnAdd(); b.Property(p => p.Name).IsRequired().HasMaxLength(200); b.Property(p => p.Groups).StoreAsJson(); + b.Property(p => p.AlertHandlerId).HasMaxLength(200).IsUnicode(false); + b.Property(p => p.AlertHandlerProperties).StoreAsJson(); }); modelBuilder.Entity(b => @@ -237,10 +239,36 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) b.HasKey(p => p.Id); b.Property(p => p.Id).IsUnicode(false).HasMaxLength(50); b.Property(p => p.On); - b.Property(p => p.GroupAttemptCounts).StoreAsJson(); b.HasIndex(p => p.On); }); + modelBuilder.Entity(b => + { + b.ToTable("ReceiveAttempts"); + b.Property(p => p.Id).ValueGeneratedOnAdd(); + b.Property(p => p.ErrorMessage).HasMaxLength(4000); + b.Property(p => p.ExchangeIds).IsSeparatorDelimited(); + b.HasIndex(p => new { p.SubscriptionId, p.StartedOn }); + }); + + modelBuilder.Entity(b => + { + b.ToTable("RetryGroupUsages"); + b.HasKey(p => new { p.SubscriptionId, p.GroupId }); + b.Property(p => p.AttemptsUsed); + b.Property(p => p.LastAttemptOn); + b.Property(p => p.ExhaustedNotifiedOn); + }); + + modelBuilder.Entity(b => + { + b.ToTable("RetryAlertOverrides"); + b.HasKey(p => new { p.SubscriptionId, p.GroupId }); + b.Property(p => p.AlertMode).HasConversion(); + b.Property(p => p.AlertHandlerId).HasMaxLength(200).IsUnicode(false); + b.Property(p => p.AlertHandlerProperties).StoreAsJson(); + }); + modelBuilder.Entity(b => { b.ToTable("Xchanges"); @@ -253,7 +281,6 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) b.Property(p => p.HandlerId).HasMaxLength(200).IsUnicode(false); b.Property(p => p.HandlerProperties).StoreAsJson(); b.Property(p => p.MapperProperties).StoreAsJson(); - b.Property(p => p.GroupAttemptCounts).StoreAsJson(); b.Property(p => p.InputContentType).IsUnicode(false).HasMaxLength(200); b.Property(p => p.ResponseMessageTypeName).IsUnicode(false).HasMaxLength(500); @@ -284,6 +311,10 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) b.Property(p => p.ResponseName).HasMaxLength(200); b.Property(p => p.ResponseContentType).IsUnicode(false).HasMaxLength(200); b.Property(p => p.OutputContentType).IsUnicode(false).HasMaxLength(200); + b.Property(p => p.RetryBlockedReason).HasMaxLength(500); + b.Property(p => p.RetryGroupId); + b.Property(p => p.AttemptNumber); + b.HasIndex(p => p.RetryGroupId); b.HasOne().WithOne().HasForeignKey(p => p.Id).OnDelete(DeleteBehavior.Cascade); @@ -368,7 +399,8 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) Disabled = false, Password = defaultPasswordHash, Deleted = false, - Role = AccountRole.Admin + Role = AccountRole.Admin, + FailedLoginCount = 0 }); }); diff --git a/SW.Bitween.Api/Domain/Accounts/Account.cs b/SW.Bitween.Api/Domain/Accounts/Account.cs index bbd28832..01e3b2ce 100644 --- a/SW.Bitween.Api/Domain/Accounts/Account.cs +++ b/SW.Bitween.Api/Domain/Accounts/Account.cs @@ -28,6 +28,24 @@ public Account(string displayName, string email, string password, AccountRole ro public string Password { get; set; } + public int FailedLoginCount { get; private set; } + public DateTime? LockoutEnd { get; private set; } + + public bool IsLockedOut(DateTime nowUtc) => LockoutEnd.HasValue && LockoutEnd.Value > nowUtc; + + public void RegisterSuccessfulLogin() + { + FailedLoginCount = 0; + LockoutEnd = null; + } + + // Admin action: clear a lockout before it expires. + public void Unlock() + { + FailedLoginCount = 0; + LockoutEnd = null; + } + public bool AddEmailLoginMethod(string email, string password) { diff --git a/SW.Bitween.Api/Domain/DelayedRetry.cs b/SW.Bitween.Api/Domain/DelayedRetry.cs index c744320a..c8bd1ffc 100644 --- a/SW.Bitween.Api/Domain/DelayedRetry.cs +++ b/SW.Bitween.Api/Domain/DelayedRetry.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using SW.PrimitiveTypes; namespace SW.Bitween.Domain; @@ -7,5 +6,4 @@ namespace SW.Bitween.Domain; public class DelayedRetry : BaseEntity { public DateTime On { get; set; } - public Dictionary GroupAttemptCounts { get; set; } = new(); } diff --git a/SW.Bitween.Api/Domain/Gateway/ApiGateway.cs b/SW.Bitween.Api/Domain/Gateway/ApiGateway.cs index 7f304f7f..4f1aef35 100644 --- a/SW.Bitween.Api/Domain/Gateway/ApiGateway.cs +++ b/SW.Bitween.Api/Domain/Gateway/ApiGateway.cs @@ -8,6 +8,13 @@ public class ApiGateway : BaseEntity,IAudited { public string Name { get; set; } public string UrlName { get; set; } + + /// + /// Turns the gateway off without deleting it. Deleting is the only alternative today, + /// and it takes the partner attachments with it — so a gateway that needs stopping for + /// an afternoon gets rebuilt by hand afterwards, or left running. + /// + public bool Inactive { get; set; } public ICollection Partners { get; set; } public DateTime CreatedOn { get; set; } public string CreatedBy { get; set; } diff --git a/SW.Bitween.Api/Domain/Gateway/BusGateway.cs b/SW.Bitween.Api/Domain/Gateway/BusGateway.cs index d2c83d41..0e61eaa5 100644 --- a/SW.Bitween.Api/Domain/Gateway/BusGateway.cs +++ b/SW.Bitween.Api/Domain/Gateway/BusGateway.cs @@ -8,6 +8,12 @@ public class BusGateway : BaseEntity, IAudited { public string Name { get; set; } public int DocumentId { get; set; } + + /// + /// Turns the gateway off without deleting it — its routes stop being offered the + /// message. See . + /// + public bool Inactive { get; set; } public ICollection Routes { get; set; } public DateTime CreatedOn { get; set; } public string CreatedBy { get; set; } diff --git a/SW.Bitween.Api/Domain/ReceiveAttempt.cs b/SW.Bitween.Api/Domain/ReceiveAttempt.cs new file mode 100644 index 00000000..a7ceaa54 --- /dev/null +++ b/SW.Bitween.Api/Domain/ReceiveAttempt.cs @@ -0,0 +1,20 @@ +using System; +using SW.Bitween.Model; +using SW.PrimitiveTypes; + +namespace SW.Bitween.Domain; + +/// +/// One execution of a Receiving subscription's receive step, written by ReceivingJob +/// itself right where it already catches the receiver's own failures — kept independent of +/// Quartz's own run history and unaffected by how Quartz treats a thrown exception. +/// +public class ReceiveAttempt : BaseEntity +{ + public int SubscriptionId { get; set; } + public DateTime StartedOn { get; set; } + public DateTime FinishedOn { get; set; } + public ReceiveOutcome Outcome { get; set; } + public string ErrorMessage { get; set; } + public string[] ExchangeIds { get; set; } = Array.Empty(); +} diff --git a/SW.Bitween.Api/Domain/RetryAlertOverride.cs b/SW.Bitween.Api/Domain/RetryAlertOverride.cs new file mode 100644 index 00000000..61d1b6b9 --- /dev/null +++ b/SW.Bitween.Api/Domain/RetryAlertOverride.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; +using SW.Bitween.Model; + +namespace SW.Bitween.Domain; + +/// +/// The most specific level of the retry-alert hierarchy: where one subscription's failures in one +/// retry group should be alerted, overriding whatever the group or the policy says. +/// +/// +/// Deliberately its own table rather than columns on . Usage rows are +/// deleted by RetryPolicies/resetusage, so config stored there would be silently discarded +/// every time someone cleared a spent budget. +/// +public class RetryAlertOverride +{ + /// The subscription this override applies to. + public int SubscriptionId { get; set; } + + /// RetryGroup.Id, which survives policy edits, so the override does too. + public Guid GroupId { get; set; } + + /// + /// Whether this level sends, stays silent, or defers upward. A row whose mode is + /// is equivalent to having no row at all. + /// + public RetryAlertMode AlertMode { get; set; } + + /// Adapter that delivers the alert. Required when is Send. + public string AlertHandlerId { get; set; } + + /// That adapter's own settings — api key, recipients, subject. + public IReadOnlyDictionary AlertHandlerProperties { get; set; } +} diff --git a/SW.Bitween.Api/Domain/RetryBudgetExhaustedEvent.cs b/SW.Bitween.Api/Domain/RetryBudgetExhaustedEvent.cs new file mode 100644 index 00000000..b5f7e29f --- /dev/null +++ b/SW.Bitween.Api/Domain/RetryBudgetExhaustedEvent.cs @@ -0,0 +1,42 @@ +using System; +using SW.PrimitiveTypes; + +namespace SW.Bitween.Domain; + +/// +/// Raised the one time a retry group's MaxAttemptsTotal runs out for a subscription, so the +/// configured alert handler can be told that failures matching that group have stopped being retried. +/// +/// +/// +/// Deliberately not an IHasWorkGroup event: it publishes under its own type name and is picked +/// up by a dedicated IConsume<RetryBudgetExhaustedEvent> consumer with its own queue. A +/// slow or broken alert handler therefore cannot delay or fail the ordinary notifier path, which +/// shares the work group's result queue. +/// +/// +/// Carried on rather than published directly, so it only reaches the bus +/// once the failure it describes has actually been committed. +/// +/// +public class RetryBudgetExhaustedEvent : BaseDomainEvent +{ + /// The failure that found the budget empty. + public string XchangeId { get; set; } + + public int SubscriptionId { get; set; } + + public Guid GroupId { get; set; } + + /// The group's name as it was when the budget ran out, in case it is later renamed. + public string GroupName { get; set; } + + /// + /// The ceiling that was reached. Not paired with an "used" count, because at exhaustion the two + /// are the same number — except when the ceiling was lowered below what had already been spent, + /// where the ceiling is still the meaningful figure. + /// + public int MaxAttemptsTotal { get; set; } + + public DateTime OccurredOn { get; set; } +} diff --git a/SW.Bitween.Api/Domain/RetryGroupUsage.cs b/SW.Bitween.Api/Domain/RetryGroupUsage.cs new file mode 100644 index 00000000..b19f7a9b --- /dev/null +++ b/SW.Bitween.Api/Domain/RetryGroupUsage.cs @@ -0,0 +1,41 @@ +using System; + +namespace SW.Bitween.Domain; + +/// +/// Running total of the retries one retry group has spent for one integration, backing +/// RetryBudget.MaxAttemptsTotal. That cap is shared by every message hitting the +/// group, so it cannot be tracked on an individual xchange. +/// +/// +/// Once reaches the group's MaxAttemptsTotal the group stops +/// retrying for that integration until this row is cleared. A row that has reached the cap is cleared +/// by the integration's next success — the only signal that the downstream it was failing against has +/// recovered — or by one of the reset endpoints. A row still below the cap is left alone by a success: +/// the cap is there for a downstream that fails some messages and succeeds others, which is exactly +/// when crediting it back would stop it ever being reached. +/// +public class RetryGroupUsage +{ + /// The integration whose budget this is. A shared policy gives each one its own total. + public int SubscriptionId { get; set; } + + /// RetryGroup.Id, which survives policy edits, so the total does too. + public Guid GroupId { get; set; } + + public int AttemptsUsed { get; set; } + + /// When the last attempt was claimed — the only clue left once a group is exhausted. + public DateTime LastAttemptOn { get; set; } + + /// + /// When the exhaustion alert for this integration and group was claimed, or null while + /// the budget still has room. + /// + /// + /// Claiming this is what makes the alert fire exactly once: every failure after the budget runs + /// out would otherwise raise another one. Reset deletes the whole row, which re-arms the alert + /// along with the budget. + /// + public DateTime? ExhaustedNotifiedOn { get; set; } +} diff --git a/SW.Bitween.Api/Domain/RetryPolicy.cs b/SW.Bitween.Api/Domain/RetryPolicy.cs index c1793896..c452e6f3 100644 --- a/SW.Bitween.Api/Domain/RetryPolicy.cs +++ b/SW.Bitween.Api/Domain/RetryPolicy.cs @@ -9,6 +9,15 @@ public class RetryPolicy : BaseEntity, IAudited, IRetryPolicy { public string Name { get; set; } public List Groups { get; set; } = []; + + /// + /// Default destination for "retry budget exhausted" alerts, used by every group that does not + /// override it. Null means no alert unless a group or a subscription+group override defines one. + /// + public string AlertHandlerId { get; set; } + + /// That adapter's own settings — api key, recipients, subject. + public IReadOnlyDictionary AlertHandlerProperties { get; set; } public DateTime CreatedOn { get; set; } public string CreatedBy { get; set; } public DateTime? ModifiedOn { get; set; } diff --git a/SW.Bitween.Api/Domain/Xchange/Xchange.cs b/SW.Bitween.Api/Domain/Xchange/Xchange.cs index e051fa49..164ae8f7 100644 --- a/SW.Bitween.Api/Domain/Xchange/Xchange.cs +++ b/SW.Bitween.Api/Domain/Xchange/Xchange.cs @@ -60,9 +60,10 @@ public Xchange(Subscription subscription, XchangeFile file, string[] references } //retry xchange - public Xchange(Xchange xchange, XchangeFile file, IWorkGroup workGroup, IReadOnlyDictionary groupAttemptCounts = null) : + public Xchange(Xchange xchange, XchangeFile file, IWorkGroup workGroup, bool manualRetry = false) : this(xchange.DocumentId, workGroup, file, xchange.References) { + ManualRetry = manualRetry; SubscriptionId = xchange.SubscriptionId; PartnerId = xchange.PartnerId; MapperId = xchange.MapperId; @@ -72,14 +73,15 @@ public Xchange(Xchange xchange, XchangeFile file, IWorkGroup workGroup, IReadOnl ResponseSubscriptionId = xchange.ResponseSubscriptionId; RetryFor = xchange.Id; CorrelationId = xchange.CorrelationId; - GroupAttemptCounts = groupAttemptCounts == null ? null : new Dictionary(groupAttemptCounts); } //retry with reset subscription properties public Xchange(Subscription subscription, Xchange xchange, XchangeFile file, Partner gatewayPartner = null, - GlobalAdapterValuesSet[] globalAdapterValuesSets = null, IReadOnlyDictionary groupAttemptCounts = null) : + GlobalAdapterValuesSet[] globalAdapterValuesSets = null, IReadOnlyDictionary groupAttemptCounts = null, + bool manualRetry = false) : this(xchange.DocumentId, subscription.WorkGroup, file, xchange.References) { + ManualRetry = manualRetry; SubscriptionId = xchange.SubscriptionId; PartnerId = xchange.PartnerId ?? subscription.PartnerId; MapperId = subscription.MapperId; @@ -89,7 +91,6 @@ public Xchange(Subscription subscription, Xchange xchange, XchangeFile file, Par ResponseSubscriptionId = subscription.ResponseSubscriptionId; RetryFor = xchange.Id; CorrelationId = xchange.CorrelationId; - GroupAttemptCounts = groupAttemptCounts == null ? null : new Dictionary(groupAttemptCounts); } public int? SubscriptionId { get; private set; } @@ -109,7 +110,14 @@ public Xchange(Subscription subscription, Xchange xchange, XchangeFile file, Par public string ResponseMessageTypeName { get; private set; } public string RetryFor { get; private set; } + + /// + /// true when a person asked for this retry, rather than the retry policy scheduling + /// it. The policy leaves these alone, so pressing Retry never spends the group's shared + /// budget and never quietly starts an automatic chain behind the person who pressed it. + /// + public bool ManualRetry { get; private set; } + public string CorrelationId { get; set; } - public IReadOnlyDictionary GroupAttemptCounts { get; private set; } } } \ No newline at end of file diff --git a/SW.Bitween.Api/Domain/XchangeNotification.cs b/SW.Bitween.Api/Domain/XchangeNotification.cs index 56a52186..97e8094b 100644 --- a/SW.Bitween.Api/Domain/XchangeNotification.cs +++ b/SW.Bitween.Api/Domain/XchangeNotification.cs @@ -5,9 +5,12 @@ namespace SW.Bitween.Domain { public class XchangeNotification:BaseEntity { + /// Name recorded for rows written by the retry-budget alert rather than a notifier. + public const string RetryBudgetAlertName = "Retry budget alert"; + private XchangeNotification(){} - public XchangeNotification(string xchangeId, int notifierId, string notifierName, string exception = null) + public XchangeNotification(string xchangeId, int? notifierId, string notifierName, string exception = null) { XchangeId = xchangeId; FinishedOn = DateTime.UtcNow; @@ -16,12 +19,21 @@ public XchangeNotification(string xchangeId, int notifierId, string notifierName NotifierId = notifierId; NotifierName = notifierName; } - + + /// + /// Logs an attempt to deliver a "retry budget exhausted" alert. These rows have no + /// because the alert is configured on the retry policy rather than + /// on a notifier — which is also how the send is recognised as already done on a redelivery. + /// + public static XchangeNotification ForRetryBudgetAlert(string xchangeId, string exception = null) => + new(xchangeId, null, RetryBudgetAlertName, exception); + public string XchangeId { get; private set; } public bool Success { get; set; } - public int NotifierId { get; set; } + /// The notifier that produced this row, or null for a retry-budget alert. + public int? NotifierId { get; set; } public string NotifierName { get; set; } diff --git a/SW.Bitween.Api/Domain/XchangeResult/XchangeResult.cs b/SW.Bitween.Api/Domain/XchangeResult/XchangeResult.cs index ac674785..ed666dee 100644 --- a/SW.Bitween.Api/Domain/XchangeResult/XchangeResult.cs +++ b/SW.Bitween.Api/Domain/XchangeResult/XchangeResult.cs @@ -62,7 +62,53 @@ public XchangeResult(string xchangeId,WorkGroup workGroup, XchangeFile outputFil public bool ResponseBad { get; private set; } public string ResponseContentType { get; private set; } + /// + /// Why the retry policy declined to schedule another attempt for this failure, or + /// null when a retry was scheduled or no policy applied. Without it a group that + /// has exhausted its budget looks identical to one that never matched. + /// + public string RetryBlockedReason { get; private set; } + /// Records the policy's refusal so it can be shown alongside the failure. + public void SetRetryBlocked(string reason) => RetryBlockedReason = reason; + /// + /// The retry group that matched this failure, or null when no policy applied or none + /// matched. The evaluator works this out and would otherwise discard it, leaving no way to + /// ask which failures a group is responsible for. + /// + public Guid? RetryGroupId { get; private set; } + + /// + /// How many times this message had already been attempted when the policy evaluated it + /// (0 on the original run). Stored because deriving it means walking the whole + /// Xchange.RetryFor chain one query at a time. + /// + public int? AttemptNumber { get; private set; } + + /// Records which group owned this failure, and how far into its retries it was. + public void SetRetryEvaluation(Guid groupId, int attemptNumber) + { + RetryGroupId = groupId; + AttemptNumber = attemptNumber; + } + + /// + /// Announces that this failure was the one that emptied the group's shared budget. Only ever + /// called by the caller that won the claim, so the event is raised once per exhaustion. + /// + public void RaiseBudgetExhausted(int subscriptionId, Guid groupId, string groupName, + int maxAttemptsTotal) + { + Events.Add(new RetryBudgetExhaustedEvent + { + XchangeId = Id, + SubscriptionId = subscriptionId, + GroupId = groupId, + GroupName = groupName, + MaxAttemptsTotal = maxAttemptsTotal, + OccurredOn = DateTime.UtcNow + }); + } } } diff --git a/SW.Bitween.Api/Extensions/PasswordValidationExtensions.cs b/SW.Bitween.Api/Extensions/PasswordValidationExtensions.cs new file mode 100644 index 00000000..3da0b26a --- /dev/null +++ b/SW.Bitween.Api/Extensions/PasswordValidationExtensions.cs @@ -0,0 +1,17 @@ +using FluentValidation; + +namespace SW.Bitween +{ + public static class PasswordValidationExtensions + { + // Shared server-side password policy so every password-setting path + // (create account, change password) enforces the exact same rule. + public static IRuleBuilderOptions Password(this IRuleBuilder rule) => + rule.NotEmpty().WithMessage("Password is required.") + .MinimumLength(8).WithMessage("Password must be at least 8 characters.") + .Matches("[A-Z]").WithMessage("Password must contain an uppercase letter.") + .Matches("[a-z]").WithMessage("Password must contain a lowercase letter.") + .Matches("[0-9]").WithMessage("Password must contain a number.") + .Matches("[^A-Za-z0-9]").WithMessage("Password must contain a special character."); + } +} diff --git a/SW.Bitween.Api/Resources/Accounts/ChangePassword.cs b/SW.Bitween.Api/Resources/Accounts/ChangePassword.cs index 7c6e8800..bd06695c 100644 --- a/SW.Bitween.Api/Resources/Accounts/ChangePassword.cs +++ b/SW.Bitween.Api/Resources/Accounts/ChangePassword.cs @@ -1,5 +1,6 @@ using System; using System.Threading.Tasks; +using FluentValidation; using Microsoft.EntityFrameworkCore; using SW.Bitween.Domain.Accounts; using SW.Bitween.Model; @@ -37,4 +38,12 @@ public async Task Handle(ChangePasswordModel request) return null; } + + private class Validate : AbstractValidator + { + public Validate() + { + RuleFor(i => i.NewPassword).Password(); + } + } } \ No newline at end of file diff --git a/SW.Bitween.Api/Resources/Accounts/Create.cs b/SW.Bitween.Api/Resources/Accounts/Create.cs index 173f3b14..83dd0ec8 100644 --- a/SW.Bitween.Api/Resources/Accounts/Create.cs +++ b/SW.Bitween.Api/Resources/Accounts/Create.cs @@ -75,7 +75,8 @@ public Validate(BitweenOptions bitweenOptions) { RuleFor(i => i.Name).NotEmpty(); RuleFor(i => i.Email).NotEmpty(); - RuleFor(i => i.Password).NotEmpty().When(_ => !bitweenOptions.DisableEmailPasswordLogin); + RuleFor(i => i.Password).Password().When(_ => !bitweenOptions.DisableEmailPasswordLogin); + RuleFor(i => i.Role).NotNull(); } } } diff --git a/SW.Bitween.Api/Resources/Accounts/Login.cs b/SW.Bitween.Api/Resources/Accounts/Login.cs index b031b2bf..e47bc8dd 100644 --- a/SW.Bitween.Api/Resources/Accounts/Login.cs +++ b/SW.Bitween.Api/Resources/Accounts/Login.cs @@ -15,6 +15,9 @@ namespace SW.Bitween.Resources.Accounts [Unprotect] public class Login : ICommandHandler { + private const int MaxFailedLoginAttempts = 5; + private static readonly TimeSpan LockoutDuration = TimeSpan.FromMinutes(15); + private readonly BitweenDbContext _dbContext; private readonly BitweenOptions _BitweenSettings; private readonly JwtTokenParameters _jwtTokenParameters; @@ -68,6 +71,16 @@ public async Task Handle(UserLogin request) throw new SWException("Email and password login is disabled. Please sign in with Microsoft."); } + // A credential login must carry both a username and a password. Without this guard a + // request with a valid username but empty/missing password would skip verification + // below and still be issued a token. + if (string.IsNullOrEmpty(refreshTokenValue) && string.IsNullOrEmpty(request.MsToken) && + (string.IsNullOrEmpty(request.Username) || string.IsNullOrEmpty(request.Password))) + { + _logger.LogWarning("Login rejected: missing username or password on a credential login."); + throw new SWException("Invalid username or password."); + } + if (!string.IsNullOrEmpty(refreshTokenValue)) { // account query already filtered above @@ -118,20 +131,45 @@ public async Task Handle(UserLogin request) if (string.IsNullOrEmpty(refreshTokenValue) && !string.IsNullOrEmpty(request.Username) && !string.IsNullOrEmpty(request.Password) && string.IsNullOrEmpty(request.MsToken)) { + var nowUtc = DateTime.UtcNow; + if (account.IsLockedOut(nowUtc)) + { + var minutes = (int)Math.Ceiling((account.LockoutEnd!.Value - nowUtc).TotalMinutes); + _logger.LogWarning("Login rejected: account '{Email}' is temporarily locked.", account.Email); + throw new SWException( + $"Your account is temporarily locked due to multiple failed login attempts. " + + $"Please try again in {minutes} minute{(minutes == 1 ? "" : "s")}."); + } + if (request.Password == null || !SecurePasswordHasher.Verify(request.Password, account.Password)) + { + // Atomic DB-side update so concurrent wrong-password attempts can't read the + // same count and lose increments, which would let them slip past the lockout. + var lockoutEnd = nowUtc.Add(LockoutDuration); + await _dbContext.Set() + .Where(a => a.Id == account.Id) + .ExecuteUpdateAsync(s => s + .SetProperty(a => a.LockoutEnd, + a => a.FailedLoginCount + 1 >= MaxFailedLoginAttempts ? lockoutEnd : a.LockoutEnd) + .SetProperty(a => a.FailedLoginCount, + a => a.FailedLoginCount + 1 >= MaxFailedLoginAttempts ? 0 : a.FailedLoginCount + 1)); throw new SWException("Invalid username or password."); + } + + account.RegisterSuccessfulLogin(); } var newRefreshToken = CreateRefreshToken(account, LoginMethod.EmailAndPassword); await _dbContext.SaveChangesAsync(); - // Set refresh token as HttpOnly cookie — not accessible to JavaScript - var isHttps = _httpContextAccessor.HttpContext?.Request.IsHttps ?? false; + // Set refresh token as a secure, HttpOnly cookie — not accessible to JavaScript. + // Secure is always on: the app is served over HTTPS, and TLS is terminated at the + // reverse proxy, so Request.IsHttps would otherwise be false and drop the attribute. _httpContextAccessor.HttpContext?.Response.Cookies.Append("refresh_token", newRefreshToken, new CookieOptions { HttpOnly = true, - Secure = isHttps, + Secure = true, SameSite = SameSiteMode.Lax, Expires = DateTimeOffset.UtcNow.AddDays(30) }); diff --git a/SW.Bitween.Api/Resources/Accounts/Logout.cs b/SW.Bitween.Api/Resources/Accounts/Logout.cs index 5a8826ac..e6fe503e 100644 --- a/SW.Bitween.Api/Resources/Accounts/Logout.cs +++ b/SW.Bitween.Api/Resources/Accounts/Logout.cs @@ -40,6 +40,9 @@ public async Task Handle(UserLogout request) httpContext.Response.Cookies.Delete("refresh_token"); } + // Tell the browser to wipe cookies, cache and storage for this origin on logout. + httpContext?.Response.Headers.Append("Clear-Site-Data", "\"cache\", \"cookies\", \"storage\""); + return new { }; } } diff --git a/SW.Bitween.Api/Resources/Accounts/Search.cs b/SW.Bitween.Api/Resources/Accounts/Search.cs index be767dcd..3c32e5ed 100644 --- a/SW.Bitween.Api/Resources/Accounts/Search.cs +++ b/SW.Bitween.Api/Resources/Accounts/Search.cs @@ -32,6 +32,7 @@ public async Task Handle(SearchMembersModel request) if (request.Lookup) { + // id -> display name only; needed across the app (e.g. audit trails) return await query.OrderBy(i => i.DisplayName) .ToDictionaryAsync(i => i.Id, i => i.DisplayName); } @@ -48,7 +49,8 @@ public async Task Handle(SearchMembersModel request) Name = a.DisplayName, Id = a.Id, Disabled = a.Disabled, - Role = a.Role.ToString() + Role = a.Role.ToString(), + LockoutEnd = a.LockoutEnd }) .ToListAsync(); diff --git a/SW.Bitween.Api/Resources/Accounts/Unlock.cs b/SW.Bitween.Api/Resources/Accounts/Unlock.cs new file mode 100644 index 00000000..50c765cb --- /dev/null +++ b/SW.Bitween.Api/Resources/Accounts/Unlock.cs @@ -0,0 +1,33 @@ +using System.Threading.Tasks; +using SW.Bitween.Domain.Accounts; +using SW.Bitween.Model; +using SW.PrimitiveTypes; + +namespace SW.Bitween.Resources.Accounts; + +[HandlerName("unlock")] +public class Unlock : ICommandHandler +{ + private readonly BitweenDbContext _dbContext; + private readonly RequestContext _requestContext; + + public Unlock(BitweenDbContext dbContext, RequestContext requestContext) + { + _dbContext = dbContext; + _requestContext = requestContext; + } + + public async Task Handle(int key, UnlockAccountModel request) + { + await _requestContext.EnsurePermission(_dbContext, Model.Permissions.Users.Edit); + + var account = await _dbContext.Set().FindAsync(key); + if (account is null) + throw new SWValidationException("ACCOUNT_NOT_FOUND", $"No account exists with the id {key}"); + + account.Unlock(); + await _dbContext.SaveChangesAsync(); + + return null; + } +} diff --git a/SW.Bitween.Api/Resources/ApiGateways/AddPartner.cs b/SW.Bitween.Api/Resources/ApiGateways/AddPartner.cs index 764460ec..55edeed6 100644 --- a/SW.Bitween.Api/Resources/ApiGateways/AddPartner.cs +++ b/SW.Bitween.Api/Resources/ApiGateways/AddPartner.cs @@ -5,6 +5,7 @@ using Microsoft.EntityFrameworkCore; using System.Linq; using SW.Bitween.Domain; +using SW.Bitween.Resources.Subscriptions; namespace SW.Bitween.Resources.ApiGateways { @@ -13,11 +14,14 @@ public class AddPartner : ICommandHandler { private readonly BitweenDbContext _dbContext; private readonly RequestContext _requestContext; + private readonly AdapterRequirements _adapterRequirements; - public AddPartner(BitweenDbContext dbContext, RequestContext requestContext) + public AddPartner(BitweenDbContext dbContext, RequestContext requestContext, + AdapterRequirements adapterRequirements) { _dbContext = dbContext; _requestContext = requestContext; + _adapterRequirements = adapterRequirements; } public async Task Handle(int gatewayId, ApiGatewayPartnerCreate model) @@ -31,31 +35,50 @@ public async Task Handle(int gatewayId, ApiGatewayPartnerCreate model) if (gateway == null) throw new SWNotFoundException($"ApiGateway with Id {gatewayId} not found"); - // Validate subscription exists and is of type GatewayApiCall - var subscription = await _dbContext.Set() - .FirstOrDefaultAsync(s => s.Id == model.SubscriptionId); - - if (subscription == null) - throw new SWNotFoundException($"Subscription with Id {model.SubscriptionId} not found"); - - if (subscription.Type != SubscriptionType.GatewayApiCall) - throw new SWException($"Subscription must be of type GatewayApiCall. Current type: {subscription.Type}"); - - // Check if partner already exists - var existingPartner = gateway.Partners != null - ? gateway.Partners.FirstOrDefault(p => p.PartnerId == model.PartnerId && p.SubscriptionId == model.SubscriptionId) - : null; - - if (existingPartner != null) - throw new SWException("Partner already exists in this gateway"); + InlineIntegration.EnsureExactlyOne(model.SubscriptionId, model.NewIntegration); var partnerLink = new ApiGatewayPartner { ApiGatewayId = gatewayId, - PartnerId = model.PartnerId, - SubscriptionId = model.SubscriptionId + PartnerId = model.PartnerId }; + if (model.NewIntegration != null) + { + // Staged, not saved — the attachment takes its foreign key from the subscription EF + // is tracking, so the pair lands on the one SaveChangesAsync below or not at all. + // Nothing to check for a duplicate against: an integration that does not exist yet + // cannot already be attached. + // An API gateway is not bound to an information type the way a bus gateway is, + // so this one comes from the caller. + var integration = await InlineIntegration.Stage( + _dbContext, _adapterRequirements, model.NewIntegration, + model.NewIntegration.DocumentId, SubscriptionType.GatewayApiCall); + partnerLink.Subscription = integration; + } + else + { + // Validate subscription exists and is of type GatewayApiCall + var subscription = await _dbContext.Set() + .FirstOrDefaultAsync(s => s.Id == model.SubscriptionId.Value); + + if (subscription == null) + throw new SWNotFoundException($"Subscription with Id {model.SubscriptionId} not found"); + + if (subscription.Type != SubscriptionType.GatewayApiCall) + throw new SWException($"Subscription must be of type GatewayApiCall. Current type: {subscription.Type}"); + + // Check if partner already exists + var existingPartner = gateway.Partners != null + ? gateway.Partners.FirstOrDefault(p => p.PartnerId == model.PartnerId && p.SubscriptionId == model.SubscriptionId) + : null; + + if (existingPartner != null) + throw new SWException("Partner already exists in this gateway"); + + partnerLink.SubscriptionId = model.SubscriptionId.Value; + } + _dbContext.Add(partnerLink); await _dbContext.SaveChangesAsync(); diff --git a/SW.Bitween.Api/Resources/ApiGateways/Create.cs b/SW.Bitween.Api/Resources/ApiGateways/Create.cs index 60f633a8..4462994c 100644 --- a/SW.Bitween.Api/Resources/ApiGateways/Create.cs +++ b/SW.Bitween.Api/Resources/ApiGateways/Create.cs @@ -20,13 +20,13 @@ public async Task Handle(ApiGatewayCreate model) { await _requestContext.EnsurePermission(_dbContext, Model.Permissions.ApiGateways.Create); - if (string.IsNullOrWhiteSpace(model.UrlName)) - throw new SWException("UrlName is required"); + GatewayUrlName.Validate(model.UrlName); var entity = new ApiGateway { Name = model.Name, - UrlName = model.UrlName + UrlName = model.UrlName, + Inactive = model.Inactive }; _dbContext.Add(entity); diff --git a/SW.Bitween.Api/Resources/ApiGateways/GatewayUrlName.cs b/SW.Bitween.Api/Resources/ApiGateways/GatewayUrlName.cs new file mode 100644 index 00000000..3742d3e7 --- /dev/null +++ b/SW.Bitween.Api/Resources/ApiGateways/GatewayUrlName.cs @@ -0,0 +1,27 @@ +using System.Text.RegularExpressions; +using SW.PrimitiveTypes; + +namespace SW.Bitween.Resources.ApiGateways; + +/// +/// The url name is a path segment — partners call /api/Gateway/{urlName}/sync — so +/// anything needing escaping there makes a gateway that reads as configured and cannot be +/// reached. A space is the one that actually happens: it saves, the endpoint shown on the +/// page is the one the partner copies, and the call 404s with nothing on screen to explain it. +/// +internal static partial class GatewayUrlName +{ + [GeneratedRegex("^[a-z0-9]+(?:[-_][a-z0-9]+)*$")] + private static partial Regex Allowed(); + + public static void Validate(string urlName) + { + if (string.IsNullOrWhiteSpace(urlName)) + throw new SWException("UrlName is required"); + + if (!Allowed().IsMatch(urlName)) + throw new SWValidationException("GATEWAY_URL_NAME_INVALID", + $"'{urlName}' cannot be used in a URL. Use lowercase letters, digits, hyphens " + + "and underscores only — no spaces, and not starting or ending with a separator."); + } +} diff --git a/SW.Bitween.Api/Resources/ApiGateways/Get.cs b/SW.Bitween.Api/Resources/ApiGateways/Get.cs index 6d8ce6aa..23397cea 100644 --- a/SW.Bitween.Api/Resources/ApiGateways/Get.cs +++ b/SW.Bitween.Api/Resources/ApiGateways/Get.cs @@ -38,6 +38,7 @@ public async Task Handle(int key) Id = gateway.Id, Name = gateway.Name, UrlName = gateway.UrlName, + Inactive = gateway.Inactive, PartnersCount = gateway.Partners.Count, Partners = gateway.Partners.Select(p => new ApiGatewayPartnerDto { diff --git a/SW.Bitween.Api/Resources/ApiGateways/Search.cs b/SW.Bitween.Api/Resources/ApiGateways/Search.cs index 12db87cf..ca00a616 100644 --- a/SW.Bitween.Api/Resources/ApiGateways/Search.cs +++ b/SW.Bitween.Api/Resources/ApiGateways/Search.cs @@ -32,6 +32,7 @@ public async Task Handle(SearchyRequest searchyRequest, bool lookup = fa Id = gateway.Id, Name = gateway.Name, UrlName = gateway.UrlName, + Inactive = gateway.Inactive, PartnersCount = gateway.Partners.Count }; diff --git a/SW.Bitween.Api/Resources/ApiGateways/SearchAttachments.cs b/SW.Bitween.Api/Resources/ApiGateways/SearchAttachments.cs new file mode 100644 index 00000000..5590e6b3 --- /dev/null +++ b/SW.Bitween.Api/Resources/ApiGateways/SearchAttachments.cs @@ -0,0 +1,64 @@ +using Microsoft.EntityFrameworkCore; +using SW.Bitween.Domain.Gateway; +using SW.Bitween.Model; +using SW.PrimitiveTypes; +using System.Linq; +using System.Threading.Tasks; + +namespace SW.Bitween.Resources.ApiGateways +{ + /// + /// Paged, searched view of one gateway's attachments — the full list lives on + /// for callers that need every attached partner id (e.g. the + /// attach-partner picker's exclude list), this is only for the gateway page's own table. + /// + [HandlerName("attachments")] + public class SearchAttachments : IQueryHandler + { + private readonly BitweenDbContext _dbContext; + private readonly RequestContext _requestContext; + + public SearchAttachments(BitweenDbContext dbContext, RequestContext requestContext) + { + _dbContext = dbContext; + _requestContext = requestContext; + } + + public async Task Handle(SearchApiGatewayAttachmentsModel request) + { + await _requestContext.EnsurePermission(_dbContext, Model.Permissions.ApiGateways.View); + + var offset = request.Offset ?? 0; + var limit = request.Limit ?? 25; + var term = request.Search?.Trim(); + + var query = _dbContext.Set() + .AsNoTracking() + .Where(p => p.ApiGatewayId == request.ApiGatewayId); + + if (!string.IsNullOrEmpty(term)) + query = query.Where(p => p.Partner.Name.Contains(term) || p.Subscription.Name.Contains(term)); + + var totalCount = await query.CountAsync(); + + var result = await query + .OrderBy(p => p.Partner.Name) + .Skip(offset) + .Take(limit) + .Select(p => new ApiGatewayPartnerDto + { + PartnerId = p.PartnerId, + SubscriptionId = p.SubscriptionId, + PartnerName = p.Partner.Name, + SubscriptionName = p.Subscription.Name + }) + .ToListAsync(); + + return new + { + Result = result, + TotalCount = totalCount + }; + } + } +} diff --git a/SW.Bitween.Api/Resources/ApiGateways/Update.cs b/SW.Bitween.Api/Resources/ApiGateways/Update.cs index ddf88beb..489cc936 100644 --- a/SW.Bitween.Api/Resources/ApiGateways/Update.cs +++ b/SW.Bitween.Api/Resources/ApiGateways/Update.cs @@ -30,11 +30,11 @@ public async Task Handle(int key, ApiGatewayUpdate model) if (entity == null) throw new SWNotFoundException($"ApiGateway with Id {key} not found"); - if (string.IsNullOrWhiteSpace(model.UrlName)) - throw new SWException("UrlName is required"); + GatewayUrlName.Validate(model.UrlName); entity.Name = model.Name; entity.UrlName = model.UrlName; + entity.Inactive = model.Inactive; await _dbContext.SaveChangesAsync(); return null; diff --git a/SW.Bitween.Api/Resources/ApiGateways/UpdatePartner.cs b/SW.Bitween.Api/Resources/ApiGateways/UpdatePartner.cs index ac5b0733..03f543b0 100644 --- a/SW.Bitween.Api/Resources/ApiGateways/UpdatePartner.cs +++ b/SW.Bitween.Api/Resources/ApiGateways/UpdatePartner.cs @@ -32,6 +32,12 @@ public async Task Handle(int gatewayId, ApiGatewayPartnerCreate model) throw new SWNotFoundException($"ApiGateway with Id {gatewayId} not found"); // Validate subscription exists and is of type GatewayApiCall + // Repointing an existing attachment always names an integration that already + // exists; defining one inline is only for the attachment being created. + if (!model.SubscriptionId.HasValue) + throw new SWValidationException(GatewayLinkTarget.NeitherGiven, + "Pick the integration this partner runs."); + var subscription = await _dbContext.Set() .FirstOrDefaultAsync(s => s.Id == model.SubscriptionId); @@ -47,7 +53,7 @@ public async Task Handle(int gatewayId, ApiGatewayPartnerCreate model) if (partnerLink == null) throw new SWNotFoundException($"Partner with Id {model.PartnerId} not found in gateway {gatewayId}"); - partnerLink.SubscriptionId = model.SubscriptionId; + partnerLink.SubscriptionId = model.SubscriptionId.Value; await _dbContext.SaveChangesAsync(); diff --git a/SW.Bitween.Api/Resources/BusGateways/AddRoute.cs b/SW.Bitween.Api/Resources/BusGateways/AddRoute.cs index 57ac2bfd..3d713c97 100644 --- a/SW.Bitween.Api/Resources/BusGateways/AddRoute.cs +++ b/SW.Bitween.Api/Resources/BusGateways/AddRoute.cs @@ -2,6 +2,7 @@ using SW.Bitween.Domain; using SW.Bitween.Domain.Gateway; using SW.Bitween.Model; +using SW.Bitween.Resources.Subscriptions; using SW.PrimitiveTypes; using System.Threading.Tasks; @@ -14,11 +15,15 @@ public class AddRoute : ICommandHandler private readonly RequestContext _requestContext; private readonly IInfolinkCache _cache; - public AddRoute(BitweenDbContext dbContext, RequestContext requestContext, IInfolinkCache cache) + private readonly AdapterRequirements _adapterRequirements; + + public AddRoute(BitweenDbContext dbContext, RequestContext requestContext, IInfolinkCache cache, + AdapterRequirements adapterRequirements) { _dbContext = dbContext; _requestContext = requestContext; _cache = cache; + _adapterRequirements = adapterRequirements; } public async Task Handle(int gatewayId, BusGatewayRouteCreate model) @@ -31,17 +36,32 @@ public async Task Handle(int gatewayId, BusGatewayRouteCreate model) if (gateway == null) throw new SWNotFoundException($"BusGateway with Id {gatewayId} not found"); - await ValidateSubscription(_dbContext, model.SubscriptionId, gateway.DocumentId); + InlineIntegration.EnsureExactlyOne(model.SubscriptionId, model.NewIntegration); await ValidatePartner(_dbContext, model.PartnerId); var route = new BusGatewayRoute { BusGatewayId = gatewayId, - SubscriptionId = model.SubscriptionId, PartnerId = model.PartnerId, MatchExpression = model.MatchExpression }; + if (model.NewIntegration != null) + { + // Staged, not saved: EF fills the route's foreign key from the subscription it is + // tracking, so both rows go in on the one SaveChangesAsync below. A route pointing + // at an integration that was never committed is not a state that can happen. + var integration = await InlineIntegration.Stage( + _dbContext, _adapterRequirements, model.NewIntegration, gateway.DocumentId, + SubscriptionType.BusGateway); + route.Subscription = integration; + } + else + { + await ValidateSubscription(_dbContext, model.SubscriptionId.Value, gateway.DocumentId); + route.SubscriptionId = model.SubscriptionId.Value; + } + _dbContext.Add(route); await _dbContext.SaveChangesAsync(); await _cache.BroadcastRevoke(); diff --git a/SW.Bitween.Api/Resources/BusGateways/Create.cs b/SW.Bitween.Api/Resources/BusGateways/Create.cs index ddf94b56..fb85fa61 100644 --- a/SW.Bitween.Api/Resources/BusGateways/Create.cs +++ b/SW.Bitween.Api/Resources/BusGateways/Create.cs @@ -32,7 +32,8 @@ public async Task Handle(BusGatewayCreate model) var entity = new BusGateway { Name = model.Name, - DocumentId = model.DocumentId + DocumentId = model.DocumentId, + Inactive = model.Inactive }; _dbContext.Add(entity); diff --git a/SW.Bitween.Api/Resources/BusGateways/Get.cs b/SW.Bitween.Api/Resources/BusGateways/Get.cs index b9faeff7..ccaf6aa8 100644 --- a/SW.Bitween.Api/Resources/BusGateways/Get.cs +++ b/SW.Bitween.Api/Resources/BusGateways/Get.cs @@ -44,6 +44,7 @@ public async Task Handle(int key) Id = gateway.Id, Name = gateway.Name, DocumentId = gateway.DocumentId, + Inactive = gateway.Inactive, DocumentName = documentName, RoutesCount = gateway.Routes.Count, Routes = gateway.Routes.Select(r => new BusGatewayRouteDto diff --git a/SW.Bitween.Api/Resources/BusGateways/Search.cs b/SW.Bitween.Api/Resources/BusGateways/Search.cs index 62d8442c..d59daa5b 100644 --- a/SW.Bitween.Api/Resources/BusGateways/Search.cs +++ b/SW.Bitween.Api/Resources/BusGateways/Search.cs @@ -35,6 +35,7 @@ public async Task Handle(SearchyRequest searchyRequest, bool lookup = fa Id = gateway.Id, Name = gateway.Name, DocumentId = gateway.DocumentId, + Inactive = gateway.Inactive, DocumentName = documents.Where(d => d.Id == gateway.DocumentId) .Select(d => d.Name).FirstOrDefault(), RoutesCount = gateway.Routes.Count diff --git a/SW.Bitween.Api/Resources/BusGateways/Update.cs b/SW.Bitween.Api/Resources/BusGateways/Update.cs index 1235636f..51b0dcaa 100644 --- a/SW.Bitween.Api/Resources/BusGateways/Update.cs +++ b/SW.Bitween.Api/Resources/BusGateways/Update.cs @@ -32,6 +32,7 @@ public async Task Handle(int key, BusGatewayUpdate model) // Name only; the bound document is fixed at creation (routes' subscriptions belong to it). entity.Name = model.Name; + entity.Inactive = model.Inactive; await _dbContext.SaveChangesAsync(); await _cache.BroadcastRevoke(); diff --git a/SW.Bitween.Api/Resources/BusGateways/UpdateRoute.cs b/SW.Bitween.Api/Resources/BusGateways/UpdateRoute.cs index 32b6597d..5cb4eeb1 100644 --- a/SW.Bitween.Api/Resources/BusGateways/UpdateRoute.cs +++ b/SW.Bitween.Api/Resources/BusGateways/UpdateRoute.cs @@ -36,10 +36,16 @@ public async Task Handle(int gatewayId, BusGatewayRouteUpdate model) if (route == null) throw new SWNotFoundException($"Route with Id {model.RouteId} not found in gateway {gatewayId}"); - await AddRoute.ValidateSubscription(_dbContext, model.SubscriptionId, gateway.DocumentId); + // Repointing an existing route always names an integration that already exists; + // defining one inline is only for the route being created. + if (!model.SubscriptionId.HasValue) + throw new SWValidationException(GatewayLinkTarget.NeitherGiven, + "Pick the integration this route runs."); + + await AddRoute.ValidateSubscription(_dbContext, model.SubscriptionId.Value, gateway.DocumentId); await AddRoute.ValidatePartner(_dbContext, model.PartnerId); - route.SubscriptionId = model.SubscriptionId; + route.SubscriptionId = model.SubscriptionId.Value; route.PartnerId = model.PartnerId; route.MatchExpression = model.MatchExpression; diff --git a/SW.Bitween.Api/Resources/DelayedRetries/RunNow.cs b/SW.Bitween.Api/Resources/DelayedRetries/RunNow.cs index 85c1b9d7..3efee3e6 100644 --- a/SW.Bitween.Api/Resources/DelayedRetries/RunNow.cs +++ b/SW.Bitween.Api/Resources/DelayedRetries/RunNow.cs @@ -31,7 +31,18 @@ public async Task Handle(string key, DelayedRetryRunNow request) throw new SWValidationException("NOT_FOUND", "No auto-retry is currently scheduled for this exchange."); if (!await _xchangeService.ExecuteDelayedRetry(delayedRetry)) - throw new SWValidationException("NOT_FOUND", "The original exchange or its subscription no longer exists."); + { + await _dbContext.SaveChangesAsync(); + + // Every other refusal writes its reason onto the exchange, so the message sends the + // caller there instead of listing them. A missing exchange is the one case with + // nowhere to write it, and pointing at something that is gone explains nothing. + var exchangeExists = await _dbContext.Set().AnyAsync(x => x.Id == key); + + throw new SWValidationException("CANNOT_RETRY", exchangeExists + ? "This retry could not be carried out. The exchange it belongs to says why." + : "This retry could not be carried out: the exchange it belonged to no longer exists."); + } await _dbContext.SaveChangesAsync(); return null; diff --git a/SW.Bitween.Api/Resources/Documents/Create.cs b/SW.Bitween.Api/Resources/Documents/Create.cs index c05848cd..87b791f2 100644 --- a/SW.Bitween.Api/Resources/Documents/Create.cs +++ b/SW.Bitween.Api/Resources/Documents/Create.cs @@ -29,6 +29,14 @@ public async Task Handle(DocumentCreate model) { await _requestContext.EnsurePermission(_dbContext, Model.Permissions.Documents.Create); + // Same check Update makes, and compared the same way. Without it two types + // could be created under one name, and then neither could be saved again — + // Update refuses the name it already has. Ignoring case, because a list + // holding both "Invoice" and "invoice" reads as a mistake, not a choice. + var wantedName = (model.Name ?? string.Empty).ToLower(); + if (await _dbContext.Set().AsNoTracking().AnyAsync(d => d.Name.ToLower() == wantedName)) + throw new SWValidationException("NAME_TAKEN", "An information type with this name already exists."); + var code = string.IsNullOrWhiteSpace(model.Code) ? null : model.Code; if (code != null && await _dbContext.Set().AsNoTracking().AnyAsync(d => d.Code == code)) @@ -36,19 +44,36 @@ public async Task Handle(DocumentCreate model) if (model.BusEnabled && !string.IsNullOrEmpty(model.BusMessageTypeName)) { + // Compared lower-cased, because that is how the bus compares them: both + // BasicPublisher and ConsumerDefinition derive the routing key with + // ToLower(), so "Foo" and "foo" are one message on the wire. Matching + // exactly here let both exist, and then every message published under + // either name reached both gateways, silently. ToLower() rather than a + // provider-specific collation — this runs on Postgres, MySql and MsSql. + var wanted = model.BusMessageTypeName.ToLower(); var busTypeNameDuplicated = await _dbContext.Set() .AsNoTracking() - .AnyAsync(d => d.BusMessageTypeName == model.BusMessageTypeName); + .AnyAsync(d => d.BusMessageTypeName.ToLower() == wanted); if (busTypeNameDuplicated) throw new SWValidationException("DUPLICATED_BUS_TYPE_NAME", - "Cant use duplicated bus Message type name"); + $"Another information type already publishes as '{model.BusMessageTypeName}'. " + + "Names are compared ignoring case, because the bus does."); } + PromotedPropertyValidation.Check(model.PromotedProperties, model.DocumentFormat); + var entity = new Document(code, model.Name, model.DocumentFormat) { BusEnabled = model.BusEnabled, BusMessageTypeName = model.BusEnabled ? model.BusMessageTypeName : null, + DuplicateInterval = model.DuplicateInterval, + DisregardsUnfilteredMessages = model.DisregardsUnfilteredMessages, }; + if (model.PromotedProperties != null) + entity.SetDictionaries(model.PromotedProperties.ToDictionary()); + + // After the entity is complete: the trail serialises it in the constructor + // when isNew, so anything set later would be missing from the created state. var trail = new DocumentTrail(DocumentTrailCode.Created, entity, true); _dbContext.Add(trail); _dbContext.Add(entity); diff --git a/SW.Bitween.Api/Resources/Documents/PromotedPropertyValidation.cs b/SW.Bitween.Api/Resources/Documents/PromotedPropertyValidation.cs new file mode 100644 index 00000000..0be807c3 --- /dev/null +++ b/SW.Bitween.Api/Resources/Documents/PromotedPropertyValidation.cs @@ -0,0 +1,56 @@ +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; +using SW.Bitween.Model; +using SW.PrimitiveTypes; + +namespace SW.Bitween.Resources.Documents +{ + /// + /// The rules a promoted property has to satisfy, shared by Create and Update so + /// the two paths cannot drift apart on what a valid path is. + /// + public static class PromotedPropertyValidation + { + public static void Check(ICollection promotedProperties, DocumentFormat format) + { + if (promotedProperties == null) return; + + foreach (var pp in promotedProperties) + { + if (string.IsNullOrWhiteSpace(pp.Key)) + throw new SWValidationException("INVALID_PROMOTED_PROPERTY_KEY", + "Promoted property key cannot be null or empty."); + + if (string.IsNullOrWhiteSpace(pp.Value)) + throw new SWValidationException("INVALID_PROMOTED_PROPERTY_VALUE", + $"Promoted property '{pp.Key}' must have a non-empty path value."); + + var trimmed = pp.Value.Trim(); + + if (format == DocumentFormat.Json) + { + // Must be a JSONPath: starts with '$' or a simple dot-separated identifier path + if (!trimmed.StartsWith("$") && !Regex.IsMatch(trimmed, @"^[a-zA-Z_][a-zA-Z0-9_]*(?:(\.[a-zA-Z_][a-zA-Z0-9_]*)|(\[[0-9]+\]))*$")) + throw new SWValidationException("INVALID_PROMOTED_PROPERTY_PATH", + $"Promoted property '{pp.Key}' has an invalid JSON path: '{pp.Value}'. Expected a JSONPath expression (e.g. '$.field.subField') or dot-notation path."); + } + else if (format == DocumentFormat.Xml) + { + // Basic XPath sanity: must start with '/' or '//' or be a valid element path + if (!trimmed.StartsWith("/") && !Regex.IsMatch(trimmed, @"^[a-zA-Z_][a-zA-Z0-9_/\[\]@.:*-]*$")) + throw new SWValidationException("INVALID_PROMOTED_PROPERTY_PATH", + $"Promoted property '{pp.Key}' has an invalid XML path: '{pp.Value}'. Expected an XPath expression (e.g. '/root/element')."); + } + } + + var duplicateKey = promotedProperties + .GroupBy(pp => pp.Key, System.StringComparer.OrdinalIgnoreCase) + .FirstOrDefault(g => g.Count() > 1)?.Key; + + if (duplicateKey != null) + throw new SWValidationException("DUPLICATE_PROMOTED_PROPERTY_KEY", + $"Promoted property key '{duplicateKey}' appears more than once."); + } + } +} diff --git a/SW.Bitween.Api/Resources/Documents/Update.cs b/SW.Bitween.Api/Resources/Documents/Update.cs index a76b7522..a30b8829 100644 --- a/SW.Bitween.Api/Resources/Documents/Update.cs +++ b/SW.Bitween.Api/Resources/Documents/Update.cs @@ -35,10 +35,13 @@ public async Task Handle(int key, DocumentUpdate model) if (string.IsNullOrWhiteSpace(model.Name)) throw new SWValidationException("INVALID_NAME", "Give the information type a name."); + // Ignoring case, as Create does: two types whose names differ only in case + // are indistinguishable in every list that shows them. + var wantedName = model.Name.ToLower(); var nameDuplicated = await _dbContext.Set() .AsNoTracking() .Where(i => i.Id != key) - .AnyAsync(i => i.Name == model.Name); + .AnyAsync(i => i.Name.ToLower() == wantedName); if (nameDuplicated) throw new SWValidationException("NAME_TAKEN", "An information type with this name already exists."); @@ -62,58 +65,28 @@ public async Task Handle(int key, DocumentUpdate model) throw new SWValidationException("INVALID_BUS_TYPE_NAME", "Bus message type name cannot contain spaces."); + // Ignoring case, for the reason spelled out in Create: the routing key is + // lower-cased at both ends, so two names differing only in case are one message. + var wanted = (model.BusMessageTypeName ?? string.Empty).ToLower(); var busTypeNameDuplicated = await _dbContext.Set() .AsNoTracking() .Where(i => i.Id != key) .Where(i => !string.IsNullOrEmpty(i.BusMessageTypeName)) - .Where(i => i.BusMessageTypeName == model.BusMessageTypeName) + .Where(i => i.BusMessageTypeName.ToLower() == wanted) .AnyAsync(); if (busTypeNameDuplicated) throw new SWValidationException("DUPLICATED_BUS_TYPE_NAME", - "Cant use duplicated bus Message type name"); + $"Another information type already publishes as '{model.BusMessageTypeName}'. " + + "Names are compared ignoring case, because the bus does."); - if (model.PromotedProperties != null) - { - foreach (var pp in model.PromotedProperties) - { - if (string.IsNullOrWhiteSpace(pp.Key)) - throw new SWValidationException("INVALID_PROMOTED_PROPERTY_KEY", - "Promoted property key cannot be null or empty."); - - if (string.IsNullOrWhiteSpace(pp.Value)) - throw new SWValidationException("INVALID_PROMOTED_PROPERTY_VALUE", - $"Promoted property '{pp.Key}' must have a non-empty path value."); - - if (model.DocumentFormat == DocumentFormat.Json) - { - // Must be a JSONPath: starts with '$' or a simple dot-separated identifier path - var trimmed = pp.Value.Trim(); - if (!trimmed.StartsWith("$") && !Regex.IsMatch(trimmed, @"^[a-zA-Z_][a-zA-Z0-9_]*(?:(\.[a-zA-Z_][a-zA-Z0-9_]*)|(\[[0-9]+\]))*$")) - throw new SWValidationException("INVALID_PROMOTED_PROPERTY_PATH", - $"Promoted property '{pp.Key}' has an invalid JSON path: '{pp.Value}'. Expected a JSONPath expression (e.g. '$.field.subField') or dot-notation path."); - } - else if (model.DocumentFormat == DocumentFormat.Xml) - { - // Basic XPath sanity: must start with '/' or '//' or be a valid element path - var trimmed = pp.Value.Trim(); - if (!trimmed.StartsWith("/") && !Regex.IsMatch(trimmed, @"^[a-zA-Z_][a-zA-Z0-9_/\[\]@.:*-]*$")) - throw new SWValidationException("INVALID_PROMOTED_PROPERTY_PATH", - $"Promoted property '{pp.Key}' has an invalid XML path: '{pp.Value}'. Expected an XPath expression (e.g. '/root/element')."); - } - } - - var duplicateKey = model.PromotedProperties - .GroupBy(pp => pp.Key, System.StringComparer.OrdinalIgnoreCase) - .FirstOrDefault(g => g.Count() > 1)?.Key; - - if (duplicateKey != null) - throw new SWValidationException("DUPLICATE_PROMOTED_PROPERTY_KEY", - $"Promoted property key '{duplicateKey}' appears more than once."); - } + PromotedPropertyValidation.Check(model.PromotedProperties, model.DocumentFormat); var trail = new DocumentTrail(DocumentTrailCode.Updated, entity); - entity.SetDictionaries(model.PromotedProperties.ToDictionary()); + // An absent list means none, the same as it does for retry policy groups. + // Left implicit it threw ArgumentNullException — a 500 for a request the + // API had simply never decided the meaning of. + entity.SetDictionaries((model.PromotedProperties ?? []).ToDictionary()); // Name/Code have private setters — SetProperties only writes public-setter // properties, so it silently no-ops on these two (verified empirically). entity.SetName(model.Name); diff --git a/SW.Bitween.Api/Resources/Notifiers/Delete.cs b/SW.Bitween.Api/Resources/Notifiers/Delete.cs new file mode 100644 index 00000000..611e0eb9 --- /dev/null +++ b/SW.Bitween.Api/Resources/Notifiers/Delete.cs @@ -0,0 +1,32 @@ +using SW.EfCoreExtensions; +using SW.Bitween.Domain; +using SW.PrimitiveTypes; +using System.Threading.Tasks; + +namespace SW.Bitween.Resources.Notifiers +{ + public class Delete : IDeleteHandler + { + private readonly BitweenDbContext _dbContext; + private readonly RequestContext _requestContext; + + public Delete(BitweenDbContext dbContext, RequestContext requestContext) + { + _dbContext = dbContext; + _requestContext = requestContext; + } + + /// + /// No reference check, unlike an integration's delete: nothing has a foreign key to a + /// notifier. RunOnSubscriptions points the other way — the notifier names the + /// integrations it watches, so deleting it takes the whole list with it. + /// + public async Task Handle(int key) + { + await _requestContext.EnsurePermission(_dbContext, Model.Permissions.Notifiers.Delete); + + await _dbContext.DeleteByKeyAsync(key); + return null; + } + } +} diff --git a/SW.Bitween.Api/Resources/Notifiers/Search.cs b/SW.Bitween.Api/Resources/Notifiers/Search.cs index d1171ec4..b1dad100 100644 --- a/SW.Bitween.Api/Resources/Notifiers/Search.cs +++ b/SW.Bitween.Api/Resources/Notifiers/Search.cs @@ -35,7 +35,8 @@ public async Task Handle(SearchyRequest searchyRequest, bool lookup = fa RunOnBadResult = notifier.RunOnBadResult, RunOnFailedResult = notifier.RunOnFailedResult, RunOnSuccessfulResult = notifier.RunOnSuccessfulResult, - Inactive = notifier.Inactive + Inactive = notifier.Inactive, + RunOnSubscriptions = notifier.RunOnSubscriptions }; query = query.AsNoTracking(); diff --git a/SW.Bitween.Api/Resources/Notifiers/Update.cs b/SW.Bitween.Api/Resources/Notifiers/Update.cs index 690bbbd4..302f4c83 100644 --- a/SW.Bitween.Api/Resources/Notifiers/Update.cs +++ b/SW.Bitween.Api/Resources/Notifiers/Update.cs @@ -31,7 +31,9 @@ public async Task Handle(int key, NotifierUpdate request) request.Inactive, request.RunOnSubscriptions?.Select(r => r.Id)?.ToArray()); - notifier.SetDictionaries(request.HandlerProperties.ToDictionary()); + // An absent list means none, as it does for a document's promoted properties + // and a retry policy's groups. Left implicit it threw ArgumentNullException. + notifier.SetDictionaries((request.HandlerProperties ?? []).ToDictionary()); await _dbContext.SaveChangesAsync(); diff --git a/SW.Bitween.Api/Resources/RetryPolicies/Attempts.cs b/SW.Bitween.Api/Resources/RetryPolicies/Attempts.cs new file mode 100644 index 00000000..7d5376d3 --- /dev/null +++ b/SW.Bitween.Api/Resources/RetryPolicies/Attempts.cs @@ -0,0 +1,97 @@ +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using SW.Bitween.Domain; +using SW.Bitween.Domain.Accounts; +using SW.Bitween.Model; +using SW.PrimitiveTypes; + +namespace SW.Bitween.Resources.RetryPolicies; + +/// +/// Lists the failures one group caught for one subscription — what a row of +/// spent its budget on. +/// +/// +/// +/// Separate from and asked for one pair at a time, because a policy with fifty +/// subscriptions would otherwise pay for fifty of these joins to answer a question about one row. +/// +/// +/// Only failures carrying a group id appear, so nothing recorded before the group was stamped onto +/// results is listed. A pair whose counter is well spent can therefore come back empty, which is +/// why is the count of what is listable rather than the +/// counter's own value. +/// +/// +[HandlerName("attempts")] +public class Attempts : ICommandHandler +{ + /// + /// Enough to show what keeps failing without turning one table row into a page. The caller is + /// told the total, so a short list never reads as the whole story. + /// + private const int Limit = 10; + + private readonly BitweenDbContext _dbContext; + private readonly RequestContext _requestContext; + + public Attempts(BitweenDbContext dbContext, RequestContext requestContext) + { + _dbContext = dbContext; + _requestContext = requestContext; + } + + public async Task Handle(int key, RetryGroupAttemptsRequest request) + { + await _requestContext.EnsurePermission(_dbContext, Model.Permissions.RetryPolicies.View); + + // Both halves of the pair have to belong to the policy in the route. For the subscription + // that keeps this from becoming a way to read any subscription's failures through any + // policy id; for the group it is about the answer being readable — an unknown group would + // otherwise report zero failures, which is indistinguishable from a group that genuinely + // has none. + var policy = await _dbContext.Set().AsNoTracking() + .FirstOrDefaultAsync(p => p.Id == key); + if (policy == null) throw new SWNotFoundException(key.ToString()); + + if (policy.Groups.All(g => g.Id != request.GroupId)) + throw new SWNotFoundException($"{key}/{request.GroupId}"); + + var belongs = await _dbContext.Set().AsNoTracking() + .AnyAsync(s => s.Id == request.SubscriptionId && s.RetryPolicyId == key); + if (!belongs) throw new SWNotFoundException($"{key}/{request.SubscriptionId}"); + + var query = from result in _dbContext.Set() + join xchange in _dbContext.Set() on result.Id equals xchange.Id + join pending in _dbContext.Set() on result.Id equals pending.Id into scheduled + from pending in scheduled.DefaultIfEmpty() + where xchange.SubscriptionId == request.SubscriptionId + && result.RetryGroupId == request.GroupId + select new RetryGroupAttemptRow + { + XchangeId = result.Id, + AttemptNumber = result.AttemptNumber, + FailedOn = result.FinishedOn, + Exception = result.Exception, + // A row survives here only until its retry runs, which is what separates a failure + // still being worked on from one that has been given up. + RetryPending = pending != null, + RetryBlockedReason = result.RetryBlockedReason + }; + + query = query.AsNoTracking(); + + return new RetryGroupAttempts + { + Total = await query.CountAsync(), + // Pending first, so the ones still moving cannot be pushed out of the list by a long + // history of failures that are already over. + Attempts = await query + .OrderByDescending(r => r.RetryPending) + .ThenByDescending(r => r.FailedOn) + .Take(Limit) + .ToListAsync() + }; + } +} diff --git a/SW.Bitween.Api/Resources/RetryPolicies/Create.cs b/SW.Bitween.Api/Resources/RetryPolicies/Create.cs index 1c2ee391..e7ea0d07 100644 --- a/SW.Bitween.Api/Resources/RetryPolicies/Create.cs +++ b/SW.Bitween.Api/Resources/RetryPolicies/Create.cs @@ -19,11 +19,21 @@ public Create(BitweenDbContext dbContext, RequestContext requestContext) public async Task Handle(RetryPolicyCreate model) { await _requestContext.EnsurePermission(_dbContext, Model.Permissions.RetryPolicies.Create); + RetryGroupValidation.EnsureCanFire(model.Groups); + RetryGroupValidation.EnsureAlertTransportIsSecure( + model.AlertHandlerId, model.AlertHandlerProperties); + + // A new policy has nothing stored behind a sentinel, so any that arrives — from a policy + // copied out of Get, say — is dropped rather than saved as the literal password. + foreach (var group in model.Groups ?? []) + AdapterSecretProperties.MergeInPlace(null, group.AlertHandlerProperties); var entity = new RetryPolicy { Name = model.Name, - Groups = model.Groups ?? [] + Groups = model.Groups ?? [], + AlertHandlerId = model.AlertHandlerId, + AlertHandlerProperties = AdapterSecretProperties.Merge(null, model.AlertHandlerProperties) }; _dbContext.Add(entity); await _dbContext.SaveChangesAsync(); diff --git a/SW.Bitween.Api/Resources/RetryPolicies/Delete.cs b/SW.Bitween.Api/Resources/RetryPolicies/Delete.cs index ba06bfdc..0afbdc67 100644 --- a/SW.Bitween.Api/Resources/RetryPolicies/Delete.cs +++ b/SW.Bitween.Api/Resources/RetryPolicies/Delete.cs @@ -27,7 +27,29 @@ public async Task Handle(int key) if (inUse) throw new SWException("Cannot delete a retry policy that is assigned to one or more subscriptions."); + // Same reason as Update: the policy's groups are about to stop existing, so clear their + // usage rows rather than strand them. + var policy = await _dbContext.FindAsync(key); + var groupIds = policy.Groups.Select(g => g.Id).ToList(); + + // One change, one commit — same reasoning as Update: a half-done delete leaves rows keyed + // by groups that no longer exist anywhere, which nothing can then reach. + await using var transaction = await _dbContext.Database.BeginTransactionAsync(); + await _dbContext.DeleteByKeyAsync(key); + + if (groupIds.Count > 0) + { + await _dbContext.Set() + .Where(u => groupIds.Contains(u.GroupId)) + .ExecuteDeleteAsync(); + + await _dbContext.Set() + .Where(o => groupIds.Contains(o.GroupId)) + .ExecuteDeleteAsync(); + } + + await transaction.CommitAsync(); return null; } } diff --git a/SW.Bitween.Api/Resources/RetryPolicies/Get.cs b/SW.Bitween.Api/Resources/RetryPolicies/Get.cs index 6b5b866a..d5c279e6 100644 --- a/SW.Bitween.Api/Resources/RetryPolicies/Get.cs +++ b/SW.Bitween.Api/Resources/RetryPolicies/Get.cs @@ -12,25 +12,40 @@ public class Get : IGetHandler { private readonly BitweenDbContext _dbContext; private readonly RequestContext _requestContext; + private readonly AdapterSecretProperties _secrets; - public Get(BitweenDbContext dbContext, RequestContext requestContext) + public Get(BitweenDbContext dbContext, RequestContext requestContext, AdapterSecretProperties secrets) { _dbContext = dbContext; _requestContext = requestContext; + _secrets = secrets; } public async Task Handle(int key) { await _requestContext.EnsurePermission(_dbContext, Model.Permissions.RetryPolicies.View); - return await _dbContext.Set() + // Materialize first: AlertHandlerProperties is a JSON-converted dictionary, and EF cannot + // translate a further .ToDictionary() over it into SQL inside a projection. + var policy = await _dbContext.Set() .AsNoTracking() .Search("Id", key) - .Select(p => new RetryPolicyUpdate - { - Name = p.Name, - Groups = p.Groups - }) .SingleOrDefaultAsync(); + + if (policy == null) return null; + + // Every level that can carry a handler can carry that handler's password, so every level is + // masked. Groups are edited in place by the caller, which is what Update then merges back. + foreach (var group in policy.Groups) + await _secrets.MaskInPlace(group.AlertHandlerId, group.AlertHandlerProperties); + + return new RetryPolicyUpdate + { + Name = policy.Name, + Groups = policy.Groups, + AlertHandlerId = policy.AlertHandlerId, + AlertHandlerProperties = + await _secrets.Mask(policy.AlertHandlerId, policy.AlertHandlerProperties) + }; } } diff --git a/SW.Bitween.Api/Resources/RetryPolicies/ResetUsage.cs b/SW.Bitween.Api/Resources/RetryPolicies/ResetUsage.cs new file mode 100644 index 00000000..33286630 --- /dev/null +++ b/SW.Bitween.Api/Resources/RetryPolicies/ResetUsage.cs @@ -0,0 +1,57 @@ +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using SW.Bitween.Domain; +using SW.Bitween.Domain.Accounts; +using SW.Bitween.Model; +using SW.PrimitiveTypes; + +namespace SW.Bitween.Resources.RetryPolicies; + +/// +/// Clears spent group budget, letting an exhausted group retry again. A budget that has run out also +/// clears itself when the integration next succeeds, so this is for putting one back before that +/// happens, or for handing back a total that is spent but not yet exhausted. +/// +[HandlerName("resetusage")] +public class ResetUsage : ICommandHandler +{ + private readonly BitweenDbContext _dbContext; + private readonly RequestContext _requestContext; + + public ResetUsage(BitweenDbContext dbContext, RequestContext requestContext) + { + _dbContext = dbContext; + _requestContext = requestContext; + } + + public async Task Handle(int key, RetryPolicyResetUsage request) + { + await _requestContext.EnsurePermission(_dbContext, Model.Permissions.RetryPolicies.Edit); + + var policy = await _dbContext.Set().AsNoTracking() + .FirstOrDefaultAsync(p => p.Id == key); + if (policy == null) throw new SWNotFoundException(key.ToString()); + + // Scope the reset to this policy's own integrations and groups, so a policy id in the + // route can never clear a counter belonging to a different policy. + var subscriptionIds = await _dbContext.Set() + .Where(s => s.RetryPolicyId == key) + .Select(s => s.Id) + .ToListAsync(); + + var groupIds = policy.Groups.Select(g => g.Id).ToList(); + + var query = _dbContext.Set() + .Where(u => subscriptionIds.Contains(u.SubscriptionId) && groupIds.Contains(u.GroupId)); + + if (request.SubscriptionId.HasValue) + query = query.Where(u => u.SubscriptionId == request.SubscriptionId.Value); + + if (request.GroupId.HasValue) + query = query.Where(u => u.GroupId == request.GroupId.Value); + + await query.ExecuteDeleteAsync(); + return null; + } +} diff --git a/SW.Bitween.Api/Resources/RetryPolicies/RetryGroupValidation.cs b/SW.Bitween.Api/Resources/RetryPolicies/RetryGroupValidation.cs new file mode 100644 index 00000000..2586b33b --- /dev/null +++ b/SW.Bitween.Api/Resources/RetryPolicies/RetryGroupValidation.cs @@ -0,0 +1,113 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using SW.Bitween.Model; +using SW.Bitween.NativeAdapters.SmtpHandler; +using SW.PrimitiveTypes; + +namespace SW.Bitween.Resources.RetryPolicies; + +/// +/// Rejects retry groups that could never fire. The evaluator skips such groups silently +/// (matchers must support the result type being evaluated — see +/// ), which reads as "retries just don't work", so the +/// misconfiguration is caught at write time instead. +/// +public static class RetryGroupValidation +{ + public static void EnsureCanFire(IEnumerable groups) + { + foreach (var group in groups ?? []) + { + if ((group.AppliesTo?.Count ?? 0) == 0) + throw new SWValidationException("RETRY_GROUP_NO_RESULT_TYPE", + $"Group '{group.Name}' applies to no result type, so it would never be evaluated. " + + "Select Error, Bad result, or both."); + + if ((group.Matchers?.Count ?? 0) == 0) + throw new SWValidationException("RETRY_GROUP_NO_MATCHERS", + $"Group '{group.Name}' has no matchers, so it would never match a failure. " + + "Add at least one matcher."); + + foreach (var resultType in group.AppliesTo) + if (!group.Matchers.Any(m => m.Supports(resultType))) + throw new SWValidationException("RETRY_GROUP_INCOMPATIBLE_MATCHERS", + $"Group '{group.Name}' applies to {resultType} but none of its matchers can be " + + $"evaluated against {resultType} content. {SupportedMatchersFor(resultType)}"); + + // Allow with no budget has nothing to work from — no per-message cap, no total, no delay — + // so the evaluator can only refuse it. Caught here because a group saved that way silently + // stops retrying, which reads as the whole feature being broken. + if (group.Action == RetryAction.Allow && group.Budget == null) + throw new SWValidationException("RETRY_GROUP_ALLOW_WITHOUT_BUDGET", + $"Group '{group.Name}' allows retries but has no budget. Set the attempt caps and " + + "delay, or change the action to block."); + + // An overriding level replaces the one above it rather than merging into it, so a group + // set to Send with no handler would silence the policy's alert instead of redirecting it. + if (group.AlertMode == RetryAlertMode.Send && string.IsNullOrWhiteSpace(group.AlertHandlerId)) + throw new SWValidationException("RETRY_GROUP_ALERT_NO_HANDLER", + $"Group '{group.Name}' is set to send its own budget alert but has no handler. " + + "Choose a handler, or set the alert back to inherit."); + + EnsureAlertTransportIsSecure(group.AlertHandlerId, group.AlertHandlerProperties); + } + } + + /// + /// Rejects an alert override that claims to send but names nothing to send with — the same trap + /// as guards at group level. + /// + public static void EnsureAlertCanSend(RetryAlertMode mode, string handlerId) + { + if (mode == RetryAlertMode.Send && string.IsNullOrWhiteSpace(handlerId)) + throw new SWValidationException("RETRY_ALERT_NO_HANDLER", + "This override is set to send its own budget alert but has no handler. " + + "Choose a handler, or set it back to inherit."); + } + + /// + /// Rejects mail alert settings that would hand the password to an unencrypted connection. + /// + /// + /// + /// The handler refuses this at send time too, which is the guarantee that matters — properties + /// can also arrive straight through the API or out of a global values set. Catching it here is + /// so the person configuring it finds out when they save, rather than from a missing alert and a + /// line in the log days later. + /// + /// + /// Only the mail handler is named, because only it has a password. A general answer belongs in + /// the adapter contract — an adapter saying which of its own settings conflict — not here. + /// + /// + public static void EnsureAlertTransportIsSecure( + string handlerId, IReadOnlyDictionary properties) + { + if (properties == null || properties.Count == 0) return; + if (!nameof(NativeSmtpHandler).Equals(handlerId, StringComparison.OrdinalIgnoreCase)) return; + + // A masked password counts as set: the sentinel means one is stored, not that the field is + // empty. Only an explicit "false" turns encryption off — absent means the adapter's own + // default, which is on. + var password = Value(properties, nameof(SmtpHandlerInput.Password)); + var useTls = Value(properties, nameof(SmtpHandlerInput.UseTls)); + + if (string.IsNullOrWhiteSpace(password)) return; + if (!bool.TryParse(useTls, out var encrypted) || encrypted) return; + + throw new SWValidationException("ALERT_PASSWORD_WITHOUT_TLS", + "This alert would send its mail password over an unencrypted connection. " + + "Turn UseTls on, or clear the password if the relay does not need one."); + } + + private static string Value(IReadOnlyDictionary properties, string key) => + properties.FirstOrDefault(kv => kv.Key.Equals(key, StringComparison.OrdinalIgnoreCase)).Value; + + private static string SupportedMatchersFor(XchangeResultType resultType) => resultType switch + { + XchangeResultType.Error => "Error supports Contains, Regex and Exception type matchers.", + XchangeResultType.BadResult => "Bad result supports Contains, Regex and JSON path matchers.", + _ => "Successful results are never retried." + }; +} diff --git a/SW.Bitween.Api/Resources/RetryPolicies/SaveAlertOverride.cs b/SW.Bitween.Api/Resources/RetryPolicies/SaveAlertOverride.cs new file mode 100644 index 00000000..45d2bf4a --- /dev/null +++ b/SW.Bitween.Api/Resources/RetryPolicies/SaveAlertOverride.cs @@ -0,0 +1,103 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using SW.Bitween.Domain; +using SW.Bitween.Domain.Accounts; +using SW.Bitween.Model; +using SW.PrimitiveTypes; + +namespace SW.Bitween.Resources.RetryPolicies; + +/// +/// Sets, changes or clears where one subscription's alerts go for one group of this policy — the most +/// specific level of the hierarchy. +/// +[HandlerName("savealertoverride")] +public class SaveAlertOverride : ICommandHandler +{ + private readonly BitweenDbContext _dbContext; + private readonly RequestContext _requestContext; + private readonly AdapterSecretProperties _secrets; + + public SaveAlertOverride(BitweenDbContext dbContext, RequestContext requestContext, + AdapterSecretProperties secrets) + { + _dbContext = dbContext; + _requestContext = requestContext; + _secrets = secrets; + } + + public async Task Handle(int key, RetryAlertOverrideSave request) + { + await _requestContext.EnsurePermission(_dbContext, Model.Permissions.RetryPolicies.Edit); + RetryGroupValidation.EnsureAlertCanSend(request.AlertMode, request.AlertHandlerId); + RetryGroupValidation.EnsureAlertTransportIsSecure( + request.AlertHandlerId, request.AlertHandlerProperties); + + var policy = await _dbContext.Set().AsNoTracking() + .FirstOrDefaultAsync(p => p.Id == key); + if (policy == null) throw new SWNotFoundException(key.ToString()); + + // Scoped to this policy's own groups and subscriptions, so a policy id in the route cannot + // reach an override belonging to a different policy. + if (policy.Groups.All(g => g.Id != request.GroupId)) + throw new SWValidationException("GROUP_NOT_IN_POLICY", + "That group does not belong to this retry policy."); + + var usesPolicy = await _dbContext.Set() + .AnyAsync(s => s.Id == request.SubscriptionId && s.RetryPolicyId == key); + if (!usesPolicy) + throw new SWValidationException("SUBSCRIPTION_NOT_USING_POLICY", + "That subscription does not use this retry policy."); + + var existing = await _dbContext.Set() + .FirstOrDefaultAsync(o => o.SubscriptionId == request.SubscriptionId + && o.GroupId == request.GroupId); + + // Inherit is the absence of an override, so store nothing rather than a row that does nothing + // — otherwise the routing list would have to explain a row that changes no behaviour. + if (request.AlertMode == RetryAlertMode.Inherit) + { + if (existing != null) _dbContext.Remove(existing); + await _dbContext.SaveChangesAsync(); + return null; + } + + // A masked secret has to be restored from whichever level the caller was shown it at. Usage + // masks two things for a pair: the override's own properties, and the properties of the level + // it currently inherits from. Overriding an inherited alert starts from the second — there is + // no override row yet — so both are offered here, with the override's own winning. + var group = policy.Groups.First(g => g.Id == request.GroupId); + var inherited = RetryAlertResolver.Resolve(existing, group, policy); + + var restoreFrom = new Dictionary(); + foreach (var kv in inherited?.HandlerProperties ?? new Dictionary()) + restoreFrom[kv.Key] = kv.Value; + foreach (var kv in existing?.AlertHandlerProperties ?? new Dictionary()) + restoreFrom[kv.Key] = kv.Value; + + var properties = AdapterSecretProperties.Merge(restoreFrom, request.AlertHandlerProperties); + + if (existing == null) + { + _dbContext.Add(new RetryAlertOverride + { + SubscriptionId = request.SubscriptionId, + GroupId = request.GroupId, + AlertMode = request.AlertMode, + AlertHandlerId = request.AlertHandlerId, + AlertHandlerProperties = properties + }); + } + else + { + existing.AlertMode = request.AlertMode; + existing.AlertHandlerId = request.AlertHandlerId; + existing.AlertHandlerProperties = properties; + } + + await _dbContext.SaveChangesAsync(); + return null; + } +} diff --git a/SW.Bitween.Api/Resources/RetryPolicies/Test.cs b/SW.Bitween.Api/Resources/RetryPolicies/Test.cs index e5a6f0a0..3a89613e 100644 --- a/SW.Bitween.Api/Resources/RetryPolicies/Test.cs +++ b/SW.Bitween.Api/Resources/RetryPolicies/Test.cs @@ -32,13 +32,14 @@ public async Task Handle(TestRetryPolicyRequest request) "Choose Error or Bad result — a successful result is never retried."); var policy = new CustomRetryPolicy { Groups = request.Groups ?? [] }; - var evaluator = new RetryPolicyEvaluator(policy); + // In-memory budget: a dry-run must not spend any real integration's total. + var evaluator = new RetryPolicyEvaluator(policy, new InMemoryRetryGroupBudget()); var attemptsToSimulate = Math.Clamp(request.AttemptsToSimulate, 1, 20); var attempts = new List(); for (var attemptIndex = 0; attemptIndex < attemptsToSimulate; attemptIndex++) { - var decision = evaluator.Evaluate(request.ResultType, request.Content, attemptIndex); + var decision = await evaluator.Evaluate(request.ResultType, request.Content, attemptIndex); attempts.Add(new TestRetryAttemptResult { diff --git a/SW.Bitween.Api/Resources/RetryPolicies/Update.cs b/SW.Bitween.Api/Resources/RetryPolicies/Update.cs index 64eafbac..05e6b357 100644 --- a/SW.Bitween.Api/Resources/RetryPolicies/Update.cs +++ b/SW.Bitween.Api/Resources/RetryPolicies/Update.cs @@ -1,4 +1,6 @@ +using System.Linq; using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; using SW.Bitween.Domain; using SW.Bitween.Model; using SW.PrimitiveTypes; @@ -19,11 +21,57 @@ public Update(BitweenDbContext dbContext, RequestContext requestContext) public async Task Handle(int key, RetryPolicyUpdate model) { await _requestContext.EnsurePermission(_dbContext, Model.Permissions.RetryPolicies.Edit); + RetryGroupValidation.EnsureCanFire(model.Groups); + RetryGroupValidation.EnsureAlertTransportIsSecure( + model.AlertHandlerId, model.AlertHandlerProperties); var entity = await _dbContext.FindAsync(key); + + // Spent budget is keyed by group id, so a group removed here would leave usage rows + // that no policy claims — invisible to the usage report and beyond the reach of reset. + var removedGroupIds = entity.Groups + .Select(g => g.Id) + .Except((model.Groups ?? []).Select(g => g.Id)) + .ToList(); + + // Dropping the groups and clearing what belonged to them is one change, so it commits as + // one: a group no policy claims whose usage and override rows survive is unreachable from + // the usage report and from reset alike. Safe to span, because RetryPolicy raises no domain + // events — nothing reaches the bus before the commit. + await using var transaction = await _dbContext.Database.BeginTransactionAsync(); + + // Secrets came out of Get masked, so put them back from what this same level already holds. + // A group matched by id, because a group added in this very save has nothing to restore from. + foreach (var group in model.Groups ?? []) + { + var storedGroup = entity.Groups.FirstOrDefault(g => g.Id == group.Id); + AdapterSecretProperties.MergeInPlace( + storedGroup?.AlertHandlerProperties, group.AlertHandlerProperties); + } + + var storedPolicyProperties = entity.AlertHandlerProperties; + entity.Name = model.Name; entity.Groups = model.Groups ?? []; + entity.AlertHandlerId = model.AlertHandlerId; + entity.AlertHandlerProperties = + AdapterSecretProperties.Merge(storedPolicyProperties, model.AlertHandlerProperties); await _dbContext.SaveChangesAsync(); + + if (removedGroupIds.Count > 0) + { + await _dbContext.Set() + .Where(u => removedGroupIds.Contains(u.GroupId)) + .ExecuteDeleteAsync(); + + // Alert overrides are keyed by group id for the same reason usage is, so they strand the + // same way when a group disappears. + await _dbContext.Set() + .Where(o => removedGroupIds.Contains(o.GroupId)) + .ExecuteDeleteAsync(); + } + + await transaction.CommitAsync(); return null; } } diff --git a/SW.Bitween.Api/Resources/RetryPolicies/Usage.cs b/SW.Bitween.Api/Resources/RetryPolicies/Usage.cs new file mode 100644 index 00000000..f72cb526 --- /dev/null +++ b/SW.Bitween.Api/Resources/RetryPolicies/Usage.cs @@ -0,0 +1,60 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using SW.Bitween.Domain; +using SW.Bitween.Domain.Accounts; +using SW.Bitween.Model; +using SW.PrimitiveTypes; + +namespace SW.Bitween.Resources.RetryPolicies; + +/// +/// Reports the state of every subscription-and-group pair using this policy: how much of the +/// group's total budget that subscription has spent, and where the pair's budget-exhausted alert +/// would go. +/// +/// +/// +/// Both halves share the (SubscriptionId, GroupId) key, so they are reported together — the +/// question worth asking about an exhausted budget is whether anyone was told about it, and +/// splitting that across two reports leaves the caller to join them by eye. +/// +/// +/// Starts from the policy's groups rather than from the stored counters, so a subscription that has +/// never failed still gets a row and its alert override stays configurable before the first failure. +/// Counters for groups no longer in the policy are therefore left out, which is what +/// and delete outright. +/// +/// +[HandlerName("usage")] +public class Usage : ICommandHandler +{ + private readonly BitweenDbContext _dbContext; + private readonly RequestContext _requestContext; + private readonly RetryUsageReport _report; + + public Usage(BitweenDbContext dbContext, RequestContext requestContext, RetryUsageReport report) + { + _dbContext = dbContext; + _requestContext = requestContext; + _report = report; + } + + public async Task Handle(int key, RetryPolicyUsageRequest request) + { + await _requestContext.EnsurePermission(_dbContext, Model.Permissions.RetryPolicies.View); + + var policy = await _dbContext.Set().AsNoTracking() + .FirstOrDefaultAsync(p => p.Id == key); + if (policy == null) throw new SWNotFoundException(key.ToString()); + + var subscriptions = await _dbContext.Set().AsNoTracking() + .Where(s => s.RetryPolicyId == key) + .Select(s => new { s.Id, s.Name }) + .ToListAsync(); + + return await _report.Build( + subscriptions.Select(s => (s.Id, s.Name)).ToList(), policy.Groups, policy); + } +} diff --git a/SW.Bitween.Api/Resources/Subscriptions/Create.cs b/SW.Bitween.Api/Resources/Subscriptions/Create.cs index 74dad73f..f9f74ccb 100644 --- a/SW.Bitween.Api/Resources/Subscriptions/Create.cs +++ b/SW.Bitween.Api/Resources/Subscriptions/Create.cs @@ -89,8 +89,24 @@ public async Task Handle(SubscriptionCreate model) private class Validate : AbstractValidator { - public Validate(AdapterRequirements adapterRequirements) + public Validate(BitweenDbContext dbContext, AdapterRequirements adapterRequirements) { + // Same rule Documents enforces on BusMessageTypeName. Publishing to a name no + // information type carries is legitimate — the consumer may be another product — + // but it still becomes a RabbitMQ routing key, and a name with a space in it + // could never be answered by an information type anyway. + RuleFor(i => i.ResponseMessageTypeName) + .Matches("^\\S+$") + .When(i => !string.IsNullOrEmpty(i.ResponseMessageTypeName)) + .WithMessage("A bus message name cannot contain spaces."); + + RuleFor(i => i.ResponseSubscriptionId).CustomAsync(async (responseSubId, context, _) => + { + var failure = await ResponseRoutingValidation.CheckDestination(dbContext, responseSubId); + if (failure != null) + context.AddFailure(nameof(SubscriptionCreate.ResponseSubscriptionId), failure); + }); + RuleFor(i => i.Name).NotEmpty(); RuleFor(i => i.DocumentId).NotEmpty().When(i => i.Type != SubscriptionType.Aggregation); RuleFor(i => i.PartnerId).NotEqual(Partner.SystemId); diff --git a/SW.Bitween.Api/Resources/Subscriptions/Delete.cs b/SW.Bitween.Api/Resources/Subscriptions/Delete.cs index c4c31e92..2fe856e4 100644 --- a/SW.Bitween.Api/Resources/Subscriptions/Delete.cs +++ b/SW.Bitween.Api/Resources/Subscriptions/Delete.cs @@ -1,9 +1,10 @@ -using SW.EfCoreExtensions; +using Microsoft.EntityFrameworkCore; +using SW.EfCoreExtensions; using SW.Bitween.Domain; +using SW.Bitween.Domain.Gateway; using SW.PrimitiveTypes; -using System; using System.Collections.Generic; -using System.Text; +using System.Linq; using System.Threading.Tasks; namespace SW.Bitween.Resources.Subscriptions @@ -24,8 +25,67 @@ public async Task Handle(int key) { await _requestContext.EnsurePermission(_dbContext, Model.Permissions.Subscriptions.Delete); + await EnsureNothingPointsAtIt(key); + await _dbContext.DeleteByKeyAsync(key); return null; } + + /// + /// Says what still points at the integration, before the database says it less politely. + /// + /// + /// All four references are RESTRICT, so the delete was already refused — but as a + /// foreign key violation surfacing as a 500, which tells the operator nothing about which + /// route or gateway is holding it, and reads like a broken screen rather than a decision. + /// Exchanges are deliberately not checked: their reference is nullable and history is not + /// a reason to keep configuration alive. + /// + private async Task EnsureNothingPointsAtIt(int key) + { + var heldBy = new List(); + + var routeGateways = await _dbContext.Set() + .Where(r => r.SubscriptionId == key) + .Select(r => r.BusGateway.Name) + .Distinct() + .ToArrayAsync(); + if (routeGateways.Length > 0) + heldBy.Add($"a route on {Join(routeGateways)}"); + + var attachmentGateways = await _dbContext.Set() + .Where(p => p.SubscriptionId == key) + .Select(p => p.ApiGateway.Name) + .Distinct() + .ToArrayAsync(); + if (attachmentGateways.Length > 0) + heldBy.Add($"a partner attached to {Join(attachmentGateways)}"); + + var fedBy = await _dbContext.Set() + .Where(s => s.ResponseSubscriptionId == key) + .Select(s => s.Name) + .ToArrayAsync(); + if (fedBy.Length > 0) + heldBy.Add($"the response of {Join(fedBy)}"); + + var aggregatedBy = await _dbContext.Set() + .Where(s => s.AggregationForId == key) + .Select(s => s.Name) + .ToArrayAsync(); + if (aggregatedBy.Length > 0) + heldBy.Add($"the aggregation {Join(aggregatedBy)}"); + + if (heldBy.Count == 0) + return; + + throw new SWValidationException("SUBSCRIPTION_IN_USE", + $"This integration is still used by {Join(heldBy.ToArray())}. " + + "Remove that first, or point it at another integration."); + } + + private static string Join(string[] names) => + names.Length == 1 + ? names[0] + : $"{string.Join(", ", names[..^1])} and {names[^1]}"; } -} \ No newline at end of file +} diff --git a/SW.Bitween.Api/Resources/Subscriptions/GetReceiveAttempts.cs b/SW.Bitween.Api/Resources/Subscriptions/GetReceiveAttempts.cs new file mode 100644 index 00000000..c162e898 --- /dev/null +++ b/SW.Bitween.Api/Resources/Subscriptions/GetReceiveAttempts.cs @@ -0,0 +1,88 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using SW.Bitween.Domain; +using SW.Bitween.Model; +using SW.PrimitiveTypes; + +namespace SW.Bitween.Resources.Subscriptions; + +/// +/// Paged, filterable history of one Receiving subscription's own +/// rows — independent of , which reads the scheduler's own (always-succeeds) +/// history instead. +/// +[HandlerName("receiveattempts")] +public class GetReceiveAttempts : IQueryHandler +{ + private readonly BitweenDbContext _dbContext; + private readonly RequestContext _requestContext; + + public GetReceiveAttempts(BitweenDbContext dbContext, RequestContext requestContext) + { + _dbContext = dbContext; + _requestContext = requestContext; + } + + public async Task Handle(SearchReceiveAttemptsModel request) + { + await _requestContext.EnsurePermission(_dbContext, Model.Permissions.Subscriptions.View); + + var offset = request.Offset ?? 0; + var limit = request.Limit ?? 25; + + var query = _dbContext.Set() + .AsNoTracking() + .Where(a => a.SubscriptionId == request.SubscriptionId); + + if (request.Outcome.HasValue) + query = query.Where(a => a.Outcome == request.Outcome.Value); + + var totalCount = await query.CountAsync(); + + var page = await query + .OrderByDescending(a => a.StartedOn) + .Skip(offset) + .Take(limit) + .ToListAsync(); + + var exchangeIds = page.SelectMany(a => a.ExchangeIds ?? Array.Empty()).Distinct().ToList(); + + // Left join: an id an attempt still points at but whose Xchange got cleaned up some + // other way shows up with nulls rather than silently dropping the row's own history. + var exchangesById = await ( + from x in _dbContext.Set() + join r in _dbContext.Set() on x.Id equals r.Id into xr + from r in xr.DefaultIfEmpty() + join p in _dbContext.Set() on x.Id equals p.Id into xp + from p in xp.DefaultIfEmpty() + where exchangeIds.Contains(x.Id) + select new ReceiveAttemptExchangeRef + { + Id = x.Id, + Status = r.Success, + ResponseBad = r.ResponseBad, + PromotedProperties = p == null ? null : p.Properties.ToDictionary(), + } + ).ToDictionaryAsync(e => e.Id); + + var result = page.Select(a => new ReceiveAttemptModel + { + Id = a.Id, + StartedOn = a.StartedOn, + FinishedOn = a.FinishedOn, + Outcome = a.Outcome, + ErrorMessage = a.ErrorMessage, + Exchanges = (a.ExchangeIds ?? Array.Empty()) + .Select(id => exchangesById.TryGetValue(id, out var x) ? x : new ReceiveAttemptExchangeRef { Id = id }) + .ToList(), + }).ToList(); + + return new + { + Result = result, + TotalCount = totalCount, + }; + } +} diff --git a/SW.Bitween.Api/Resources/Subscriptions/InlineIntegration.cs b/SW.Bitween.Api/Resources/Subscriptions/InlineIntegration.cs new file mode 100644 index 00000000..212c86eb --- /dev/null +++ b/SW.Bitween.Api/Resources/Subscriptions/InlineIntegration.cs @@ -0,0 +1,123 @@ +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using System.Text.RegularExpressions; +using SW.Bitween.Domain; +using SW.Bitween.Model; +using SW.PrimitiveTypes; + +namespace SW.Bitween.Resources.Subscriptions +{ + /// + /// Turns an integration defined inline — while a gateway route or attachment is being made — + /// into a subscription, ready to be saved alongside whatever points at it. + /// + /// Nothing here duplicates : the pipeline is applied by the very same + /// , and the caller does the single + /// SaveChangesAsync, so the integration and its link either both exist or neither does. + /// + /// + public static class InlineIntegration + { + /// + /// Which integration a gateway link points at. Exactly one of an existing id and an + /// inline definition has to be given — both, or neither, is a mistake worth naming. + /// + public static void EnsureExactlyOne(int? subscriptionId, InlineIntegrationCreate inline) + { + if (subscriptionId.HasValue && inline != null) + throw new SWValidationException(GatewayLinkTarget.BothGiven, + "Give either an existing integration or a new one to create, not both."); + + if (!subscriptionId.HasValue && inline == null) + throw new SWValidationException(GatewayLinkTarget.NeitherGiven, + "Pick the integration this runs, or define a new one."); + } + + + /// + /// The rules an ordinary create enforces through FluentValidation, applied to an integration + /// arriving this way. Without them this door was a hole in the same validation: a response + /// message name with a space in it went straight to a RabbitMQ routing key nothing can + /// answer. Each check calls the one implementation the create handler calls. + /// + private static async Task CheckConfiguration( + BitweenDbContext dbContext, + AdapterRequirements adapterRequirements, + InlineIntegrationCreate model) + { + if (!string.IsNullOrEmpty(model.ResponseMessageTypeName) + && Regex.IsMatch(model.ResponseMessageTypeName, @"\s")) + throw new SWValidationException("INVALID_BUS_TYPE_NAME", + "A bus message name cannot contain spaces."); + + var responseFailure = await ResponseRoutingValidation.CheckDestination( + dbContext, model.ResponseSubscriptionId); + if (responseFailure != null) + throw new SWValidationException( + ResponseRoutingValidation.BusGatewayCode, responseFailure); + + // Neither gateway type carries its own partner — a partner reaches them through the + // attachment or the route, which is the very thing being made. + if (model.PartnerId.HasValue) + throw new SWValidationException("PARTNER_NOT_ALLOWED", + "A gateway integration does not carry its own partner."); + + // A named adapter has to be usable. Naming none is still fine; a half-configured one + // is exactly what committing here would make permanent. + foreach (var (kind, adapterId, provided) in new[] + { + ("receiver", model.ReceiverId, model.ReceiverProperties), + ("validator", model.ValidatorId, model.ValidatorProperties), + ("mapper", model.MapperId, model.MapperProperties), + ("handler", model.HandlerId, model.HandlerProperties), + }) + { + var missing = await adapterRequirements.MissingFor(adapterId, provided); + if (missing.Count > 0) + throw new SWValidationException("ADAPTER_INCOMPLETE", + $"The {kind} is missing {string.Join(", ", missing)}."); + } + } + + /// + /// Builds the subscription and adds it to the change tracker, so its id is available to + /// the link being created in the same transaction. Does not save. + /// + public static async Task Stage( + BitweenDbContext dbContext, + AdapterRequirements adapterRequirements, + InlineIntegrationCreate model, + int documentId, + SubscriptionType type) + { + if (string.IsNullOrWhiteSpace(model.Name)) + throw new SWValidationException("INVALID_NAME", "Give the integration a name."); + + await CheckConfiguration(dbContext, adapterRequirements, model); + + // Who chooses the information type differs by gateway kind, so the caller passes it: + // a bus gateway is bound to one and imposes it, an API gateway is not and the caller + // picks. Either way it is settled before Apply runs. + if (!await dbContext.Set().AnyAsync(d => d.Id == documentId)) + throw new SWValidationException("INVALID_DOCUMENT", + "Choose the information type this integration carries."); + + model.DocumentId = documentId; + + var entity = new Subscription(model.Name, documentId, type); + var trail = new SubscriptionTrail(SubscriptionTrialCode.Created, entity, true); + dbContext.Add(trail); + + // The same code an ordinary create runs, so a field cannot work through one door + // and not the other. + await SubscriptionConfigurationApplier.Apply(dbContext, entity, model); + + // Neither gateway type runs on its own — a GatewayApiCall waits for an attachment, a + // BusGateway for a route — and the one being made is in this same transaction. + entity.Inactive = false; + + dbContext.Add(entity); + return entity; + } + } +} diff --git a/SW.Bitween.Api/Resources/Subscriptions/ResetRetryUsage.cs b/SW.Bitween.Api/Resources/Subscriptions/ResetRetryUsage.cs new file mode 100644 index 00000000..122e19b1 --- /dev/null +++ b/SW.Bitween.Api/Resources/Subscriptions/ResetRetryUsage.cs @@ -0,0 +1,49 @@ +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using SW.Bitween.Domain; +using SW.Bitween.Domain.Accounts; +using SW.Bitween.Model; +using SW.PrimitiveTypes; + +namespace SW.Bitween.Resources.Subscriptions; + +/// +/// Clears one subscription's spent retry budget so its groups start retrying again. +/// +/// +/// The policy-scoped reset finds subscriptions by policy id, which leaves an inline +/// CustomRetryPolicy unreachable: its counters are written like any other and then nothing can +/// clear them, so once exhausted that subscription would never retry again. This resets by +/// subscription instead, which also picks up counters left behind by groups that no longer exist. +/// +[HandlerName("resetretryusage")] +public class ResetRetryUsage : ICommandHandler +{ + private readonly BitweenDbContext _dbContext; + private readonly RequestContext _requestContext; + + public ResetRetryUsage(BitweenDbContext dbContext, RequestContext requestContext) + { + _dbContext = dbContext; + _requestContext = requestContext; + } + + public async Task Handle(int key, SubscriptionRetryResetUsage request) + { + await _requestContext.EnsurePermission(_dbContext, Model.Permissions.Subscriptions.Operate); + + if (!await _dbContext.Set().AnyAsync(s => s.Id == key)) + throw new SWNotFoundException(key.ToString()); + + // Scoped by subscription rather than by policy, so it cannot reach anyone else's counters no + // matter which kind of policy this subscription uses. + var query = _dbContext.Set().Where(u => u.SubscriptionId == key); + + if (request.GroupId.HasValue) + query = query.Where(u => u.GroupId == request.GroupId.Value); + + await query.ExecuteDeleteAsync(); + return null; + } +} diff --git a/SW.Bitween.Api/Resources/Subscriptions/ResponseRoutingValidation.cs b/SW.Bitween.Api/Resources/Subscriptions/ResponseRoutingValidation.cs new file mode 100644 index 00000000..82b4e9d6 --- /dev/null +++ b/SW.Bitween.Api/Resources/Subscriptions/ResponseRoutingValidation.cs @@ -0,0 +1,42 @@ +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using SW.Bitween.Domain; +using SW.Bitween.Model; +using SW.PrimitiveTypes; + +namespace SW.Bitween.Resources.Subscriptions; + +/// +/// Guards what a delivery response may be fed into. +/// +/// A bus gateway's route is defined by the message that runs it, but +/// creates the response exchange against the chosen +/// subscription directly. Picking a route as the response destination therefore runs that one +/// route with the bus skipped entirely: no message published, no route matching, no filter, and +/// none of the other routes bound to the same message. It looks like publishing and is not, so +/// it is refused rather than left as a trap. ResponseMessageTypeName is the field that +/// actually puts a response on the bus. +/// +/// +internal static class ResponseRoutingValidation +{ + public const string BusGatewayCode = "RESPONSE_SUBSCRIPTION_IS_BUS_GATEWAY"; + + /// Returns the failure message, or null when the destination is allowed. + public static async Task CheckDestination(BitweenDbContext dbContext, int? responseSubscriptionId) + { + if (responseSubscriptionId is null) return null; + + var type = await dbContext.Set().AsNoTracking() + .Where(s => s.Id == responseSubscriptionId.Value) + .Select(s => (SubscriptionType?)s.Type) + .SingleOrDefaultAsync(); + + if (type != SubscriptionType.BusGateway) return null; + + return "A bus gateway route cannot receive a response: it would run with the bus skipped, " + + "so no other route bound to the same message would see it. Publish the response on " + + "the bus instead, and let the gateway's routes pick it up."; + } +} diff --git a/SW.Bitween.Api/Resources/Subscriptions/RetryUsage.cs b/SW.Bitween.Api/Resources/Subscriptions/RetryUsage.cs new file mode 100644 index 00000000..af5ad0e1 --- /dev/null +++ b/SW.Bitween.Api/Resources/Subscriptions/RetryUsage.cs @@ -0,0 +1,50 @@ +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using SW.Bitween.Domain; +using SW.Bitween.Domain.Accounts; +using SW.Bitween.Model; +using SW.PrimitiveTypes; + +namespace SW.Bitween.Resources.Subscriptions; + +/// +/// Reports one subscription's spent retry budget and where each group's exhaustion alert would go. +/// +/// +/// The policy-scoped report answers the same question for every subscription sharing a policy, but it +/// can only find subscriptions by policy id — so a subscription carrying an inline +/// CustomRetryPolicy is invisible to it while still spending and recording budget. Asking from +/// the subscription's side reaches those too. +/// +[HandlerName("retryusage")] +public class RetryUsage : ICommandHandler +{ + private readonly BitweenDbContext _dbContext; + private readonly RequestContext _requestContext; + private readonly RetryUsageReport _report; + + public RetryUsage(BitweenDbContext dbContext, RequestContext requestContext, RetryUsageReport report) + { + _dbContext = dbContext; + _requestContext = requestContext; + _report = report; + } + + public async Task Handle(int key, RetryPolicyUsageRequest request) + { + await _requestContext.EnsurePermission(_dbContext, Model.Permissions.Subscriptions.View); + + var subscription = await _dbContext.Set().AsNoTracking() + .Include(s => s.RetryPolicy) + .FirstOrDefaultAsync(s => s.Id == key); + if (subscription == null) throw new SWNotFoundException(key.ToString()); + + // Whichever policy actually applies. An inline one has no row, so the policy level of the + // alert hierarchy simply is not there for it — passed as null, which the resolver expects. + var groups = subscription.CustomRetryPolicy?.Groups ?? subscription.RetryPolicy?.Groups ?? []; + + return await _report.Build( + [(subscription.Id, subscription.Name)], groups, subscription.RetryPolicy); + } +} diff --git a/SW.Bitween.Api/Resources/Subscriptions/Update.cs b/SW.Bitween.Api/Resources/Subscriptions/Update.cs index 8f1dc450..209478e7 100644 --- a/SW.Bitween.Api/Resources/Subscriptions/Update.cs +++ b/SW.Bitween.Api/Resources/Subscriptions/Update.cs @@ -1,4 +1,5 @@ using FluentValidation; +using Microsoft.EntityFrameworkCore; using SW.EfCoreExtensions; using SW.Bitween.Domain; using SW.Bitween.Model; @@ -7,6 +8,7 @@ using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; +using SW.Bitween.Resources.RetryPolicies; namespace SW.Bitween.Resources.Subscriptions { @@ -38,6 +40,14 @@ public async Task Handle(int key, SubscriptionUpdate model) // Name and the runtime-state fields, which only an update may set. _dbContext.Entry(entity).SetProperties(model); + if (model.CustomRetryPolicy == null && model.RetryPolicyId != null && + !await _dbContext.Set().AnyAsync(p => p.Id == model.RetryPolicyId)) + throw new SWValidationException("RETRY_POLICY_NOT_FOUND", + $"Retry policy {model.RetryPolicyId} was not found."); + + if (model.CustomRetryPolicy != null) + RetryGroupValidation.EnsureCanFire(model.CustomRetryPolicy.Groups); + // Everything a person configures, through the same code the create handler runs. await SubscriptionConfigurationApplier.Apply(_dbContext, entity, model); @@ -110,6 +120,11 @@ public Validate(BitweenDbContext dbContext, IHttpContextAccessor httpContextAcce RuleFor(i => i.Name).NotEmpty(); RuleFor(i => i.MatchExpression).Must(ValidateMatch); RuleFor(i => i.PartnerId).NotEqual(Partner.SystemId); + // Matches the rule Documents enforces on BusMessageTypeName; see Create. + RuleFor(i => i.ResponseMessageTypeName) + .Matches("^\\S+$") + .When(i => !string.IsNullOrEmpty(i.ResponseMessageTypeName)) + .WithMessage("A bus message name cannot contain spaces."); When(i => i.MapperId != null, () => { @@ -141,6 +156,15 @@ public Validate(BitweenDbContext dbContext, IHttpContextAccessor httpContextAcce }); }); + // Outside the handler check above: a response destination that can never work is + // wrong whether or not this same request also sets a handler. + RuleFor(i => i.ResponseSubscriptionId).CustomAsync(async (responseSubId, context, ct) => + { + var failure = await ResponseRoutingValidation.CheckDestination(dbContext, responseSubId); + if (failure != null) + context.AddFailure(nameof(SubscriptionUpdate.ResponseSubscriptionId), failure); + }); + RuleFor(i => i).CustomAsync(async (model, context, ct) => { var subscription = await GetSub(dbContext, httpContextAccessor); diff --git a/SW.Bitween.Api/Resources/WorkGroups/Search.cs b/SW.Bitween.Api/Resources/WorkGroups/Search.cs index 8a9868ae..d5c85e7d 100644 --- a/SW.Bitween.Api/Resources/WorkGroups/Search.cs +++ b/SW.Bitween.Api/Resources/WorkGroups/Search.cs @@ -32,7 +32,9 @@ public async Task Handle(SearchWorkGroupModel request) // until the cache's own TTL expires whenever that broadcast doesn't // land. GlobalAdapterValuesSets and RetryPolicies already read the DB // directly for the same reason. - var workGroups = await dbContext.Set().AsNoTracking().ToArrayAsync(); + var workGroups = await dbContext.Set().AsNoTracking() + .Where(w => request.Name == null || w.Name.Contains(request.Name)) + .ToArrayAsync(); var consumerCounts = Array.Empty(); try diff --git a/SW.Bitween.Api/Resources/Xchanges/BulkRetry.cs b/SW.Bitween.Api/Resources/Xchanges/BulkRetry.cs index 2d2a356d..49065a29 100644 --- a/SW.Bitween.Api/Resources/Xchanges/BulkRetry.cs +++ b/SW.Bitween.Api/Resources/Xchanges/BulkRetry.cs @@ -44,12 +44,18 @@ public async Task Handle(XchangeBulkRetry request) if (subscription == null) throw new SWValidationException("SUBSCRIPTION_NOT_FOUND", "Cant reset properties, subscription doesnt exist anymore"); - await _xchangeService.CreateXchange(subscription, xchange, xchangeFile); + await _xchangeService.CreateXchange(subscription, xchange, xchangeFile, + manualRetry: true); } else { - await _xchangeService.CreateXchange(xchange, xchangeFile, subscription?.WorkGroup); + // Null when the subscription has since been deleted, which a document-only + // exchange also has from the start. The single-exchange retry has always allowed + // for it; without the same here, one such id in a selection threw and took the + // whole bulk retry down with it. + await _xchangeService.CreateXchange(xchange, xchangeFile, subscription?.WorkGroup, + manualRetry: true); } } diff --git a/SW.Bitween.Api/Resources/Xchanges/Retry.cs b/SW.Bitween.Api/Resources/Xchanges/Retry.cs index 6b2c034b..3c76cd8f 100644 --- a/SW.Bitween.Api/Resources/Xchanges/Retry.cs +++ b/SW.Bitween.Api/Resources/Xchanges/Retry.cs @@ -34,11 +34,12 @@ public async Task Handle(string key, XchangeRetry xchangeRetry) if (subscription == null) throw new SWValidationException("SUBSCRIPTION_NOT_FOUND", "Cant reset properties, subscription doesnt exist anymore"); - await xchangeService.CreateXchange(subscription, xchange, xchangeFile); + await xchangeService.CreateXchange(subscription, xchange, xchangeFile, manualRetry: true); } else { - await xchangeService.CreateXchange(xchange,xchangeFile,subscription?.WorkGroup ); + await xchangeService.CreateXchange(xchange, xchangeFile, subscription?.WorkGroup, + manualRetry: true); } diff --git a/SW.Bitween.Api/Resources/Xchanges/Search.cs b/SW.Bitween.Api/Resources/Xchanges/Search.cs index e096981e..cadf7daf 100644 --- a/SW.Bitween.Api/Resources/Xchanges/Search.cs +++ b/SW.Bitween.Api/Resources/Xchanges/Search.cs @@ -85,7 +85,8 @@ from delayedRetry in drGroup.DefaultIfEmpty() // pre-migration xchanges have it null even when their subscription carries // a direct PartnerId — fall back to that for those legacy rows. PartnerId = xchange.PartnerId ?? subscriber.PartnerId, - ScheduledRetryOn = delayedRetry != null ? delayedRetry.On : (DateTime?)null + ScheduledRetryOn = delayedRetry != null ? delayedRetry.On : (DateTime?)null, + RetryBlockedReason = result.RetryBlockedReason }; var condition = searchyRequest.Conditions.FirstOrDefault(); @@ -150,7 +151,11 @@ from delayedRetry in drGroup.DefaultIfEmpty() { var value = propertyFilter.Value.ToString()!.ToLower(); - query = query.Where(i => i.PromotedPropertiesRaw.Contains(value)); + // Both sides lower-cased at query time. Promoted values keep the case the + // payload had (see FilterService), so the column has to be folded here for + // the search to stay case-insensitive. No index is lost: a Contains is a + // leading-wildcard LIKE, which the b-tree on this column could never serve. + query = query.Where(i => i.PromotedPropertiesRaw.ToLower().Contains(value)); condition.Filters.Remove(propertyFilter); } } diff --git a/SW.Bitween.Api/Services/AdapterInvoker.cs b/SW.Bitween.Api/Services/AdapterInvoker.cs new file mode 100644 index 00000000..f310d484 --- /dev/null +++ b/SW.Bitween.Api/Services/AdapterInvoker.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using SW.PrimitiveTypes; + +namespace SW.Bitween; + +/// +/// Runs a handler adapter, whichever kind it is: in-process for a native handler, or through +/// serverless for an uploaded one. +/// +/// +/// The choice between the two is made from the id alone and is identical wherever a handler is +/// invoked, so it lives here once. Kept as the single place that knows the adapter contract — when +/// that contract changes, a copy of this block somewhere else is what gets left behind. +/// +public class AdapterInvoker( + NativeAdapterDiscoveryService nativeAdapterDiscovery, + IServiceProvider serviceProvider) +{ + /// + /// Hands to the handler and returns whatever it produced, which is + /// null for a handler that only consumes. + /// + public async Task Handle(string handlerId, Dictionary handlerProperties, + string correlationId, XchangeFile payload) + { + if (handlerId.StartsWith(NativeAdapterDiscoveryService.NativePrefix, StringComparison.OrdinalIgnoreCase)) + { + var handler = nativeAdapterDiscovery.GetNativeHandler(handlerId, handlerProperties); + return await handler.Handle(payload); + } + + var serverless = serviceProvider.GetRequiredService(); + await serverless.StartAsync(handlerId, correlationId, handlerProperties); + return await serverless.InvokeAsync(nameof(IInfolinkHandler.Handle), payload); + } +} diff --git a/SW.Bitween.Api/Services/AdapterSecretProperties.cs b/SW.Bitween.Api/Services/AdapterSecretProperties.cs new file mode 100644 index 00000000..331f3bde --- /dev/null +++ b/SW.Bitween.Api/Services/AdapterSecretProperties.cs @@ -0,0 +1,145 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using SW.Bitween.Model; +using SW.PrimitiveTypes; + +namespace SW.Bitween; + +/// +/// Keeps adapter secrets — an api key, a mail password — out of responses, and puts them back when +/// an unchanged one is saved again. +/// +/// +/// +/// An adapter marks a startup value [Secure], which is what +/// reports. replaces those values with on the way out, and +/// reads the sentinel on the way back in as "keep what is stored" — so a form +/// that only changed the subject line does not overwrite the password with a row of dots. +/// +/// +/// The same sentinel and the same pair of steps already guard subscription adapter properties inside +/// Subscriptions/Get and Subscriptions/Update; this is the reusable form of it. +/// +/// +public class AdapterSecretProperties( + NativeAdapterDiscoveryService nativeAdapterDiscovery, + IServiceProvider serviceProvider) +{ + /// Stands in for a secret value in any response that carries adapter properties. + public const string Sentinel = "__private__"; + + // Describing a serverless adapter means starting it and asking, which is far too expensive to + // repeat per row of a report. Scoped service, so the memo lives exactly as long as one request. + private readonly Dictionary> _described = new(); + + /// + /// Returns a copy with every secret value replaced. Values that are already empty are left + /// alone, so "not set" stays distinguishable from "set but hidden". + /// + public async Task> Mask( + string adapterId, IReadOnlyDictionary properties) + { + if (properties == null || properties.Count == 0) + return properties?.ToDictionary(kv => kv.Key, kv => kv.Value); + + // No adapter to ask about: mask nothing rather than guess. There is also nothing to send + // the properties to, so they cannot be credentials in use. + if (string.IsNullOrEmpty(adapterId)) + return properties.ToDictionary(kv => kv.Key, kv => kv.Value); + + IDictionary startupValues; + try + { + startupValues = await Describe(adapterId); + } + catch + { + // Fail closed: when the adapter cannot be described there is no way to tell which value + // is a secret, and guessing wrong one way leaks it. + return properties.ToDictionary(kv => kv.Key, _ => Sentinel); + } + + return properties.ToDictionary(kv => kv.Key, kv => + startupValues.TryGetValue(kv.Key, out var startupValue) + && startupValue.Private + && !string.IsNullOrEmpty(kv.Value) + ? Sentinel + : kv.Value); + } + + /// + /// Resolves the sentinels in against what is already stored. A + /// sentinel with nothing stored under that key is dropped rather than saved literally. + /// + public static Dictionary Merge( + IReadOnlyDictionary stored, IReadOnlyDictionary incoming) + { + if (incoming == null) return null; + + var result = new Dictionary(); + foreach (var kv in incoming) + { + if (kv.Value != Sentinel) + { + result[kv.Key] = kv.Value; + } + else if (stored != null && stored.TryGetValue(kv.Key, out var storedValue)) + { + result[kv.Key] = storedValue; + } + } + return result; + } + + /// + /// applied to the dictionary the caller already holds. + /// + /// + /// is an immutable value object — every property is init — so a + /// group's properties cannot be swapped for a masked copy. Editing the dictionary in place is + /// the way to reach them without either loosening that contract or rebuilding each group + /// property by property, which would silently drop whatever property is added to it next. + /// + public async Task MaskInPlace(string adapterId, Dictionary properties) + { + if (properties == null || properties.Count == 0) return; + + var masked = await Mask(adapterId, properties); + properties.Clear(); + foreach (var kv in masked) properties[kv.Key] = kv.Value; + } + + /// applied to the dictionary the caller already holds. + public static void MergeInPlace( + IReadOnlyDictionary stored, Dictionary incoming) + { + if (incoming == null || incoming.Count == 0) return; + + var merged = Merge(stored, incoming); + incoming.Clear(); + foreach (var kv in merged) incoming[kv.Key] = kv.Value; + } + + private async Task> Describe(string adapterId) + { + if (_described.TryGetValue(adapterId, out var cached)) return cached; + + IDictionary startupValues; + if (adapterId.StartsWith(NativeAdapterDiscoveryService.NativePrefix, StringComparison.OrdinalIgnoreCase)) + { + startupValues = nativeAdapterDiscovery.GetStartupValues(adapterId); + } + else + { + var serverless = serviceProvider.GetRequiredService(); + await serverless.StartAsync(adapterId, null); + startupValues = await serverless.GetExpectedStartupValues(); + } + + _described[adapterId] = startupValues; + return startupValues; + } +} diff --git a/SW.Bitween.Api/Services/BitweenOptions.cs b/SW.Bitween.Api/Services/BitweenOptions.cs index a6b8da35..d9d426a3 100644 --- a/SW.Bitween.Api/Services/BitweenOptions.cs +++ b/SW.Bitween.Api/Services/BitweenOptions.cs @@ -104,5 +104,18 @@ public BitweenOptions() /// Format: second minute hour dayOfMonth month dayOfWeek /// public string RetryJobCron { get; set; } = "0 * * * * ?"; + + /// + /// How many days to keep ReceiveAttempt rows before ReceiveAttemptCleanupJob + /// deletes them. Matches the scheduler library's own JobExecution retention default. + /// + public int ReceiveAttemptRetentionDays { get; set; } = 30; + + /// + /// Quartz cron expression for ReceiveAttemptCleanupJob. Defaults to daily at 3am — + /// offset from the scheduler library's own cleanup job (2am) so they don't run at once. + /// Format: second minute hour dayOfMonth month dayOfWeek + /// + public string ReceiveAttemptCleanupCron { get; set; } = "0 0 3 * * ?"; } } \ No newline at end of file diff --git a/SW.Bitween.Api/Services/Caching/InMemoryInfolinkCache.cs b/SW.Bitween.Api/Services/Caching/InMemoryInfolinkCache.cs index 61dd86e7..df5be0f7 100644 --- a/SW.Bitween.Api/Services/Caching/InMemoryInfolinkCache.cs +++ b/SW.Bitween.Api/Services/Caching/InMemoryInfolinkCache.cs @@ -71,8 +71,11 @@ public async Task ListBusGatewayRoutesByDocumentAsync(int doc cachedBusGateways = _cache.Get(nameof(BusGateway)); } + // A deactivated gateway offers no routes, which is the whole of what deactivating + // one means on the bus side: the message still publishes, this gateway just stops + // being one of the places it lands. return cachedBusGateways - .Where(g => g.DocumentId == documentId) + .Where(g => g.DocumentId == documentId && !g.Inactive) .SelectMany(g => g.Routes ?? Enumerable.Empty()) .ToArray(); } diff --git a/SW.Bitween.Api/Services/FilterService.cs b/SW.Bitween.Api/Services/FilterService.cs index 6dd8ecf0..9c52ed6e 100644 --- a/SW.Bitween.Api/Services/FilterService.cs +++ b/SW.Bitween.Api/Services/FilterService.cs @@ -38,16 +38,44 @@ public async Task Filter(int documentId, XchangeFile xchangeFile) //TODO check if we need to validate here //if (ppValue is null) // throw new SWValidationException("PROMOTED_PROPERTY_NOT_FOUND", $"The path {pp.Value} is null on the docuemnt"); - filterResult.Properties.Add(pp.Key, ppValue?.ToLower()); + // Stored as the payload sent it. It used to be lower-cased here, which was + // only ever to pair with the lower-cased term in Xchanges/Search — nothing + // matches on this dictionary (match expressions read the payload directly), + // so the one thing it changed was what every screen displays: an order for + // "Acme Retail" listed as "acme retail". Search now lower-cases the column + // instead, which keeps it case-insensitive without rewriting the data. + filterResult.Properties.Add(pp.Key, ppValue); } var subs = await _cache.ListSubscriptionsByDocumentAsync(documentId); var matches = subs.Where(sub => { - // Bus-gateway subscriptions only run via their gateway routes (with the route's - // filter and optional partner), never through the normal auto-match flow. - if (sub.Type == SubscriptionType.BusGateway) + // An integration with an entry point of its own is never started by a document + // merely arriving on its information type — it is started through that entry + // point, which is what decides it should run at all: + // + // BusGateway — its gateway's routes (route filter + optional partner) + // Receiving — its schedule, via ReceivingJob + // GatewayApiCall — a partner calling the gateway it is attached to + // ApiCall — its own partner posting to Xchanges/Update, which runs the + // subscription belonging to the caller (legacy GatewayApiCall) + // + // All four are started by name, through SubmitSubscriptionXchange. Auto-matching + // them as well ran them a second time, on traffic addressed to nobody: a scheduled + // job publishing the very message type it is bound to fed itself forever, and an + // ApiCall integration belonging to one partner ran on another partner's message. + // Both stayed hidden only while those handlers happened to be unreachable. The + // second run also arrived without the partner the entry point would have passed, + // so every {{partner.…}} in its adapters stayed a literal token. + // + // Internal keeps matching. Reacting to a document of its type arriving is the + // whole definition of the type — it has no other trigger. Aggregation is driven + // by AggregationJob. + if (sub.Type is SubscriptionType.BusGateway + or SubscriptionType.Receiving + or SubscriptionType.GatewayApiCall + or SubscriptionType.ApiCall) return false; var exp = sub.BackwardCompatibleMatchExpression(doc); diff --git a/SW.Bitween.Api/Services/ReceiveAttemptCleanupJob.cs b/SW.Bitween.Api/Services/ReceiveAttemptCleanupJob.cs new file mode 100644 index 00000000..9abb9bc8 --- /dev/null +++ b/SW.Bitween.Api/Services/ReceiveAttemptCleanupJob.cs @@ -0,0 +1,26 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using SW.Bitween.Domain; +using SW.Scheduler; + +namespace SW.Bitween; + +/// +/// Deletes rows older than +/// . +/// Scheduled via (registered by +/// SchedulerSeedService). +/// +[ScheduleConfig(AllowConcurrentExecution = false, MisfireInstructions = MisfireInstructions.Skip)] +public class ReceiveAttemptCleanupJob(BitweenDbContext dbContext, BitweenOptions options) : IScheduledJob +{ + public async Task Execute() + { + var cutoff = DateTime.UtcNow.AddDays(-options.ReceiveAttemptRetentionDays); + await dbContext.Set() + .Where(a => a.StartedOn < cutoff) + .ExecuteDeleteAsync(); + } +} diff --git a/SW.Bitween.Api/Services/ReceivingJob.cs b/SW.Bitween.Api/Services/ReceivingJob.cs index 83a10462..cad0dfab 100644 --- a/SW.Bitween.Api/Services/ReceivingJob.cs +++ b/SW.Bitween.Api/Services/ReceivingJob.cs @@ -1,6 +1,7 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using SW.Bitween.Domain; +using SW.Bitween.Model; using SW.PrimitiveTypes; using SW.Scheduler; using System; @@ -32,17 +33,40 @@ public async Task Execute(ReceivingJobParams jobParams) var isIdle = await runFlagUpdater.MarkAsRunning(rec.Id); if (!isIdle) return; + var startedOn = DateTime.UtcNow; + // Populated as files come in, so a mid-loop failure still leaves the ones that + // did make it through visible on the attempt record rather than orphaned. + var createdExchangeIds = new List(); + + // Advances regardless of outcome: the Quartz trigger fires on its own cron no + // matter what happens below, so "next run" has to track that, not the receive + // step's success — otherwise a receiver that keeps failing freezes ReceiveOn + // in the past forever while the job keeps firing on schedule underneath it. + // Isolated in its own try: a schedule problem is unrelated to receiving and + // must not stop the step below from running. + try + { + rec.SetSchedules(); + } + catch (Exception ex) + { + logger.LogError(ex, "Could not advance the schedule for subscription {SubscriptionId}", jobParams.SubscriptionId); + } + try { var globals = await dbContext.Set().ToArrayAsync(); var startupParameters = rec.ReceiverProperties.ToDictionary().Fill(null, globals); - await RunReceiver(rec.ReceiverId, startupParameters, rec.Id); - rec.SetSchedules(); + await RunReceiver(rec.ReceiverId, startupParameters, rec.Id, createdExchangeIds); rec.SetHealth(); + RecordAttempt(rec.Id, startedOn, + createdExchangeIds.Count > 0 ? ReceiveOutcome.Received : ReceiveOutcome.NoNewData, + null, createdExchangeIds); } catch (Exception ex) { rec.SetHealth(ex.ToString()); + RecordAttempt(rec.Id, startedOn, ReceiveOutcome.Failed, ex.ToString(), createdExchangeIds); logger.LogError(ex, "Error processing receiver for subscription {SubscriptionId}", jobParams.SubscriptionId); } finally @@ -53,7 +77,24 @@ public async Task Execute(ReceivingJobParams jobParams) await dbContext.SaveChangesAsync(); } - private async Task RunReceiver(string serverlessId, IDictionary startupParameters, int subId) + private void RecordAttempt( + int subscriptionId, DateTime startedOn, ReceiveOutcome outcome, string errorMessage, + List exchangeIds) + { + dbContext.Add(new ReceiveAttempt + { + SubscriptionId = subscriptionId, + StartedOn = startedOn, + FinishedOn = DateTime.UtcNow, + Outcome = outcome, + ErrorMessage = errorMessage, + ExchangeIds = exchangeIds.ToArray(), + }); + } + + private async Task RunReceiver( + string serverlessId, IDictionary startupParameters, int subId, + List createdExchangeIds) { if (serverlessId.StartsWith(NativeAdapterDiscoveryService.NativePrefix, StringComparison.OrdinalIgnoreCase)) { @@ -67,7 +108,7 @@ private async Task RunReceiver(string serverlessId, IDictionary { var xchangeFile = await receiver.GetFile(file); logger.LogInformation("Submitting received file for subscriber: '{SubId}'.", subId); - await xchangeService.SubmitSubscriptionXchange(subId, xchangeFile); + createdExchangeIds.Add(await xchangeService.SubmitSubscriptionXchange(subId, xchangeFile)); await receiver.DeleteFile(file); } @@ -85,7 +126,7 @@ private async Task RunReceiver(string serverlessId, IDictionary { var xchangeFile = await serverless.InvokeAsync(nameof(IInfolinkReceiver.GetFile), file); logger.LogInformation("Submitting received file for subscriber: '{SubId}'.", subId); - await xchangeService.SubmitSubscriptionXchange(subId, xchangeFile); + createdExchangeIds.Add(await xchangeService.SubmitSubscriptionXchange(subId, xchangeFile)); await serverless.InvokeAsync(nameof(IInfolinkReceiver.DeleteFile), file); } diff --git a/SW.Bitween.Api/Services/RetryAlertResolver.cs b/SW.Bitween.Api/Services/RetryAlertResolver.cs new file mode 100644 index 00000000..3e9a95a7 --- /dev/null +++ b/SW.Bitween.Api/Services/RetryAlertResolver.cs @@ -0,0 +1,84 @@ +using System.Collections.Generic; +using SW.Bitween.Domain; +using SW.Bitween.Model; + +namespace SW.Bitween; + +/// Where a resolved alert should be delivered, and which level of the hierarchy decided it. +public class RetryAlertTarget +{ + public required string HandlerId { get; init; } + public IReadOnlyDictionary HandlerProperties { get; init; } + + /// Which level won — shown in the UI so a surprising destination can be traced. + public required RetryAlertLevel Level { get; init; } +} + +/// +/// Resolves where a group's exhaustion alert goes, walking from the most specific level to the +/// least: the subscription+group override, then the group, then the policy. +/// +/// +/// +/// A level that overrides replaces the level above rather than merging into it, so +/// whichever level wins must carry the handler and every property it needs. That keeps what the UI +/// shows for a level identical to what actually gets sent. +/// +/// +/// Resolved at send time rather than stored, so editing a policy's default immediately affects +/// everything still inheriting it. +/// +/// +public static class RetryAlertResolver +{ + /// + /// Returns the destination for one subscription's alert in one group, or null when no + /// level configures one or a level explicitly silences it. + /// + /// The subscription+group override, or null if none exists. + /// The matched group. null when the group no longer exists in the policy. + /// + /// The named policy, or null when the subscription uses an inline + /// — those have no policy row, so only the group and the + /// override levels can configure an alert. + /// + public static RetryAlertTarget Resolve(RetryAlertOverride subscriptionOverride, RetryGroup group, + RetryPolicy policy) + { + switch (subscriptionOverride?.AlertMode) + { + case RetryAlertMode.Silent: + return null; + case RetryAlertMode.Send when !string.IsNullOrWhiteSpace(subscriptionOverride.AlertHandlerId): + return new RetryAlertTarget + { + HandlerId = subscriptionOverride.AlertHandlerId, + HandlerProperties = subscriptionOverride.AlertHandlerProperties, + Level = RetryAlertLevel.SubscriptionGroup + }; + } + + switch (group?.AlertMode) + { + case RetryAlertMode.Silent: + return null; + case RetryAlertMode.Send when !string.IsNullOrWhiteSpace(group.AlertHandlerId): + return new RetryAlertTarget + { + HandlerId = group.AlertHandlerId, + HandlerProperties = group.AlertHandlerProperties, + Level = RetryAlertLevel.Group + }; + } + + if (!string.IsNullOrWhiteSpace(policy?.AlertHandlerId)) + return new RetryAlertTarget + { + HandlerId = policy.AlertHandlerId, + HandlerProperties = policy.AlertHandlerProperties, + Level = RetryAlertLevel.Policy + }; + + return null; + } +} diff --git a/SW.Bitween.Api/Services/RetryAlertService.cs b/SW.Bitween.Api/Services/RetryAlertService.cs new file mode 100644 index 00000000..414e3be1 --- /dev/null +++ b/SW.Bitween.Api/Services/RetryAlertService.cs @@ -0,0 +1,131 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using SW.Bitween.Domain; +using SW.Bitween.Model; +using SW.PrimitiveTypes; + +namespace SW.Bitween; + +/// +/// Delivers "retry budget exhausted" alerts, on its own queue. +/// +/// +/// Deliberately a separate consumer rather than another branch inside XchangeService's +/// result handling: alerts go through a customer-configured adapter that may be slow or broken, and +/// on the shared result queue that would hold up — or fail — the ordinary notifiers for the same +/// exchange. Its own means its own queue, and the two fail apart. +/// +public class RetryAlertService( + BitweenDbContext dbContext, + AdapterInvoker adapterInvoker, + ILogger logger) : IConsume +{ + public async Task Process(RetryBudgetExhaustedEvent message) + { + // The bus is at-least-once, and the exhaustion is stamped on the exchange rather than on + // the send, so a redelivery would otherwise email the same alert twice. A *successful* log + // row is the record that it already went out — matching any alert row would let one failed + // send stand in for a delivery and silence every later attempt. The name is checked too, so + // a row written by some other path can never be mistaken for this alert. + var alreadySent = await dbContext.Set() + .AnyAsync(n => n.XchangeId == message.XchangeId + && n.NotifierName == XchangeNotification.RetryBudgetAlertName + && n.Success); + if (alreadySent) return; + + var subscription = await dbContext.Set() + .Include(s => s.RetryPolicy) + .FirstOrDefaultAsync(s => s.Id == message.SubscriptionId); + if (subscription == null) return; + + // An inline custom policy has no policy row, so only the group and override levels of the + // hierarchy can configure an alert for it. + IRetryPolicy policy = subscription.CustomRetryPolicy ?? (IRetryPolicy)subscription.RetryPolicy; + var group = policy?.Groups?.FirstOrDefault(g => g.Id == message.GroupId); + + var subscriptionOverride = await dbContext.Set() + .FirstOrDefaultAsync(o => o.SubscriptionId == message.SubscriptionId + && o.GroupId == message.GroupId); + + var target = RetryAlertResolver.Resolve(subscriptionOverride, group, subscription.RetryPolicy); + if (target == null) return; + + var notification = await BuildNotification(message, subscription); + await Send(target, notification, message.XchangeId); + } + + private async Task BuildNotification( + RetryBudgetExhaustedEvent message, Subscription subscription) + { + var context = await (from xchange in dbContext.Set().AsNoTracking() + where xchange.Id == message.XchangeId + join document in dbContext.Set() on xchange.DocumentId equals document.Id + join result in dbContext.Set() on xchange.Id equals result.Id into xr + from result in xr.DefaultIfEmpty() + select new + { + document.Name, + xchange.CorrelationId, + result.Exception, + result.RetryBlockedReason + }) + .FirstOrDefaultAsync(); + + return new RetryBudgetExhaustedNotification + { + XchangeId = message.XchangeId, + SubscriptionId = message.SubscriptionId, + SubscriptionName = subscription.Name, + DocumentName = context?.Name, + CorrelationId = context?.CorrelationId, + PolicyName = subscription.RetryPolicy?.Name, + GroupName = message.GroupName, + MaxAttemptsTotal = message.MaxAttemptsTotal, + BlockedReason = context?.RetryBlockedReason, + Exception = context?.Exception, + OccurredOn = message.OccurredOn + }; + } + + /// + /// Invokes the resolved handler and records the attempt either way. + /// + /// + /// A throw is logged rather than propagated, and the failure is recorded so someone can answer + /// "did the alert actually go out?". Because the guard above only counts a successful row, a + /// failed send leaves the way open for a redelivery to try again rather than closing it. + /// + private async Task Send(RetryAlertTarget target, RetryBudgetExhaustedNotification notification, + string xchangeId) + { + var handlerProperties = new Dictionary( + target.HandlerProperties ?? new Dictionary()) + { + ["xchangeid"] = xchangeId + }; + + var payload = new XchangeFile(JsonConvert.SerializeObject(notification), xchangeId); + + try + { + await adapterInvoker.Handle(target.HandlerId, handlerProperties, + notification.CorrelationId ?? xchangeId, payload); + + dbContext.Add(XchangeNotification.ForRetryBudgetAlert(xchangeId)); + } + catch (Exception ex) + { + logger.LogError(ex, + "Retry budget alert for xchange {XchangeId} could not be delivered through {HandlerId}.", + xchangeId, target.HandlerId); + dbContext.Add(XchangeNotification.ForRetryBudgetAlert(xchangeId, ex.ToString())); + } + + await dbContext.SaveChangesAsync(); + } +} diff --git a/SW.Bitween.Api/Services/RetryGroupBudget.cs b/SW.Bitween.Api/Services/RetryGroupBudget.cs new file mode 100644 index 00000000..deb10836 --- /dev/null +++ b/SW.Bitween.Api/Services/RetryGroupBudget.cs @@ -0,0 +1,174 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using SW.Bitween.Domain; +using SW.Bitween.Model; + +namespace SW.Bitween; + +/// +/// backed by the RetryGroupUsage table and scoped to one +/// integration, so a policy template shared by many integrations gives each its own total +/// instead of letting one noisy integration spend everyone's budget. +/// +public class RetryGroupBudget( + BitweenDbContext dbContext, + IServiceProvider serviceProvider, + int subscriptionId) : IRetryGroupBudget +{ + /// + /// + /// + /// The claim is a single conditional UPDATE, so the check and the increment happen as + /// one database operation. Bitween runs several instances, and a read-then-write would let two + /// simultaneous failures both observe the last free slot and both retry, exceeding the cap. + /// + /// + /// Because it commits on its own rather than with the caller's SaveChangesAsync, a slot + /// can be charged for a retry that is never scheduled if that later save fails. That errs + /// toward retrying less than the cap allows, and RetryPolicies/resetusage can return it. + /// The alternative — an explicit transaction spanning the caller's save — would publish this + /// xchange's bus events before the commit, since BitweenDbContext dispatches domain events + /// inside SaveChangesAsync. + /// + /// + public async Task TryConsume(Guid groupId, int maxAttemptsTotal) + { + // A group configured to allow no retries at all has no budget to exhaust, so it never + // alerts — otherwise every single failure under it would raise one. + if (maxAttemptsTotal <= 0) return RetryBudgetClaim.Denied; + + if (await TryIncrement(dbContext, groupId, maxAttemptsTotal)) return RetryBudgetClaim.Allowed; + + // Nothing was updated: either the ceiling is reached, or this integration and group have + // never failed before and so have no row yet. + var exists = await dbContext.Set() + .AnyAsync(u => u.SubscriptionId == subscriptionId && u.GroupId == groupId); + if (exists) return await ClaimExhaustionAlert(groupId, maxAttemptsTotal); + + // Create that first row on its own context so it commits independently of whatever the + // caller still has pending. Losing this race is harmless: the primary key rejects the + // duplicate and the conditional increment is then applied to the winner's row. + using var scope = serviceProvider.CreateScope(); + var isolated = scope.ServiceProvider.GetRequiredService(); + + isolated.Add(new RetryGroupUsage + { + SubscriptionId = subscriptionId, + GroupId = groupId, + AttemptsUsed = 1, + LastAttemptOn = DateTime.UtcNow + }); + + try + { + await isolated.SaveChangesAsync(); + return RetryBudgetClaim.Allowed; + } + catch (DbUpdateException) + { + // The winner of the insert race already holds a row, so this is the ordinary + // increment path again — including the case where their row is already full. + return await TryIncrement(dbContext, groupId, maxAttemptsTotal) + ? RetryBudgetClaim.Allowed + : await ClaimExhaustionAlert(groupId, maxAttemptsTotal); + } + } + + /// + /// Lifts this integration's group budgets that have run out, because it has just succeeded. + /// + /// + /// + /// An exhausted total is a statement about a downstream that was failing, and one success says + /// that is no longer true. Nothing else can say it: an exhausted group schedules no further + /// retries, so no retry will ever succeed to report the recovery — only ordinary traffic getting + /// through can. Without this, one bad afternoon stops retrying for good until somebody notices + /// and resets it by hand. + /// + /// + /// Only budgets that are actually used up. A partly-spent total is left alone. + /// The cap exists to stop a flaky downstream being hammered, and that is precisely a downstream + /// where some messages succeed and others fail — crediting the total back on every ordinary + /// success would mean such a subscription never reaches its cap at all. + /// + /// + /// keeps this from erasing a charge it never saw. Bitween runs + /// several instances, so a failure can claim a slot while this success is still being processed; + /// deleting that row would hand back a slot already spent and let the group exceed its total. + /// Only rows whose last attempt predates the success are released. + /// + /// + /// Deleting a row re-arms the exhaustion alert along with the budget, so if the total runs out + /// again somebody is told again rather than the second outage passing in silence. + /// + /// + /// How many group budgets were released. + public async Task ReleaseExhaustedBudgets(DateTime succeededFrom) + { + // Cheapest question first, and for almost every success the answer ends it here: a + // subscription that has never spent a retry has no row, and must not pay for a policy load + // or a write on the strength of having worked. + var spent = await dbContext.Set().AsNoTracking() + .Where(u => u.SubscriptionId == subscriptionId) + .Select(u => new { u.GroupId, u.AttemptsUsed }) + .ToListAsync(); + if (spent.Count == 0) return 0; + + var subscription = await dbContext.Set().AsNoTracking() + .Include(s => s.RetryPolicy) + .FirstOrDefaultAsync(s => s.Id == subscriptionId); + + IRetryPolicy policy = subscription?.CustomRetryPolicy ?? (IRetryPolicy)subscription?.RetryPolicy; + if (policy?.Groups == null) return 0; + + // A group whose total is gone from the policy is left to Update and Delete to clean up, which + // they already do — releasing it here would be guessing at a cap that no longer exists. + var exhausted = spent + .Where(u => policy.Groups.Any(g => g.Id == u.GroupId + && g.Budget is { MaxAttemptsTotal: > 0 } + && u.AttemptsUsed >= g.Budget.MaxAttemptsTotal)) + .Select(u => u.GroupId) + .ToList(); + if (exhausted.Count == 0) return 0; + + return await dbContext.Set() + .Where(u => u.SubscriptionId == subscriptionId + && exhausted.Contains(u.GroupId) + && u.LastAttemptOn < succeededFrom) + .ExecuteDeleteAsync(); + } + + /// + /// Takes responsibility for alerting that this integration's budget for the group is spent. + /// + /// + /// One conditional UPDATE for the same reason the increment is one: several instances can + /// discover the empty budget at the same moment, and a read-then-write would let each of them + /// decide it was the first. Exactly one caller updates a row, so exactly one alert is raised — + /// and because Reset deletes the row outright, clearing a budget re-arms the alert with it. + /// + private async Task ClaimExhaustionAlert(Guid groupId, int maxAttemptsTotal) + { + var claimed = await dbContext.Set() + .Where(u => u.SubscriptionId == subscriptionId + && u.GroupId == groupId + && u.AttemptsUsed >= maxAttemptsTotal + && u.ExhaustedNotifiedOn == null) + .ExecuteUpdateAsync(s => s + .SetProperty(u => u.ExhaustedNotifiedOn, _ => DateTime.UtcNow)) > 0; + + return claimed ? RetryBudgetClaim.DeniedAndJustExhausted : RetryBudgetClaim.Denied; + } + + private async Task TryIncrement(BitweenDbContext db, Guid groupId, int maxAttemptsTotal) => + await db.Set() + .Where(u => u.SubscriptionId == subscriptionId + && u.GroupId == groupId + && u.AttemptsUsed < maxAttemptsTotal) + .ExecuteUpdateAsync(s => s + .SetProperty(u => u.AttemptsUsed, u => u.AttemptsUsed + 1) + .SetProperty(u => u.LastAttemptOn, _ => DateTime.UtcNow)) > 0; +} diff --git a/SW.Bitween.Api/Services/RetryJob.cs b/SW.Bitween.Api/Services/RetryJob.cs index f371b4cd..1b3400c9 100644 --- a/SW.Bitween.Api/Services/RetryJob.cs +++ b/SW.Bitween.Api/Services/RetryJob.cs @@ -2,6 +2,7 @@ using System.Linq; using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; using SW.Bitween.Domain; using SW.Bitween.Model; using SW.PrimitiveTypes; @@ -13,23 +14,62 @@ namespace SW.Bitween; /// Polls for due records and re-submits the failed Xchanges. /// Scheduled via (registered by SchedulerSeedService). /// +/// +/// Works through every retry that is already due rather than a hundred a minute, and commits one row +/// at a time. Committing the batch in one go meant a single row that could not be carried out +/// discarded the work of all the others and left their schedules in place, so the same batch came +/// back a minute later and failed the same way — no retry would ever have run again. +/// [ScheduleConfig(AllowConcurrentExecution = false, MisfireInstructions = MisfireInstructions.Skip)] -public class RetryJob(BitweenDbContext dbContext, XchangeService xchangeService) : IScheduledJob +public class RetryJob(BitweenDbContext dbContext, XchangeService xchangeService, ILogger logger) + : IScheduledJob { private const int BatchSize = 100; public async Task Execute() { - var ready = await dbContext.Set() - .Where(r => r.On <= DateTime.UtcNow) - .Take(BatchSize) - .ToListAsync(); + // Fixed before the first batch: a retry scheduled while this run is working belongs to the next + // tick, otherwise a fast-failing subscription could keep this run going indefinitely. + var due = DateTime.UtcNow; - foreach (var delayedRetry in ready) + while (true) { - await xchangeService.ExecuteDelayedRetry(delayedRetry); - } + var ready = await dbContext.Set() + .Where(r => r.On <= due) + .OrderBy(r => r.On) + .Take(BatchSize) + .ToListAsync(); + + if (ready.Count == 0) return; + + foreach (var delayedRetry in ready) + { + try + { + await xchangeService.ExecuteDelayedRetry(delayedRetry); + await dbContext.SaveChangesAsync(); + } + catch (Exception ex) + { + // Not "dropped": SaveChangesAsync commits before it publishes, so a failure in the + // publish leaves the replacement exchange committed and only its announcement + // missing. Saying the retry was dropped would send whoever reads this looking for + // an exchange that does exist. + logger.LogError(ex, + "The scheduled retry of xchange {XchangeId} did not complete; clearing its " + + "schedule so the queue keeps draining.", delayedRetry.Id); - await dbContext.SaveChangesAsync(); + // Whatever the failed run left staged goes first — saving it would commit the very + // changes that failing was meant to prevent. + dbContext.ChangeTracker.Clear(); + + // Every row leaves the queue one way or another, which is what stops the loop above + // from meeting the same row again and turning the drain into a spin. + await dbContext.Set() + .Where(r => r.Id == delayedRetry.Id) + .ExecuteDeleteAsync(); + } + } + } } } diff --git a/SW.Bitween.Api/Services/RetryUsageReport.cs b/SW.Bitween.Api/Services/RetryUsageReport.cs new file mode 100644 index 00000000..3fd4d6a3 --- /dev/null +++ b/SW.Bitween.Api/Services/RetryUsageReport.cs @@ -0,0 +1,151 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using SW.Bitween.Domain; +using SW.Bitween.Model; + +namespace SW.Bitween; + +/// +/// Builds the state of subscription-and-group pairs — spent budget, and where the pair's +/// budget-exhausted alert would go. +/// +/// +/// Shared because the same pairs are asked about from two directions: a policy wants every +/// subscription using it, and a subscription wants its own, whether its policy is a shared one or an +/// inline CustomRetryPolicy that no policy id can reach. Two copies of alert resolution and +/// secret masking would drift, and the half that drifted would be the half that leaks. +/// +public class RetryUsageReport(BitweenDbContext dbContext, AdapterSecretProperties secrets) +{ + /// + /// One row per subscription and per group that could actually exhaust. + /// + /// The pairs' subscriptions, with the names to report. + /// Every group of the applicable policy; the unusable ones are dropped here. + /// + /// The shared policy, or null for an inline one — which has no row to carry a + /// policy-level alert, so those pairs can only resolve to a group or an override. + /// + public async Task> Build( + IReadOnlyList<(int Id, string Name)> subscriptions, + IReadOnlyList allGroups, + RetryPolicy policy) + { + // Only groups that allow retries have a budget to spend — and a group that can never spend + // one can never exhaust it, so it can never alert either. Listing those would invite + // configuring an alert that cannot fire. A ceiling of zero counts as "never": TryConsume + // denies it outright rather than claiming and exhausting it. + var groups = allGroups.Where(g => g.Budget is { MaxAttemptsTotal: > 0 }).ToList(); + if (groups.Count == 0 || subscriptions.Count == 0) return []; + + var subscriptionIds = subscriptions.Select(s => s.Id).ToList(); + + var usages = await dbContext.Set().AsNoTracking() + .Where(u => subscriptionIds.Contains(u.SubscriptionId)) + .ToListAsync(); + + var overrides = await dbContext.Set().AsNoTracking() + .Where(o => subscriptionIds.Contains(o.SubscriptionId)) + .ToListAsync(); + + // What became of the alerts that were raised. The counter records only that one was + // claimed — claiming is what stops a redelivery sending it twice, and it happens before + // the send is tried — so whether anyone was actually told lives here instead, in the + // delivery log. The pair is reconstructed the same way the alert reached it: the exchange + // names the integration, its result names the group. + var alertLog = await ( + from notification in dbContext.Set().AsNoTracking() + where notification.NotifierName == XchangeNotification.RetryBudgetAlertName + join xchange in dbContext.Set().AsNoTracking() + on notification.XchangeId equals xchange.Id + join result in dbContext.Set().AsNoTracking() + on notification.XchangeId equals result.Id + where xchange.SubscriptionId != null + && subscriptionIds.Contains(xchange.SubscriptionId.Value) + && result.RetryGroupId != null + select new + { + SubscriptionId = xchange.SubscriptionId!.Value, + GroupId = result.RetryGroupId!.Value, + notification.Success, + notification.Exception, + notification.FinishedOn + }) + .ToListAsync(); + + // One success is delivery, however many failures preceded it — the same rule the sender + // itself applies when it decides an alert has already gone out. Failing that, the most + // recent failure is the outcome. + var alertByPair = alertLog + .GroupBy(a => (a.SubscriptionId, a.GroupId)) + .ToDictionary( + g => g.Key, + g => g.OrderByDescending(a => a.Success).ThenByDescending(a => a.FinishedOn).First()); + + // Keyed once rather than scanned per pair: both lists are already keyed by exactly this + // pair, and a policy shared by many subscriptions turns the scan into the cost of the + // whole request. + var usageByPair = usages.ToDictionary(u => (u.SubscriptionId, u.GroupId)); + var overrideByPair = overrides.ToDictionary(o => (o.SubscriptionId, o.GroupId)); + + var rows = new List(); + + foreach (var subscription in subscriptions) + foreach (var group in groups) + { + usageByPair.TryGetValue((subscription.Id, group.Id), out var usage); + overrideByPair.TryGetValue((subscription.Id, group.Id), out var subscriptionOverride); + alertByPair.TryGetValue((subscription.Id, group.Id), out var alert); + + var target = RetryAlertResolver.Resolve(subscriptionOverride, group, policy); + + // Mirrors the order the resolver walks, so the reported reason is the level that + // actually decided: an override silences before the group is consulted at all. + var silencedAt = subscriptionOverride?.AlertMode == RetryAlertMode.Silent + ? RetryAlertLevel.SubscriptionGroup + : group.AlertMode == RetryAlertMode.Silent + ? RetryAlertLevel.Group + : (RetryAlertLevel?)null; + + rows.Add(new RetryGroupUsageRow + { + SubscriptionId = subscription.Id, + SubscriptionName = subscription.Name, + GroupId = group.Id, + GroupName = group.Name, + AttemptsUsed = usage?.AttemptsUsed ?? 0, + MaxAttemptsTotal = group.Budget!.MaxAttemptsTotal, + Exhausted = usage != null && usage.AttemptsUsed >= group.Budget.MaxAttemptsTotal, + LastAttemptOn = usage?.LastAttemptOn, + ExhaustedNotifiedOn = usage?.ExhaustedNotifiedOn, + // Only meaningful once an alert has been claimed. A delivery row surviving from + // before a reset would otherwise report an outcome for an alert that has since + // been re-armed and not raised again. + AlertDelivered = usage?.ExhaustedNotifiedOn == null ? null : alert?.Success, + AlertError = usage?.ExhaustedNotifiedOn != null && alert is { Success: false } + ? alert.Exception + : null, + AlertMode = subscriptionOverride?.AlertMode ?? RetryAlertMode.Inherit, + OverrideHandlerId = subscriptionOverride?.AlertHandlerId, + OverrideHandlerProperties = await secrets.Mask( + subscriptionOverride?.AlertHandlerId, subscriptionOverride?.AlertHandlerProperties), + ResolvedHandlerId = target?.HandlerId, + ResolvedHandlerProperties = await secrets.Mask( + target?.HandlerId, target?.HandlerProperties), + ResolvedFrom = target?.Level, + SilencedAt = target == null ? silencedAt : null + }); + } + + return rows + // Worst first: stopped retrying, then alerting nowhere, then whatever has spent most. + .OrderByDescending(r => r.Exhausted) + .ThenBy(r => r.ResolvedHandlerId != null) + .ThenByDescending(r => r.AttemptsUsed) + .ThenBy(r => r.SubscriptionName) + .ThenBy(r => r.GroupName) + .ToList(); + } +} diff --git a/SW.Bitween.Api/Services/SchedulerSeedService.cs b/SW.Bitween.Api/Services/SchedulerSeedService.cs index 056d42e7..e895bd64 100644 --- a/SW.Bitween.Api/Services/SchedulerSeedService.cs +++ b/SW.Bitween.Api/Services/SchedulerSeedService.cs @@ -25,6 +25,7 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) var options = scope.ServiceProvider.GetRequiredService(); await scheduleRepo.Schedule(options.RetryJobCron); + await scheduleRepo.Schedule(options.ReceiveAttemptCleanupCron); var subscriptions = await dbContext.Set() .Where(s => diff --git a/SW.Bitween.Api/Services/XchangeService.cs b/SW.Bitween.Api/Services/XchangeService.cs index 78c3f2eb..3f7260ca 100644 --- a/SW.Bitween.Api/Services/XchangeService.cs +++ b/SW.Bitween.Api/Services/XchangeService.cs @@ -35,13 +35,15 @@ public class XchangeService : private readonly ILogger _logger; private readonly IInfolinkCache _BitweenCache; private readonly NativeAdapterDiscoveryService _nativeAdapterDiscovery; + private readonly AdapterInvoker _adapterInvoker; public XchangeService(BitweenOptions BitweenSettings, BitweenDbContext dbContext, FilterService filterService, ICloudFilesService cloudFiles, IServiceProvider serviceProvider, IPublish publish, ILogger logger, IInfolinkCache BitweenCache, - NativeAdapterDiscoveryService nativeAdapterDiscovery) + NativeAdapterDiscoveryService nativeAdapterDiscovery, AdapterInvoker adapterInvoker) { + _adapterInvoker = adapterInvoker; _BitweenSettings = BitweenSettings; _dbContext = dbContext; _filterService = filterService; @@ -85,20 +87,22 @@ public async Task SubmitFilterXchange(int documentId, XchangeFile file, string[] await _dbContext.SaveChangesAsync(); } - public async Task CreateXchange(Xchange xchange, XchangeFile file, WorkGroup workGroup, Dictionary groupAttemptCounts = null) + public async Task CreateXchange(Xchange xchange, XchangeFile file, WorkGroup workGroup, + bool manualRetry = false) { - var newXchange = new Xchange(xchange, file, workGroup, groupAttemptCounts); + var newXchange = new Xchange(xchange, file, workGroup, manualRetry); await AddFile(newXchange.Id, XchangeFileType.Input, file); _dbContext.Add(newXchange); } public async Task CreateXchange(Subscription subscription, Xchange xchange, XchangeFile file, - string[] references = null, Dictionary groupAttemptCounts = null) + string[] references = null, Dictionary groupAttemptCounts = null, bool manualRetry = false) { var partnerId = xchange.PartnerId ?? subscription.PartnerId; var partner = partnerId.HasValue ? await _dbContext.FindAsync(partnerId.Value) : null; var globalAdapterValuesSets = await _BitweenCache.ListGlobalAdapterValuesSetsAsync(); - var newXchange = new Xchange(subscription, xchange, file, partner, globalAdapterValuesSets, groupAttemptCounts); + var newXchange = new Xchange(subscription, xchange, file, partner, globalAdapterValuesSets, + groupAttemptCounts, manualRetry); await AddFile(newXchange.Id, XchangeFileType.Input, file); _dbContext.Add(newXchange); } @@ -122,6 +126,18 @@ public async Task CreateXchange(Subscription subscription, XchangeFile // this null — resolve it here so {{globals.…}} always gets a chance to translate, // instead of silently no-op'ing for whichever caller forgot to load it. globalAdapterValuesSets ??= await _BitweenCache.ListGlobalAdapterValuesSetsAsync(); + + // And the same for the partner, for the same reason. Only a caller that learned the + // partner from somewhere other than the subscription — a bus gateway route, a partner + // calling an API gateway — has one to hand in; everyone else left it null and the + // subscription's own partner went unused, so every {{partner.…}} in its adapters was + // written out literally and the handler ran against the template. This is the value + // the Xchange is attributed to either way (see PartnerId below), so filling from it + // adds a resolution that was missing rather than changing whose exchange it is. + gatewayPartner ??= subscription.PartnerId.HasValue + ? await _dbContext.FindAsync(subscription.PartnerId.Value) + : null; + var xchange = new Xchange(subscription, file, references, correlationId, gatewayPartner, globalAdapterValuesSets); await AddFile(xchange.Id, XchangeFileType.Input, file); @@ -149,17 +165,52 @@ public async Task ExecuteDelayedRetry(DelayedRetry delayedRetry) .FirstOrDefaultAsync(s => s.Id == xchange.SubscriptionId); if (subscription == null) { + // Recorded on the result like the unreadable-input case below, rather than only dropping + // the schedule: the exchange is still there for someone to look at, so leaving it with no + // reason means the retry simply stopped happening with nothing to explain it. _dbContext.Remove(delayedRetry); + + var orphaned = await _dbContext.FindAsync(xchange.Id); + orphaned?.SetRetryBlocked( + "The scheduled retry was dropped: the subscription it belonged to no longer exists."); + return false; + } + + var inputFile = await ReadInputFile(xchange); + if (inputFile == null) + { + // The input is what a retry re-sends, so without it there is nothing to retry with. Handled + // like a missing subscription — drop the schedule and move on — but recorded on the result + // as well, because unlike a deleted subscription this needs someone to look into it. + _dbContext.Remove(delayedRetry); + + var result = await _dbContext.FindAsync(xchange.Id); + result?.SetRetryBlocked("The scheduled retry was dropped: the input file could not be read."); return false; } - var inputFileData = await GetFile(xchange.Id, XchangeFileType.Input); - var inputFile = new XchangeFile(inputFileData, xchange.InputName); - await CreateXchange(subscription, xchange, inputFile, groupAttemptCounts: delayedRetry.GroupAttemptCounts); + await CreateXchange(subscription, xchange, inputFile); _dbContext.Remove(delayedRetry); return true; } + /// + /// The original input, or null when it cannot be read — deleted from storage, expired by a + /// lifecycle rule, or storage itself unavailable. + /// + private async Task ReadInputFile(Xchange xchange) + { + try + { + return new XchangeFile(await GetFile(xchange.Id, XchangeFileType.Input), xchange.InputName); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "The input file of xchange {XchangeId} could not be read.", xchange.Id); + return null; + } + } + private Task CreateOnHoldXchange(Subscription subscription, XchangeFile file, string[] references = null) { var xchange = new OnHoldXchange(subscription, file.Data, file.Filename, file.BadData, references); @@ -426,24 +477,101 @@ private async Task Process(XchangeMessage message) await CreateXchangesForHits(xchange, result, inputFile); } - _dbContext.Add(new XchangeResult(xchange.Id, workGroup, outputFile, responseFile, responseXchange?.Id)); + var xchangeResult = new XchangeResult(xchange.Id, workGroup, outputFile, responseFile, + responseXchange?.Id); + _dbContext.Add(xchangeResult); if (responseFile?.BadData == true) - await TryScheduleAutoRetry(xchange, XchangeResultType.BadResult, responseFile.Data); + await TrySchedulingWithoutLosingTheResult(xchange, XchangeResultType.BadResult, responseFile.Data, + xchangeResult); + else + await TryClearingRetryBudgetAfterSuccess(xchange); await _dbContext.SaveChangesAsync(); } catch (Exception ex) { - _dbContext.Add(new XchangeResult(xchange.Id, workGroup, outputFile, responseFile, responseXchange?.Id, - ex.ToString())); - await TryScheduleAutoRetry(xchange, XchangeResultType.Error, ex.ToString()); + var xchangeResult = new XchangeResult(xchange.Id, workGroup, outputFile, responseFile, + responseXchange?.Id, ex.ToString()); + _dbContext.Add(xchangeResult); + await TrySchedulingWithoutLosingTheResult(xchange, XchangeResultType.Error, ex.ToString(), xchangeResult); await _dbContext.SaveChangesAsync(); } } - private async Task TryScheduleAutoRetry(Xchange xchange, XchangeResultType resultType, string content) + /// + /// Evaluates the retry policy without ever costing the caller its failure record. + /// + /// + /// Scheduling runs before the is saved and touches the database + /// several times. Letting it throw would replace the original exception with its own and abort + /// the save, so the failure would vanish from the UI entirely and only reappear as a silent + /// redelivery. Losing the retry is recoverable; losing the record of what went wrong is not. + /// + private async Task TrySchedulingWithoutLosingTheResult(Xchange xchange, XchangeResultType resultType, + string content, XchangeResult xchangeResult) + { + try + { + await TryScheduleAutoRetry(xchange, resultType, content, xchangeResult); + } + catch (Exception ex) + { + _logger.LogError(ex, "Auto-retry evaluation failed for xchange {XchangeId}; the failure result is still recorded.", + xchange.Id); + } + } + + /// + /// Gives the subscription its retry budget back after a success, without ever costing the caller + /// its successful result. + /// + /// + /// Guarded for the same reason scheduling is, and with more at stake: this runs after the handler + /// has already delivered, so letting it throw would abort the save of a result whose side effects + /// have happened, and the redelivery would repeat them. A budget left spent is a nuisance somebody + /// can undo by hand; a duplicated delivery cannot be undone at all. + /// + private async Task TryClearingRetryBudgetAfterSuccess(Xchange xchange) + { + if (xchange.SubscriptionId == null) return; + + try + { + // The exchange's own start time is the watermark: anything charged after this run began + // belongs to a failure this success knows nothing about, and is left where it is. + await new RetryGroupBudget(_dbContext, _serviceProvider, xchange.SubscriptionId.Value) + .ReleaseExhaustedBudgets(xchange.StartedOn); + } + catch (Exception ex) + { + _logger.LogError(ex, + "Retry budget of subscription {SubscriptionId} could not be cleared after a success; " + + "it may still refuse retries until it is reset.", xchange.SubscriptionId.Value); + } + } + + private async Task TryScheduleAutoRetry(Xchange xchange, XchangeResultType resultType, string content, + XchangeResult xchangeResult) { if (xchange.SubscriptionId == null) return; + // A person asked for this attempt, so the policy stays out of it. Otherwise pressing Retry + // spends a slot of the group's shared total — the budget meant for unattended retries — and + // can be what finally exhausts it and raises the alert. Recorded rather than skipped + // silently, so the absence of a follow-up attempt has a visible reason. + if (xchange.ManualRetry) + { + xchangeResult.SetRetryBlocked( + "This attempt was started by hand, so the retry policy left it alone and its budget is untouched."); + return; + } + + // DelayedRetry.Id is xchange.Id, so an existing row means this failure was already + // evaluated and already spent a slot of the group's total budget. Re-evaluating it + // (e.g. on an at-least-once redelivery) would both violate the PK on Add and spend a + // second slot for the same failure. + var alreadyScheduled = await _dbContext.Set().FindAsync(xchange.Id); + if (alreadyScheduled != null) return; + var subscription = await _dbContext.Set() .Include(s => s.RetryPolicy) .FirstOrDefaultAsync(s => s.Id == xchange.SubscriptionId.Value); @@ -451,35 +579,37 @@ private async Task TryScheduleAutoRetry(Xchange xchange, XchangeResultType resul IRetryPolicy policy = subscription?.CustomRetryPolicy ?? (IRetryPolicy)subscription?.RetryPolicy; if (policy?.Groups == null || policy.Groups.Count == 0) return; - var evaluator = new RetryPolicyEvaluator(policy); - evaluator.RestoreGroupAttemptCounts(xchange.GroupAttemptCounts == null - ? new Dictionary() - : new Dictionary(xchange.GroupAttemptCounts)); + var evaluator = new RetryPolicyEvaluator(policy, + new RetryGroupBudget(_dbContext, _serviceProvider, xchange.SubscriptionId.Value)); var attemptIndex = await CountRetryChainDepth(xchange); - var decision = evaluator.Evaluate(resultType, content, attemptIndex); + var decision = await evaluator.Evaluate(resultType, content, attemptIndex); + + // Which group owned this failure, so the group's retries can later be listed without + // re-deriving the match, and how deep the chain already was without walking it again. + if (decision.MatchedGroup is not null) + xchangeResult.SetRetryEvaluation(decision.MatchedGroup.Id, attemptIndex); if (decision.ShouldRetry) - { - // Guard against duplicate scheduling (e.g. an at-least-once redelivery - // reprocessing the same xchange) — DelayedRetry.Id is xchange.Id, so a - // blind Add would violate the PK and fail the whole SaveChangesAsync. - var existing = await _dbContext.Set().FindAsync(xchange.Id); - if (existing != null) + _dbContext.Add(new DelayedRetry { - existing.On = DateTime.UtcNow + decision.Delay; - existing.GroupAttemptCounts = evaluator.GetGroupAttemptCounts(); - } - else - { - _dbContext.Add(new DelayedRetry - { - Id = xchange.Id, - On = DateTime.UtcNow + decision.Delay, - GroupAttemptCounts = evaluator.GetGroupAttemptCounts() - }); - } - } + Id = xchange.Id, + On = DateTime.UtcNow + decision.Delay + }); + else + // A policy applied but refused. Recorded so an exhausted budget is distinguishable + // from an error no group was ever configured to catch. + xchangeResult.SetRetryBlocked(decision.Reason); + + // Raised on the result rather than published here, so the alert only reaches the bus once + // this failure is committed. Its own event type means its own queue and its own consumer, + // keeping a slow alert handler away from the ordinary notifier path. + if (decision.BudgetJustExhausted) + xchangeResult.RaiseBudgetExhausted( + xchange.SubscriptionId.Value, + decision.MatchedGroup!.Id, + decision.MatchedGroup.Name, + decision.MatchedGroup.Budget!.MaxAttemptsTotal); } private async Task CountRetryChainDepth(Xchange xchange) @@ -614,20 +744,8 @@ private async Task NotifyResult(Notifier notifier, XchangeResult xchangeResult, try { - // Check if it's a native adapter - if (notifier.HandlerId.StartsWith(NativeAdapterDiscoveryService.NativePrefix, StringComparison.OrdinalIgnoreCase)) - { - var handler = _nativeAdapterDiscovery.GetNativeHandler(notifier.HandlerId, handlerProperties); - await handler.Handle(new XchangeFile(JsonConvert.SerializeObject(notificationData), xchangeResult.Id)); - } - else - { - // Use serverless for external adapters - var serverless = _serviceProvider.GetRequiredService(); - await serverless.StartAsync(notifier.HandlerId, correlationId, handlerProperties); - await serverless.InvokeAsync(nameof(IInfolinkHandler.Handle), - new XchangeFile(JsonConvert.SerializeObject(notificationData), xchangeResult.Id)); - } + await _adapterInvoker.Handle(notifier.HandlerId, handlerProperties, correlationId, + new XchangeFile(JsonConvert.SerializeObject(notificationData), xchangeResult.Id)); _dbContext.Add(new XchangeNotification(xchangeResult.Id, notifier.Id, notifier.Name)); } diff --git a/SW.Bitween.IntegrationTests/Adapters/NativeEmptyTestReceiver.cs b/SW.Bitween.IntegrationTests/Adapters/NativeEmptyTestReceiver.cs new file mode 100644 index 00000000..489b470a --- /dev/null +++ b/SW.Bitween.IntegrationTests/Adapters/NativeEmptyTestReceiver.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using SW.Bitween.NativeAdapters; +using SW.PrimitiveTypes; + +namespace SW.Bitween.IntegrationTests.Adapters; + +/// Always finds nothing — for exercising ReceivingJob's no-new-data path. +public class NativeEmptyTestReceiver : INativeInfolinkReceiver +{ + public string Name => nameof(NativeEmptyTestReceiver); + public Type StartupValuesType => typeof(object); + + public void InitializeStartupValues(IDictionary settings) { } + + public Task Initialize() => Task.CompletedTask; + + public Task> ListFiles() => Task.FromResult>(Array.Empty()); + + public Task GetFile(string fileId) => throw new NotSupportedException(); + + public Task DeleteFile(string fileId) => Task.CompletedTask; + + public Task Finalize() => Task.CompletedTask; +} diff --git a/SW.Bitween.IntegrationTests/Adapters/NativeFailingTestReceiver.cs b/SW.Bitween.IntegrationTests/Adapters/NativeFailingTestReceiver.cs new file mode 100644 index 00000000..1a4d1d51 --- /dev/null +++ b/SW.Bitween.IntegrationTests/Adapters/NativeFailingTestReceiver.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using SW.Bitween.NativeAdapters; +using SW.PrimitiveTypes; + +namespace SW.Bitween.IntegrationTests.Adapters; + +/// Always fails to list files — for exercising ReceivingJob's failure path. +public class NativeFailingTestReceiver : INativeInfolinkReceiver +{ + public string Name => nameof(NativeFailingTestReceiver); + public Type StartupValuesType => typeof(object); + + public void InitializeStartupValues(IDictionary settings) { } + + public Task Initialize() => Task.CompletedTask; + + public Task> ListFiles() => throw new InvalidOperationException("Connection refused"); + + public Task GetFile(string fileId) => throw new NotSupportedException(); + + public Task DeleteFile(string fileId) => Task.CompletedTask; + + public Task Finalize() => Task.CompletedTask; +} diff --git a/SW.Bitween.IntegrationTests/Fixtures/BitweenFixture.cs b/SW.Bitween.IntegrationTests/Fixtures/BitweenFixture.cs index 33824ad2..c4274999 100644 --- a/SW.Bitween.IntegrationTests/Fixtures/BitweenFixture.cs +++ b/SW.Bitween.IntegrationTests/Fixtures/BitweenFixture.cs @@ -11,12 +11,15 @@ using SW.Bitween.Domain; using SW.Bitween.IntegrationTests.Adapters; using SW.Bitween.NativeAdapters; +using SW.Bitween.NativeAdapters.SmtpHandler; using SW.Bitween.PgSql; using SW.Bus; using SW.CloudFiles.Extensions; using SW.CloudFiles.LocalTests; using SW.PrimitiveTypes; using SW.Serverless; +using DotNet.Testcontainers.Builders; +using DotNet.Testcontainers.Containers; using Testcontainers.PostgreSql; using Testcontainers.RabbitMq; using Xunit; @@ -24,15 +27,35 @@ namespace SW.Bitween.IntegrationTests.Fixtures; /// -/// Collection-scoped fixture that starts a PostgreSQL container and a RabbitMQ container, -/// applies EF migrations, installs serverless adapters to local cloud storage, and builds -/// a fully wired service provider. +/// Collection-scoped fixture that starts a PostgreSQL container, a RabbitMQ container and a +/// MailHog container, applies EF migrations, installs serverless adapters to local cloud storage, +/// and builds a fully wired service provider. /// public sealed class BitweenFixture : IAsyncLifetime { private readonly PostgreSqlContainer _postgres = new PostgreSqlBuilder().Build(); private readonly RabbitMqContainer _rabbitMq = new RabbitMqBuilder().Build(); + // A real SMTP server, because the one thing no unit test can prove about the alert feature is + // that an actual handshake succeeds. Started here rather than expected on the developer's + // machine: a test that quietly does nothing when a local service is missing reports a green run + // while the whole delivery path goes unexercised. + private readonly IContainer _mailHog = new ContainerBuilder() + .WithImage("mailhog/mailhog:v1.0.1") + .WithPortBinding(SmtpContainerPort, true) + .WithPortBinding(ApiContainerPort, true) + .WithWaitStrategy(Wait.ForUnixContainer().UntilPortIsAvailable(ApiContainerPort)) + .Build(); + + private const int SmtpContainerPort = 1025; + private const int ApiContainerPort = 8025; + + /// Host port the MailHog SMTP listener is mapped to, for a handler's Port setting. + public int MailHogSmtpPort => _mailHog.GetMappedPublicPort(SmtpContainerPort); + + /// Base address of MailHog's own API, for reading back what was delivered. + public string MailHogApi => $"http://{_mailHog.Hostname}:{_mailHog.GetMappedPublicPort(ApiContainerPort)}"; + public IHost App { get; private set; } = null!; private ExceptionDispatchInfo? _initError; @@ -41,7 +64,7 @@ public async Task InitializeAsync() { try { - await Task.WhenAll(_postgres.StartAsync(), _rabbitMq.StartAsync()); + await Task.WhenAll(_postgres.StartAsync(), _rabbitMq.StartAsync(), _mailHog.StartAsync()); var dataSourceBuilder = new NpgsqlDataSourceBuilder(_postgres.GetConnectionString()); dataSourceBuilder.EnableDynamicJson(); @@ -92,14 +115,22 @@ public async Task InitializeAsync() services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddScoped(); + services.AddScoped(); services.AddSingleton(); services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); }) .Build(); @@ -143,6 +174,7 @@ public async Task DisposeAsync() } await _postgres.DisposeAsync(); await _rabbitMq.DisposeAsync(); + await _mailHog.DisposeAsync(); } } diff --git a/SW.Bitween.IntegrationTests/Tests/PartnerTokenTests.cs b/SW.Bitween.IntegrationTests/Tests/PartnerTokenTests.cs new file mode 100644 index 00000000..5bb86af1 --- /dev/null +++ b/SW.Bitween.IntegrationTests/Tests/PartnerTokenTests.cs @@ -0,0 +1,143 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using SW.Bitween.Domain; +using SW.Bitween.IntegrationTests.Fixtures; +using SW.Bitween.Model; +using SW.PrimitiveTypes; +using Xunit; + +namespace SW.Bitween.IntegrationTests.Tests; + +/// +/// {{partner.…}} in an adapter's properties has to be replaced by the time the Xchange is +/// written, because that is the copy the handler runs on — nothing resolves it later. +/// +/// +/// Every caller of CreateXchange that has a partner in hand (a bus gateway route, an API +/// gateway call) passed one, and every caller that didn't left the token literal — so a handler +/// posted to the URL {{partner.webhookUrl}}. The subscription's own partner was sitting +/// on the subscription the whole time; these tests pin down that it is now used. +/// +[Collection("Bitween")] +public class PartnerTokenTests +{ + private readonly BitweenFixture _fixture; + + public PartnerTokenTests(BitweenFixture fixture) + { + _fixture = fixture; + } + + [Fact] + public async Task Subscription_own_partner_fills_handler_tokens() + { + await using var scope = _fixture.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var xchangeService = scope.ServiceProvider.GetRequiredService(); + + var partner = new Partner("Token Partner") + { + AdapterProperties = new Dictionary { ["merchantSlug"] = "acme" } + }; + db.Set().Add(partner); + await db.SaveChangesAsync(); + + var doc = new Document(6101, "Partner Token Doc"); + db.Set().Add(doc); + await db.SaveChangesAsync(); + + // Internal: the type whose only trigger is a document of its type arriving, so it reaches + // CreateXchange without a partner being handed in from outside. + var sub = new Subscription("Token Sub", doc.Id, SubscriptionType.Internal, partner.Id); + sub.Inactive = false; + sub.SetDictionaries( + new Dictionary { ["Url"] = "http://host/{{partner.merchantSlug}}" }, + new Dictionary(), + new Dictionary(), + new Dictionary(), + new Dictionary()); + db.Set().Add(sub); + await db.SaveChangesAsync(); + + var xchange = await xchangeService.CreateXchange(sub, new XchangeFile("{\"id\":1}")); + await db.SaveChangesAsync(); + + Assert.Equal("http://host/acme", xchange.HandlerProperties["Url"]); + Assert.Equal(partner.Id, xchange.PartnerId); + } + + [Fact] + public async Task Partner_handed_in_wins_over_the_subscriptions_own() + { + await using var scope = _fixture.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var xchangeService = scope.ServiceProvider.GetRequiredService(); + + var own = new Partner("Own Partner") + { + AdapterProperties = new Dictionary { ["merchantSlug"] = "own" } + }; + var routed = new Partner("Routed Partner") + { + AdapterProperties = new Dictionary { ["merchantSlug"] = "routed" } + }; + db.Set().AddRange(own, routed); + await db.SaveChangesAsync(); + + var doc = new Document(6102, "Partner Token Doc 2"); + db.Set().Add(doc); + await db.SaveChangesAsync(); + + var sub = new Subscription("Token Sub 2", doc.Id, SubscriptionType.Internal, own.Id); + sub.Inactive = false; + sub.SetDictionaries( + new Dictionary { ["Url"] = "http://host/{{partner.merchantSlug}}" }, + new Dictionary(), + new Dictionary(), + new Dictionary(), + new Dictionary()); + db.Set().Add(sub); + await db.SaveChangesAsync(); + + // A bus gateway route's partner: the caller knows better than the subscription does, + // so the fallback must not override it. + var xchange = await xchangeService.CreateXchange(sub, new XchangeFile("{\"id\":2}"), + gatewayPartner: routed); + await db.SaveChangesAsync(); + + Assert.Equal("http://host/routed", xchange.HandlerProperties["Url"]); + Assert.Equal(routed.Id, xchange.PartnerId); + } + + [Fact] + public async Task No_partner_anywhere_leaves_the_token_alone() + { + await using var scope = _fixture.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var xchangeService = scope.ServiceProvider.GetRequiredService(); + + var doc = new Document(6103, "Partner Token Doc 3"); + db.Set().Add(doc); + await db.SaveChangesAsync(); + + // A bus gateway subscription carries no partner of its own; a route may or may not + // supply one. With none, there is nothing to resolve against and the token stands. + var sub = new Subscription("Token Sub 3", doc.Id, SubscriptionType.BusGateway); + sub.Inactive = false; + sub.SetDictionaries( + new Dictionary { ["Url"] = "http://host/{{partner.merchantSlug}}" }, + new Dictionary(), + new Dictionary(), + new Dictionary(), + new Dictionary()); + db.Set().Add(sub); + await db.SaveChangesAsync(); + + var xchange = await xchangeService.CreateXchange(sub, new XchangeFile("{\"id\":3}")); + await db.SaveChangesAsync(); + + Assert.Equal("http://host/{{partner.merchantSlug}}", xchange.HandlerProperties["Url"]); + Assert.Null(xchange.PartnerId); + } +} diff --git a/SW.Bitween.IntegrationTests/Tests/ReceivingTests.cs b/SW.Bitween.IntegrationTests/Tests/ReceivingTests.cs index 7fdbeb18..ef6900bb 100644 --- a/SW.Bitween.IntegrationTests/Tests/ReceivingTests.cs +++ b/SW.Bitween.IntegrationTests/Tests/ReceivingTests.cs @@ -1,3 +1,5 @@ +using System; +using System.Linq; using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; @@ -47,6 +49,98 @@ public async Task Receiving_job_creates_one_xchange_per_received_file() Assert.Equal(2, count); } + [Fact] + public async Task Receiving_job_records_one_attempt_with_the_exchanges_it_created() + { + await using var scope = _fixture.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var job = scope.ServiceProvider.GetRequiredService(); + var cache = _fixture.App.Services.GetRequiredService(); + + var document = new Document(6006, "Receiving Attempt Doc"); + db.Set().Add(document); + + var subscription = new Subscription("Receive Attempt Test", document.Id); + subscription.ReceiverId = nameof(NativeTestReceiver); + subscription.Inactive = false; + // SetSchedules() throws "Invalid schedule" with none configured — a real Receiving + // subscription always has one, so give this test one too. + subscription.SetSchedules(new[] { new Schedule(Recurrence.Hourly, TimeSpan.FromMinutes(30)) }); + db.Set().Add(subscription); + await db.SaveChangesAsync(); + + cache.Revoke(); + + await job.Execute(new ReceivingJobParams(subscription.Id, null)); + + var attempt = await db.Set().SingleAsync(a => a.SubscriptionId == subscription.Id); + Assert.Equal(ReceiveOutcome.Received, attempt.Outcome); + Assert.Null(attempt.ErrorMessage); + Assert.Equal(2, attempt.ExchangeIds.Length); + + var xchangeIds = await db.Set() + .Where(x => x.SubscriptionId == subscription.Id) + .Select(x => x.Id) + .ToListAsync(); + Assert.Equal(xchangeIds.OrderBy(i => i), attempt.ExchangeIds.OrderBy(i => i)); + } + + [Fact] + public async Task Receiving_job_records_a_failed_attempt_when_listing_files_throws() + { + await using var scope = _fixture.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var job = scope.ServiceProvider.GetRequiredService(); + var cache = _fixture.App.Services.GetRequiredService(); + + var document = new Document(6007, "Receiving Failure Doc"); + db.Set().Add(document); + + var subscription = new Subscription("Receive Failure Test", document.Id); + subscription.ReceiverId = nameof(NativeFailingTestReceiver); + subscription.Inactive = false; + subscription.SetSchedules(new[] { new Schedule(Recurrence.Hourly, TimeSpan.FromMinutes(30)) }); + db.Set().Add(subscription); + await db.SaveChangesAsync(); + + cache.Revoke(); + + await job.Execute(new ReceivingJobParams(subscription.Id, null)); + + var attempt = await db.Set().SingleAsync(a => a.SubscriptionId == subscription.Id); + Assert.Equal(ReceiveOutcome.Failed, attempt.Outcome); + Assert.Contains("Connection refused", attempt.ErrorMessage); + Assert.Empty(attempt.ExchangeIds); + } + + [Fact] + public async Task Receiving_job_records_no_new_data_when_nothing_is_found() + { + await using var scope = _fixture.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var job = scope.ServiceProvider.GetRequiredService(); + var cache = _fixture.App.Services.GetRequiredService(); + + var document = new Document(6008, "Receiving Empty Doc"); + db.Set().Add(document); + + var subscription = new Subscription("Receive Empty Test", document.Id); + subscription.ReceiverId = nameof(NativeEmptyTestReceiver); + subscription.Inactive = false; + subscription.SetSchedules(new[] { new Schedule(Recurrence.Hourly, TimeSpan.FromMinutes(30)) }); + db.Set().Add(subscription); + await db.SaveChangesAsync(); + + cache.Revoke(); + + await job.Execute(new ReceivingJobParams(subscription.Id, null)); + + var attempt = await db.Set().SingleAsync(a => a.SubscriptionId == subscription.Id); + Assert.Equal(ReceiveOutcome.NoNewData, attempt.Outcome); + Assert.Null(attempt.ErrorMessage); + Assert.Empty(attempt.ExchangeIds); + } + [Fact] public async Task Receiving_job_does_nothing_for_inactive_subscription() { diff --git a/SW.Bitween.IntegrationTests/Tests/RetryAlertServiceTests.cs b/SW.Bitween.IntegrationTests/Tests/RetryAlertServiceTests.cs new file mode 100644 index 00000000..5e445634 --- /dev/null +++ b/SW.Bitween.IntegrationTests/Tests/RetryAlertServiceTests.cs @@ -0,0 +1,315 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Text.Json; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using SW.Bitween.Domain; +using SW.Bitween.IntegrationTests.Fixtures; +using SW.Bitween.Model; +using SW.PrimitiveTypes; +using Xunit; + +namespace SW.Bitween.IntegrationTests.Tests; + +/// +/// Sends a real "retry budget exhausted" alert through to a local +/// MailHog instance and reads it back over MailHog's own API — the one part of the feature no unit +/// test can prove, because it depends on an actual SMTP handshake succeeding. +/// +/// +/// MailHog is started by alongside PostgreSQL and RabbitMQ, so these +/// tests run everywhere the rest of the suite does. They used to return early when a local MailHog +/// was missing, which xunit reports as a pass — a green run then said nothing about whether an alert +/// can actually be delivered. +/// +[Collection("Bitween")] +public class RetryAlertServiceTests +{ + // MailHog answers instantly or not at all, so the default 100 seconds only ever means "the run + // hangs instead of failing". + private static readonly TimeSpan MailHogTimeout = TimeSpan.FromSeconds(5); + + private readonly BitweenFixture _fixture; + + private string MessagesApi => $"{_fixture.MailHogApi}/api/v2/messages"; + + public RetryAlertServiceTests(BitweenFixture fixture) + { + _fixture = fixture; + } + + // Deleting is only exposed on MailHog's v1 API — the v2 route 404s and would silently leave + // messages behind, making the assertions depend on leftovers from the previous run. + private async Task ClearMailHog() + { + using var http = new HttpClient { Timeout = MailHogTimeout }; + var response = await http.DeleteAsync($"{_fixture.MailHogApi}/api/v1/messages"); + response.EnsureSuccessStatusCode(); + } + + private async Task LatestMailHogMessage() + { + using var http = new HttpClient { Timeout = MailHogTimeout }; + var json = await http.GetStringAsync(MessagesApi); + using var doc = JsonDocument.Parse(json); + var items = doc.RootElement.GetProperty("items").Clone(); + return items.GetArrayLength() > 0 ? items[0] : null; + } + + private async Task MailHogTotal() + { + using var http = new HttpClient { Timeout = MailHogTimeout }; + var json = await http.GetStringAsync(MessagesApi); + using var doc = JsonDocument.Parse(json); + return doc.RootElement.GetProperty("total").GetInt32(); + } + + [Fact] + public async Task Exhausted_budget_alert_arrives_in_MailHog_with_the_group_and_subscription_named() + { + await ClearMailHog(); + + await using var scope = _fixture.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var alertService = scope.ServiceProvider.GetRequiredService(); + + var doc = new Document(7201, "MailHog Alert Doc"); + db.Set().Add(doc); + await db.SaveChangesAsync(); + + // A group whose own alert config points at MailHog directly — the narrowest level, so the + // resolver has nothing to fall through to and the test proves that level specifically. + var groupId = Guid.NewGuid(); + var policy = new RetryPolicy + { + Name = "MailHog Alert Policy", + Groups = + [ + new RetryGroup + { + Id = groupId, + Name = "FRT charges cannot be found", + Priority = 10, + AppliesTo = [XchangeResultType.Error], + Matchers = [new ContainsMatcher { Value = "timeout" }], + Budget = new RetryBudget + { + MaxAttemptsPerError = 1, + MaxAttemptsTotal = 1, + DelayStrategy = new FixedDelayStrategy { DelayMs = 1000 } + }, + AlertMode = RetryAlertMode.Send, + AlertHandlerId = "NativeSmtpHandler", + AlertHandlerProperties = new Dictionary + { + ["Host"] = "localhost", + ["Port"] = _fixture.MailHogSmtpPort.ToString(), + ["UseTls"] = "false", + ["From"] = "bitween-alerts@example.com", + ["To"] = "ops@example.com", + ["Subject"] = "Retries stopped for {{ SubscriptionName }}", + ["Body"] = "{{ GroupName }} used all {{ MaxAttemptsTotal }} retries." + } + } + ] + }; + db.Set().Add(policy); + await db.SaveChangesAsync(); + + var sub = new Subscription("MailHog Alert Sub", doc.Id); + db.Set().Add(sub); + await db.SaveChangesAsync(); + sub.SetRetryPolicy(policy.Id, null); + await db.SaveChangesAsync(); + + var xchange = await scope.ServiceProvider.GetRequiredService() + .CreateXchange(sub, new XchangeFile("{}")); + await db.SaveChangesAsync(); + + // Reproduces exactly what TryScheduleAutoRetry does: evaluate against the real budget table, + // and when it reports exhaustion, raise the event the same way XchangeResult does in + // production. RetryAlertService.Process is then invoked directly rather than over the bus — + // this suite calls handlers directly throughout (see RetryJobTests, DelayedRetriesTests) + // rather than relying on live message transport, which is SW.Bus's own concern, not this + // feature's. + var evaluator = new RetryPolicyEvaluator(policy, + new RetryGroupBudget(db, scope.ServiceProvider, sub.Id)); + + // Total is 1, so the first message (a different "parcel" failing the same way) spends the + // whole budget and is itself allowed to retry — exhaustion only shows up for the next one. + var firstMessage = await evaluator.Evaluate(XchangeResultType.Error, + "System.TimeoutException: contains timeout", 0); + Assert.True(firstMessage.ShouldRetry); + + var decision = await evaluator.Evaluate(XchangeResultType.Error, + "System.TimeoutException: contains timeout", 0); + Assert.False(decision.ShouldRetry); + Assert.True(decision.BudgetJustExhausted); + + var xchangeResult = new XchangeResult(xchange.Id, null, null, exception: "System.TimeoutException: contains timeout"); + xchangeResult.RaiseBudgetExhausted(sub.Id, groupId, decision.MatchedGroup!.Name, + decision.MatchedGroup.Budget!.MaxAttemptsTotal); + + // SaveChangesAsync dispatches and clears Events (see BitweenDbContext), same as it does in + // production, so the event has to be captured before saving rather than read back after. + // The constructor raises its own XchangeResultCreatedEvent alongside it. + var raisedEvent = Assert.Single(xchangeResult.Events.OfType()); + + db.Add(xchangeResult); + await db.SaveChangesAsync(); + + await alertService.Process(raisedEvent); + + var message = await LatestMailHogMessage(); + Assert.NotNull(message); + + var subject = message!.Value.GetProperty("Content").GetProperty("Headers") + .GetProperty("Subject")[0].GetString(); + Assert.Equal("Retries stopped for MailHog Alert Sub", subject); + + var body = message.Value.GetProperty("Content").GetProperty("Body").GetString(); + Assert.Contains("FRT charges cannot be found used all 1 retries", body); + + var loggedNotification = await db.Set().AsNoTracking() + .SingleAsync(n => n.XchangeId == xchange.Id); + Assert.True(loggedNotification.Success); + Assert.Equal(XchangeNotification.RetryBudgetAlertName, loggedNotification.NotifierName); + + // Redelivery of the same event must not double-send — same guard the real bus retry path + // relies on. Compared against the count after the first send rather than an absolute + // number, so a stray message could never make this pass by accident. + var totalAfterFirstSend = await MailHogTotal(); + await alertService.Process(raisedEvent); + Assert.Equal(totalAfterFirstSend, await MailHogTotal()); + } + + [Fact] + public async Task A_failed_send_does_not_stop_a_later_delivery() + { + await ClearMailHog(); + + await using var scope = _fixture.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var alertService = scope.ServiceProvider.GetRequiredService(); + + var doc = new Document(7202, "Failed Send Doc"); + db.Set().Add(doc); + await db.SaveChangesAsync(); + + var groupId = Guid.NewGuid(); + var policy = new RetryPolicy + { + Name = "Failed Send Policy", + Groups = + [ + new RetryGroup + { + Id = groupId, + Name = "Timeout", + Priority = 10, + AppliesTo = [XchangeResultType.Error], + Matchers = [new ContainsMatcher { Value = "timeout" }], + Budget = new RetryBudget + { + MaxAttemptsPerError = 1, + MaxAttemptsTotal = 1, + DelayStrategy = new FixedDelayStrategy { DelayMs = 1000 } + }, + AlertMode = RetryAlertMode.Send, + AlertHandlerId = "NativeSmtpHandler", + AlertHandlerProperties = new Dictionary + { + ["Host"] = "localhost", + ["Port"] = _fixture.MailHogSmtpPort.ToString(), + ["UseTls"] = "false", + ["From"] = "bitween-alerts@example.com", + ["To"] = "ops@example.com", + ["Subject"] = "Retries stopped for {{ SubscriptionName }}", + ["Body"] = "{{ GroupName }} used all {{ MaxAttemptsTotal }} retries." + } + } + ] + }; + db.Set().Add(policy); + await db.SaveChangesAsync(); + + var sub = new Subscription("Failed Send Sub", doc.Id); + db.Set().Add(sub); + await db.SaveChangesAsync(); + sub.SetRetryPolicy(policy.Id, null); + await db.SaveChangesAsync(); + + var xchange = await scope.ServiceProvider.GetRequiredService() + .CreateXchange(sub, new XchangeFile("{}")); + await db.SaveChangesAsync(); + + // Stands in for a first attempt that threw — a dropped connection, a refused relay. Written + // directly because what matters is the row it leaves behind, not how the send failed. + db.Add(XchangeNotification.ForRetryBudgetAlert(xchange.Id, "System.Net.Sockets.SocketException: refused")); + await db.SaveChangesAsync(); + + var xchangeResult = new XchangeResult(xchange.Id, null, null, exception: "timeout"); + xchangeResult.RaiseBudgetExhausted(sub.Id, groupId, "Timeout", 1); + var raisedEvent = Assert.Single(xchangeResult.Events.OfType()); + db.Add(xchangeResult); + await db.SaveChangesAsync(); + + // The recoverable failure must not read as "already delivered": a transient error would + // otherwise silence the alert for good, which is the opposite of what a retry system owes. + await alertService.Process(raisedEvent); + + Assert.Equal(1, await MailHogTotal()); + Assert.True(await db.Set() + .AnyAsync(n => n.XchangeId == xchange.Id + && n.NotifierName == XchangeNotification.RetryBudgetAlertName + && n.Success)); + + // And now that one did get through, the guard has to hold: no third row, no second email. + // Both are asserted — a redelivery that wrongly logged another success while sending nothing + // would otherwise pass here. + var rowsAfterDelivery = await db.Set() + .CountAsync(n => n.XchangeId == xchange.Id); + + await alertService.Process(raisedEvent); + + Assert.Equal(1, await MailHogTotal()); + Assert.Equal(rowsAfterDelivery, + await db.Set().CountAsync(n => n.XchangeId == xchange.Id)); + } + + [Fact] + public async Task The_handler_refuses_to_send_a_password_over_an_unencrypted_connection() + { + await ClearMailHog(); + + await using var scope = _fixture.CreateScope(); + var discovery = scope.ServiceProvider.GetRequiredService(); + + // MailHog speaks plain SMTP on 1025, which is exactly the shape of the mistake worth + // catching: a working relay, no encryption, and a password to hand over. + var handler = discovery.GetNativeHandler("NativeSmtpHandler", new Dictionary + { + ["Host"] = "localhost", + ["Port"] = _fixture.MailHogSmtpPort.ToString(), + ["UseTls"] = "false", + ["Password"] = "hunter2", + ["From"] = "bitween-alerts@example.com", + ["To"] = "ops@example.com", + ["Subject"] = "Should never be sent", + ["Body"] = "Should never be sent" + }); + + // Matched on the message, not just the type: the handler also throws + // InvalidOperationException for a missing recipient, so dropping the To above would otherwise + // leave this passing without ever reaching the credential guard. + var refusal = await Assert.ThrowsAsync( + () => handler.Handle(new XchangeFile("{}"))); + Assert.Contains("will not send a password over an unencrypted connection", refusal.Message); + + // Refusing has to mean refusing: no message, and therefore no password, left the process. + Assert.Equal(0, await MailHogTotal()); + } +} diff --git a/SW.Bitween.IntegrationTests/Tests/RetryJobTests.cs b/SW.Bitween.IntegrationTests/Tests/RetryJobTests.cs index 5bf58c4d..e150a9b4 100644 --- a/SW.Bitween.IntegrationTests/Tests/RetryJobTests.cs +++ b/SW.Bitween.IntegrationTests/Tests/RetryJobTests.cs @@ -3,6 +3,7 @@ using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging.Abstractions; using SW.Bitween.Domain; using SW.Bitween.IntegrationTests.Fixtures; using SW.Bitween.Model; @@ -24,7 +25,7 @@ public RetryJobTests(BitweenFixture fixture) // ─── Helpers ────────────────────────────────────────────────────────────── private RetryJob BuildJob(BitweenDbContext db, XchangeService xchangeService) => - new(db, xchangeService); + new(db, xchangeService, NullLogger.Instance); // ─── Batch query ────────────────────────────────────────────────────────── @@ -101,93 +102,162 @@ public async Task RetryJob_removes_delayed_retry_when_subscription_is_missing() "A DelayedRetry whose Subscription no longer exists must be removed without creating a retry Xchange."); } - // ─── Full execution path ────────────────────────────────────────────────── + // ─── One bad row must not stop the rest ─────────────────────────────────── + + /// + /// An Xchange whose input file was never uploaded: reading it fails, which is what a retry whose + /// file has since been deleted from storage looks like. + /// + private static async Task AddUnreadableXchange(BitweenDbContext db, Subscription sub) + { + var xchange = new Xchange(sub, new XchangeFile("{}")); + db.Set().Add(xchange); + db.Set().Add(new XchangeResult(xchange.Id, null, null, exception: "boom")); + await db.SaveChangesAsync(); + return xchange; + } [Fact] - public async Task RetryJob_processes_due_delayed_retry_and_creates_retry_xchange() + public async Task RetryJob_drops_a_retry_whose_input_is_gone_and_still_runs_the_others() { await using var scope = _fixture.CreateScope(); var db = scope.ServiceProvider.GetRequiredService(); var xs = scope.ServiceProvider.GetRequiredService(); - var doc = new Document(8001, "RetryJob Due Doc"); + var doc = new Document(8010, "RetryJob Missing Input Doc"); db.Set().Add(doc); await db.SaveChangesAsync(); - var sub = new Subscription("RetryJob Sub", doc.Id); - sub.Inactive = false; + var sub = new Subscription("RetryJob Missing Input Sub", doc.Id) { Inactive = false }; db.Set().Add(sub); await db.SaveChangesAsync(); - // Use CreateXchange so the input file is uploaded to real cloud storage - var originalXchange = await xs.CreateXchange(sub, new XchangeFile("{}")); + var unreadable = await AddUnreadableXchange(db, sub); + var healthy = await xs.CreateXchange(sub, new XchangeFile("{}")); - var groupCounts = new System.Collections.Generic.Dictionary - { - [Guid.NewGuid().ToString()] = 1 - }; - var delayedRetry = new DelayedRetry + db.Set().AddRange( + new DelayedRetry { Id = unreadable.Id, On = DateTime.UtcNow.AddMinutes(-2) }, + new DelayedRetry { Id = healthy.Id, On = DateTime.UtcNow.AddMinutes(-1) }); + await db.SaveChangesAsync(); + + await BuildJob(db, xs).Execute(); + + // With one commit per row, the retry that could not be made does not undo the one that could. + // Committing the batch in one go would have lost both and left both schedules behind. + Assert.False(await db.Set().AnyAsync(r => r.Id == healthy.Id)); + Assert.True(await db.Set().AnyAsync(x => x.RetryFor == healthy.Id)); + + // The unusable one leaves the queue too, rather than being tried again every minute for good. + Assert.False(await db.Set().AnyAsync(r => r.Id == unreadable.Id)); + + // And it says so where a reader already looks for "why is this not being retried?". + var result = await db.Set().AsNoTracking().SingleAsync(r => r.Id == unreadable.Id); + Assert.Contains("input file could not be read", result.RetryBlockedReason); + } + + [Fact] + public async Task RetryJob_works_through_more_than_one_batch_in_a_single_run() + { + await using var scope = _fixture.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var xs = scope.ServiceProvider.GetRequiredService(); + + // Schedules pointing at exchanges that no longer exist: the cheapest row to process, and enough + // of them to need more than one batch of 100. + var ids = Enumerable.Range(0, 105) + .Select(i => $"rjt-batch-{Guid.NewGuid():N}-{i}") + .ToList(); + + db.Set().AddRange(ids.Select((id, i) => new DelayedRetry { - Id = originalXchange.Id, - On = DateTime.UtcNow.AddMinutes(-1), - GroupAttemptCounts = groupCounts - }; - db.Set().Add(delayedRetry); + Id = id, + On = DateTime.UtcNow.AddMinutes(-(i + 1)) + })); await db.SaveChangesAsync(); await BuildJob(db, xs).Execute(); - // DelayedRetry must be gone - var retryGone = !await db.Set().AnyAsync(r => r.Id == originalXchange.Id); - Assert.True(retryGone, "The processed DelayedRetry record must be deleted."); + // All of them, not the first hundred: a backlog should not have to wait a minute per hundred. + var left = await db.Set().CountAsync(r => ids.Contains(r.Id)); + Assert.Equal(0, left); + } - // A new Xchange with RetryFor pointing to the original must exist - var retryXchange = await db.Set() - .FirstOrDefaultAsync(x => x.RetryFor == originalXchange.Id); - Assert.NotNull(retryXchange); - Assert.Equal(originalXchange.Id, retryXchange.RetryFor); - Assert.Equal(sub.Id, retryXchange.SubscriptionId); + // ─── Bulk retry with no subscription ────────────────────────────────────── + + [Fact] + public async Task BulkRetry_handles_an_exchange_with_no_subscription() + { + await using var scope = _fixture.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var xs = scope.ServiceProvider.GetRequiredService(); + + var doc = new Document(8012, "BulkRetry No Sub Doc"); + db.Set().Add(doc); + await db.SaveChangesAsync(); + + // A document-only exchange: SubscriptionId is null from the start, which is also what an + // exchange whose subscription was later deleted looks like. Created through the service so its + // input file is really in storage, since bulk retry reads it before looking anything else up. + var orphan = await xs.CreateXchange(doc, WorkGroup.None, new XchangeFile("{}")); + await db.SaveChangesAsync(); + + var healthy = await xs.CreateXchange(doc, WorkGroup.None, new XchangeFile("{}")); + await db.SaveChangesAsync(); + + // One selection containing both. This threw before, so the whole bulk retry failed — including + // for the exchanges that were perfectly retryable. + await new Resources.Xchanges.BulkRetry(db, xs).Handle(new XchangeBulkRetry + { + Ids = [orphan.Id, healthy.Id], + Reset = false + }); + await db.SaveChangesAsync(); + + Assert.True(await db.Set().AnyAsync(x => x.RetryFor == orphan.Id)); + Assert.True(await db.Set().AnyAsync(x => x.RetryFor == healthy.Id)); } + // ─── Full execution path ────────────────────────────────────────────────── + [Fact] - public async Task RetryJob_carries_group_attempt_counts_onto_retry_xchange() + public async Task RetryJob_processes_due_delayed_retry_and_creates_retry_xchange() { await using var scope = _fixture.CreateScope(); var db = scope.ServiceProvider.GetRequiredService(); var xs = scope.ServiceProvider.GetRequiredService(); - var doc = new Document(8002, "RetryJob GroupCounts Doc"); + var doc = new Document(8001, "RetryJob Due Doc"); db.Set().Add(doc); await db.SaveChangesAsync(); - var sub = new Subscription("RetryJob GroupCounts Sub", doc.Id); + var sub = new Subscription("RetryJob Sub", doc.Id); sub.Inactive = false; db.Set().Add(sub); await db.SaveChangesAsync(); + // Use CreateXchange so the input file is uploaded to real cloud storage var originalXchange = await xs.CreateXchange(sub, new XchangeFile("{}")); - var groupId = Guid.NewGuid().ToString(); var delayedRetry = new DelayedRetry { Id = originalXchange.Id, - On = DateTime.UtcNow.AddMinutes(-1), - GroupAttemptCounts = new System.Collections.Generic.Dictionary - { - [groupId] = 2 - } + On = DateTime.UtcNow.AddMinutes(-1) }; db.Set().Add(delayedRetry); await db.SaveChangesAsync(); await BuildJob(db, xs).Execute(); + // DelayedRetry must be gone + var retryGone = !await db.Set().AnyAsync(r => r.Id == originalXchange.Id); + Assert.True(retryGone, "The processed DelayedRetry record must be deleted."); + + // A new Xchange with RetryFor pointing to the original must exist var retryXchange = await db.Set() .FirstOrDefaultAsync(x => x.RetryFor == originalXchange.Id); Assert.NotNull(retryXchange); - Assert.NotNull(retryXchange.GroupAttemptCounts); - Assert.True(retryXchange.GroupAttemptCounts.TryGetValue(groupId, out var count)); - Assert.Equal(2, count); + Assert.Equal(originalXchange.Id, retryXchange.RetryFor); + Assert.Equal(sub.Id, retryXchange.SubscriptionId); } [Fact] diff --git a/SW.Bitween.IntegrationTests/Tests/RetryPolicyTests.cs b/SW.Bitween.IntegrationTests/Tests/RetryPolicyTests.cs index c144950c..1c81406b 100644 --- a/SW.Bitween.IntegrationTests/Tests/RetryPolicyTests.cs +++ b/SW.Bitween.IntegrationTests/Tests/RetryPolicyTests.cs @@ -1,7 +1,9 @@ using System; +using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; +using Newtonsoft.Json; using Microsoft.Extensions.DependencyInjection; using SW.Bitween.Domain; using SW.Bitween.IntegrationTests.Fixtures; @@ -24,10 +26,16 @@ public RetryPolicyTests(BitweenFixture fixture) // ─── Helpers ────────────────────────────────────────────────────────────── + private static AdapterSecretProperties Secrets(AsyncServiceScope scope) => + scope.ServiceProvider.GetRequiredService(); + + private static RetryUsageReport Report(AsyncServiceScope scope) => + scope.ServiceProvider.GetRequiredService(); + private static (Create create, Get get, Update update, Delete delete) - Handlers(BitweenDbContext db, RequestContext ctx) => ( + Handlers(BitweenDbContext db, RequestContext ctx, AdapterSecretProperties secrets) => ( new Create(db, ctx), - new Get(db, ctx), + new Get(db, ctx, secrets), new Update(db, ctx), new Delete(db, ctx)); @@ -60,7 +68,7 @@ public async Task Can_create_and_get_retry_policy() await using var scope = _fixture.CreateScope(); var db = scope.ServiceProvider.GetRequiredService(); var ctx = scope.Superuser(); - var (create, get, _, _) = Handlers(db, ctx); + var (create, get, _, _) = Handlers(db, ctx, Secrets(scope)); var id = (int)await create.Handle(SimplePolicy("Round-trip Policy")); @@ -78,7 +86,7 @@ public async Task Create_policy_with_complex_groups_round_trips_json_correctly() await using var scope = _fixture.CreateScope(); var db = scope.ServiceProvider.GetRequiredService(); var ctx = scope.Superuser(); - var (create, _, _, _) = Handlers(db, ctx); + var (create, _, _, _) = Handlers(db, ctx, Secrets(scope)); var policy = new RetryPolicyCreate { @@ -130,7 +138,7 @@ public async Task Can_update_retry_policy_name_and_groups() await using var scope = _fixture.CreateScope(); var db = scope.ServiceProvider.GetRequiredService(); var ctx = scope.Superuser(); - var (create, _, update, _) = Handlers(db, ctx); + var (create, _, update, _) = Handlers(db, ctx, Secrets(scope)); var id = (int)await create.Handle(SimplePolicy("Before Update")); @@ -169,7 +177,7 @@ public async Task Can_delete_retry_policy_not_assigned_to_any_subscription() await using var scope = _fixture.CreateScope(); var db = scope.ServiceProvider.GetRequiredService(); var ctx = scope.Superuser(); - var (create, _, _, delete) = Handlers(db, ctx); + var (create, _, _, delete) = Handlers(db, ctx, Secrets(scope)); var id = (int)await create.Handle(SimplePolicy("Deletable Policy")); @@ -187,7 +195,7 @@ public async Task Cannot_delete_retry_policy_that_is_assigned_to_a_subscription( await using var scope = _fixture.CreateScope(); var db = scope.ServiceProvider.GetRequiredService(); var ctx = scope.Superuser(); - var (create, _, _, delete) = Handlers(db, ctx); + var (create, _, _, delete) = Handlers(db, ctx, Secrets(scope)); var doc = new Document(7001, "Delete Guard Doc"); db.Set().Add(doc); @@ -239,7 +247,7 @@ public async Task Subscription_retry_policy_id_is_persisted_and_fk_resolves() await using var scope = _fixture.CreateScope(); var db = scope.ServiceProvider.GetRequiredService(); var ctx = scope.Superuser(); - var (create, _, _, _) = Handlers(db, ctx); + var (create, _, _, _) = Handlers(db, ctx, Secrets(scope)); var doc = new Document(7002, "Sub FK Doc"); db.Set().Add(doc); @@ -333,6 +341,411 @@ public async Task Removing_retry_policy_nullifies_subscription_fk_via_set_null_c Assert.Null(reloaded.RetryPolicyId); } + // ─── Shared group total (MaxAttemptsTotal) ────────────────────────────────── + + [Fact] + public async Task Group_total_is_shared_across_separate_messages_of_the_same_integration() + { + await using var scope = _fixture.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + + var doc = new Document(7005, "Shared Total Doc"); + db.Set().Add(doc); + var sub = new Subscription("Shared Total Sub", doc.Id); + db.Set().Add(sub); + await db.SaveChangesAsync(); + + var group = new RetryGroup + { + Name = "Timeout", + Priority = 10, + AppliesTo = [XchangeResultType.Error], + Matchers = [new ContainsMatcher { Value = "timeout" }], + Budget = new RetryBudget + { + MaxAttemptsPerError = 3, + MaxAttemptsTotal = 10, + DelayStrategy = new FixedDelayStrategy { DelayMs = 5_000 } + } + }; + var policy = new CustomRetryPolicy { Groups = [group] }; + + // Reproduces the reported bug: four failing messages, each retried up to its own + // per-message cap of 3, under a shared total of 10 — 12 retries before the fix. + var allowed = 0; + for (var message = 0; message < 4; message++) + for (var attempt = 0; attempt < 3; attempt++) + { + // A fresh evaluator and store per failure, exactly as XchangeService builds them. + var evaluator = new RetryPolicyEvaluator(policy, new RetryGroupBudget(db, scope.ServiceProvider, sub.Id)); + var decision = await evaluator.Evaluate(XchangeResultType.Error, "timeout", attempt); + if (decision.ShouldRetry) allowed++; + await db.SaveChangesAsync(); + } + + Assert.Equal(10, allowed); + + var usage = await db.Set().AsNoTracking() + .SingleAsync(u => u.SubscriptionId == sub.Id && u.GroupId == group.Id); + Assert.Equal(10, usage.AttemptsUsed); + } + + [Fact] + public async Task Group_total_is_tracked_per_integration_not_per_policy() + { + await using var scope = _fixture.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + + var doc = new Document(7006, "Per Integration Doc"); + db.Set().Add(doc); + var subA = new Subscription("Per Integration Sub A", doc.Id); + var subB = new Subscription("Per Integration Sub B", doc.Id); + db.Set().AddRange(subA, subB); + await db.SaveChangesAsync(); + + var group = new RetryGroup + { + Name = "Timeout", + Priority = 10, + AppliesTo = [XchangeResultType.Error], + Matchers = [new ContainsMatcher { Value = "timeout" }], + Budget = new RetryBudget + { + MaxAttemptsPerError = 10, + MaxAttemptsTotal = 1, + DelayStrategy = new FixedDelayStrategy { DelayMs = 5_000 } + } + }; + var policy = new CustomRetryPolicy { Groups = [group] }; + + // Each integration gets its own single attempt, so one integration exhausting a + // shared policy template cannot starve the others. + Assert.True(await Allow(subA.Id)); + Assert.True(await Allow(subB.Id)); + Assert.False(await Allow(subA.Id)); + Assert.False(await Allow(subB.Id)); + return; + + async Task Allow(int subscriptionId) + { + var evaluator = new RetryPolicyEvaluator(policy, new RetryGroupBudget(db, scope.ServiceProvider, subscriptionId)); + var decision = await evaluator.Evaluate(XchangeResultType.Error, "timeout", 0); + await db.SaveChangesAsync(); + return decision.ShouldRetry; + } + } + + [Fact] + public async Task Concurrent_claims_never_exceed_the_group_total() + { + await using var setup = _fixture.CreateScope(); + var setupDb = setup.ServiceProvider.GetRequiredService(); + + var doc = new Document(7010, "Concurrent Budget Doc"); + setupDb.Set().Add(doc); + var sub = new Subscription("Concurrent Budget Sub", doc.Id); + setupDb.Set().Add(sub); + await setupDb.SaveChangesAsync(); + + var groupId = Guid.NewGuid(); + const int cap = 5; + const int racers = 16; + + // Bitween runs several instances, so simultaneous failures of the same integration and + // group are normal. Each racer gets its own scope and context, mimicking separate + // instances: a read-then-write would let several observe the same free slot at once. + var tasks = Enumerable.Range(0, racers).Select(async _ => + { + await using var scope = _fixture.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + return await new RetryGroupBudget(db, scope.ServiceProvider, sub.Id).TryConsume(groupId, cap); + }); + + var claims = await Task.WhenAll(tasks); + var granted = claims.Count(claim => claim.Granted); + + Assert.Equal(cap, granted); + + var usage = await setupDb.Set().AsNoTracking() + .SingleAsync(u => u.SubscriptionId == sub.Id && u.GroupId == groupId); + Assert.Equal(cap, usage.AttemptsUsed); + } + + // ─── Usage reporting and reset ────────────────────────────────────────────── + + [Fact] + public async Task Usage_reports_spent_budget_and_reset_clears_it() + { + await using var scope = _fixture.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var ctx = scope.Superuser(); + + var doc = new Document(7007, "Usage Doc"); + db.Set().Add(doc); + await db.SaveChangesAsync(); + + var policyId = (int)await new Create(db, ctx).Handle(SimplePolicy("Usage Policy")); + var saved = await db.Set().AsNoTracking().SingleAsync(p => p.Id == policyId); + var groupId = saved.Groups[0].Id; + + var sub = new Subscription("Usage Sub", doc.Id); + db.Set().Add(sub); + await db.SaveChangesAsync(); + sub.SetRetryPolicy(policyId, null); + await db.SaveChangesAsync(); + + // Spend the whole budget (SimplePolicy allows 10 in total). + var budget = new RetryGroupBudget(db, scope.ServiceProvider, sub.Id); + for (var i = 0; i < 10; i++) await budget.TryConsume(groupId, 10); + await db.SaveChangesAsync(); + + var rows = (List)await new Usage(db, ctx, Report(scope)).Handle(policyId, new RetryPolicyUsageRequest()); + var row = Assert.Single(rows); + Assert.Equal(sub.Id, row.SubscriptionId); + Assert.Equal("Usage Sub", row.SubscriptionName); + Assert.Equal("Timeout", row.GroupName); + Assert.Equal(10, row.AttemptsUsed); + Assert.True(row.Exhausted); + + await new ResetUsage(db, ctx).Handle(policyId, new RetryPolicyResetUsage + { + SubscriptionId = sub.Id, + GroupId = groupId + }); + + // The pair keeps its row — every subscription-and-group pair gets one so an alert override + // stays configurable before the first failure — but with nothing spent against the ceiling. + var afterReset = Assert.Single( + (List)await new Usage(db, ctx, Report(scope)).Handle(policyId, new RetryPolicyUsageRequest())); + Assert.Equal(0, afterReset.AttemptsUsed); + Assert.False(afterReset.Exhausted); + Assert.Null(afterReset.LastAttemptOn); + + // And the group can retry again. + Assert.True((await new RetryGroupBudget(db, scope.ServiceProvider, sub.Id).TryConsume(groupId, 10)).Granted); + } + + [Fact] + public async Task Usage_lists_never_failed_pairs_and_skips_groups_that_cannot_exhaust() + { + await using var scope = _fixture.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var ctx = scope.Superuser(); + + var doc = new Document(7011, "Never Failed Doc"); + db.Set().Add(doc); + await db.SaveChangesAsync(); + + var model = SimplePolicy("Never Failed Policy"); + model.AlertHandlerId = "NativeSmtpHandler"; + + // A Block group carries no budget, and the evaluator refuses before it ever claims one, so + // it can never exhaust and never alert. Reporting it would invite configuring an alert that + // cannot fire. + model.Groups.Add(new RetryGroup + { + Name = "Never retry", + Priority = 20, + Action = RetryAction.Block, + AppliesTo = [XchangeResultType.Error], + Matchers = [new ContainsMatcher { Value = "fatal" }] + }); + + var policyId = (int)await new Create(db, ctx).Handle(model); + + var sub = new Subscription("Never Failed Sub", doc.Id); + db.Set().Add(sub); + await db.SaveChangesAsync(); + sub.SetRetryPolicy(policyId, null); + await db.SaveChangesAsync(); + + var rows = (List)await new Usage(db, ctx, Report(scope)) + .Handle(policyId, new RetryPolicyUsageRequest()); + + // One row, not two: the pair is reported even though nothing has ever failed — otherwise its + // alert override would be unreachable until after the first failure — while the Block group + // is left out entirely. + var row = Assert.Single(rows); + Assert.Equal("Timeout", row.GroupName); + Assert.Equal(0, row.AttemptsUsed); + Assert.Equal(10, row.MaxAttemptsTotal); + Assert.False(row.Exhausted); + Assert.Null(row.LastAttemptOn); + Assert.Equal("NativeSmtpHandler", row.ResolvedHandlerId); + Assert.Equal(RetryAlertLevel.Policy, row.ResolvedFrom); + } + + [Fact] + public async Task Reset_does_not_touch_counters_of_another_policy() + { + await using var scope = _fixture.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var ctx = scope.Superuser(); + + var doc = new Document(7008, "Reset Scope Doc"); + db.Set().Add(doc); + await db.SaveChangesAsync(); + + var mineId = (int)await new Create(db, ctx).Handle(SimplePolicy("Reset Scope Mine")); + var otherId = (int)await new Create(db, ctx).Handle(SimplePolicy("Reset Scope Other")); + var otherGroupId = (await db.Set().AsNoTracking() + .SingleAsync(p => p.Id == otherId)).Groups[0].Id; + + var otherSub = new Subscription("Reset Scope Other Sub", doc.Id); + db.Set().Add(otherSub); + await db.SaveChangesAsync(); + otherSub.SetRetryPolicy(otherId, null); + await db.SaveChangesAsync(); + + await new RetryGroupBudget(db, scope.ServiceProvider, otherSub.Id).TryConsume(otherGroupId, 10); + await db.SaveChangesAsync(); + + // Resetting everything under one policy must leave the other policy's counters alone. + await new ResetUsage(db, ctx).Handle(mineId, new RetryPolicyResetUsage()); + + // A row now exists for every pair whether or not it has failed, so assert the spent counter + // itself survived — row count alone would pass even if the reset had wrongly cleared it. + var otherRow = Assert.Single( + (List)await new Usage(db, ctx, Report(scope)).Handle(otherId, new RetryPolicyUsageRequest())); + Assert.Equal(1, otherRow.AttemptsUsed); + } + + [Fact] + public async Task Removing_a_group_clears_its_spent_budget() + { + await using var scope = _fixture.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var ctx = scope.Superuser(); + + var doc = new Document(7009, "Removed Group Doc"); + db.Set().Add(doc); + await db.SaveChangesAsync(); + + var policyId = (int)await new Create(db, ctx).Handle(SimplePolicy("Removed Group Policy")); + var saved = await db.Set().AsNoTracking().SingleAsync(p => p.Id == policyId); + var groupId = saved.Groups[0].Id; + + var sub = new Subscription("Removed Group Sub", doc.Id); + db.Set().Add(sub); + await db.SaveChangesAsync(); + sub.SetRetryPolicy(policyId, null); + await db.SaveChangesAsync(); + + await new RetryGroupBudget(db, scope.ServiceProvider, sub.Id).TryConsume(groupId, 10); + await db.SaveChangesAsync(); + Assert.True(await db.Set().AnyAsync(u => u.GroupId == groupId)); + + // Dropping the group must take its counter with it, or the row is stranded where + // neither the usage report nor reset can reach it. + await new Update(db, ctx).Handle(policyId, new RetryPolicyUpdate + { + Name = "Removed Group Policy", + Groups = [] + }); + + Assert.False(await db.Set().AnyAsync(u => u.GroupId == groupId)); + } + + // ─── Attempts drill-down ──────────────────────────────────────────────────── + + [Fact] + public async Task Attempts_lists_only_this_pairs_stamped_failures_pending_first() + { + await using var scope = _fixture.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var ctx = scope.Superuser(); + + var doc = new Document(7012, "Attempts Doc"); + db.Set().Add(doc); + await db.SaveChangesAsync(); + + var policyId = (int)await new Create(db, ctx).Handle(SimplePolicy("Attempts Policy")); + var groupId = (await db.Set().AsNoTracking() + .SingleAsync(p => p.Id == policyId)).Groups[0].Id; + + var sub = new Subscription("Attempts Sub", doc.Id); + var otherSub = new Subscription("Attempts Other Sub", doc.Id); + db.Set().AddRange(sub, otherSub); + await db.SaveChangesAsync(); + sub.SetRetryPolicy(policyId, null); + otherSub.SetRetryPolicy(policyId, null); + await db.SaveChangesAsync(); + + // Still being worked on: a scheduled retry is outstanding for it. + var pending = new Xchange(sub, new XchangeFile("{}")); + var pendingResult = new XchangeResult(pending.Id, null, null, exception: "first timeout"); + pendingResult.SetRetryEvaluation(groupId, 0); + + // Given up on, and the reason recorded. + var stopped = new Xchange(sub, new XchangeFile("{}")); + var stoppedResult = new XchangeResult(stopped.Id, null, null, exception: "second timeout"); + stoppedResult.SetRetryEvaluation(groupId, 1); + stoppedResult.SetRetryBlocked("Group 'Timeout' has used all 10 of its total attempts"); + + // Carries no group: this is what every failure recorded before the group was stamped onto + // results looks like, and it has no pair to be listed under. + var unstamped = new Xchange(sub, new XchangeFile("{}")); + var unstampedResult = new XchangeResult(unstamped.Id, null, null, exception: "older timeout"); + + // Same policy and same group, different subscription — a row of its own, not this one's. + var otherPair = new Xchange(otherSub, new XchangeFile("{}")); + var otherPairResult = new XchangeResult(otherPair.Id, null, null, exception: "someone else's timeout"); + otherPairResult.SetRetryEvaluation(groupId, 0); + + db.Set().AddRange(pending, stopped, unstamped, otherPair); + db.Set().AddRange(pendingResult, stoppedResult, unstampedResult, otherPairResult); + db.Set().Add(new DelayedRetry { Id = pending.Id, On = DateTime.UtcNow.AddMinutes(5) }); + await db.SaveChangesAsync(); + + var result = (RetryGroupAttempts)await new Attempts(db, ctx).Handle(policyId, + new RetryGroupAttemptsRequest { SubscriptionId = sub.Id, GroupId = groupId }); + + // Two, not four: the unstamped failure and the other subscription's are both out. + Assert.Equal(2, result.Total); + Assert.Equal(2, result.Attempts.Count); + + // Pending leads, so a long history of finished failures can never push the one still moving + // out of a capped list. + Assert.Equal(pending.Id, result.Attempts[0].XchangeId); + Assert.True(result.Attempts[0].RetryPending); + Assert.Equal(0, result.Attempts[0].AttemptNumber); + Assert.Equal("first timeout", result.Attempts[0].Exception); + Assert.Null(result.Attempts[0].RetryBlockedReason); + + Assert.Equal(stopped.Id, result.Attempts[1].XchangeId); + Assert.False(result.Attempts[1].RetryPending); + Assert.Equal(1, result.Attempts[1].AttemptNumber); + Assert.Contains("used all 10", result.Attempts[1].RetryBlockedReason); + } + + [Fact] + public async Task Attempts_rejects_a_subscription_that_does_not_use_the_policy() + { + await using var scope = _fixture.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var ctx = scope.Superuser(); + + var doc = new Document(7013, "Attempts Scope Doc"); + db.Set().Add(doc); + await db.SaveChangesAsync(); + + var mineId = (int)await new Create(db, ctx).Handle(SimplePolicy("Attempts Scope Mine")); + var theirsId = (int)await new Create(db, ctx).Handle(SimplePolicy("Attempts Scope Theirs")); + var theirGroupId = (await db.Set().AsNoTracking() + .SingleAsync(p => p.Id == theirsId)).Groups[0].Id; + + var theirSub = new Subscription("Attempts Scope Their Sub", doc.Id); + db.Set().Add(theirSub); + await db.SaveChangesAsync(); + theirSub.SetRetryPolicy(theirsId, null); + await db.SaveChangesAsync(); + + // Asking one policy for another policy's subscription must fail rather than quietly answer: + // the route key is what the caller was authorised against. + await Assert.ThrowsAsync(() => new Attempts(db, ctx).Handle(mineId, + new RetryGroupAttemptsRequest { SubscriptionId = theirSub.Id, GroupId = theirGroupId })); + } + // ─── Test / dry-run endpoint ──────────────────────────────────────────────── [Fact] @@ -407,4 +820,687 @@ public async Task Test_reports_no_match_when_no_group_applies() Assert.Null(response.Attempts[0].MatchedGroupName); } + // ─── Exhaustion alert claim ───────────────────────────────────────────────── + + [Fact] + public async Task Exhausting_a_budget_claims_the_alert_exactly_once() + { + await using var scope = _fixture.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + + var doc = new Document(7101, "Alert Claim Doc"); + db.Set().Add(doc); + await db.SaveChangesAsync(); + + var sub = new Subscription("Alert Claim Sub", doc.Id); + db.Set().Add(sub); + await db.SaveChangesAsync(); + + var groupId = Guid.NewGuid(); + var budget = new RetryGroupBudget(db, scope.ServiceProvider, sub.Id); + + // Spending the budget never alerts — nothing has been refused yet. + for (var i = 0; i < 3; i++) + { + var spending = await budget.TryConsume(groupId, 3); + Assert.True(spending.Granted); + Assert.False(spending.JustExhausted); + } + + // The first refusal owns the alert. + var first = await budget.TryConsume(groupId, 3); + Assert.False(first.Granted); + Assert.True(first.JustExhausted); + + // Every refusal after it stays quiet, however many failures arrive. + var second = await budget.TryConsume(groupId, 3); + Assert.False(second.Granted); + Assert.False(second.JustExhausted); + + var usage = await db.Set().AsNoTracking() + .SingleAsync(u => u.SubscriptionId == sub.Id && u.GroupId == groupId); + Assert.NotNull(usage.ExhaustedNotifiedOn); + } + + [Fact] + public async Task Concurrent_refusals_claim_the_alert_only_once() + { + await using var setupScope = _fixture.CreateScope(); + var setupDb = setupScope.ServiceProvider.GetRequiredService(); + + var doc = new Document(7102, "Alert Race Doc"); + setupDb.Set().Add(doc); + await setupDb.SaveChangesAsync(); + + var sub = new Subscription("Alert Race Sub", doc.Id); + setupDb.Set().Add(sub); + await setupDb.SaveChangesAsync(); + + var groupId = Guid.NewGuid(); + + // Spends the only attempt, so every racer below meets an empty budget. Asserted, or a + // failure here would surface as a confusing claim count further down. + var setupClaim = await new RetryGroupBudget(setupDb, setupScope.ServiceProvider, sub.Id) + .TryConsume(groupId, 1); + Assert.True(setupClaim.Granted); + + // Several instances can discover the empty budget in the same instant; a read-then-write + // would let each of them decide it was the first and send its own email. + var tasks = Enumerable.Range(0, 12).Select(async _ => + { + await using var scope = _fixture.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + return await new RetryGroupBudget(db, scope.ServiceProvider, sub.Id).TryConsume(groupId, 1); + }); + + var claims = await Task.WhenAll(tasks); + + Assert.Equal(1, claims.Count(c => c.JustExhausted)); + Assert.DoesNotContain(claims, c => c.Granted); + } + + [Fact] + public async Task Resetting_usage_re_arms_the_exhaustion_alert() + { + await using var scope = _fixture.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var ctx = scope.Superuser(); + + var doc = new Document(7103, "Alert Rearm Doc"); + db.Set().Add(doc); + await db.SaveChangesAsync(); + + var policyId = (int)await new Create(db, ctx).Handle(SimplePolicy("Alert Rearm Policy")); + var saved = await db.Set().AsNoTracking().SingleAsync(p => p.Id == policyId); + var groupId = saved.Groups[0].Id; + + var sub = new Subscription("Alert Rearm Sub", doc.Id); + db.Set().Add(sub); + await db.SaveChangesAsync(); + sub.SetRetryPolicy(policyId, null); + await db.SaveChangesAsync(); + + var budget = new RetryGroupBudget(db, scope.ServiceProvider, sub.Id); + for (var i = 0; i < 10; i++) await budget.TryConsume(groupId, 10); + Assert.True((await budget.TryConsume(groupId, 10)).JustExhausted); + + await new ResetUsage(db, ctx).Handle(policyId, new RetryPolicyResetUsage + { + SubscriptionId = sub.Id, + GroupId = groupId + }); + + // Reset deletes the row, so the budget and its alert come back together. + for (var i = 0; i < 10; i++) await budget.TryConsume(groupId, 10); + Assert.True((await budget.TryConsume(groupId, 10)).JustExhausted); + } + + // ─── Alert config validation ─────────────────────────────────────────────── + + [Fact] + public async Task Cannot_save_a_group_that_sends_its_own_alert_without_a_handler() + { + await using var scope = _fixture.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var ctx = scope.Superuser(); + + var model = SimplePolicy("Alert Validation Policy"); + model.Groups = + [ + new RetryGroup + { + Name = model.Groups[0].Name, + Priority = model.Groups[0].Priority, + AppliesTo = model.Groups[0].AppliesTo, + Matchers = model.Groups[0].Matchers, + Budget = model.Groups[0].Budget, + AlertMode = RetryAlertMode.Send + } + ]; + + await Assert.ThrowsAsync(() => new Create(db, ctx).Handle(model)); + } + + // ─── Reaching an inline policy's counters ──────────────────────────────────── + + [Fact] + public async Task An_inline_policy_budget_can_be_reported_and_reset_by_subscription() + { + await using var scope = _fixture.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var ctx = scope.Superuser(); + + var doc = new Document(7015, "Inline Policy Doc"); + db.Set().Add(doc); + await db.SaveChangesAsync(); + + var sub = new Subscription("Inline Policy Sub", doc.Id); + db.Set().Add(sub); + await db.SaveChangesAsync(); + + // Carried on the subscription itself, so there is no policy id anywhere to ask about it. + var inline = new CustomRetryPolicy { Groups = SimplePolicy("unused").Groups }; + sub.SetRetryPolicy(null, inline); + await db.SaveChangesAsync(); + + var groupId = inline.Groups[0].Id; + + // Spends the whole shared budget, which is what stops this subscription retrying at all. + for (var i = 0; i < 10; i++) + await new RetryGroupBudget(db, scope.ServiceProvider, sub.Id).TryConsume(groupId, 10); + await db.SaveChangesAsync(); + + var row = Assert.Single((List)await new Resources.Subscriptions.RetryUsage( + db, ctx, Report(scope)).Handle(sub.Id, new RetryPolicyUsageRequest())); + + Assert.Equal(10, row.AttemptsUsed); + Assert.True(row.Exhausted); + + // An inline policy has no row to hold a policy-level alert, so nothing resolves from there — + // and that has to read as "nothing configured" rather than as a level being consulted. + Assert.Null(row.ResolvedHandlerId); + Assert.Null(row.ResolvedFrom); + + // The point of the whole endpoint: before this, no reset could reach these counters, so the + // subscription stayed stopped for good. + await new Resources.Subscriptions.ResetRetryUsage(db, ctx) + .Handle(sub.Id, new SubscriptionRetryResetUsage()); + + var afterReset = Assert.Single((List)await new Resources.Subscriptions.RetryUsage( + db, ctx, Report(scope)).Handle(sub.Id, new RetryPolicyUsageRequest())); + Assert.Equal(0, afterReset.AttemptsUsed); + Assert.False(afterReset.Exhausted); + + // And it stays scoped to this subscription: a policy-scoped report must still not see it, + // which is precisely why the subscription-scoped one had to exist. + Assert.False(await db.Set().AnyAsync(u => u.SubscriptionId == sub.Id)); + } + + // ─── Allow with no budget ─────────────────────────────────────────────────── + + [Fact] + public async Task Allow_without_a_budget_is_rejected_on_save() + { + await using var scope = _fixture.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var ctx = scope.Superuser(); + + RetryPolicyCreate PolicyWithBudgetlessGroup(string name, RetryAction action) => new() + { + Name = name, + Groups = + [ + new RetryGroup + { + Name = "No budget", + Priority = 10, + Action = action, + AppliesTo = [XchangeResultType.Error], + Matchers = [new ContainsMatcher { Value = "timeout" }] + } + ] + }; + + // Nothing to work from — no caps, no delay — so the evaluator could only ever refuse it, and + // refusing quietly reads as retries being broken. Rejected where it is configured instead. + await Assert.ThrowsAsync( + () => new Create(db, ctx).Handle(PolicyWithBudgetlessGroup("Budgetless Allow", RetryAction.Allow))); + + // Block is the shape that legitimately has no budget, and it must still save. + await new Create(db, ctx).Handle(PolicyWithBudgetlessGroup("Budgetless Block", RetryAction.Block)); + } + + // ─── Alert secrets ────────────────────────────────────────────────────────── + + // What the browser is shown in place of a secret. Spelled out rather than taken from the + // constant: the UI has its own copy of this string, and the two have to stay the same. + private const string Sentinel = "__private__"; + + private static Dictionary SmtpProperties(string password, bool useTls) => new() + { + ["Host"] = "localhost", + ["Port"] = "1025", + ["UseTls"] = useTls ? "true" : "false", + ["Password"] = password, + ["From"] = "bitween-alerts@example.com", + ["To"] = "ops@example.com", + ["Subject"] = "Retries stopped", + ["Body"] = "Budget spent." + }; + + [Fact] + public async Task An_alert_password_is_masked_on_read_and_survives_being_saved_back() + { + await using var scope = _fixture.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var ctx = scope.Superuser(); + + var model = SimplePolicy("Masked Alert Policy"); + model.AlertHandlerId = "NativeSmtpHandler"; + model.AlertHandlerProperties = SmtpProperties("hunter2", useTls: true); + + var policyId = (int)await new Create(db, ctx).Handle(model); + + var loaded = (RetryPolicyUpdate)await new Get(db, ctx, Secrets(scope)).Handle(policyId); + + // The password never leaves the server; everything that is not a secret still does, or the + // form would have nothing to show. + Assert.Equal(Sentinel, loaded.AlertHandlerProperties["Password"]); + Assert.Equal("localhost", loaded.AlertHandlerProperties["Host"]); + Assert.Equal("Retries stopped", loaded.AlertHandlerProperties["Subject"]); + + // Exactly what the page does when someone edits the subject and saves: the password comes + // back as the mask, and must not be stored as one. + loaded.AlertHandlerProperties["Subject"] = "Retries stopped for real"; + await new Update(db, ctx).Handle(policyId, new RetryPolicyUpdate + { + Name = loaded.Name, + Groups = loaded.Groups, + AlertHandlerId = loaded.AlertHandlerId, + AlertHandlerProperties = loaded.AlertHandlerProperties + }); + + var stored = await db.Set().AsNoTracking().SingleAsync(p => p.Id == policyId); + Assert.Equal("hunter2", stored.AlertHandlerProperties["Password"]); + Assert.Equal("Retries stopped for real", stored.AlertHandlerProperties["Subject"]); + } + + [Fact] + public async Task Overriding_an_inherited_alert_keeps_the_password_it_was_only_shown_masked() + { + await using var scope = _fixture.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var ctx = scope.Superuser(); + + var doc = new Document(7014, "Copied Secret Doc"); + db.Set().Add(doc); + await db.SaveChangesAsync(); + + var model = SimplePolicy("Copied Secret Policy"); + model.AlertHandlerId = "NativeSmtpHandler"; + model.AlertHandlerProperties = SmtpProperties("hunter2", useTls: true); + var policyId = (int)await new Create(db, ctx).Handle(model); + + var groupId = (await db.Set().AsNoTracking() + .SingleAsync(p => p.Id == policyId)).Groups[0].Id; + + var sub = new Subscription("Copied Secret Sub", doc.Id); + db.Set().Add(sub); + await db.SaveChangesAsync(); + sub.SetRetryPolicy(policyId, null); + await db.SaveChangesAsync(); + + var row = Assert.Single((List)await new Usage(db, ctx, Report(scope)) + .Handle(policyId, new RetryPolicyUsageRequest())); + Assert.Equal(Sentinel, row.ResolvedHandlerProperties["Password"]); + + // The page offers "start from what this currently sends", so the masked value is what comes + // back — and there is no override row yet to restore it from. It has to be recovered from the + // level the caller was shown it at, or the new override would send with no password at all. + await new SaveAlertOverride(db, ctx, Secrets(scope)).Handle(policyId, new RetryAlertOverrideSave + { + SubscriptionId = sub.Id, + GroupId = groupId, + AlertMode = RetryAlertMode.Send, + AlertHandlerId = "NativeSmtpHandler", + AlertHandlerProperties = row.ResolvedHandlerProperties + }); + + var stored = await db.Set().AsNoTracking() + .SingleAsync(o => o.SubscriptionId == sub.Id && o.GroupId == groupId); + Assert.Equal("hunter2", stored.AlertHandlerProperties["Password"]); + Assert.Equal("ops@example.com", stored.AlertHandlerProperties["To"]); + } + + [Fact] + public async Task A_mail_alert_with_a_password_and_no_encryption_is_rejected_on_save() + { + await using var scope = _fixture.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var ctx = scope.Superuser(); + + var model = SimplePolicy("Cleartext Alert Policy"); + model.AlertHandlerId = "NativeSmtpHandler"; + model.AlertHandlerProperties = SmtpProperties("hunter2", useTls: false); + + // Caught on save, where the person configuring it is looking — the handler refuses this at + // send time too, but by then the only trace is a missing alert. + await Assert.ThrowsAsync(() => new Create(db, ctx).Handle(model)); + + // Encryption off is fine on its own; it is only the password that must not travel in clear. + model.AlertHandlerProperties = SmtpProperties("", useTls: false); + await new Create(db, ctx).Handle(model); + } + + [Fact] + public async Task Policy_alert_handler_round_trips() + { + await using var scope = _fixture.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var ctx = scope.Superuser(); + + var model = SimplePolicy("Alert Handler Policy"); + model.AlertHandlerId = "NativeSmtpHandler"; + model.AlertHandlerProperties = new Dictionary { ["to"] = "ops@example.com" }; + + var policyId = (int)await new Create(db, ctx).Handle(model); + var loaded = (RetryPolicyUpdate)await new Get(db, ctx, Secrets(scope)).Handle(policyId); + + Assert.Equal("NativeSmtpHandler", loaded.AlertHandlerId); + Assert.Equal("ops@example.com", loaded.AlertHandlerProperties["to"]); + } + + // ─── Manual retries and the shared budget ───────────────────────────────── + + /// + /// A person pressing Retry must not spend the budget set aside for unattended retries. + /// + /// + /// Both attempts in this test are children of the same failed exchange, fail the same way against + /// the same group, and differ only in who asked for them. Without that pairing the test could pass + /// simply because nothing was ever evaluated. + /// + [Fact] + public async Task A_retry_started_by_hand_is_left_alone_by_the_policy() + { + await using var scope = _fixture.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var xs = scope.ServiceProvider.GetRequiredService(); + + const string failure = "manual retry budget probe failed"; + + var doc = new Document(7031, "Manual Retry Doc"); + db.Set().Add(doc); + await db.SaveChangesAsync(); + + var groupId = Guid.NewGuid(); + var policy = new RetryPolicy + { + Name = "Manual Retry Policy " + Guid.NewGuid().ToString("N")[..6], + Groups = + [ + new RetryGroup + { + Id = groupId, + Name = "Probe", + Priority = 10, + AppliesTo = [XchangeResultType.Error], + Matchers = [new ContainsMatcher { Value = failure }], + Budget = new RetryBudget + { + MaxAttemptsPerError = 3, + MaxAttemptsTotal = 5, + DelayStrategy = new FixedDelayStrategy { DelayMs = 60_000 } + } + } + ] + }; + db.Set().Add(policy); + await db.SaveChangesAsync(); + + // A handler that fails on demand, so the failure text is chosen here rather than inherited + // from whatever the environment happens to throw, and the matcher above can be exact. + var sub = new Subscription("Manual Retry Sub", doc.Id); + sub.HandlerId = "sw.bitween.sampleconfigurableadapter"; + sub.SetDictionaries( + new Dictionary { ["SimulateError"] = "true", ["ErrorMessage"] = failure }, + null, null, null, null); + db.Set().Add(sub); + await db.SaveChangesAsync(); + sub.SetRetryPolicy(policy.Id, null); + await db.SaveChangesAsync(); + + + // The document cache is a warm singleton shared by the whole collection, and production + // clears it over the bus whenever a document changes. Cleared here for the same reason: a + // document created after the cache warmed is invisible to the filter step, which then fails + // on its own before any handler runs. + scope.ServiceProvider.GetRequiredService().Revoke(); + + var original = await xs.CreateXchange(sub, new XchangeFile("{}")); + await db.SaveChangesAsync(); + + // One of each, exactly as their callers build them: the endpoint behind the Retry button, and + // RetryJob working through a due DelayedRetry. + await xs.CreateXchange(sub, original, new XchangeFile("{}"), manualRetry: true); + await xs.CreateXchange(sub, original, new XchangeFile("{}")); + await db.SaveChangesAsync(); + + var children = await db.Set().AsNoTracking() + .Where(x => x.RetryFor == original.Id).ToListAsync(); + var byHand = Assert.Single(children, x => x.ManualRetry); + var byPolicy = Assert.Single(children, x => !x.ManualRetry); + + await Run(byHand.Id); + await Run(byPolicy.Id); + + var handResult = await db.Set().AsNoTracking().SingleAsync(r => r.Id == byHand.Id); + var policyResult = await db.Set().AsNoTracking().SingleAsync(r => r.Id == byPolicy.Id); + + // Both genuinely failed, and failed the way the group is written for, so the policy had + // something to match in either case. + Assert.False(handResult.Success); + Assert.False(policyResult.Success); + Assert.Contains(failure, handResult.Exception); + Assert.Contains(failure, policyResult.Exception); + + Assert.Null(handResult.RetryGroupId); + Assert.Contains("by hand", handResult.RetryBlockedReason); + Assert.False(await db.Set().AsNoTracking().AnyAsync(r => r.Id == byHand.Id)); + + // The control: the same failure, evaluated, charged for and scheduled. + Assert.Equal(groupId, policyResult.RetryGroupId); + Assert.True(await db.Set().AsNoTracking().AnyAsync(r => r.Id == byPolicy.Id)); + + var usage = await db.Set().AsNoTracking() + .SingleAsync(u => u.SubscriptionId == sub.Id && u.GroupId == groupId); + Assert.Equal(1, usage.AttemptsUsed); + + // Runs the exchange through the same entry point the bus calls, so the guard is exercised + // where it actually sits rather than through a seam opened up for the test. + async Task Run(string xchangeId) + { + await using var runScope = _fixture.CreateScope(); + await runScope.ServiceProvider.GetRequiredService() + .Process("XchangeCreated", JsonConvert.SerializeObject(new { Id = xchangeId })); + } + } + + // ─── Recovery ───────────────────────────────────────────────────────────── + + /// + /// A success is what tells Bitween the downstream is back, so it is what gives the budget back. + /// + /// + /// Nothing else can: an exhausted group schedules no more retries, so no retry will ever succeed + /// to report the recovery. Only ordinary traffic getting through can, which is what this drives. + /// + [Fact] + public async Task A_success_gives_the_group_its_spent_budget_back() + { + await using var scope = _fixture.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var xs = scope.ServiceProvider.GetRequiredService(); + + var doc = new Document(7032, "Recovery Doc"); + db.Set().Add(doc); + var sub = new Subscription("Recovery Sub", doc.Id); + db.Set().Add(sub); + await db.SaveChangesAsync(); + + var group = new RetryGroup + { + Name = "Timeout", + Priority = 10, + AppliesTo = [XchangeResultType.Error], + Matchers = [new ContainsMatcher { Value = "timeout" }], + Budget = new RetryBudget + { + MaxAttemptsPerError = 5, + MaxAttemptsTotal = 2, + DelayStrategy = new FixedDelayStrategy { DelayMs = 60_000 } + } + }; + // Attached to the subscription, not just handed to the evaluator: releasing a budget reads the + // group's cap back from the policy the subscription actually holds, because a total that is + // only partly spent must be left alone. + sub.SetRetryPolicy(null, new CustomRetryPolicy { Groups = [group] }); + await db.SaveChangesAsync(); + + async Task Fail() => + await new RetryPolicyEvaluator(sub.CustomRetryPolicy, + new RetryGroupBudget(db, scope.ServiceProvider, sub.Id)) + .Evaluate(XchangeResultType.Error, "System.TimeoutException: timeout", 0); + + Assert.True((await Fail()).ShouldRetry); + Assert.True((await Fail()).ShouldRetry); + + var exhausted = await Fail(); + Assert.False(exhausted.ShouldRetry); + Assert.True(exhausted.BudgetJustExhausted); + + + // The document cache is a warm singleton shared by the whole collection, and production + // clears it over the bus whenever a document changes. Cleared here for the same reason: a + // document created after the cache warmed is invisible to the filter step, which then fails + // on its own before any handler runs. + scope.ServiceProvider.GetRequiredService().Revoke(); + + // The subscription has no handler, so this exchange simply succeeds — an ordinary message + // getting through after the outage, which is the only evidence of recovery there is. + var recovered = await xs.CreateXchange(sub, new XchangeFile("{}")); + await db.SaveChangesAsync(); + + await using (var runScope = _fixture.CreateScope()) + await runScope.ServiceProvider.GetRequiredService() + .Process("XchangeCreated", JsonConvert.SerializeObject(new { Id = recovered.Id })); + + var result = await db.Set().AsNoTracking().SingleAsync(r => r.Id == recovered.Id); + Assert.True(result.Success); + + Assert.Empty(await db.Set().AsNoTracking() + .Where(u => u.SubscriptionId == sub.Id).ToListAsync()); + + // Retrying works again, and because the row is gone the next exhaustion alerts afresh. + var afterRecovery = await Fail(); + Assert.True(afterRecovery.ShouldRetry); + } + + /// + /// A total that is only partly spent is not credited back by an ordinary success. + /// + /// + /// The cap is there for a downstream that fails some messages and succeeds others. Handing the + /// total back on every success would mean exactly that downstream never reaches its cap, so the + /// release is deliberately limited to a budget that has actually run out. + /// + [Fact] + public async Task A_partly_spent_budget_is_left_alone_by_a_success() + { + await using var scope = _fixture.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var xs = scope.ServiceProvider.GetRequiredService(); + + var doc = new Document(7033, "Partly Spent Doc"); + db.Set().Add(doc); + var sub = new Subscription("Partly Spent Sub", doc.Id); + db.Set().Add(sub); + await db.SaveChangesAsync(); + + var group = new RetryGroup + { + Name = "Timeout", + Priority = 10, + AppliesTo = [XchangeResultType.Error], + Matchers = [new ContainsMatcher { Value = "timeout" }], + Budget = new RetryBudget + { + MaxAttemptsPerError = 5, + MaxAttemptsTotal = 4, + DelayStrategy = new FixedDelayStrategy { DelayMs = 60_000 } + } + }; + sub.SetRetryPolicy(null, new CustomRetryPolicy { Groups = [group] }); + await db.SaveChangesAsync(); + + // One of four spent, so the group is still allowed to retry and has nothing to recover from. + var spend = await new RetryPolicyEvaluator(sub.CustomRetryPolicy, + new RetryGroupBudget(db, scope.ServiceProvider, sub.Id)) + .Evaluate(XchangeResultType.Error, "System.TimeoutException: timeout", 0); + Assert.True(spend.ShouldRetry); + + scope.ServiceProvider.GetRequiredService().Revoke(); + + var succeeded = await xs.CreateXchange(sub, new XchangeFile("{}")); + await db.SaveChangesAsync(); + + await using (var runScope = _fixture.CreateScope()) + await runScope.ServiceProvider.GetRequiredService() + .Process("XchangeCreated", JsonConvert.SerializeObject(new { Id = succeeded.Id })); + + Assert.True((await db.Set().AsNoTracking().SingleAsync(r => r.Id == succeeded.Id)).Success); + + var usage = await db.Set().AsNoTracking() + .SingleAsync(u => u.SubscriptionId == sub.Id && u.GroupId == group.Id); + Assert.Equal(1, usage.AttemptsUsed); + } + + /// + /// A slot charged after the success began is not handed back by it. + /// + /// + /// Bitween runs several instances, so a failure can claim a slot while a success is still being + /// processed. Releasing that row would give back a slot already spent and let the group retry past + /// its total. The row's last attempt is compared against the successful exchange's start, which is + /// what this drives directly — the timing is otherwise a race no test could pin down. + /// + [Fact] + public async Task A_slot_charged_after_the_success_began_is_not_handed_back() + { + await using var scope = _fixture.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + + var doc = new Document(7034, "Watermark Doc"); + db.Set().Add(doc); + var sub = new Subscription("Watermark Sub", doc.Id); + db.Set().Add(sub); + await db.SaveChangesAsync(); + + var group = new RetryGroup + { + Name = "Timeout", + Priority = 10, + AppliesTo = [XchangeResultType.Error], + Matchers = [new ContainsMatcher { Value = "timeout" }], + Budget = new RetryBudget + { + MaxAttemptsPerError = 5, + MaxAttemptsTotal = 1, + DelayStrategy = new FixedDelayStrategy { DelayMs = 60_000 } + } + }; + sub.SetRetryPolicy(null, new CustomRetryPolicy { Groups = [group] }); + await db.SaveChangesAsync(); + + // Exhausted, and charged after the moment the success below claims to have started. + db.Set().Add(new RetryGroupUsage + { + SubscriptionId = sub.Id, + GroupId = group.Id, + AttemptsUsed = 1, + LastAttemptOn = DateTime.UtcNow.AddMinutes(5) + }); + await db.SaveChangesAsync(); + + var budget = new RetryGroupBudget(db, scope.ServiceProvider, sub.Id); + + Assert.Equal(0, await budget.ReleaseExhaustedBudgets(DateTime.UtcNow)); + Assert.Equal(1, (await db.Set().AsNoTracking() + .SingleAsync(u => u.SubscriptionId == sub.Id)).AttemptsUsed); + + // The same budget, released once the success is known to postdate the charge. + Assert.Equal(1, await budget.ReleaseExhaustedBudgets(DateTime.UtcNow.AddMinutes(10))); + Assert.Empty(await db.Set().AsNoTracking() + .Where(u => u.SubscriptionId == sub.Id).ToListAsync()); + } } diff --git a/SW.Bitween.MsSql/Migrations/20260811081235_SharedRetryGroupTotals.Designer.cs b/SW.Bitween.MsSql/Migrations/20260811081235_SharedRetryGroupTotals.Designer.cs new file mode 100644 index 00000000..ca317971 --- /dev/null +++ b/SW.Bitween.MsSql/Migrations/20260811081235_SharedRetryGroupTotals.Designer.cs @@ -0,0 +1,1902 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using SW.Bitween.MsSql; + +#nullable disable + +namespace SW.Bitween.MsSql.Migrations +{ + [DbContext(typeof(BitweenDbContext))] + [Migration("20260811081235_SharedRetryGroupTotals")] + partial class SharedRetryGroupTotals + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.23") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Deleted") + .HasColumnType("bit"); + + b.Property("Disabled") + .HasColumnType("bit"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Email") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("EmailProvider") + .HasColumnType("tinyint"); + + b.Property("LoginMethods") + .HasColumnType("tinyint"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Password") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("Phone") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("varchar(20)"); + + b.Property("Role") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique() + .HasFilter("[Email] IS NOT NULL"); + + b.ToTable("Accounts", (string)null); + + b.HasData( + new + { + Id = 9999, + CreatedOn = new DateTime(2021, 12, 31, 22, 0, 0, 0, DateTimeKind.Utc), + Deleted = false, + Disabled = false, + DisplayName = "Admin", + Email = "admin@Bitween.systems", + EmailProvider = (byte)0, + LoginMethods = (byte)2, + Password = "$SWHASH$V1$10000$VQCi48eitH4Ml5juvBMOFZrMdQwBbhuIQVXe6RR7qJdDF2bJ", + Role = 0 + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AccountId") + .HasColumnType("int"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("LoginMethod") + .HasColumnType("tinyint"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.ToTable("RefreshTokens", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DelayedRetry", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("On") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("On"); + + b.ToTable("DelayedRetries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Document", b => + { + b.Property("Id") + .HasColumnType("int"); + + b.Property("BusEnabled") + .HasColumnType("bit"); + + b.Property("BusMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("DisregardsUnfilteredMessages") + .HasColumnType("bit"); + + b.Property("DocumentFormat") + .HasColumnType("int"); + + b.Property("DuplicateInterval") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("PromotedProperties") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("BusMessageTypeName") + .IsUnique() + .HasFilter("[BusMessageTypeName] IS NOT NULL"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Documents", (string)null); + + b.HasData( + new + { + Id = 10001, + BusEnabled = false, + DocumentFormat = 0, + DuplicateInterval = 0, + Name = "Aggregation Document", + PromotedProperties = "{}" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DocumentTrail", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("Code") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("StateAfter") + .HasColumnType("nvarchar(max)"); + + b.Property("StateBefore") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("CreatedOn"); + + b.HasIndex("DocumentId"); + + b.ToTable("DocumentTrail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("UrlName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("UrlName") + .IsUnique(); + + b.ToTable("ApiGateways", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.Property("ApiGatewayId") + .HasColumnType("int"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.HasKey("ApiGatewayId", "PartnerId", "SubscriptionId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("ApiGatewayPartners", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.ToTable("BusGateways", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BusGatewayId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("MatchExpression") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("BusGatewayId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("BusGatewayRoutes", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.GlobalAdapterValuesSet", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("Values") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("GlobalAdapterValuesSets", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Notifier", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("Inactive") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("RunOnBadResult") + .HasColumnType("bit"); + + b.Property("RunOnFailedResult") + .HasColumnType("bit"); + + b.Property("RunOnSubscriptions") + .HasColumnType("nvarchar(max)"); + + b.Property("RunOnSuccessfulResult") + .HasColumnType("bit"); + + b.HasKey("Id"); + + b.ToTable("Notifiers", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OnHoldXchange", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BadData") + .HasColumnType("bit"); + + b.Property("Data") + .HasColumnType("nvarchar(max)"); + + b.Property("FileName") + .HasColumnType("nvarchar(max)"); + + b.Property("References") + .HasMaxLength(1024) + .HasColumnType("nvarchar(1024)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("OnHoldXchanges", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AdapterProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.ToTable("Partners", (string)null); + + b.HasData( + new + { + Id = 1, + Name = "SYSTEM" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryGroupUsage", b => + { + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("GroupId") + .HasColumnType("uniqueidentifier"); + + b.Property("AttemptsUsed") + .HasColumnType("int"); + + b.Property("LastAttemptOn") + .HasColumnType("datetime2"); + + b.HasKey("SubscriptionId", "GroupId"); + + b.ToTable("RetryGroupUsages", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryPolicy", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Groups") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.ToTable("RetryPolicies", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AggregateOn") + .HasColumnType("datetime2"); + + b.Property("AggregationForId") + .HasColumnType("int"); + + b.Property("AggregationTarget") + .HasColumnType("tinyint"); + + b.Property("CategoryId") + .HasColumnType("int"); + + b.Property("ConsecutiveFailures") + .HasColumnType("int"); + + b.Property("CustomRetryPolicy") + .HasColumnType("nvarchar(max)"); + + b.Property("DocumentFilter") + .HasColumnType("nvarchar(max)"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("Inactive") + .HasColumnType("bit"); + + b.Property("IsRunning") + .HasColumnType("bit"); + + b.Property("LastException") + .HasColumnType("nvarchar(max)"); + + b.Property("MapperId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("MapperProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("MatchExpression") + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("PausedOn") + .HasColumnType("datetime2"); + + b.Property("ReceiveOn") + .HasColumnType("datetime2"); + + b.Property("ReceiverId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ReceiverProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("int"); + + b.Property("RetryPolicyId") + .HasColumnType("int"); + + b.Property("Temporary") + .HasColumnType("bit"); + + b.Property("Type") + .HasColumnType("tinyint"); + + b.Property("ValidatorId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ValidatorProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("WorkGroupId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("AggregationForId"); + + b.HasIndex("CategoryId"); + + b.HasIndex("DocumentId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("ResponseSubscriptionId"); + + b.HasIndex("RetryPolicyId"); + + b.HasIndex("WorkGroupId"); + + b.ToTable("Subscriptions", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Code") + .HasColumnType("nvarchar(450)"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique() + .HasFilter("[Code] IS NOT NULL"); + + b.ToTable("SubscriptionCategory"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionTrail", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("Code") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("StateAfter") + .HasColumnType("nvarchar(max)"); + + b.Property("StateBefore") + .HasColumnType("nvarchar(max)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("CreatedOn"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("SubscriptionTrail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.WorkGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BusMessageName") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("Options") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("CorrelationId") + .HasColumnType("nvarchar(max)"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("InputContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("InputHash") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("InputName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("InputSize") + .HasColumnType("int"); + + b.Property("MapperId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("MapperProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("References") + .HasMaxLength(1024) + .HasColumnType("nvarchar(1024)"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("int"); + + b.Property("RetryFor") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("StartedOn") + .HasColumnType("datetime2"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.HasIndex("InputHash"); + + b.HasIndex("RetryFor"); + + b.HasIndex("StartedOn"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("Xchanges", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AggregatedOn") + .HasColumnType("datetime2"); + + b.Property("AggregationXchangeId") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("AggregationXchangeId"); + + b.ToTable("XchangeAggregations", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("DeliveredOn") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("DeliveredOn"); + + b.ToTable("XchangeDeliveries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeNotification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Exception") + .HasColumnType("nvarchar(max)"); + + b.Property("FinishedOn") + .HasColumnType("datetime2"); + + b.Property("NotifierId") + .HasColumnType("int"); + + b.Property("NotifierName") + .HasColumnType("nvarchar(max)"); + + b.Property("Success") + .HasColumnType("bit"); + + b.Property("XchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.ToTable("XchangeNotifications", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("Hits") + .HasMaxLength(2000) + .IsUnicode(false) + .HasColumnType("varchar(2000)"); + + b.Property("Properties") + .HasColumnType("nvarchar(max)"); + + b.Property("PropertiesRaw") + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("PropertiesRaw"); + + b.ToTable("XchangePromotedProperties", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("Exception") + .HasColumnType("nvarchar(max)"); + + b.Property("FinishedOn") + .HasColumnType("datetime2"); + + b.Property("OutputBad") + .HasColumnType("bit"); + + b.Property("OutputContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("OutputHash") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("OutputName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("OutputSize") + .HasColumnType("int"); + + b.Property("ResponseBad") + .HasColumnType("bit"); + + b.Property("ResponseContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ResponseHash") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("ResponseName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("ResponseSize") + .HasColumnType("int"); + + b.Property("ResponseXchangeId") + .HasColumnType("nvarchar(max)"); + + b.Property("Success") + .HasColumnType("bit"); + + b.HasKey("Id"); + + b.ToTable("XchangeResults", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.RunFlagUpdater+RunningResult", b => + { + b.Property("IsRunning") + .HasColumnType("bit"); + + b.ToTable((string)null); + + b.ToView(null, (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.JobExecution", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Context") + .HasColumnType("nvarchar(max)") + .HasColumnName("context"); + + b.Property("DurationMs") + .HasColumnType("bigint") + .HasColumnName("duration_ms"); + + b.Property("EndTimeUtc") + .HasColumnType("datetime2") + .HasColumnName("end_time_utc"); + + b.Property("Error") + .HasColumnType("nvarchar(max)") + .HasColumnName("error"); + + b.Property("FireInstanceId") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("fire_instance_id"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_name"); + + b.Property("JobTypeName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_type_name"); + + b.Property("Node") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("node"); + + b.Property("StartTimeUtc") + .HasColumnType("datetime2") + .HasColumnName("start_time_utc"); + + b.Property("Success") + .HasColumnType("bit") + .HasColumnName("success"); + + b.HasKey("Id"); + + b.HasIndex("FireInstanceId") + .IsUnique() + .HasDatabaseName("idx_je_fire_instance_id"); + + b.HasIndex("StartTimeUtc") + .HasDatabaseName("idx_je_start_time"); + + b.HasIndex("Success") + .HasDatabaseName("idx_je_success"); + + b.HasIndex("JobGroup", "JobName", "StartTimeUtc") + .HasDatabaseName("idx_je_group_name_start"); + + b.ToTable("job_executions", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("BlobData") + .HasColumnType("varbinary(max)") + .HasColumnName("blob_data"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_blob_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCalendar", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("CalendarName") + .HasColumnType("nvarchar(450)") + .HasColumnName("calendar_name"); + + b.Property("Calendar") + .IsRequired() + .HasColumnType("varbinary(max)") + .HasColumnName("calendar"); + + b.HasKey("SchedulerName", "CalendarName"); + + b.ToTable("QRTZ_calendars", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("CronExpression") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("cron_expression"); + + b.Property("TimeZoneId") + .HasColumnType("nvarchar(450)") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_cron_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzFiredTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("EntryId") + .HasColumnType("nvarchar(450)") + .HasColumnName("entry_id"); + + b.Property("FiredTime") + .HasColumnType("bigint") + .HasColumnName("fired_time"); + + b.Property("InstanceName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("instance_name"); + + b.Property("IsNonConcurrent") + .HasColumnType("bit") + .HasColumnName("is_nonconcurrent"); + + b.Property("JobGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("job_group"); + + b.Property("JobName") + .HasColumnType("nvarchar(450)") + .HasColumnName("job_name"); + + b.Property("Priority") + .HasColumnType("int") + .HasColumnName("priority"); + + b.Property("RequestsRecovery") + .HasColumnType("bit") + .HasColumnName("requests_recovery"); + + b.Property("ScheduledTime") + .HasColumnType("bigint") + .HasColumnName("sched_time"); + + b.Property("State") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("state"); + + b.Property("TriggerGroup") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("TriggerName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.HasKey("SchedulerName", "EntryId"); + + b.HasIndex("InstanceName") + .HasDatabaseName("idx_QRTZ_ft_trig_inst_name"); + + b.HasIndex("JobGroup") + .HasDatabaseName("idx_QRTZ_ft_job_group"); + + b.HasIndex("JobName") + .HasDatabaseName("idx_QRTZ_ft_job_name"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_QRTZ_ft_job_req_recovery"); + + b.HasIndex("TriggerGroup") + .HasDatabaseName("idx_QRTZ_ft_trig_group"); + + b.HasIndex("TriggerName") + .HasDatabaseName("idx_QRTZ_ft_trig_name"); + + b.HasIndex("SchedulerName", "TriggerName", "TriggerGroup") + .HasDatabaseName("idx_QRTZ_ft_trig_nm_gp"); + + b.ToTable("QRTZ_fired_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("JobName") + .HasColumnType("nvarchar(450)") + .HasColumnName("job_name"); + + b.Property("JobGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("job_group"); + + b.Property("Description") + .HasColumnType("nvarchar(450)") + .HasColumnName("description"); + + b.Property("IsDurable") + .HasColumnType("bit") + .HasColumnName("is_durable"); + + b.Property("IsNonConcurrent") + .HasColumnType("bit") + .HasColumnName("is_nonconcurrent"); + + b.Property("IsUpdateData") + .HasColumnType("bit") + .HasColumnName("is_update_data"); + + b.Property("JobClassName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_class_name"); + + b.Property("JobData") + .HasColumnType("varbinary(max)") + .HasColumnName("job_data"); + + b.Property("RequestsRecovery") + .HasColumnType("bit") + .HasColumnName("requests_recovery"); + + b.HasKey("SchedulerName", "JobName", "JobGroup"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_j_req_recovery"); + + b.ToTable("QRTZ_job_details", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzLock", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("LockName") + .HasColumnType("nvarchar(450)") + .HasColumnName("lock_name"); + + b.HasKey("SchedulerName", "LockName"); + + b.ToTable("QRTZ_locks", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzPausedTriggerGroup", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.HasKey("SchedulerName", "TriggerGroup"); + + b.ToTable("QRTZ_paused_trigger_grps", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSchedulerState", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("InstanceName") + .HasColumnType("nvarchar(450)") + .HasColumnName("instance_name"); + + b.Property("CheckInInterval") + .HasColumnType("bigint") + .HasColumnName("checkin_interval"); + + b.Property("LastCheckInTime") + .HasColumnType("bigint") + .HasColumnName("last_checkin_time"); + + b.HasKey("SchedulerName", "InstanceName"); + + b.ToTable("QRTZ_scheduler_state", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("BooleanProperty1") + .HasColumnType("bit") + .HasColumnName("bool_prop_1"); + + b.Property("BooleanProperty2") + .HasColumnType("bit") + .HasColumnName("bool_prop_2"); + + b.Property("DecimalProperty1") + .HasColumnType("numeric") + .HasColumnName("dec_prop_1"); + + b.Property("DecimalProperty2") + .HasColumnType("numeric") + .HasColumnName("dec_prop_2"); + + b.Property("IntegerProperty1") + .HasColumnType("int") + .HasColumnName("int_prop_1"); + + b.Property("IntegerProperty2") + .HasColumnType("int") + .HasColumnName("int_prop_2"); + + b.Property("LongProperty1") + .HasColumnType("bigint") + .HasColumnName("long_prop_1"); + + b.Property("LongProperty2") + .HasColumnType("bigint") + .HasColumnName("long_prop_2"); + + b.Property("StringProperty1") + .HasColumnType("nvarchar(450)") + .HasColumnName("str_prop_1"); + + b.Property("StringProperty2") + .HasColumnType("nvarchar(450)") + .HasColumnName("str_prop_2"); + + b.Property("StringProperty3") + .HasColumnType("nvarchar(450)") + .HasColumnName("str_prop_3"); + + b.Property("TimeZoneId") + .HasColumnType("nvarchar(450)") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_simprop_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("RepeatCount") + .HasColumnType("bigint") + .HasColumnName("repeat_count"); + + b.Property("RepeatInterval") + .HasColumnType("bigint") + .HasColumnName("repeat_interval"); + + b.Property("TimesTriggered") + .HasColumnType("bigint") + .HasColumnName("times_triggered"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_simple_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("CalendarName") + .HasColumnType("nvarchar(450)") + .HasColumnName("calendar_name"); + + b.Property("Description") + .HasColumnType("nvarchar(450)") + .HasColumnName("description"); + + b.Property("EndTime") + .HasColumnType("bigint") + .HasColumnName("end_time"); + + b.Property("JobData") + .HasColumnType("varbinary(max)") + .HasColumnName("job_data"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_name"); + + b.Property("MisfireInstruction") + .HasColumnType("int") + .HasColumnName("misfire_instr"); + + b.Property("NextFireTime") + .HasColumnType("bigint") + .HasColumnName("next_fire_time"); + + b.Property("PreviousFireTime") + .HasColumnType("bigint") + .HasColumnName("prev_fire_time"); + + b.Property("Priority") + .HasColumnType("int") + .HasColumnName("priority"); + + b.Property("StartTime") + .HasColumnType("bigint") + .HasColumnName("start_time"); + + b.Property("TriggerState") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_state"); + + b.Property("TriggerType") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_type"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.HasIndex("NextFireTime") + .HasDatabaseName("idx_t_next_fire_time"); + + b.HasIndex("TriggerState") + .HasDatabaseName("idx_t_state"); + + b.HasIndex("NextFireTime", "TriggerState") + .HasDatabaseName("idx_t_nft_st"); + + b.HasIndex("SchedulerName", "JobName", "JobGroup"); + + b.ToTable("QRTZ_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DocumentTrail", b => + { + b.HasOne("SW.Bitween.Domain.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Document"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.ApiGateway", "ApiGateway") + .WithMany("Partners") + .HasForeignKey("ApiGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("ApiGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.BusGateway", "BusGateway") + .WithMany("Routes") + .HasForeignKey("BusGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("BusGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.OwnsMany("SW.Bitween.Domain.ApiCredential", "ApiCredentials", b1 => + { + b1.Property("PartnerId") + .HasColumnType("int"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b1.Property("Id")); + + b1.Property("Key") + .IsRequired() + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b1.Property("Name") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b1.HasKey("PartnerId", "Id"); + + b1.HasIndex("Key") + .IsUnique(); + + b1.ToTable("PartnerApiCredentials", (string)null); + + b1.WithOwner() + .HasForeignKey("PartnerId"); + + b1.HasData( + new + { + PartnerId = 1, + Id = 1, + Key = "7facc758283844b49cc4ffd26a75b1de", + Name = "default" + }); + }); + + b.Navigation("ApiCredentials"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.HasOne("SW.Bitween.Domain.Subscription", null) + .WithMany() + .HasForeignKey("AggregationForId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("FK_Subscriptions_AggFor"); + + b.HasOne("SW.Bitween.Domain.SubscriptionCategory", "Category") + .WithMany() + .HasForeignKey("CategoryId"); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", null) + .WithMany("Subscriptions") + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Subscription", null) + .WithMany() + .HasForeignKey("ResponseSubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("FK_Subscriptions_RespSub"); + + b.HasOne("SW.Bitween.Domain.RetryPolicy", "RetryPolicy") + .WithMany() + .HasForeignKey("RetryPolicyId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("SW.Bitween.Domain.WorkGroup", "WorkGroup") + .WithMany() + .HasForeignKey("WorkGroupId"); + + b.OwnsMany("SW.Bitween.Domain.Schedule", "Schedules", b1 => + { + b1.Property("SubscriptionId") + .HasColumnType("int"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b1.Property("Id")); + + b1.Property("Backwards") + .HasColumnType("bit"); + + b1.Property("On") + .HasColumnType("bigint"); + + b1.Property("Recurrence") + .HasColumnType("tinyint"); + + b1.HasKey("SubscriptionId", "Id"); + + b1.ToTable("SubscriptionSchedules", (string)null); + + b1.WithOwner() + .HasForeignKey("SubscriptionId"); + }); + + b.Navigation("Category"); + + b.Navigation("RetryPolicy"); + + b.Navigation("Schedules"); + + b.Navigation("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionTrail", b => + { + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeAggregation", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeDelivery", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangePromotedProperties", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeResult", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("BlobTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("CronTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimplePropertyTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimpleTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzJobDetail", "JobDetail") + .WithMany("Triggers") + .HasForeignKey("SchedulerName", "JobName", "JobGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("JobDetail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Navigation("Partners"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Navigation("Routes"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Navigation("Subscriptions"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Navigation("Triggers"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Navigation("BlobTriggers"); + + b.Navigation("CronTriggers"); + + b.Navigation("SimplePropertyTriggers"); + + b.Navigation("SimpleTriggers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/SW.Bitween.MsSql/Migrations/20260811081235_SharedRetryGroupTotals.cs b/SW.Bitween.MsSql/Migrations/20260811081235_SharedRetryGroupTotals.cs new file mode 100644 index 00000000..f52cc72f --- /dev/null +++ b/SW.Bitween.MsSql/Migrations/20260811081235_SharedRetryGroupTotals.cs @@ -0,0 +1,56 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace SW.Bitween.MsSql.Migrations +{ + /// + public partial class SharedRetryGroupTotals : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "GroupAttemptCounts", + table: "Xchanges"); + + migrationBuilder.DropColumn( + name: "GroupAttemptCounts", + table: "DelayedRetries"); + + migrationBuilder.CreateTable( + name: "RetryGroupUsages", + columns: table => new + { + SubscriptionId = table.Column(type: "int", nullable: false), + GroupId = table.Column(type: "uniqueidentifier", nullable: false), + AttemptsUsed = table.Column(type: "int", nullable: false), + LastAttemptOn = table.Column(type: "datetime2", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_RetryGroupUsages", x => new { x.SubscriptionId, x.GroupId }); + }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "RetryGroupUsages"); + + migrationBuilder.AddColumn( + name: "GroupAttemptCounts", + table: "Xchanges", + type: "nvarchar(max)", + nullable: true); + + migrationBuilder.AddColumn( + name: "GroupAttemptCounts", + table: "DelayedRetries", + type: "nvarchar(max)", + nullable: true); + } + } +} diff --git a/SW.Bitween.MsSql/Migrations/20260811092327_RetryBlockedReason.Designer.cs b/SW.Bitween.MsSql/Migrations/20260811092327_RetryBlockedReason.Designer.cs new file mode 100644 index 00000000..8608dffa --- /dev/null +++ b/SW.Bitween.MsSql/Migrations/20260811092327_RetryBlockedReason.Designer.cs @@ -0,0 +1,1906 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using SW.Bitween.MsSql; + +#nullable disable + +namespace SW.Bitween.MsSql.Migrations +{ + [DbContext(typeof(BitweenDbContext))] + [Migration("20260811092327_RetryBlockedReason")] + partial class RetryBlockedReason + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.23") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Deleted") + .HasColumnType("bit"); + + b.Property("Disabled") + .HasColumnType("bit"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Email") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("EmailProvider") + .HasColumnType("tinyint"); + + b.Property("LoginMethods") + .HasColumnType("tinyint"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Password") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("Phone") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("varchar(20)"); + + b.Property("Role") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique() + .HasFilter("[Email] IS NOT NULL"); + + b.ToTable("Accounts", (string)null); + + b.HasData( + new + { + Id = 9999, + CreatedOn = new DateTime(2021, 12, 31, 22, 0, 0, 0, DateTimeKind.Utc), + Deleted = false, + Disabled = false, + DisplayName = "Admin", + Email = "admin@Bitween.systems", + EmailProvider = (byte)0, + LoginMethods = (byte)2, + Password = "$SWHASH$V1$10000$VQCi48eitH4Ml5juvBMOFZrMdQwBbhuIQVXe6RR7qJdDF2bJ", + Role = 0 + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AccountId") + .HasColumnType("int"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("LoginMethod") + .HasColumnType("tinyint"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.ToTable("RefreshTokens", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DelayedRetry", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("On") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("On"); + + b.ToTable("DelayedRetries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Document", b => + { + b.Property("Id") + .HasColumnType("int"); + + b.Property("BusEnabled") + .HasColumnType("bit"); + + b.Property("BusMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("DisregardsUnfilteredMessages") + .HasColumnType("bit"); + + b.Property("DocumentFormat") + .HasColumnType("int"); + + b.Property("DuplicateInterval") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("PromotedProperties") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("BusMessageTypeName") + .IsUnique() + .HasFilter("[BusMessageTypeName] IS NOT NULL"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Documents", (string)null); + + b.HasData( + new + { + Id = 10001, + BusEnabled = false, + DocumentFormat = 0, + DuplicateInterval = 0, + Name = "Aggregation Document", + PromotedProperties = "{}" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DocumentTrail", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("Code") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("StateAfter") + .HasColumnType("nvarchar(max)"); + + b.Property("StateBefore") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("CreatedOn"); + + b.HasIndex("DocumentId"); + + b.ToTable("DocumentTrail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("UrlName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("UrlName") + .IsUnique(); + + b.ToTable("ApiGateways", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.Property("ApiGatewayId") + .HasColumnType("int"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.HasKey("ApiGatewayId", "PartnerId", "SubscriptionId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("ApiGatewayPartners", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.ToTable("BusGateways", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BusGatewayId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("MatchExpression") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("BusGatewayId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("BusGatewayRoutes", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.GlobalAdapterValuesSet", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("Values") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("GlobalAdapterValuesSets", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Notifier", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("Inactive") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("RunOnBadResult") + .HasColumnType("bit"); + + b.Property("RunOnFailedResult") + .HasColumnType("bit"); + + b.Property("RunOnSubscriptions") + .HasColumnType("nvarchar(max)"); + + b.Property("RunOnSuccessfulResult") + .HasColumnType("bit"); + + b.HasKey("Id"); + + b.ToTable("Notifiers", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OnHoldXchange", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BadData") + .HasColumnType("bit"); + + b.Property("Data") + .HasColumnType("nvarchar(max)"); + + b.Property("FileName") + .HasColumnType("nvarchar(max)"); + + b.Property("References") + .HasMaxLength(1024) + .HasColumnType("nvarchar(1024)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("OnHoldXchanges", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AdapterProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.ToTable("Partners", (string)null); + + b.HasData( + new + { + Id = 1, + Name = "SYSTEM" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryGroupUsage", b => + { + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("GroupId") + .HasColumnType("uniqueidentifier"); + + b.Property("AttemptsUsed") + .HasColumnType("int"); + + b.Property("LastAttemptOn") + .HasColumnType("datetime2"); + + b.HasKey("SubscriptionId", "GroupId"); + + b.ToTable("RetryGroupUsages", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryPolicy", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Groups") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.ToTable("RetryPolicies", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AggregateOn") + .HasColumnType("datetime2"); + + b.Property("AggregationForId") + .HasColumnType("int"); + + b.Property("AggregationTarget") + .HasColumnType("tinyint"); + + b.Property("CategoryId") + .HasColumnType("int"); + + b.Property("ConsecutiveFailures") + .HasColumnType("int"); + + b.Property("CustomRetryPolicy") + .HasColumnType("nvarchar(max)"); + + b.Property("DocumentFilter") + .HasColumnType("nvarchar(max)"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("Inactive") + .HasColumnType("bit"); + + b.Property("IsRunning") + .HasColumnType("bit"); + + b.Property("LastException") + .HasColumnType("nvarchar(max)"); + + b.Property("MapperId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("MapperProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("MatchExpression") + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("PausedOn") + .HasColumnType("datetime2"); + + b.Property("ReceiveOn") + .HasColumnType("datetime2"); + + b.Property("ReceiverId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ReceiverProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("int"); + + b.Property("RetryPolicyId") + .HasColumnType("int"); + + b.Property("Temporary") + .HasColumnType("bit"); + + b.Property("Type") + .HasColumnType("tinyint"); + + b.Property("ValidatorId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ValidatorProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("WorkGroupId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("AggregationForId"); + + b.HasIndex("CategoryId"); + + b.HasIndex("DocumentId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("ResponseSubscriptionId"); + + b.HasIndex("RetryPolicyId"); + + b.HasIndex("WorkGroupId"); + + b.ToTable("Subscriptions", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Code") + .HasColumnType("nvarchar(450)"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique() + .HasFilter("[Code] IS NOT NULL"); + + b.ToTable("SubscriptionCategory"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionTrail", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("Code") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("StateAfter") + .HasColumnType("nvarchar(max)"); + + b.Property("StateBefore") + .HasColumnType("nvarchar(max)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("CreatedOn"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("SubscriptionTrail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.WorkGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BusMessageName") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("Options") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("CorrelationId") + .HasColumnType("nvarchar(max)"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("InputContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("InputHash") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("InputName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("InputSize") + .HasColumnType("int"); + + b.Property("MapperId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("MapperProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("References") + .HasMaxLength(1024) + .HasColumnType("nvarchar(1024)"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("int"); + + b.Property("RetryFor") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("StartedOn") + .HasColumnType("datetime2"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.HasIndex("InputHash"); + + b.HasIndex("RetryFor"); + + b.HasIndex("StartedOn"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("Xchanges", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AggregatedOn") + .HasColumnType("datetime2"); + + b.Property("AggregationXchangeId") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("AggregationXchangeId"); + + b.ToTable("XchangeAggregations", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("DeliveredOn") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("DeliveredOn"); + + b.ToTable("XchangeDeliveries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeNotification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Exception") + .HasColumnType("nvarchar(max)"); + + b.Property("FinishedOn") + .HasColumnType("datetime2"); + + b.Property("NotifierId") + .HasColumnType("int"); + + b.Property("NotifierName") + .HasColumnType("nvarchar(max)"); + + b.Property("Success") + .HasColumnType("bit"); + + b.Property("XchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.ToTable("XchangeNotifications", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("Hits") + .HasMaxLength(2000) + .IsUnicode(false) + .HasColumnType("varchar(2000)"); + + b.Property("Properties") + .HasColumnType("nvarchar(max)"); + + b.Property("PropertiesRaw") + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("PropertiesRaw"); + + b.ToTable("XchangePromotedProperties", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("Exception") + .HasColumnType("nvarchar(max)"); + + b.Property("FinishedOn") + .HasColumnType("datetime2"); + + b.Property("OutputBad") + .HasColumnType("bit"); + + b.Property("OutputContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("OutputHash") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("OutputName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("OutputSize") + .HasColumnType("int"); + + b.Property("ResponseBad") + .HasColumnType("bit"); + + b.Property("ResponseContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ResponseHash") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("ResponseName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("ResponseSize") + .HasColumnType("int"); + + b.Property("ResponseXchangeId") + .HasColumnType("nvarchar(max)"); + + b.Property("RetryBlockedReason") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("Success") + .HasColumnType("bit"); + + b.HasKey("Id"); + + b.ToTable("XchangeResults", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.RunFlagUpdater+RunningResult", b => + { + b.Property("IsRunning") + .HasColumnType("bit"); + + b.ToTable((string)null); + + b.ToView(null, (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.JobExecution", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Context") + .HasColumnType("nvarchar(max)") + .HasColumnName("context"); + + b.Property("DurationMs") + .HasColumnType("bigint") + .HasColumnName("duration_ms"); + + b.Property("EndTimeUtc") + .HasColumnType("datetime2") + .HasColumnName("end_time_utc"); + + b.Property("Error") + .HasColumnType("nvarchar(max)") + .HasColumnName("error"); + + b.Property("FireInstanceId") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("fire_instance_id"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_name"); + + b.Property("JobTypeName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_type_name"); + + b.Property("Node") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("node"); + + b.Property("StartTimeUtc") + .HasColumnType("datetime2") + .HasColumnName("start_time_utc"); + + b.Property("Success") + .HasColumnType("bit") + .HasColumnName("success"); + + b.HasKey("Id"); + + b.HasIndex("FireInstanceId") + .IsUnique() + .HasDatabaseName("idx_je_fire_instance_id"); + + b.HasIndex("StartTimeUtc") + .HasDatabaseName("idx_je_start_time"); + + b.HasIndex("Success") + .HasDatabaseName("idx_je_success"); + + b.HasIndex("JobGroup", "JobName", "StartTimeUtc") + .HasDatabaseName("idx_je_group_name_start"); + + b.ToTable("job_executions", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("BlobData") + .HasColumnType("varbinary(max)") + .HasColumnName("blob_data"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_blob_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCalendar", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("CalendarName") + .HasColumnType("nvarchar(450)") + .HasColumnName("calendar_name"); + + b.Property("Calendar") + .IsRequired() + .HasColumnType("varbinary(max)") + .HasColumnName("calendar"); + + b.HasKey("SchedulerName", "CalendarName"); + + b.ToTable("QRTZ_calendars", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("CronExpression") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("cron_expression"); + + b.Property("TimeZoneId") + .HasColumnType("nvarchar(450)") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_cron_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzFiredTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("EntryId") + .HasColumnType("nvarchar(450)") + .HasColumnName("entry_id"); + + b.Property("FiredTime") + .HasColumnType("bigint") + .HasColumnName("fired_time"); + + b.Property("InstanceName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("instance_name"); + + b.Property("IsNonConcurrent") + .HasColumnType("bit") + .HasColumnName("is_nonconcurrent"); + + b.Property("JobGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("job_group"); + + b.Property("JobName") + .HasColumnType("nvarchar(450)") + .HasColumnName("job_name"); + + b.Property("Priority") + .HasColumnType("int") + .HasColumnName("priority"); + + b.Property("RequestsRecovery") + .HasColumnType("bit") + .HasColumnName("requests_recovery"); + + b.Property("ScheduledTime") + .HasColumnType("bigint") + .HasColumnName("sched_time"); + + b.Property("State") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("state"); + + b.Property("TriggerGroup") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("TriggerName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.HasKey("SchedulerName", "EntryId"); + + b.HasIndex("InstanceName") + .HasDatabaseName("idx_QRTZ_ft_trig_inst_name"); + + b.HasIndex("JobGroup") + .HasDatabaseName("idx_QRTZ_ft_job_group"); + + b.HasIndex("JobName") + .HasDatabaseName("idx_QRTZ_ft_job_name"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_QRTZ_ft_job_req_recovery"); + + b.HasIndex("TriggerGroup") + .HasDatabaseName("idx_QRTZ_ft_trig_group"); + + b.HasIndex("TriggerName") + .HasDatabaseName("idx_QRTZ_ft_trig_name"); + + b.HasIndex("SchedulerName", "TriggerName", "TriggerGroup") + .HasDatabaseName("idx_QRTZ_ft_trig_nm_gp"); + + b.ToTable("QRTZ_fired_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("JobName") + .HasColumnType("nvarchar(450)") + .HasColumnName("job_name"); + + b.Property("JobGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("job_group"); + + b.Property("Description") + .HasColumnType("nvarchar(450)") + .HasColumnName("description"); + + b.Property("IsDurable") + .HasColumnType("bit") + .HasColumnName("is_durable"); + + b.Property("IsNonConcurrent") + .HasColumnType("bit") + .HasColumnName("is_nonconcurrent"); + + b.Property("IsUpdateData") + .HasColumnType("bit") + .HasColumnName("is_update_data"); + + b.Property("JobClassName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_class_name"); + + b.Property("JobData") + .HasColumnType("varbinary(max)") + .HasColumnName("job_data"); + + b.Property("RequestsRecovery") + .HasColumnType("bit") + .HasColumnName("requests_recovery"); + + b.HasKey("SchedulerName", "JobName", "JobGroup"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_j_req_recovery"); + + b.ToTable("QRTZ_job_details", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzLock", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("LockName") + .HasColumnType("nvarchar(450)") + .HasColumnName("lock_name"); + + b.HasKey("SchedulerName", "LockName"); + + b.ToTable("QRTZ_locks", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzPausedTriggerGroup", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.HasKey("SchedulerName", "TriggerGroup"); + + b.ToTable("QRTZ_paused_trigger_grps", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSchedulerState", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("InstanceName") + .HasColumnType("nvarchar(450)") + .HasColumnName("instance_name"); + + b.Property("CheckInInterval") + .HasColumnType("bigint") + .HasColumnName("checkin_interval"); + + b.Property("LastCheckInTime") + .HasColumnType("bigint") + .HasColumnName("last_checkin_time"); + + b.HasKey("SchedulerName", "InstanceName"); + + b.ToTable("QRTZ_scheduler_state", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("BooleanProperty1") + .HasColumnType("bit") + .HasColumnName("bool_prop_1"); + + b.Property("BooleanProperty2") + .HasColumnType("bit") + .HasColumnName("bool_prop_2"); + + b.Property("DecimalProperty1") + .HasColumnType("numeric") + .HasColumnName("dec_prop_1"); + + b.Property("DecimalProperty2") + .HasColumnType("numeric") + .HasColumnName("dec_prop_2"); + + b.Property("IntegerProperty1") + .HasColumnType("int") + .HasColumnName("int_prop_1"); + + b.Property("IntegerProperty2") + .HasColumnType("int") + .HasColumnName("int_prop_2"); + + b.Property("LongProperty1") + .HasColumnType("bigint") + .HasColumnName("long_prop_1"); + + b.Property("LongProperty2") + .HasColumnType("bigint") + .HasColumnName("long_prop_2"); + + b.Property("StringProperty1") + .HasColumnType("nvarchar(450)") + .HasColumnName("str_prop_1"); + + b.Property("StringProperty2") + .HasColumnType("nvarchar(450)") + .HasColumnName("str_prop_2"); + + b.Property("StringProperty3") + .HasColumnType("nvarchar(450)") + .HasColumnName("str_prop_3"); + + b.Property("TimeZoneId") + .HasColumnType("nvarchar(450)") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_simprop_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("RepeatCount") + .HasColumnType("bigint") + .HasColumnName("repeat_count"); + + b.Property("RepeatInterval") + .HasColumnType("bigint") + .HasColumnName("repeat_interval"); + + b.Property("TimesTriggered") + .HasColumnType("bigint") + .HasColumnName("times_triggered"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_simple_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("CalendarName") + .HasColumnType("nvarchar(450)") + .HasColumnName("calendar_name"); + + b.Property("Description") + .HasColumnType("nvarchar(450)") + .HasColumnName("description"); + + b.Property("EndTime") + .HasColumnType("bigint") + .HasColumnName("end_time"); + + b.Property("JobData") + .HasColumnType("varbinary(max)") + .HasColumnName("job_data"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_name"); + + b.Property("MisfireInstruction") + .HasColumnType("int") + .HasColumnName("misfire_instr"); + + b.Property("NextFireTime") + .HasColumnType("bigint") + .HasColumnName("next_fire_time"); + + b.Property("PreviousFireTime") + .HasColumnType("bigint") + .HasColumnName("prev_fire_time"); + + b.Property("Priority") + .HasColumnType("int") + .HasColumnName("priority"); + + b.Property("StartTime") + .HasColumnType("bigint") + .HasColumnName("start_time"); + + b.Property("TriggerState") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_state"); + + b.Property("TriggerType") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_type"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.HasIndex("NextFireTime") + .HasDatabaseName("idx_t_next_fire_time"); + + b.HasIndex("TriggerState") + .HasDatabaseName("idx_t_state"); + + b.HasIndex("NextFireTime", "TriggerState") + .HasDatabaseName("idx_t_nft_st"); + + b.HasIndex("SchedulerName", "JobName", "JobGroup"); + + b.ToTable("QRTZ_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DocumentTrail", b => + { + b.HasOne("SW.Bitween.Domain.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Document"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.ApiGateway", "ApiGateway") + .WithMany("Partners") + .HasForeignKey("ApiGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("ApiGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.BusGateway", "BusGateway") + .WithMany("Routes") + .HasForeignKey("BusGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("BusGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.OwnsMany("SW.Bitween.Domain.ApiCredential", "ApiCredentials", b1 => + { + b1.Property("PartnerId") + .HasColumnType("int"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b1.Property("Id")); + + b1.Property("Key") + .IsRequired() + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b1.Property("Name") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b1.HasKey("PartnerId", "Id"); + + b1.HasIndex("Key") + .IsUnique(); + + b1.ToTable("PartnerApiCredentials", (string)null); + + b1.WithOwner() + .HasForeignKey("PartnerId"); + + b1.HasData( + new + { + PartnerId = 1, + Id = 1, + Key = "7facc758283844b49cc4ffd26a75b1de", + Name = "default" + }); + }); + + b.Navigation("ApiCredentials"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.HasOne("SW.Bitween.Domain.Subscription", null) + .WithMany() + .HasForeignKey("AggregationForId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("FK_Subscriptions_AggFor"); + + b.HasOne("SW.Bitween.Domain.SubscriptionCategory", "Category") + .WithMany() + .HasForeignKey("CategoryId"); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", null) + .WithMany("Subscriptions") + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Subscription", null) + .WithMany() + .HasForeignKey("ResponseSubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("FK_Subscriptions_RespSub"); + + b.HasOne("SW.Bitween.Domain.RetryPolicy", "RetryPolicy") + .WithMany() + .HasForeignKey("RetryPolicyId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("SW.Bitween.Domain.WorkGroup", "WorkGroup") + .WithMany() + .HasForeignKey("WorkGroupId"); + + b.OwnsMany("SW.Bitween.Domain.Schedule", "Schedules", b1 => + { + b1.Property("SubscriptionId") + .HasColumnType("int"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b1.Property("Id")); + + b1.Property("Backwards") + .HasColumnType("bit"); + + b1.Property("On") + .HasColumnType("bigint"); + + b1.Property("Recurrence") + .HasColumnType("tinyint"); + + b1.HasKey("SubscriptionId", "Id"); + + b1.ToTable("SubscriptionSchedules", (string)null); + + b1.WithOwner() + .HasForeignKey("SubscriptionId"); + }); + + b.Navigation("Category"); + + b.Navigation("RetryPolicy"); + + b.Navigation("Schedules"); + + b.Navigation("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionTrail", b => + { + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeAggregation", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeDelivery", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangePromotedProperties", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeResult", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("BlobTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("CronTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimplePropertyTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimpleTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzJobDetail", "JobDetail") + .WithMany("Triggers") + .HasForeignKey("SchedulerName", "JobName", "JobGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("JobDetail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Navigation("Partners"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Navigation("Routes"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Navigation("Subscriptions"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Navigation("Triggers"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Navigation("BlobTriggers"); + + b.Navigation("CronTriggers"); + + b.Navigation("SimplePropertyTriggers"); + + b.Navigation("SimpleTriggers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/SW.Bitween.MsSql/Migrations/20260811092327_RetryBlockedReason.cs b/SW.Bitween.MsSql/Migrations/20260811092327_RetryBlockedReason.cs new file mode 100644 index 00000000..4dc41bbf --- /dev/null +++ b/SW.Bitween.MsSql/Migrations/20260811092327_RetryBlockedReason.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace SW.Bitween.MsSql.Migrations +{ + /// + public partial class RetryBlockedReason : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "RetryBlockedReason", + table: "XchangeResults", + type: "nvarchar(500)", + maxLength: 500, + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "RetryBlockedReason", + table: "XchangeResults"); + } + } +} diff --git a/SW.Bitween.MsSql/Migrations/20260812092708_AddAccountLockout.Designer.cs b/SW.Bitween.MsSql/Migrations/20260812092708_AddAccountLockout.Designer.cs new file mode 100644 index 00000000..a61e46f5 --- /dev/null +++ b/SW.Bitween.MsSql/Migrations/20260812092708_AddAccountLockout.Designer.cs @@ -0,0 +1,1896 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using SW.Bitween.MsSql; + +#nullable disable + +namespace SW.Bitween.MsSql.Migrations +{ + [DbContext(typeof(BitweenDbContext))] + [Migration("20260812092708_AddAccountLockout")] + partial class AddAccountLockout + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.23") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Deleted") + .HasColumnType("bit"); + + b.Property("Disabled") + .HasColumnType("bit"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Email") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("EmailProvider") + .HasColumnType("tinyint"); + + b.Property("FailedLoginCount") + .HasColumnType("int"); + + b.Property("LockoutEnd") + .HasColumnType("datetime2"); + + b.Property("LoginMethods") + .HasColumnType("tinyint"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Password") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("Phone") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("varchar(20)"); + + b.Property("Role") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique() + .HasFilter("[Email] IS NOT NULL"); + + b.ToTable("Accounts", (string)null); + + b.HasData( + new + { + Id = 9999, + CreatedOn = new DateTime(2021, 12, 31, 22, 0, 0, 0, DateTimeKind.Utc), + Deleted = false, + Disabled = false, + DisplayName = "Admin", + Email = "admin@Bitween.systems", + EmailProvider = (byte)0, + FailedLoginCount = 0, + LoginMethods = (byte)2, + Password = "$SWHASH$V1$10000$VQCi48eitH4Ml5juvBMOFZrMdQwBbhuIQVXe6RR7qJdDF2bJ", + Role = 0 + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AccountId") + .HasColumnType("int"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("LoginMethod") + .HasColumnType("tinyint"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.ToTable("RefreshTokens", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DelayedRetry", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("GroupAttemptCounts") + .HasColumnType("nvarchar(max)"); + + b.Property("On") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("On"); + + b.ToTable("DelayedRetries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Document", b => + { + b.Property("Id") + .HasColumnType("int"); + + b.Property("BusEnabled") + .HasColumnType("bit"); + + b.Property("BusMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("DisregardsUnfilteredMessages") + .HasColumnType("bit"); + + b.Property("DocumentFormat") + .HasColumnType("int"); + + b.Property("DuplicateInterval") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("PromotedProperties") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("BusMessageTypeName") + .IsUnique() + .HasFilter("[BusMessageTypeName] IS NOT NULL"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Documents", (string)null); + + b.HasData( + new + { + Id = 10001, + BusEnabled = false, + DocumentFormat = 0, + DuplicateInterval = 0, + Name = "Aggregation Document", + PromotedProperties = "{}" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DocumentTrail", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("Code") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("StateAfter") + .HasColumnType("nvarchar(max)"); + + b.Property("StateBefore") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("CreatedOn"); + + b.HasIndex("DocumentId"); + + b.ToTable("DocumentTrail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("UrlName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("UrlName") + .IsUnique(); + + b.ToTable("ApiGateways", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.Property("ApiGatewayId") + .HasColumnType("int"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.HasKey("ApiGatewayId", "PartnerId", "SubscriptionId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("ApiGatewayPartners", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.ToTable("BusGateways", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BusGatewayId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("MatchExpression") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("BusGatewayId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("BusGatewayRoutes", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.GlobalAdapterValuesSet", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("Values") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("GlobalAdapterValuesSets", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Notifier", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("Inactive") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("RunOnBadResult") + .HasColumnType("bit"); + + b.Property("RunOnFailedResult") + .HasColumnType("bit"); + + b.Property("RunOnSubscriptions") + .HasColumnType("nvarchar(max)"); + + b.Property("RunOnSuccessfulResult") + .HasColumnType("bit"); + + b.HasKey("Id"); + + b.ToTable("Notifiers", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OnHoldXchange", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BadData") + .HasColumnType("bit"); + + b.Property("Data") + .HasColumnType("nvarchar(max)"); + + b.Property("FileName") + .HasColumnType("nvarchar(max)"); + + b.Property("References") + .HasMaxLength(1024) + .HasColumnType("nvarchar(1024)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("OnHoldXchanges", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AdapterProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.ToTable("Partners", (string)null); + + b.HasData( + new + { + Id = 1, + Name = "SYSTEM" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryPolicy", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Groups") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.ToTable("RetryPolicies", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AggregateOn") + .HasColumnType("datetime2"); + + b.Property("AggregationForId") + .HasColumnType("int"); + + b.Property("AggregationTarget") + .HasColumnType("tinyint"); + + b.Property("CategoryId") + .HasColumnType("int"); + + b.Property("ConsecutiveFailures") + .HasColumnType("int"); + + b.Property("CustomRetryPolicy") + .HasColumnType("nvarchar(max)"); + + b.Property("DocumentFilter") + .HasColumnType("nvarchar(max)"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("Inactive") + .HasColumnType("bit"); + + b.Property("IsRunning") + .HasColumnType("bit"); + + b.Property("LastException") + .HasColumnType("nvarchar(max)"); + + b.Property("MapperId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("MapperProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("MatchExpression") + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("PausedOn") + .HasColumnType("datetime2"); + + b.Property("ReceiveOn") + .HasColumnType("datetime2"); + + b.Property("ReceiverId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ReceiverProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("int"); + + b.Property("RetryPolicyId") + .HasColumnType("int"); + + b.Property("Temporary") + .HasColumnType("bit"); + + b.Property("Type") + .HasColumnType("tinyint"); + + b.Property("ValidatorId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ValidatorProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("WorkGroupId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("AggregationForId"); + + b.HasIndex("CategoryId"); + + b.HasIndex("DocumentId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("ResponseSubscriptionId"); + + b.HasIndex("RetryPolicyId"); + + b.HasIndex("WorkGroupId"); + + b.ToTable("Subscriptions", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Code") + .HasColumnType("nvarchar(450)"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique() + .HasFilter("[Code] IS NOT NULL"); + + b.ToTable("SubscriptionCategory"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionTrail", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("Code") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("StateAfter") + .HasColumnType("nvarchar(max)"); + + b.Property("StateBefore") + .HasColumnType("nvarchar(max)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("CreatedOn"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("SubscriptionTrail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.WorkGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BusMessageName") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("Options") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("CorrelationId") + .HasColumnType("nvarchar(max)"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("GroupAttemptCounts") + .HasColumnType("nvarchar(max)"); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("InputContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("InputHash") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("InputName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("InputSize") + .HasColumnType("int"); + + b.Property("MapperId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("MapperProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("References") + .HasMaxLength(1024) + .HasColumnType("nvarchar(1024)"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("int"); + + b.Property("RetryFor") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("StartedOn") + .HasColumnType("datetime2"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.HasIndex("InputHash"); + + b.HasIndex("RetryFor"); + + b.HasIndex("StartedOn"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("Xchanges", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AggregatedOn") + .HasColumnType("datetime2"); + + b.Property("AggregationXchangeId") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("AggregationXchangeId"); + + b.ToTable("XchangeAggregations", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("DeliveredOn") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("DeliveredOn"); + + b.ToTable("XchangeDeliveries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeNotification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Exception") + .HasColumnType("nvarchar(max)"); + + b.Property("FinishedOn") + .HasColumnType("datetime2"); + + b.Property("NotifierId") + .HasColumnType("int"); + + b.Property("NotifierName") + .HasColumnType("nvarchar(max)"); + + b.Property("Success") + .HasColumnType("bit"); + + b.Property("XchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.ToTable("XchangeNotifications", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("Hits") + .HasMaxLength(2000) + .IsUnicode(false) + .HasColumnType("varchar(2000)"); + + b.Property("Properties") + .HasColumnType("nvarchar(max)"); + + b.Property("PropertiesRaw") + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("PropertiesRaw"); + + b.ToTable("XchangePromotedProperties", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("Exception") + .HasColumnType("nvarchar(max)"); + + b.Property("FinishedOn") + .HasColumnType("datetime2"); + + b.Property("OutputBad") + .HasColumnType("bit"); + + b.Property("OutputContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("OutputHash") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("OutputName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("OutputSize") + .HasColumnType("int"); + + b.Property("ResponseBad") + .HasColumnType("bit"); + + b.Property("ResponseContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ResponseHash") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("ResponseName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("ResponseSize") + .HasColumnType("int"); + + b.Property("ResponseXchangeId") + .HasColumnType("nvarchar(max)"); + + b.Property("Success") + .HasColumnType("bit"); + + b.HasKey("Id"); + + b.ToTable("XchangeResults", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.RunFlagUpdater+RunningResult", b => + { + b.Property("IsRunning") + .HasColumnType("bit"); + + b.ToTable((string)null); + + b.ToView(null, (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.JobExecution", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Context") + .HasColumnType("nvarchar(max)") + .HasColumnName("context"); + + b.Property("DurationMs") + .HasColumnType("bigint") + .HasColumnName("duration_ms"); + + b.Property("EndTimeUtc") + .HasColumnType("datetime2") + .HasColumnName("end_time_utc"); + + b.Property("Error") + .HasColumnType("nvarchar(max)") + .HasColumnName("error"); + + b.Property("FireInstanceId") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("fire_instance_id"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_name"); + + b.Property("JobTypeName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_type_name"); + + b.Property("Node") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("node"); + + b.Property("StartTimeUtc") + .HasColumnType("datetime2") + .HasColumnName("start_time_utc"); + + b.Property("Success") + .HasColumnType("bit") + .HasColumnName("success"); + + b.HasKey("Id"); + + b.HasIndex("FireInstanceId") + .IsUnique() + .HasDatabaseName("idx_je_fire_instance_id"); + + b.HasIndex("StartTimeUtc") + .HasDatabaseName("idx_je_start_time"); + + b.HasIndex("Success") + .HasDatabaseName("idx_je_success"); + + b.HasIndex("JobGroup", "JobName", "StartTimeUtc") + .HasDatabaseName("idx_je_group_name_start"); + + b.ToTable("job_executions", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("BlobData") + .HasColumnType("varbinary(max)") + .HasColumnName("blob_data"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_blob_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCalendar", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("CalendarName") + .HasColumnType("nvarchar(450)") + .HasColumnName("calendar_name"); + + b.Property("Calendar") + .IsRequired() + .HasColumnType("varbinary(max)") + .HasColumnName("calendar"); + + b.HasKey("SchedulerName", "CalendarName"); + + b.ToTable("QRTZ_calendars", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("CronExpression") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("cron_expression"); + + b.Property("TimeZoneId") + .HasColumnType("nvarchar(450)") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_cron_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzFiredTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("EntryId") + .HasColumnType("nvarchar(450)") + .HasColumnName("entry_id"); + + b.Property("FiredTime") + .HasColumnType("bigint") + .HasColumnName("fired_time"); + + b.Property("InstanceName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("instance_name"); + + b.Property("IsNonConcurrent") + .HasColumnType("bit") + .HasColumnName("is_nonconcurrent"); + + b.Property("JobGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("job_group"); + + b.Property("JobName") + .HasColumnType("nvarchar(450)") + .HasColumnName("job_name"); + + b.Property("Priority") + .HasColumnType("int") + .HasColumnName("priority"); + + b.Property("RequestsRecovery") + .HasColumnType("bit") + .HasColumnName("requests_recovery"); + + b.Property("ScheduledTime") + .HasColumnType("bigint") + .HasColumnName("sched_time"); + + b.Property("State") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("state"); + + b.Property("TriggerGroup") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("TriggerName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.HasKey("SchedulerName", "EntryId"); + + b.HasIndex("InstanceName") + .HasDatabaseName("idx_QRTZ_ft_trig_inst_name"); + + b.HasIndex("JobGroup") + .HasDatabaseName("idx_QRTZ_ft_job_group"); + + b.HasIndex("JobName") + .HasDatabaseName("idx_QRTZ_ft_job_name"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_QRTZ_ft_job_req_recovery"); + + b.HasIndex("TriggerGroup") + .HasDatabaseName("idx_QRTZ_ft_trig_group"); + + b.HasIndex("TriggerName") + .HasDatabaseName("idx_QRTZ_ft_trig_name"); + + b.HasIndex("SchedulerName", "TriggerName", "TriggerGroup") + .HasDatabaseName("idx_QRTZ_ft_trig_nm_gp"); + + b.ToTable("QRTZ_fired_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("JobName") + .HasColumnType("nvarchar(450)") + .HasColumnName("job_name"); + + b.Property("JobGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("job_group"); + + b.Property("Description") + .HasColumnType("nvarchar(450)") + .HasColumnName("description"); + + b.Property("IsDurable") + .HasColumnType("bit") + .HasColumnName("is_durable"); + + b.Property("IsNonConcurrent") + .HasColumnType("bit") + .HasColumnName("is_nonconcurrent"); + + b.Property("IsUpdateData") + .HasColumnType("bit") + .HasColumnName("is_update_data"); + + b.Property("JobClassName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_class_name"); + + b.Property("JobData") + .HasColumnType("varbinary(max)") + .HasColumnName("job_data"); + + b.Property("RequestsRecovery") + .HasColumnType("bit") + .HasColumnName("requests_recovery"); + + b.HasKey("SchedulerName", "JobName", "JobGroup"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_j_req_recovery"); + + b.ToTable("QRTZ_job_details", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzLock", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("LockName") + .HasColumnType("nvarchar(450)") + .HasColumnName("lock_name"); + + b.HasKey("SchedulerName", "LockName"); + + b.ToTable("QRTZ_locks", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzPausedTriggerGroup", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.HasKey("SchedulerName", "TriggerGroup"); + + b.ToTable("QRTZ_paused_trigger_grps", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSchedulerState", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("InstanceName") + .HasColumnType("nvarchar(450)") + .HasColumnName("instance_name"); + + b.Property("CheckInInterval") + .HasColumnType("bigint") + .HasColumnName("checkin_interval"); + + b.Property("LastCheckInTime") + .HasColumnType("bigint") + .HasColumnName("last_checkin_time"); + + b.HasKey("SchedulerName", "InstanceName"); + + b.ToTable("QRTZ_scheduler_state", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("BooleanProperty1") + .HasColumnType("bit") + .HasColumnName("bool_prop_1"); + + b.Property("BooleanProperty2") + .HasColumnType("bit") + .HasColumnName("bool_prop_2"); + + b.Property("DecimalProperty1") + .HasColumnType("numeric") + .HasColumnName("dec_prop_1"); + + b.Property("DecimalProperty2") + .HasColumnType("numeric") + .HasColumnName("dec_prop_2"); + + b.Property("IntegerProperty1") + .HasColumnType("int") + .HasColumnName("int_prop_1"); + + b.Property("IntegerProperty2") + .HasColumnType("int") + .HasColumnName("int_prop_2"); + + b.Property("LongProperty1") + .HasColumnType("bigint") + .HasColumnName("long_prop_1"); + + b.Property("LongProperty2") + .HasColumnType("bigint") + .HasColumnName("long_prop_2"); + + b.Property("StringProperty1") + .HasColumnType("nvarchar(450)") + .HasColumnName("str_prop_1"); + + b.Property("StringProperty2") + .HasColumnType("nvarchar(450)") + .HasColumnName("str_prop_2"); + + b.Property("StringProperty3") + .HasColumnType("nvarchar(450)") + .HasColumnName("str_prop_3"); + + b.Property("TimeZoneId") + .HasColumnType("nvarchar(450)") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_simprop_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("RepeatCount") + .HasColumnType("bigint") + .HasColumnName("repeat_count"); + + b.Property("RepeatInterval") + .HasColumnType("bigint") + .HasColumnName("repeat_interval"); + + b.Property("TimesTriggered") + .HasColumnType("bigint") + .HasColumnName("times_triggered"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_simple_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("CalendarName") + .HasColumnType("nvarchar(450)") + .HasColumnName("calendar_name"); + + b.Property("Description") + .HasColumnType("nvarchar(450)") + .HasColumnName("description"); + + b.Property("EndTime") + .HasColumnType("bigint") + .HasColumnName("end_time"); + + b.Property("JobData") + .HasColumnType("varbinary(max)") + .HasColumnName("job_data"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_name"); + + b.Property("MisfireInstruction") + .HasColumnType("int") + .HasColumnName("misfire_instr"); + + b.Property("NextFireTime") + .HasColumnType("bigint") + .HasColumnName("next_fire_time"); + + b.Property("PreviousFireTime") + .HasColumnType("bigint") + .HasColumnName("prev_fire_time"); + + b.Property("Priority") + .HasColumnType("int") + .HasColumnName("priority"); + + b.Property("StartTime") + .HasColumnType("bigint") + .HasColumnName("start_time"); + + b.Property("TriggerState") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_state"); + + b.Property("TriggerType") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_type"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.HasIndex("NextFireTime") + .HasDatabaseName("idx_t_next_fire_time"); + + b.HasIndex("TriggerState") + .HasDatabaseName("idx_t_state"); + + b.HasIndex("NextFireTime", "TriggerState") + .HasDatabaseName("idx_t_nft_st"); + + b.HasIndex("SchedulerName", "JobName", "JobGroup"); + + b.ToTable("QRTZ_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DocumentTrail", b => + { + b.HasOne("SW.Bitween.Domain.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Document"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.ApiGateway", "ApiGateway") + .WithMany("Partners") + .HasForeignKey("ApiGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("ApiGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.BusGateway", "BusGateway") + .WithMany("Routes") + .HasForeignKey("BusGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("BusGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.OwnsMany("SW.Bitween.Domain.ApiCredential", "ApiCredentials", b1 => + { + b1.Property("PartnerId") + .HasColumnType("int"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b1.Property("Id")); + + b1.Property("Key") + .IsRequired() + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b1.Property("Name") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b1.HasKey("PartnerId", "Id"); + + b1.HasIndex("Key") + .IsUnique(); + + b1.ToTable("PartnerApiCredentials", (string)null); + + b1.WithOwner() + .HasForeignKey("PartnerId"); + + b1.HasData( + new + { + PartnerId = 1, + Id = 1, + Key = "7facc758283844b49cc4ffd26a75b1de", + Name = "default" + }); + }); + + b.Navigation("ApiCredentials"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.HasOne("SW.Bitween.Domain.Subscription", null) + .WithMany() + .HasForeignKey("AggregationForId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("FK_Subscriptions_AggFor"); + + b.HasOne("SW.Bitween.Domain.SubscriptionCategory", "Category") + .WithMany() + .HasForeignKey("CategoryId"); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", null) + .WithMany("Subscriptions") + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Subscription", null) + .WithMany() + .HasForeignKey("ResponseSubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("FK_Subscriptions_RespSub"); + + b.HasOne("SW.Bitween.Domain.RetryPolicy", "RetryPolicy") + .WithMany() + .HasForeignKey("RetryPolicyId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("SW.Bitween.Domain.WorkGroup", "WorkGroup") + .WithMany() + .HasForeignKey("WorkGroupId"); + + b.OwnsMany("SW.Bitween.Domain.Schedule", "Schedules", b1 => + { + b1.Property("SubscriptionId") + .HasColumnType("int"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b1.Property("Id")); + + b1.Property("Backwards") + .HasColumnType("bit"); + + b1.Property("On") + .HasColumnType("bigint"); + + b1.Property("Recurrence") + .HasColumnType("tinyint"); + + b1.HasKey("SubscriptionId", "Id"); + + b1.ToTable("SubscriptionSchedules", (string)null); + + b1.WithOwner() + .HasForeignKey("SubscriptionId"); + }); + + b.Navigation("Category"); + + b.Navigation("RetryPolicy"); + + b.Navigation("Schedules"); + + b.Navigation("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionTrail", b => + { + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeAggregation", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeDelivery", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangePromotedProperties", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeResult", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("BlobTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("CronTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimplePropertyTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimpleTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzJobDetail", "JobDetail") + .WithMany("Triggers") + .HasForeignKey("SchedulerName", "JobName", "JobGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("JobDetail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Navigation("Partners"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Navigation("Routes"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Navigation("Subscriptions"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Navigation("Triggers"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Navigation("BlobTriggers"); + + b.Navigation("CronTriggers"); + + b.Navigation("SimplePropertyTriggers"); + + b.Navigation("SimpleTriggers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/SW.Bitween.MsSql/Migrations/20260812092708_AddAccountLockout.cs b/SW.Bitween.MsSql/Migrations/20260812092708_AddAccountLockout.cs new file mode 100644 index 00000000..662b1a62 --- /dev/null +++ b/SW.Bitween.MsSql/Migrations/20260812092708_AddAccountLockout.cs @@ -0,0 +1,47 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace SW.Bitween.MsSql.Migrations +{ + /// + public partial class AddAccountLockout : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "FailedLoginCount", + table: "Accounts", + type: "int", + nullable: false, + defaultValue: 0); + + migrationBuilder.AddColumn( + name: "LockoutEnd", + table: "Accounts", + type: "datetime2", + nullable: true); + + migrationBuilder.UpdateData( + table: "Accounts", + keyColumn: "Id", + keyValue: 9999, + columns: new[] { "FailedLoginCount", "LockoutEnd" }, + values: new object[] { 0, null }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "FailedLoginCount", + table: "Accounts"); + + migrationBuilder.DropColumn( + name: "LockoutEnd", + table: "Accounts"); + } + } +} diff --git a/SW.Bitween.MsSql/Migrations/20260817103506_RetryBudgetAlerts.Designer.cs b/SW.Bitween.MsSql/Migrations/20260817103506_RetryBudgetAlerts.Designer.cs new file mode 100644 index 00000000..a504f677 --- /dev/null +++ b/SW.Bitween.MsSql/Migrations/20260817103506_RetryBudgetAlerts.Designer.cs @@ -0,0 +1,1956 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using SW.Bitween.MsSql; + +#nullable disable + +namespace SW.Bitween.MsSql.Migrations +{ + [DbContext(typeof(BitweenDbContext))] + [Migration("20260817103506_RetryBudgetAlerts")] + partial class RetryBudgetAlerts + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.23") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Deleted") + .HasColumnType("bit"); + + b.Property("Disabled") + .HasColumnType("bit"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Email") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("EmailProvider") + .HasColumnType("tinyint"); + + b.Property("FailedLoginCount") + .HasColumnType("int"); + + b.Property("LockoutEnd") + .HasColumnType("datetime2"); + + b.Property("LoginMethods") + .HasColumnType("tinyint"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Password") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("Phone") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("varchar(20)"); + + b.Property("Role") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique() + .HasFilter("[Email] IS NOT NULL"); + + b.ToTable("Accounts", (string)null); + + b.HasData( + new + { + Id = 9999, + CreatedOn = new DateTime(2021, 12, 31, 22, 0, 0, 0, DateTimeKind.Utc), + Deleted = false, + Disabled = false, + DisplayName = "Admin", + Email = "admin@Bitween.systems", + EmailProvider = (byte)0, + FailedLoginCount = 0, + LoginMethods = (byte)2, + Password = "$SWHASH$V1$10000$VQCi48eitH4Ml5juvBMOFZrMdQwBbhuIQVXe6RR7qJdDF2bJ", + Role = 0 + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AccountId") + .HasColumnType("int"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("LoginMethod") + .HasColumnType("tinyint"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.ToTable("RefreshTokens", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DelayedRetry", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("On") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("On"); + + b.ToTable("DelayedRetries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Document", b => + { + b.Property("Id") + .HasColumnType("int"); + + b.Property("BusEnabled") + .HasColumnType("bit"); + + b.Property("BusMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("DisregardsUnfilteredMessages") + .HasColumnType("bit"); + + b.Property("DocumentFormat") + .HasColumnType("int"); + + b.Property("DuplicateInterval") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("PromotedProperties") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("BusMessageTypeName") + .IsUnique() + .HasFilter("[BusMessageTypeName] IS NOT NULL"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Documents", (string)null); + + b.HasData( + new + { + Id = 10001, + BusEnabled = false, + DocumentFormat = 0, + DuplicateInterval = 0, + Name = "Aggregation Document", + PromotedProperties = "{}" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DocumentTrail", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("Code") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("StateAfter") + .HasColumnType("nvarchar(max)"); + + b.Property("StateBefore") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("CreatedOn"); + + b.HasIndex("DocumentId"); + + b.ToTable("DocumentTrail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("UrlName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("UrlName") + .IsUnique(); + + b.ToTable("ApiGateways", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.Property("ApiGatewayId") + .HasColumnType("int"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.HasKey("ApiGatewayId", "PartnerId", "SubscriptionId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("ApiGatewayPartners", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.ToTable("BusGateways", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BusGatewayId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("MatchExpression") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("BusGatewayId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("BusGatewayRoutes", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.GlobalAdapterValuesSet", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("Values") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("GlobalAdapterValuesSets", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Notifier", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("Inactive") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("RunOnBadResult") + .HasColumnType("bit"); + + b.Property("RunOnFailedResult") + .HasColumnType("bit"); + + b.Property("RunOnSubscriptions") + .HasColumnType("nvarchar(max)"); + + b.Property("RunOnSuccessfulResult") + .HasColumnType("bit"); + + b.HasKey("Id"); + + b.ToTable("Notifiers", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OnHoldXchange", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BadData") + .HasColumnType("bit"); + + b.Property("Data") + .HasColumnType("nvarchar(max)"); + + b.Property("FileName") + .HasColumnType("nvarchar(max)"); + + b.Property("References") + .HasMaxLength(1024) + .HasColumnType("nvarchar(1024)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("OnHoldXchanges", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AdapterProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.ToTable("Partners", (string)null); + + b.HasData( + new + { + Id = 1, + Name = "SYSTEM" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryAlertOverride", b => + { + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("GroupId") + .HasColumnType("uniqueidentifier"); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("AlertHandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("AlertMode") + .HasColumnType("tinyint"); + + b.HasKey("SubscriptionId", "GroupId"); + + b.ToTable("RetryAlertOverrides", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryGroupUsage", b => + { + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("GroupId") + .HasColumnType("uniqueidentifier"); + + b.Property("AttemptsUsed") + .HasColumnType("int"); + + b.Property("ExhaustedNotifiedOn") + .HasColumnType("datetime2"); + + b.Property("LastAttemptOn") + .HasColumnType("datetime2"); + + b.HasKey("SubscriptionId", "GroupId"); + + b.ToTable("RetryGroupUsages", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryPolicy", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("AlertHandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Groups") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.ToTable("RetryPolicies", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AggregateOn") + .HasColumnType("datetime2"); + + b.Property("AggregationForId") + .HasColumnType("int"); + + b.Property("AggregationTarget") + .HasColumnType("tinyint"); + + b.Property("CategoryId") + .HasColumnType("int"); + + b.Property("ConsecutiveFailures") + .HasColumnType("int"); + + b.Property("CustomRetryPolicy") + .HasColumnType("nvarchar(max)"); + + b.Property("DocumentFilter") + .HasColumnType("nvarchar(max)"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("Inactive") + .HasColumnType("bit"); + + b.Property("IsRunning") + .HasColumnType("bit"); + + b.Property("LastException") + .HasColumnType("nvarchar(max)"); + + b.Property("MapperId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("MapperProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("MatchExpression") + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("PausedOn") + .HasColumnType("datetime2"); + + b.Property("ReceiveOn") + .HasColumnType("datetime2"); + + b.Property("ReceiverId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ReceiverProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("int"); + + b.Property("RetryPolicyId") + .HasColumnType("int"); + + b.Property("Temporary") + .HasColumnType("bit"); + + b.Property("Type") + .HasColumnType("tinyint"); + + b.Property("ValidatorId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ValidatorProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("WorkGroupId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("AggregationForId"); + + b.HasIndex("CategoryId"); + + b.HasIndex("DocumentId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("ResponseSubscriptionId"); + + b.HasIndex("RetryPolicyId"); + + b.HasIndex("WorkGroupId"); + + b.ToTable("Subscriptions", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Code") + .HasColumnType("nvarchar(450)"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique() + .HasFilter("[Code] IS NOT NULL"); + + b.ToTable("SubscriptionCategory"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionTrail", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("Code") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("StateAfter") + .HasColumnType("nvarchar(max)"); + + b.Property("StateBefore") + .HasColumnType("nvarchar(max)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("CreatedOn"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("SubscriptionTrail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.WorkGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BusMessageName") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("Options") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("CorrelationId") + .HasColumnType("nvarchar(max)"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("InputContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("InputHash") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("InputName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("InputSize") + .HasColumnType("int"); + + b.Property("MapperId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("MapperProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("References") + .HasMaxLength(1024) + .HasColumnType("nvarchar(1024)"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("int"); + + b.Property("RetryFor") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("StartedOn") + .HasColumnType("datetime2"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.HasIndex("InputHash"); + + b.HasIndex("RetryFor"); + + b.HasIndex("StartedOn"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("Xchanges", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AggregatedOn") + .HasColumnType("datetime2"); + + b.Property("AggregationXchangeId") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("AggregationXchangeId"); + + b.ToTable("XchangeAggregations", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("DeliveredOn") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("DeliveredOn"); + + b.ToTable("XchangeDeliveries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeNotification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Exception") + .HasColumnType("nvarchar(max)"); + + b.Property("FinishedOn") + .HasColumnType("datetime2"); + + b.Property("NotifierId") + .HasColumnType("int"); + + b.Property("NotifierName") + .HasColumnType("nvarchar(max)"); + + b.Property("Success") + .HasColumnType("bit"); + + b.Property("XchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.ToTable("XchangeNotifications", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("Hits") + .HasMaxLength(2000) + .IsUnicode(false) + .HasColumnType("varchar(2000)"); + + b.Property("Properties") + .HasColumnType("nvarchar(max)"); + + b.Property("PropertiesRaw") + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("PropertiesRaw"); + + b.ToTable("XchangePromotedProperties", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AttemptNumber") + .HasColumnType("int"); + + b.Property("Exception") + .HasColumnType("nvarchar(max)"); + + b.Property("FinishedOn") + .HasColumnType("datetime2"); + + b.Property("OutputBad") + .HasColumnType("bit"); + + b.Property("OutputContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("OutputHash") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("OutputName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("OutputSize") + .HasColumnType("int"); + + b.Property("ResponseBad") + .HasColumnType("bit"); + + b.Property("ResponseContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ResponseHash") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("ResponseName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("ResponseSize") + .HasColumnType("int"); + + b.Property("ResponseXchangeId") + .HasColumnType("nvarchar(max)"); + + b.Property("RetryBlockedReason") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("RetryGroupId") + .HasColumnType("uniqueidentifier"); + + b.Property("Success") + .HasColumnType("bit"); + + b.HasKey("Id"); + + b.HasIndex("RetryGroupId"); + + b.ToTable("XchangeResults", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.RunFlagUpdater+RunningResult", b => + { + b.Property("IsRunning") + .HasColumnType("bit"); + + b.ToTable((string)null); + + b.ToView(null, (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.JobExecution", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Context") + .HasColumnType("nvarchar(max)") + .HasColumnName("context"); + + b.Property("DurationMs") + .HasColumnType("bigint") + .HasColumnName("duration_ms"); + + b.Property("EndTimeUtc") + .HasColumnType("datetime2") + .HasColumnName("end_time_utc"); + + b.Property("Error") + .HasColumnType("nvarchar(max)") + .HasColumnName("error"); + + b.Property("FireInstanceId") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("fire_instance_id"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_name"); + + b.Property("JobTypeName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_type_name"); + + b.Property("Node") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("node"); + + b.Property("StartTimeUtc") + .HasColumnType("datetime2") + .HasColumnName("start_time_utc"); + + b.Property("Success") + .HasColumnType("bit") + .HasColumnName("success"); + + b.HasKey("Id"); + + b.HasIndex("FireInstanceId") + .IsUnique() + .HasDatabaseName("idx_je_fire_instance_id"); + + b.HasIndex("StartTimeUtc") + .HasDatabaseName("idx_je_start_time"); + + b.HasIndex("Success") + .HasDatabaseName("idx_je_success"); + + b.HasIndex("JobGroup", "JobName", "StartTimeUtc") + .HasDatabaseName("idx_je_group_name_start"); + + b.ToTable("job_executions", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("BlobData") + .HasColumnType("varbinary(max)") + .HasColumnName("blob_data"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_blob_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCalendar", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("CalendarName") + .HasColumnType("nvarchar(450)") + .HasColumnName("calendar_name"); + + b.Property("Calendar") + .IsRequired() + .HasColumnType("varbinary(max)") + .HasColumnName("calendar"); + + b.HasKey("SchedulerName", "CalendarName"); + + b.ToTable("QRTZ_calendars", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("CronExpression") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("cron_expression"); + + b.Property("TimeZoneId") + .HasColumnType("nvarchar(450)") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_cron_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzFiredTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("EntryId") + .HasColumnType("nvarchar(450)") + .HasColumnName("entry_id"); + + b.Property("FiredTime") + .HasColumnType("bigint") + .HasColumnName("fired_time"); + + b.Property("InstanceName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("instance_name"); + + b.Property("IsNonConcurrent") + .HasColumnType("bit") + .HasColumnName("is_nonconcurrent"); + + b.Property("JobGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("job_group"); + + b.Property("JobName") + .HasColumnType("nvarchar(450)") + .HasColumnName("job_name"); + + b.Property("Priority") + .HasColumnType("int") + .HasColumnName("priority"); + + b.Property("RequestsRecovery") + .HasColumnType("bit") + .HasColumnName("requests_recovery"); + + b.Property("ScheduledTime") + .HasColumnType("bigint") + .HasColumnName("sched_time"); + + b.Property("State") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("state"); + + b.Property("TriggerGroup") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("TriggerName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.HasKey("SchedulerName", "EntryId"); + + b.HasIndex("InstanceName") + .HasDatabaseName("idx_QRTZ_ft_trig_inst_name"); + + b.HasIndex("JobGroup") + .HasDatabaseName("idx_QRTZ_ft_job_group"); + + b.HasIndex("JobName") + .HasDatabaseName("idx_QRTZ_ft_job_name"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_QRTZ_ft_job_req_recovery"); + + b.HasIndex("TriggerGroup") + .HasDatabaseName("idx_QRTZ_ft_trig_group"); + + b.HasIndex("TriggerName") + .HasDatabaseName("idx_QRTZ_ft_trig_name"); + + b.HasIndex("SchedulerName", "TriggerName", "TriggerGroup") + .HasDatabaseName("idx_QRTZ_ft_trig_nm_gp"); + + b.ToTable("QRTZ_fired_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("JobName") + .HasColumnType("nvarchar(450)") + .HasColumnName("job_name"); + + b.Property("JobGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("job_group"); + + b.Property("Description") + .HasColumnType("nvarchar(450)") + .HasColumnName("description"); + + b.Property("IsDurable") + .HasColumnType("bit") + .HasColumnName("is_durable"); + + b.Property("IsNonConcurrent") + .HasColumnType("bit") + .HasColumnName("is_nonconcurrent"); + + b.Property("IsUpdateData") + .HasColumnType("bit") + .HasColumnName("is_update_data"); + + b.Property("JobClassName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_class_name"); + + b.Property("JobData") + .HasColumnType("varbinary(max)") + .HasColumnName("job_data"); + + b.Property("RequestsRecovery") + .HasColumnType("bit") + .HasColumnName("requests_recovery"); + + b.HasKey("SchedulerName", "JobName", "JobGroup"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_j_req_recovery"); + + b.ToTable("QRTZ_job_details", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzLock", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("LockName") + .HasColumnType("nvarchar(450)") + .HasColumnName("lock_name"); + + b.HasKey("SchedulerName", "LockName"); + + b.ToTable("QRTZ_locks", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzPausedTriggerGroup", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.HasKey("SchedulerName", "TriggerGroup"); + + b.ToTable("QRTZ_paused_trigger_grps", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSchedulerState", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("InstanceName") + .HasColumnType("nvarchar(450)") + .HasColumnName("instance_name"); + + b.Property("CheckInInterval") + .HasColumnType("bigint") + .HasColumnName("checkin_interval"); + + b.Property("LastCheckInTime") + .HasColumnType("bigint") + .HasColumnName("last_checkin_time"); + + b.HasKey("SchedulerName", "InstanceName"); + + b.ToTable("QRTZ_scheduler_state", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("BooleanProperty1") + .HasColumnType("bit") + .HasColumnName("bool_prop_1"); + + b.Property("BooleanProperty2") + .HasColumnType("bit") + .HasColumnName("bool_prop_2"); + + b.Property("DecimalProperty1") + .HasColumnType("numeric") + .HasColumnName("dec_prop_1"); + + b.Property("DecimalProperty2") + .HasColumnType("numeric") + .HasColumnName("dec_prop_2"); + + b.Property("IntegerProperty1") + .HasColumnType("int") + .HasColumnName("int_prop_1"); + + b.Property("IntegerProperty2") + .HasColumnType("int") + .HasColumnName("int_prop_2"); + + b.Property("LongProperty1") + .HasColumnType("bigint") + .HasColumnName("long_prop_1"); + + b.Property("LongProperty2") + .HasColumnType("bigint") + .HasColumnName("long_prop_2"); + + b.Property("StringProperty1") + .HasColumnType("nvarchar(450)") + .HasColumnName("str_prop_1"); + + b.Property("StringProperty2") + .HasColumnType("nvarchar(450)") + .HasColumnName("str_prop_2"); + + b.Property("StringProperty3") + .HasColumnType("nvarchar(450)") + .HasColumnName("str_prop_3"); + + b.Property("TimeZoneId") + .HasColumnType("nvarchar(450)") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_simprop_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("RepeatCount") + .HasColumnType("bigint") + .HasColumnName("repeat_count"); + + b.Property("RepeatInterval") + .HasColumnType("bigint") + .HasColumnName("repeat_interval"); + + b.Property("TimesTriggered") + .HasColumnType("bigint") + .HasColumnName("times_triggered"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_simple_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("CalendarName") + .HasColumnType("nvarchar(450)") + .HasColumnName("calendar_name"); + + b.Property("Description") + .HasColumnType("nvarchar(450)") + .HasColumnName("description"); + + b.Property("EndTime") + .HasColumnType("bigint") + .HasColumnName("end_time"); + + b.Property("JobData") + .HasColumnType("varbinary(max)") + .HasColumnName("job_data"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_name"); + + b.Property("MisfireInstruction") + .HasColumnType("int") + .HasColumnName("misfire_instr"); + + b.Property("NextFireTime") + .HasColumnType("bigint") + .HasColumnName("next_fire_time"); + + b.Property("PreviousFireTime") + .HasColumnType("bigint") + .HasColumnName("prev_fire_time"); + + b.Property("Priority") + .HasColumnType("int") + .HasColumnName("priority"); + + b.Property("StartTime") + .HasColumnType("bigint") + .HasColumnName("start_time"); + + b.Property("TriggerState") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_state"); + + b.Property("TriggerType") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_type"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.HasIndex("NextFireTime") + .HasDatabaseName("idx_t_next_fire_time"); + + b.HasIndex("TriggerState") + .HasDatabaseName("idx_t_state"); + + b.HasIndex("NextFireTime", "TriggerState") + .HasDatabaseName("idx_t_nft_st"); + + b.HasIndex("SchedulerName", "JobName", "JobGroup"); + + b.ToTable("QRTZ_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DocumentTrail", b => + { + b.HasOne("SW.Bitween.Domain.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Document"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.ApiGateway", "ApiGateway") + .WithMany("Partners") + .HasForeignKey("ApiGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("ApiGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.BusGateway", "BusGateway") + .WithMany("Routes") + .HasForeignKey("BusGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("BusGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.OwnsMany("SW.Bitween.Domain.ApiCredential", "ApiCredentials", b1 => + { + b1.Property("PartnerId") + .HasColumnType("int"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b1.Property("Id")); + + b1.Property("Key") + .IsRequired() + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b1.Property("Name") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b1.HasKey("PartnerId", "Id"); + + b1.HasIndex("Key") + .IsUnique(); + + b1.ToTable("PartnerApiCredentials", (string)null); + + b1.WithOwner() + .HasForeignKey("PartnerId"); + + b1.HasData( + new + { + PartnerId = 1, + Id = 1, + Key = "7facc758283844b49cc4ffd26a75b1de", + Name = "default" + }); + }); + + b.Navigation("ApiCredentials"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.HasOne("SW.Bitween.Domain.Subscription", null) + .WithMany() + .HasForeignKey("AggregationForId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("FK_Subscriptions_AggFor"); + + b.HasOne("SW.Bitween.Domain.SubscriptionCategory", "Category") + .WithMany() + .HasForeignKey("CategoryId"); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", null) + .WithMany("Subscriptions") + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Subscription", null) + .WithMany() + .HasForeignKey("ResponseSubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("FK_Subscriptions_RespSub"); + + b.HasOne("SW.Bitween.Domain.RetryPolicy", "RetryPolicy") + .WithMany() + .HasForeignKey("RetryPolicyId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("SW.Bitween.Domain.WorkGroup", "WorkGroup") + .WithMany() + .HasForeignKey("WorkGroupId"); + + b.OwnsMany("SW.Bitween.Domain.Schedule", "Schedules", b1 => + { + b1.Property("SubscriptionId") + .HasColumnType("int"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b1.Property("Id")); + + b1.Property("Backwards") + .HasColumnType("bit"); + + b1.Property("On") + .HasColumnType("bigint"); + + b1.Property("Recurrence") + .HasColumnType("tinyint"); + + b1.HasKey("SubscriptionId", "Id"); + + b1.ToTable("SubscriptionSchedules", (string)null); + + b1.WithOwner() + .HasForeignKey("SubscriptionId"); + }); + + b.Navigation("Category"); + + b.Navigation("RetryPolicy"); + + b.Navigation("Schedules"); + + b.Navigation("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionTrail", b => + { + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeAggregation", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeDelivery", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangePromotedProperties", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeResult", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("BlobTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("CronTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimplePropertyTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimpleTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzJobDetail", "JobDetail") + .WithMany("Triggers") + .HasForeignKey("SchedulerName", "JobName", "JobGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("JobDetail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Navigation("Partners"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Navigation("Routes"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Navigation("Subscriptions"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Navigation("Triggers"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Navigation("BlobTriggers"); + + b.Navigation("CronTriggers"); + + b.Navigation("SimplePropertyTriggers"); + + b.Navigation("SimpleTriggers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/SW.Bitween.MsSql/Migrations/20260817103506_RetryBudgetAlerts.cs b/SW.Bitween.MsSql/Migrations/20260817103506_RetryBudgetAlerts.cs new file mode 100644 index 00000000..e6d3d633 --- /dev/null +++ b/SW.Bitween.MsSql/Migrations/20260817103506_RetryBudgetAlerts.cs @@ -0,0 +1,122 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace SW.Bitween.MsSql.Migrations +{ + /// + public partial class RetryBudgetAlerts : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "AttemptNumber", + table: "XchangeResults", + type: "int", + nullable: true); + + migrationBuilder.AddColumn( + name: "RetryGroupId", + table: "XchangeResults", + type: "uniqueidentifier", + nullable: true); + + migrationBuilder.AlterColumn( + name: "NotifierId", + table: "XchangeNotifications", + type: "int", + nullable: true, + oldClrType: typeof(int), + oldType: "int"); + + migrationBuilder.AddColumn( + name: "AlertHandlerId", + table: "RetryPolicies", + type: "varchar(200)", + unicode: false, + maxLength: 200, + nullable: true); + + migrationBuilder.AddColumn( + name: "AlertHandlerProperties", + table: "RetryPolicies", + type: "nvarchar(max)", + nullable: true); + + migrationBuilder.AddColumn( + name: "ExhaustedNotifiedOn", + table: "RetryGroupUsages", + type: "datetime2", + nullable: true); + + migrationBuilder.CreateTable( + name: "RetryAlertOverrides", + columns: table => new + { + SubscriptionId = table.Column(type: "int", nullable: false), + GroupId = table.Column(type: "uniqueidentifier", nullable: false), + AlertMode = table.Column(type: "tinyint", nullable: false), + AlertHandlerId = table.Column(type: "varchar(200)", unicode: false, maxLength: 200, nullable: true), + AlertHandlerProperties = table.Column(type: "nvarchar(max)", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_RetryAlertOverrides", x => new { x.SubscriptionId, x.GroupId }); + }); + + migrationBuilder.CreateIndex( + name: "IX_XchangeResults_RetryGroupId", + table: "XchangeResults", + column: "RetryGroupId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "RetryAlertOverrides"); + + migrationBuilder.DropIndex( + name: "IX_XchangeResults_RetryGroupId", + table: "XchangeResults"); + + migrationBuilder.DropColumn( + name: "AttemptNumber", + table: "XchangeResults"); + + migrationBuilder.DropColumn( + name: "RetryGroupId", + table: "XchangeResults"); + + migrationBuilder.DropColumn( + name: "AlertHandlerId", + table: "RetryPolicies"); + + migrationBuilder.DropColumn( + name: "AlertHandlerProperties", + table: "RetryPolicies"); + + migrationBuilder.DropColumn( + name: "ExhaustedNotifiedOn", + table: "RetryGroupUsages"); + + // These rows are the alert's own delivery log, and they are the reason the column was + // made nullable. Rolling the feature back leaves nowhere to put them, and the column + // cannot go back to NOT NULL while they are here, so they go with the feature. + migrationBuilder.Sql( + "DELETE FROM [XchangeNotifications] WHERE [NotifierId] IS NULL;"); + + migrationBuilder.AlterColumn( + name: "NotifierId", + table: "XchangeNotifications", + type: "int", + nullable: false, + defaultValue: 0, + oldClrType: typeof(int), + oldType: "int", + oldNullable: true); + } + } +} diff --git a/SW.Bitween.MsSql/Migrations/20260819100503_ManualRetryFlag.Designer.cs b/SW.Bitween.MsSql/Migrations/20260819100503_ManualRetryFlag.Designer.cs new file mode 100644 index 00000000..0db64634 --- /dev/null +++ b/SW.Bitween.MsSql/Migrations/20260819100503_ManualRetryFlag.Designer.cs @@ -0,0 +1,1959 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using SW.Bitween.MsSql; + +#nullable disable + +namespace SW.Bitween.MsSql.Migrations +{ + [DbContext(typeof(BitweenDbContext))] + [Migration("20260819100503_ManualRetryFlag")] + partial class ManualRetryFlag + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.23") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Deleted") + .HasColumnType("bit"); + + b.Property("Disabled") + .HasColumnType("bit"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Email") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("EmailProvider") + .HasColumnType("tinyint"); + + b.Property("FailedLoginCount") + .HasColumnType("int"); + + b.Property("LockoutEnd") + .HasColumnType("datetime2"); + + b.Property("LoginMethods") + .HasColumnType("tinyint"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Password") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("Phone") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("varchar(20)"); + + b.Property("Role") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique() + .HasFilter("[Email] IS NOT NULL"); + + b.ToTable("Accounts", (string)null); + + b.HasData( + new + { + Id = 9999, + CreatedOn = new DateTime(2021, 12, 31, 22, 0, 0, 0, DateTimeKind.Utc), + Deleted = false, + Disabled = false, + DisplayName = "Admin", + Email = "admin@Bitween.systems", + EmailProvider = (byte)0, + FailedLoginCount = 0, + LoginMethods = (byte)2, + Password = "$SWHASH$V1$10000$VQCi48eitH4Ml5juvBMOFZrMdQwBbhuIQVXe6RR7qJdDF2bJ", + Role = 0 + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AccountId") + .HasColumnType("int"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("LoginMethod") + .HasColumnType("tinyint"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.ToTable("RefreshTokens", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DelayedRetry", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("On") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("On"); + + b.ToTable("DelayedRetries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Document", b => + { + b.Property("Id") + .HasColumnType("int"); + + b.Property("BusEnabled") + .HasColumnType("bit"); + + b.Property("BusMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("DisregardsUnfilteredMessages") + .HasColumnType("bit"); + + b.Property("DocumentFormat") + .HasColumnType("int"); + + b.Property("DuplicateInterval") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("PromotedProperties") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("BusMessageTypeName") + .IsUnique() + .HasFilter("[BusMessageTypeName] IS NOT NULL"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Documents", (string)null); + + b.HasData( + new + { + Id = 10001, + BusEnabled = false, + DocumentFormat = 0, + DuplicateInterval = 0, + Name = "Aggregation Document", + PromotedProperties = "{}" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DocumentTrail", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("Code") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("StateAfter") + .HasColumnType("nvarchar(max)"); + + b.Property("StateBefore") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("CreatedOn"); + + b.HasIndex("DocumentId"); + + b.ToTable("DocumentTrail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("UrlName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("UrlName") + .IsUnique(); + + b.ToTable("ApiGateways", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.Property("ApiGatewayId") + .HasColumnType("int"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.HasKey("ApiGatewayId", "PartnerId", "SubscriptionId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("ApiGatewayPartners", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.ToTable("BusGateways", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BusGatewayId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("MatchExpression") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("BusGatewayId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("BusGatewayRoutes", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.GlobalAdapterValuesSet", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("Values") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("GlobalAdapterValuesSets", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Notifier", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("Inactive") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("RunOnBadResult") + .HasColumnType("bit"); + + b.Property("RunOnFailedResult") + .HasColumnType("bit"); + + b.Property("RunOnSubscriptions") + .HasColumnType("nvarchar(max)"); + + b.Property("RunOnSuccessfulResult") + .HasColumnType("bit"); + + b.HasKey("Id"); + + b.ToTable("Notifiers", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OnHoldXchange", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BadData") + .HasColumnType("bit"); + + b.Property("Data") + .HasColumnType("nvarchar(max)"); + + b.Property("FileName") + .HasColumnType("nvarchar(max)"); + + b.Property("References") + .HasMaxLength(1024) + .HasColumnType("nvarchar(1024)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("OnHoldXchanges", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AdapterProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.ToTable("Partners", (string)null); + + b.HasData( + new + { + Id = 1, + Name = "SYSTEM" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryAlertOverride", b => + { + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("GroupId") + .HasColumnType("uniqueidentifier"); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("AlertHandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("AlertMode") + .HasColumnType("tinyint"); + + b.HasKey("SubscriptionId", "GroupId"); + + b.ToTable("RetryAlertOverrides", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryGroupUsage", b => + { + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("GroupId") + .HasColumnType("uniqueidentifier"); + + b.Property("AttemptsUsed") + .HasColumnType("int"); + + b.Property("ExhaustedNotifiedOn") + .HasColumnType("datetime2"); + + b.Property("LastAttemptOn") + .HasColumnType("datetime2"); + + b.HasKey("SubscriptionId", "GroupId"); + + b.ToTable("RetryGroupUsages", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryPolicy", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("AlertHandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Groups") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.ToTable("RetryPolicies", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AggregateOn") + .HasColumnType("datetime2"); + + b.Property("AggregationForId") + .HasColumnType("int"); + + b.Property("AggregationTarget") + .HasColumnType("tinyint"); + + b.Property("CategoryId") + .HasColumnType("int"); + + b.Property("ConsecutiveFailures") + .HasColumnType("int"); + + b.Property("CustomRetryPolicy") + .HasColumnType("nvarchar(max)"); + + b.Property("DocumentFilter") + .HasColumnType("nvarchar(max)"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("Inactive") + .HasColumnType("bit"); + + b.Property("IsRunning") + .HasColumnType("bit"); + + b.Property("LastException") + .HasColumnType("nvarchar(max)"); + + b.Property("MapperId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("MapperProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("MatchExpression") + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("PausedOn") + .HasColumnType("datetime2"); + + b.Property("ReceiveOn") + .HasColumnType("datetime2"); + + b.Property("ReceiverId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ReceiverProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("int"); + + b.Property("RetryPolicyId") + .HasColumnType("int"); + + b.Property("Temporary") + .HasColumnType("bit"); + + b.Property("Type") + .HasColumnType("tinyint"); + + b.Property("ValidatorId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ValidatorProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("WorkGroupId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("AggregationForId"); + + b.HasIndex("CategoryId"); + + b.HasIndex("DocumentId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("ResponseSubscriptionId"); + + b.HasIndex("RetryPolicyId"); + + b.HasIndex("WorkGroupId"); + + b.ToTable("Subscriptions", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Code") + .HasColumnType("nvarchar(450)"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique() + .HasFilter("[Code] IS NOT NULL"); + + b.ToTable("SubscriptionCategory"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionTrail", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("Code") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("StateAfter") + .HasColumnType("nvarchar(max)"); + + b.Property("StateBefore") + .HasColumnType("nvarchar(max)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("CreatedOn"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("SubscriptionTrail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.WorkGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BusMessageName") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("Options") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("CorrelationId") + .HasColumnType("nvarchar(max)"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("InputContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("InputHash") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("InputName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("InputSize") + .HasColumnType("int"); + + b.Property("ManualRetry") + .HasColumnType("bit"); + + b.Property("MapperId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("MapperProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("References") + .HasMaxLength(1024) + .HasColumnType("nvarchar(1024)"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("int"); + + b.Property("RetryFor") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("StartedOn") + .HasColumnType("datetime2"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.HasIndex("InputHash"); + + b.HasIndex("RetryFor"); + + b.HasIndex("StartedOn"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("Xchanges", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AggregatedOn") + .HasColumnType("datetime2"); + + b.Property("AggregationXchangeId") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("AggregationXchangeId"); + + b.ToTable("XchangeAggregations", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("DeliveredOn") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("DeliveredOn"); + + b.ToTable("XchangeDeliveries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeNotification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Exception") + .HasColumnType("nvarchar(max)"); + + b.Property("FinishedOn") + .HasColumnType("datetime2"); + + b.Property("NotifierId") + .HasColumnType("int"); + + b.Property("NotifierName") + .HasColumnType("nvarchar(max)"); + + b.Property("Success") + .HasColumnType("bit"); + + b.Property("XchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.ToTable("XchangeNotifications", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("Hits") + .HasMaxLength(2000) + .IsUnicode(false) + .HasColumnType("varchar(2000)"); + + b.Property("Properties") + .HasColumnType("nvarchar(max)"); + + b.Property("PropertiesRaw") + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("PropertiesRaw"); + + b.ToTable("XchangePromotedProperties", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AttemptNumber") + .HasColumnType("int"); + + b.Property("Exception") + .HasColumnType("nvarchar(max)"); + + b.Property("FinishedOn") + .HasColumnType("datetime2"); + + b.Property("OutputBad") + .HasColumnType("bit"); + + b.Property("OutputContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("OutputHash") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("OutputName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("OutputSize") + .HasColumnType("int"); + + b.Property("ResponseBad") + .HasColumnType("bit"); + + b.Property("ResponseContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ResponseHash") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("ResponseName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("ResponseSize") + .HasColumnType("int"); + + b.Property("ResponseXchangeId") + .HasColumnType("nvarchar(max)"); + + b.Property("RetryBlockedReason") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("RetryGroupId") + .HasColumnType("uniqueidentifier"); + + b.Property("Success") + .HasColumnType("bit"); + + b.HasKey("Id"); + + b.HasIndex("RetryGroupId"); + + b.ToTable("XchangeResults", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.RunFlagUpdater+RunningResult", b => + { + b.Property("IsRunning") + .HasColumnType("bit"); + + b.ToTable((string)null); + + b.ToView(null, (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.JobExecution", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Context") + .HasColumnType("nvarchar(max)") + .HasColumnName("context"); + + b.Property("DurationMs") + .HasColumnType("bigint") + .HasColumnName("duration_ms"); + + b.Property("EndTimeUtc") + .HasColumnType("datetime2") + .HasColumnName("end_time_utc"); + + b.Property("Error") + .HasColumnType("nvarchar(max)") + .HasColumnName("error"); + + b.Property("FireInstanceId") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("fire_instance_id"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_name"); + + b.Property("JobTypeName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_type_name"); + + b.Property("Node") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("node"); + + b.Property("StartTimeUtc") + .HasColumnType("datetime2") + .HasColumnName("start_time_utc"); + + b.Property("Success") + .HasColumnType("bit") + .HasColumnName("success"); + + b.HasKey("Id"); + + b.HasIndex("FireInstanceId") + .IsUnique() + .HasDatabaseName("idx_je_fire_instance_id"); + + b.HasIndex("StartTimeUtc") + .HasDatabaseName("idx_je_start_time"); + + b.HasIndex("Success") + .HasDatabaseName("idx_je_success"); + + b.HasIndex("JobGroup", "JobName", "StartTimeUtc") + .HasDatabaseName("idx_je_group_name_start"); + + b.ToTable("job_executions", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("BlobData") + .HasColumnType("varbinary(max)") + .HasColumnName("blob_data"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_blob_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCalendar", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("CalendarName") + .HasColumnType("nvarchar(450)") + .HasColumnName("calendar_name"); + + b.Property("Calendar") + .IsRequired() + .HasColumnType("varbinary(max)") + .HasColumnName("calendar"); + + b.HasKey("SchedulerName", "CalendarName"); + + b.ToTable("QRTZ_calendars", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("CronExpression") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("cron_expression"); + + b.Property("TimeZoneId") + .HasColumnType("nvarchar(450)") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_cron_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzFiredTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("EntryId") + .HasColumnType("nvarchar(450)") + .HasColumnName("entry_id"); + + b.Property("FiredTime") + .HasColumnType("bigint") + .HasColumnName("fired_time"); + + b.Property("InstanceName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("instance_name"); + + b.Property("IsNonConcurrent") + .HasColumnType("bit") + .HasColumnName("is_nonconcurrent"); + + b.Property("JobGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("job_group"); + + b.Property("JobName") + .HasColumnType("nvarchar(450)") + .HasColumnName("job_name"); + + b.Property("Priority") + .HasColumnType("int") + .HasColumnName("priority"); + + b.Property("RequestsRecovery") + .HasColumnType("bit") + .HasColumnName("requests_recovery"); + + b.Property("ScheduledTime") + .HasColumnType("bigint") + .HasColumnName("sched_time"); + + b.Property("State") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("state"); + + b.Property("TriggerGroup") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("TriggerName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.HasKey("SchedulerName", "EntryId"); + + b.HasIndex("InstanceName") + .HasDatabaseName("idx_QRTZ_ft_trig_inst_name"); + + b.HasIndex("JobGroup") + .HasDatabaseName("idx_QRTZ_ft_job_group"); + + b.HasIndex("JobName") + .HasDatabaseName("idx_QRTZ_ft_job_name"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_QRTZ_ft_job_req_recovery"); + + b.HasIndex("TriggerGroup") + .HasDatabaseName("idx_QRTZ_ft_trig_group"); + + b.HasIndex("TriggerName") + .HasDatabaseName("idx_QRTZ_ft_trig_name"); + + b.HasIndex("SchedulerName", "TriggerName", "TriggerGroup") + .HasDatabaseName("idx_QRTZ_ft_trig_nm_gp"); + + b.ToTable("QRTZ_fired_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("JobName") + .HasColumnType("nvarchar(450)") + .HasColumnName("job_name"); + + b.Property("JobGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("job_group"); + + b.Property("Description") + .HasColumnType("nvarchar(450)") + .HasColumnName("description"); + + b.Property("IsDurable") + .HasColumnType("bit") + .HasColumnName("is_durable"); + + b.Property("IsNonConcurrent") + .HasColumnType("bit") + .HasColumnName("is_nonconcurrent"); + + b.Property("IsUpdateData") + .HasColumnType("bit") + .HasColumnName("is_update_data"); + + b.Property("JobClassName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_class_name"); + + b.Property("JobData") + .HasColumnType("varbinary(max)") + .HasColumnName("job_data"); + + b.Property("RequestsRecovery") + .HasColumnType("bit") + .HasColumnName("requests_recovery"); + + b.HasKey("SchedulerName", "JobName", "JobGroup"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_j_req_recovery"); + + b.ToTable("QRTZ_job_details", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzLock", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("LockName") + .HasColumnType("nvarchar(450)") + .HasColumnName("lock_name"); + + b.HasKey("SchedulerName", "LockName"); + + b.ToTable("QRTZ_locks", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzPausedTriggerGroup", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.HasKey("SchedulerName", "TriggerGroup"); + + b.ToTable("QRTZ_paused_trigger_grps", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSchedulerState", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("InstanceName") + .HasColumnType("nvarchar(450)") + .HasColumnName("instance_name"); + + b.Property("CheckInInterval") + .HasColumnType("bigint") + .HasColumnName("checkin_interval"); + + b.Property("LastCheckInTime") + .HasColumnType("bigint") + .HasColumnName("last_checkin_time"); + + b.HasKey("SchedulerName", "InstanceName"); + + b.ToTable("QRTZ_scheduler_state", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("BooleanProperty1") + .HasColumnType("bit") + .HasColumnName("bool_prop_1"); + + b.Property("BooleanProperty2") + .HasColumnType("bit") + .HasColumnName("bool_prop_2"); + + b.Property("DecimalProperty1") + .HasColumnType("numeric") + .HasColumnName("dec_prop_1"); + + b.Property("DecimalProperty2") + .HasColumnType("numeric") + .HasColumnName("dec_prop_2"); + + b.Property("IntegerProperty1") + .HasColumnType("int") + .HasColumnName("int_prop_1"); + + b.Property("IntegerProperty2") + .HasColumnType("int") + .HasColumnName("int_prop_2"); + + b.Property("LongProperty1") + .HasColumnType("bigint") + .HasColumnName("long_prop_1"); + + b.Property("LongProperty2") + .HasColumnType("bigint") + .HasColumnName("long_prop_2"); + + b.Property("StringProperty1") + .HasColumnType("nvarchar(450)") + .HasColumnName("str_prop_1"); + + b.Property("StringProperty2") + .HasColumnType("nvarchar(450)") + .HasColumnName("str_prop_2"); + + b.Property("StringProperty3") + .HasColumnType("nvarchar(450)") + .HasColumnName("str_prop_3"); + + b.Property("TimeZoneId") + .HasColumnType("nvarchar(450)") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_simprop_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("RepeatCount") + .HasColumnType("bigint") + .HasColumnName("repeat_count"); + + b.Property("RepeatInterval") + .HasColumnType("bigint") + .HasColumnName("repeat_interval"); + + b.Property("TimesTriggered") + .HasColumnType("bigint") + .HasColumnName("times_triggered"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_simple_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("CalendarName") + .HasColumnType("nvarchar(450)") + .HasColumnName("calendar_name"); + + b.Property("Description") + .HasColumnType("nvarchar(450)") + .HasColumnName("description"); + + b.Property("EndTime") + .HasColumnType("bigint") + .HasColumnName("end_time"); + + b.Property("JobData") + .HasColumnType("varbinary(max)") + .HasColumnName("job_data"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_name"); + + b.Property("MisfireInstruction") + .HasColumnType("int") + .HasColumnName("misfire_instr"); + + b.Property("NextFireTime") + .HasColumnType("bigint") + .HasColumnName("next_fire_time"); + + b.Property("PreviousFireTime") + .HasColumnType("bigint") + .HasColumnName("prev_fire_time"); + + b.Property("Priority") + .HasColumnType("int") + .HasColumnName("priority"); + + b.Property("StartTime") + .HasColumnType("bigint") + .HasColumnName("start_time"); + + b.Property("TriggerState") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_state"); + + b.Property("TriggerType") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_type"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.HasIndex("NextFireTime") + .HasDatabaseName("idx_t_next_fire_time"); + + b.HasIndex("TriggerState") + .HasDatabaseName("idx_t_state"); + + b.HasIndex("NextFireTime", "TriggerState") + .HasDatabaseName("idx_t_nft_st"); + + b.HasIndex("SchedulerName", "JobName", "JobGroup"); + + b.ToTable("QRTZ_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DocumentTrail", b => + { + b.HasOne("SW.Bitween.Domain.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Document"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.ApiGateway", "ApiGateway") + .WithMany("Partners") + .HasForeignKey("ApiGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("ApiGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.BusGateway", "BusGateway") + .WithMany("Routes") + .HasForeignKey("BusGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("BusGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.OwnsMany("SW.Bitween.Domain.ApiCredential", "ApiCredentials", b1 => + { + b1.Property("PartnerId") + .HasColumnType("int"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b1.Property("Id")); + + b1.Property("Key") + .IsRequired() + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b1.Property("Name") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b1.HasKey("PartnerId", "Id"); + + b1.HasIndex("Key") + .IsUnique(); + + b1.ToTable("PartnerApiCredentials", (string)null); + + b1.WithOwner() + .HasForeignKey("PartnerId"); + + b1.HasData( + new + { + PartnerId = 1, + Id = 1, + Key = "7facc758283844b49cc4ffd26a75b1de", + Name = "default" + }); + }); + + b.Navigation("ApiCredentials"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.HasOne("SW.Bitween.Domain.Subscription", null) + .WithMany() + .HasForeignKey("AggregationForId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("FK_Subscriptions_AggFor"); + + b.HasOne("SW.Bitween.Domain.SubscriptionCategory", "Category") + .WithMany() + .HasForeignKey("CategoryId"); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", null) + .WithMany("Subscriptions") + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Subscription", null) + .WithMany() + .HasForeignKey("ResponseSubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("FK_Subscriptions_RespSub"); + + b.HasOne("SW.Bitween.Domain.RetryPolicy", "RetryPolicy") + .WithMany() + .HasForeignKey("RetryPolicyId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("SW.Bitween.Domain.WorkGroup", "WorkGroup") + .WithMany() + .HasForeignKey("WorkGroupId"); + + b.OwnsMany("SW.Bitween.Domain.Schedule", "Schedules", b1 => + { + b1.Property("SubscriptionId") + .HasColumnType("int"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b1.Property("Id")); + + b1.Property("Backwards") + .HasColumnType("bit"); + + b1.Property("On") + .HasColumnType("bigint"); + + b1.Property("Recurrence") + .HasColumnType("tinyint"); + + b1.HasKey("SubscriptionId", "Id"); + + b1.ToTable("SubscriptionSchedules", (string)null); + + b1.WithOwner() + .HasForeignKey("SubscriptionId"); + }); + + b.Navigation("Category"); + + b.Navigation("RetryPolicy"); + + b.Navigation("Schedules"); + + b.Navigation("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionTrail", b => + { + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeAggregation", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeDelivery", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangePromotedProperties", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeResult", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("BlobTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("CronTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimplePropertyTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimpleTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzJobDetail", "JobDetail") + .WithMany("Triggers") + .HasForeignKey("SchedulerName", "JobName", "JobGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("JobDetail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Navigation("Partners"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Navigation("Routes"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Navigation("Subscriptions"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Navigation("Triggers"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Navigation("BlobTriggers"); + + b.Navigation("CronTriggers"); + + b.Navigation("SimplePropertyTriggers"); + + b.Navigation("SimpleTriggers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/SW.Bitween.MsSql/Migrations/20260819100503_ManualRetryFlag.cs b/SW.Bitween.MsSql/Migrations/20260819100503_ManualRetryFlag.cs new file mode 100644 index 00000000..73d83836 --- /dev/null +++ b/SW.Bitween.MsSql/Migrations/20260819100503_ManualRetryFlag.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace SW.Bitween.MsSql.Migrations +{ + /// + public partial class ManualRetryFlag : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "ManualRetry", + table: "Xchanges", + type: "bit", + nullable: false, + defaultValue: false); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "ManualRetry", + table: "Xchanges"); + } + } +} diff --git a/SW.Bitween.MsSql/Migrations/20260823104233_GatewayInactiveFlag.Designer.cs b/SW.Bitween.MsSql/Migrations/20260823104233_GatewayInactiveFlag.Designer.cs new file mode 100644 index 00000000..7f28c1c5 --- /dev/null +++ b/SW.Bitween.MsSql/Migrations/20260823104233_GatewayInactiveFlag.Designer.cs @@ -0,0 +1,2104 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using SW.Bitween.MsSql; + +#nullable disable + +namespace SW.Bitween.MsSql.Migrations +{ + [DbContext(typeof(BitweenDbContext))] + [Migration("20260823104233_GatewayInactiveFlag")] + partial class GatewayInactiveFlag + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.23") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.HasSequence("DocumentIds"); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Deleted") + .HasColumnType("bit"); + + b.Property("Disabled") + .HasColumnType("bit"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Email") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("EmailProvider") + .HasColumnType("tinyint"); + + b.Property("FailedLoginCount") + .HasColumnType("int"); + + b.Property("LockoutEnd") + .HasColumnType("datetime2"); + + b.Property("LoginMethods") + .HasColumnType("tinyint"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Password") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("Role") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique() + .HasFilter("[Email] IS NOT NULL"); + + b.ToTable("Accounts", (string)null); + + b.HasData( + new + { + Id = 9999, + CreatedOn = new DateTime(2021, 12, 31, 22, 0, 0, 0, DateTimeKind.Utc), + Deleted = false, + Disabled = false, + DisplayName = "Admin", + Email = "admin@Bitween.systems", + EmailProvider = (byte)0, + FailedLoginCount = 0, + LoginMethods = (byte)2, + Password = "$SWHASH$V1$10000$VQCi48eitH4Ml5juvBMOFZrMdQwBbhuIQVXe6RR7qJdDF2bJ", + Role = 0 + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.AccountRoleLink", b => + { + b.Property("AccountId") + .HasColumnType("int"); + + b.Property("RoleId") + .HasColumnType("int"); + + b.HasKey("AccountId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AccountRoles", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AccountId") + .HasColumnType("int"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("LoginMethod") + .HasColumnType("tinyint"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.ToTable("RefreshTokens", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("IsSystem") + .HasColumnType("bit"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Permissions") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Roles", (string)null); + + b.HasData( + new + { + Id = 1, + CreatedOn = new DateTime(2021, 12, 31, 22, 0, 0, 0, DateTimeKind.Utc), + Description = "Full access to everything, including members, roles and settings.", + IsSystem = true, + Name = "Administrator", + Permissions = "[]" + }, + new + { + Id = 2, + CreatedOn = new DateTime(2021, 12, 31, 22, 0, 0, 0, DateTimeKind.Utc), + Description = "Runs and configures integrations. Can't manage members, roles or settings.", + IsSystem = true, + Name = "Member", + Permissions = "[]" + }, + new + { + Id = 3, + CreatedOn = new DateTime(2021, 12, 31, 22, 0, 0, 0, DateTimeKind.Utc), + Description = "Read-only access to integrations, exchanges and configuration.", + IsSystem = true, + Name = "Viewer", + Permissions = "[]" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DelayedRetry", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("On") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("On"); + + b.ToTable("DelayedRetries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Document", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValueSql("NEXT VALUE FOR [DocumentIds]"); + + SqlServerPropertyBuilderExtensions.UseSequence(b.Property("Id"), "DocumentIds"); + + b.Property("BusEnabled") + .HasColumnType("bit"); + + b.Property("BusMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("Code") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("DisregardsUnfilteredMessages") + .HasColumnType("bit"); + + b.Property("DocumentFormat") + .HasColumnType("int"); + + b.Property("DuplicateInterval") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("PromotedProperties") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("BusMessageTypeName") + .IsUnique() + .HasFilter("[BusMessageTypeName] IS NOT NULL"); + + b.HasIndex("Code") + .IsUnique() + .HasFilter("[Code] IS NOT NULL"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Documents", (string)null); + + b.HasData( + new + { + Id = 10001, + BusEnabled = false, + DocumentFormat = 0, + DuplicateInterval = 0, + Name = "Aggregation Document", + PromotedProperties = "{}" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DocumentTrail", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("Code") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("StateAfter") + .HasColumnType("nvarchar(max)"); + + b.Property("StateBefore") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("CreatedOn"); + + b.HasIndex("DocumentId"); + + b.ToTable("DocumentTrail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Inactive") + .HasColumnType("bit"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("UrlName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("UrlName") + .IsUnique(); + + b.ToTable("ApiGateways", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.Property("ApiGatewayId") + .HasColumnType("int"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.HasKey("ApiGatewayId", "PartnerId", "SubscriptionId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("ApiGatewayPartners", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("Inactive") + .HasColumnType("bit"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.ToTable("BusGateways", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BusGatewayId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("MatchExpression") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("BusGatewayId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("BusGatewayRoutes", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.GlobalAdapterValuesSet", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("Values") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("GlobalAdapterValuesSets", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Notifier", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("Inactive") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("RunOnBadResult") + .HasColumnType("bit"); + + b.Property("RunOnFailedResult") + .HasColumnType("bit"); + + b.Property("RunOnSubscriptions") + .HasColumnType("nvarchar(max)"); + + b.Property("RunOnSuccessfulResult") + .HasColumnType("bit"); + + b.HasKey("Id"); + + b.ToTable("Notifiers", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OnHoldXchange", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BadData") + .HasColumnType("bit"); + + b.Property("Data") + .HasColumnType("nvarchar(max)"); + + b.Property("FileName") + .HasColumnType("nvarchar(max)"); + + b.Property("References") + .HasMaxLength(1024) + .HasColumnType("nvarchar(1024)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("OnHoldXchanges", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AdapterProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.ToTable("Partners", (string)null); + + b.HasData( + new + { + Id = 1, + Name = "SYSTEM" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryAlertOverride", b => + { + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("GroupId") + .HasColumnType("uniqueidentifier"); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("AlertHandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("AlertMode") + .HasColumnType("tinyint"); + + b.HasKey("SubscriptionId", "GroupId"); + + b.ToTable("RetryAlertOverrides", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryGroupUsage", b => + { + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("GroupId") + .HasColumnType("uniqueidentifier"); + + b.Property("AttemptsUsed") + .HasColumnType("int"); + + b.Property("ExhaustedNotifiedOn") + .HasColumnType("datetime2"); + + b.Property("LastAttemptOn") + .HasColumnType("datetime2"); + + b.HasKey("SubscriptionId", "GroupId"); + + b.ToTable("RetryGroupUsages", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryPolicy", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("AlertHandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Groups") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.ToTable("RetryPolicies", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Setting", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Value") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Settings", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AggregateOn") + .HasColumnType("datetime2"); + + b.Property("AggregationForId") + .HasColumnType("int"); + + b.Property("AggregationTarget") + .HasColumnType("tinyint"); + + b.Property("CategoryId") + .HasColumnType("int"); + + b.Property("ConsecutiveFailures") + .HasColumnType("int"); + + b.Property("CustomRetryPolicy") + .HasColumnType("nvarchar(max)"); + + b.Property("DocumentFilter") + .HasColumnType("nvarchar(max)"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("Inactive") + .HasColumnType("bit"); + + b.Property("IsRunning") + .HasColumnType("bit"); + + b.Property("LastException") + .HasColumnType("nvarchar(max)"); + + b.Property("MapperId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("MapperProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("MatchExpression") + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("PausedOn") + .HasColumnType("datetime2"); + + b.Property("ReceiveOn") + .HasColumnType("datetime2"); + + b.Property("ReceiverId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ReceiverProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("int"); + + b.Property("RetryPolicyId") + .HasColumnType("int"); + + b.Property("Temporary") + .HasColumnType("bit"); + + b.Property("Type") + .HasColumnType("tinyint"); + + b.Property("ValidatorId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ValidatorProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("WorkGroupId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("AggregationForId"); + + b.HasIndex("CategoryId"); + + b.HasIndex("DocumentId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("ResponseSubscriptionId"); + + b.HasIndex("RetryPolicyId"); + + b.HasIndex("WorkGroupId"); + + b.ToTable("Subscriptions", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Code") + .HasColumnType("nvarchar(450)"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique() + .HasFilter("[Code] IS NOT NULL"); + + b.ToTable("SubscriptionCategory"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionTrail", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("Code") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("StateAfter") + .HasColumnType("nvarchar(max)"); + + b.Property("StateBefore") + .HasColumnType("nvarchar(max)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("CreatedOn"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("SubscriptionTrail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.WorkGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BusMessageName") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("Options") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("CorrelationId") + .HasColumnType("nvarchar(max)"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("InputContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("InputHash") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("InputName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("InputSize") + .HasColumnType("int"); + + b.Property("ManualRetry") + .HasColumnType("bit"); + + b.Property("MapperId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("MapperProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("References") + .HasMaxLength(1024) + .HasColumnType("nvarchar(1024)"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("int"); + + b.Property("RetryFor") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("StartedOn") + .HasColumnType("datetime2"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.HasIndex("InputHash"); + + b.HasIndex("RetryFor"); + + b.HasIndex("StartedOn"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("Xchanges", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AggregatedOn") + .HasColumnType("datetime2"); + + b.Property("AggregationXchangeId") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("AggregationXchangeId"); + + b.ToTable("XchangeAggregations", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("DeliveredOn") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("DeliveredOn"); + + b.ToTable("XchangeDeliveries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeNotification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Exception") + .HasColumnType("nvarchar(max)"); + + b.Property("FinishedOn") + .HasColumnType("datetime2"); + + b.Property("NotifierId") + .HasColumnType("int"); + + b.Property("NotifierName") + .HasColumnType("nvarchar(max)"); + + b.Property("Success") + .HasColumnType("bit"); + + b.Property("XchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.ToTable("XchangeNotifications", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("Hits") + .HasMaxLength(2000) + .IsUnicode(false) + .HasColumnType("varchar(2000)"); + + b.Property("Properties") + .HasColumnType("nvarchar(max)"); + + b.Property("PropertiesRaw") + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("PropertiesRaw"); + + b.ToTable("XchangePromotedProperties", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AttemptNumber") + .HasColumnType("int"); + + b.Property("Exception") + .HasColumnType("nvarchar(max)"); + + b.Property("FinishedOn") + .HasColumnType("datetime2"); + + b.Property("OutputBad") + .HasColumnType("bit"); + + b.Property("OutputContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("OutputHash") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("OutputName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("OutputSize") + .HasColumnType("int"); + + b.Property("ResponseBad") + .HasColumnType("bit"); + + b.Property("ResponseContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ResponseHash") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("ResponseName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("ResponseSize") + .HasColumnType("int"); + + b.Property("ResponseXchangeId") + .HasColumnType("nvarchar(max)"); + + b.Property("RetryBlockedReason") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("RetryGroupId") + .HasColumnType("uniqueidentifier"); + + b.Property("Success") + .HasColumnType("bit"); + + b.HasKey("Id"); + + b.HasIndex("RetryGroupId"); + + b.ToTable("XchangeResults", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.RunFlagUpdater+RunningResult", b => + { + b.Property("IsRunning") + .HasColumnType("bit"); + + b.ToTable((string)null); + + b.ToView(null, (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.JobExecution", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Context") + .HasColumnType("nvarchar(max)") + .HasColumnName("context"); + + b.Property("DurationMs") + .HasColumnType("bigint") + .HasColumnName("duration_ms"); + + b.Property("EndTimeUtc") + .HasColumnType("datetime2") + .HasColumnName("end_time_utc"); + + b.Property("Error") + .HasColumnType("nvarchar(max)") + .HasColumnName("error"); + + b.Property("FireInstanceId") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("fire_instance_id"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_name"); + + b.Property("JobTypeName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_type_name"); + + b.Property("Node") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("node"); + + b.Property("StartTimeUtc") + .HasColumnType("datetime2") + .HasColumnName("start_time_utc"); + + b.Property("Success") + .HasColumnType("bit") + .HasColumnName("success"); + + b.HasKey("Id"); + + b.HasIndex("FireInstanceId") + .IsUnique() + .HasDatabaseName("idx_je_fire_instance_id"); + + b.HasIndex("StartTimeUtc") + .HasDatabaseName("idx_je_start_time"); + + b.HasIndex("Success") + .HasDatabaseName("idx_je_success"); + + b.HasIndex("JobGroup", "JobName", "StartTimeUtc") + .HasDatabaseName("idx_je_group_name_start"); + + b.ToTable("job_executions", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("BlobData") + .HasColumnType("varbinary(max)") + .HasColumnName("blob_data"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_blob_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCalendar", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("CalendarName") + .HasColumnType("nvarchar(450)") + .HasColumnName("calendar_name"); + + b.Property("Calendar") + .IsRequired() + .HasColumnType("varbinary(max)") + .HasColumnName("calendar"); + + b.HasKey("SchedulerName", "CalendarName"); + + b.ToTable("QRTZ_calendars", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("CronExpression") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("cron_expression"); + + b.Property("TimeZoneId") + .HasColumnType("nvarchar(450)") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_cron_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzFiredTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("EntryId") + .HasColumnType("nvarchar(450)") + .HasColumnName("entry_id"); + + b.Property("FiredTime") + .HasColumnType("bigint") + .HasColumnName("fired_time"); + + b.Property("InstanceName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("instance_name"); + + b.Property("IsNonConcurrent") + .HasColumnType("bit") + .HasColumnName("is_nonconcurrent"); + + b.Property("JobGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("job_group"); + + b.Property("JobName") + .HasColumnType("nvarchar(450)") + .HasColumnName("job_name"); + + b.Property("Priority") + .HasColumnType("int") + .HasColumnName("priority"); + + b.Property("RequestsRecovery") + .HasColumnType("bit") + .HasColumnName("requests_recovery"); + + b.Property("ScheduledTime") + .HasColumnType("bigint") + .HasColumnName("sched_time"); + + b.Property("State") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("state"); + + b.Property("TriggerGroup") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("TriggerName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.HasKey("SchedulerName", "EntryId"); + + b.HasIndex("InstanceName") + .HasDatabaseName("idx_QRTZ_ft_trig_inst_name"); + + b.HasIndex("JobGroup") + .HasDatabaseName("idx_QRTZ_ft_job_group"); + + b.HasIndex("JobName") + .HasDatabaseName("idx_QRTZ_ft_job_name"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_QRTZ_ft_job_req_recovery"); + + b.HasIndex("TriggerGroup") + .HasDatabaseName("idx_QRTZ_ft_trig_group"); + + b.HasIndex("TriggerName") + .HasDatabaseName("idx_QRTZ_ft_trig_name"); + + b.HasIndex("SchedulerName", "TriggerName", "TriggerGroup") + .HasDatabaseName("idx_QRTZ_ft_trig_nm_gp"); + + b.ToTable("QRTZ_fired_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("JobName") + .HasColumnType("nvarchar(450)") + .HasColumnName("job_name"); + + b.Property("JobGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("job_group"); + + b.Property("Description") + .HasColumnType("nvarchar(450)") + .HasColumnName("description"); + + b.Property("IsDurable") + .HasColumnType("bit") + .HasColumnName("is_durable"); + + b.Property("IsNonConcurrent") + .HasColumnType("bit") + .HasColumnName("is_nonconcurrent"); + + b.Property("IsUpdateData") + .HasColumnType("bit") + .HasColumnName("is_update_data"); + + b.Property("JobClassName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_class_name"); + + b.Property("JobData") + .HasColumnType("varbinary(max)") + .HasColumnName("job_data"); + + b.Property("RequestsRecovery") + .HasColumnType("bit") + .HasColumnName("requests_recovery"); + + b.HasKey("SchedulerName", "JobName", "JobGroup"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_j_req_recovery"); + + b.ToTable("QRTZ_job_details", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzLock", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("LockName") + .HasColumnType("nvarchar(450)") + .HasColumnName("lock_name"); + + b.HasKey("SchedulerName", "LockName"); + + b.ToTable("QRTZ_locks", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzPausedTriggerGroup", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.HasKey("SchedulerName", "TriggerGroup"); + + b.ToTable("QRTZ_paused_trigger_grps", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSchedulerState", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("InstanceName") + .HasColumnType("nvarchar(450)") + .HasColumnName("instance_name"); + + b.Property("CheckInInterval") + .HasColumnType("bigint") + .HasColumnName("checkin_interval"); + + b.Property("LastCheckInTime") + .HasColumnType("bigint") + .HasColumnName("last_checkin_time"); + + b.HasKey("SchedulerName", "InstanceName"); + + b.ToTable("QRTZ_scheduler_state", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("BooleanProperty1") + .HasColumnType("bit") + .HasColumnName("bool_prop_1"); + + b.Property("BooleanProperty2") + .HasColumnType("bit") + .HasColumnName("bool_prop_2"); + + b.Property("DecimalProperty1") + .HasColumnType("numeric") + .HasColumnName("dec_prop_1"); + + b.Property("DecimalProperty2") + .HasColumnType("numeric") + .HasColumnName("dec_prop_2"); + + b.Property("IntegerProperty1") + .HasColumnType("int") + .HasColumnName("int_prop_1"); + + b.Property("IntegerProperty2") + .HasColumnType("int") + .HasColumnName("int_prop_2"); + + b.Property("LongProperty1") + .HasColumnType("bigint") + .HasColumnName("long_prop_1"); + + b.Property("LongProperty2") + .HasColumnType("bigint") + .HasColumnName("long_prop_2"); + + b.Property("StringProperty1") + .HasColumnType("nvarchar(450)") + .HasColumnName("str_prop_1"); + + b.Property("StringProperty2") + .HasColumnType("nvarchar(450)") + .HasColumnName("str_prop_2"); + + b.Property("StringProperty3") + .HasColumnType("nvarchar(450)") + .HasColumnName("str_prop_3"); + + b.Property("TimeZoneId") + .HasColumnType("nvarchar(450)") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_simprop_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("RepeatCount") + .HasColumnType("bigint") + .HasColumnName("repeat_count"); + + b.Property("RepeatInterval") + .HasColumnType("bigint") + .HasColumnName("repeat_interval"); + + b.Property("TimesTriggered") + .HasColumnType("bigint") + .HasColumnName("times_triggered"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_simple_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("CalendarName") + .HasColumnType("nvarchar(450)") + .HasColumnName("calendar_name"); + + b.Property("Description") + .HasColumnType("nvarchar(450)") + .HasColumnName("description"); + + b.Property("EndTime") + .HasColumnType("bigint") + .HasColumnName("end_time"); + + b.Property("JobData") + .HasColumnType("varbinary(max)") + .HasColumnName("job_data"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_name"); + + b.Property("MisfireInstruction") + .HasColumnType("int") + .HasColumnName("misfire_instr"); + + b.Property("NextFireTime") + .HasColumnType("bigint") + .HasColumnName("next_fire_time"); + + b.Property("PreviousFireTime") + .HasColumnType("bigint") + .HasColumnName("prev_fire_time"); + + b.Property("Priority") + .HasColumnType("int") + .HasColumnName("priority"); + + b.Property("StartTime") + .HasColumnType("bigint") + .HasColumnName("start_time"); + + b.Property("TriggerState") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_state"); + + b.Property("TriggerType") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_type"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.HasIndex("NextFireTime") + .HasDatabaseName("idx_t_next_fire_time"); + + b.HasIndex("TriggerState") + .HasDatabaseName("idx_t_state"); + + b.HasIndex("NextFireTime", "TriggerState") + .HasDatabaseName("idx_t_nft_st"); + + b.HasIndex("SchedulerName", "JobName", "JobGroup"); + + b.ToTable("QRTZ_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.AccountRoleLink", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Accounts.Role", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DocumentTrail", b => + { + b.HasOne("SW.Bitween.Domain.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Document"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.ApiGateway", "ApiGateway") + .WithMany("Partners") + .HasForeignKey("ApiGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("ApiGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.BusGateway", "BusGateway") + .WithMany("Routes") + .HasForeignKey("BusGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("BusGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.OwnsMany("SW.Bitween.Domain.ApiCredential", "ApiCredentials", b1 => + { + b1.Property("PartnerId") + .HasColumnType("int"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b1.Property("Id")); + + b1.Property("Key") + .IsRequired() + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b1.Property("Name") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b1.HasKey("PartnerId", "Id"); + + b1.HasIndex("Key") + .IsUnique(); + + b1.ToTable("PartnerApiCredentials", (string)null); + + b1.WithOwner() + .HasForeignKey("PartnerId"); + + b1.HasData( + new + { + PartnerId = 1, + Id = 1, + Key = "7facc758283844b49cc4ffd26a75b1de", + Name = "default" + }); + }); + + b.Navigation("ApiCredentials"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.HasOne("SW.Bitween.Domain.Subscription", null) + .WithMany() + .HasForeignKey("AggregationForId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("FK_Subscriptions_AggFor"); + + b.HasOne("SW.Bitween.Domain.SubscriptionCategory", "Category") + .WithMany() + .HasForeignKey("CategoryId"); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", null) + .WithMany("Subscriptions") + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Subscription", null) + .WithMany() + .HasForeignKey("ResponseSubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("FK_Subscriptions_RespSub"); + + b.HasOne("SW.Bitween.Domain.RetryPolicy", "RetryPolicy") + .WithMany() + .HasForeignKey("RetryPolicyId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("SW.Bitween.Domain.WorkGroup", "WorkGroup") + .WithMany() + .HasForeignKey("WorkGroupId"); + + b.OwnsMany("SW.Bitween.Domain.Schedule", "Schedules", b1 => + { + b1.Property("SubscriptionId") + .HasColumnType("int"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b1.Property("Id")); + + b1.Property("Backwards") + .HasColumnType("bit"); + + b1.Property("On") + .HasColumnType("bigint"); + + b1.Property("Recurrence") + .HasColumnType("tinyint"); + + b1.HasKey("SubscriptionId", "Id"); + + b1.ToTable("SubscriptionSchedules", (string)null); + + b1.WithOwner() + .HasForeignKey("SubscriptionId"); + }); + + b.Navigation("Category"); + + b.Navigation("RetryPolicy"); + + b.Navigation("Schedules"); + + b.Navigation("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionTrail", b => + { + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeAggregation", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeDelivery", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangePromotedProperties", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeResult", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("BlobTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("CronTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimplePropertyTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimpleTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzJobDetail", "JobDetail") + .WithMany("Triggers") + .HasForeignKey("SchedulerName", "JobName", "JobGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("JobDetail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Navigation("Partners"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Navigation("Routes"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Navigation("Subscriptions"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Navigation("Triggers"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Navigation("BlobTriggers"); + + b.Navigation("CronTriggers"); + + b.Navigation("SimplePropertyTriggers"); + + b.Navigation("SimpleTriggers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/SW.Bitween.MsSql/Migrations/20260823104233_GatewayInactiveFlag.cs b/SW.Bitween.MsSql/Migrations/20260823104233_GatewayInactiveFlag.cs new file mode 100644 index 00000000..129b1d34 --- /dev/null +++ b/SW.Bitween.MsSql/Migrations/20260823104233_GatewayInactiveFlag.cs @@ -0,0 +1,40 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace SW.Bitween.MsSql.Migrations +{ + /// + public partial class GatewayInactiveFlag : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Inactive", + table: "BusGateways", + type: "bit", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "Inactive", + table: "ApiGateways", + type: "bit", + nullable: false, + defaultValue: false); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Inactive", + table: "BusGateways"); + + migrationBuilder.DropColumn( + name: "Inactive", + table: "ApiGateways"); + } + } +} diff --git a/SW.Bitween.MsSql/Migrations/20260824093631_AddReceiveAttempts.Designer.cs b/SW.Bitween.MsSql/Migrations/20260824093631_AddReceiveAttempts.Designer.cs new file mode 100644 index 00000000..ef118cc7 --- /dev/null +++ b/SW.Bitween.MsSql/Migrations/20260824093631_AddReceiveAttempts.Designer.cs @@ -0,0 +1,2138 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using SW.Bitween.MsSql; + +#nullable disable + +namespace SW.Bitween.MsSql.Migrations +{ + [DbContext(typeof(BitweenDbContext))] + [Migration("20260824093631_AddReceiveAttempts")] + partial class AddReceiveAttempts + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.23") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.HasSequence("DocumentIds"); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Deleted") + .HasColumnType("bit"); + + b.Property("Disabled") + .HasColumnType("bit"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Email") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("EmailProvider") + .HasColumnType("tinyint"); + + b.Property("FailedLoginCount") + .HasColumnType("int"); + + b.Property("LockoutEnd") + .HasColumnType("datetime2"); + + b.Property("LoginMethods") + .HasColumnType("tinyint"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Password") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("Role") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique() + .HasFilter("[Email] IS NOT NULL"); + + b.ToTable("Accounts", (string)null); + + b.HasData( + new + { + Id = 9999, + CreatedOn = new DateTime(2021, 12, 31, 22, 0, 0, 0, DateTimeKind.Utc), + Deleted = false, + Disabled = false, + DisplayName = "Admin", + Email = "admin@Bitween.systems", + EmailProvider = (byte)0, + FailedLoginCount = 0, + LoginMethods = (byte)2, + Password = "$SWHASH$V1$10000$VQCi48eitH4Ml5juvBMOFZrMdQwBbhuIQVXe6RR7qJdDF2bJ", + Role = 0 + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.AccountRoleLink", b => + { + b.Property("AccountId") + .HasColumnType("int"); + + b.Property("RoleId") + .HasColumnType("int"); + + b.HasKey("AccountId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AccountRoles", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AccountId") + .HasColumnType("int"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("LoginMethod") + .HasColumnType("tinyint"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.ToTable("RefreshTokens", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("IsSystem") + .HasColumnType("bit"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Permissions") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Roles", (string)null); + + b.HasData( + new + { + Id = 1, + CreatedOn = new DateTime(2021, 12, 31, 22, 0, 0, 0, DateTimeKind.Utc), + Description = "Full access to everything, including members, roles and settings.", + IsSystem = true, + Name = "Administrator", + Permissions = "[]" + }, + new + { + Id = 2, + CreatedOn = new DateTime(2021, 12, 31, 22, 0, 0, 0, DateTimeKind.Utc), + Description = "Runs and configures integrations. Can't manage members, roles or settings.", + IsSystem = true, + Name = "Member", + Permissions = "[]" + }, + new + { + Id = 3, + CreatedOn = new DateTime(2021, 12, 31, 22, 0, 0, 0, DateTimeKind.Utc), + Description = "Read-only access to integrations, exchanges and configuration.", + IsSystem = true, + Name = "Viewer", + Permissions = "[]" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DelayedRetry", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("On") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("On"); + + b.ToTable("DelayedRetries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Document", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValueSql("NEXT VALUE FOR [DocumentIds]"); + + SqlServerPropertyBuilderExtensions.UseSequence(b.Property("Id"), "DocumentIds"); + + b.Property("BusEnabled") + .HasColumnType("bit"); + + b.Property("BusMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("Code") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("DisregardsUnfilteredMessages") + .HasColumnType("bit"); + + b.Property("DocumentFormat") + .HasColumnType("int"); + + b.Property("DuplicateInterval") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("PromotedProperties") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("BusMessageTypeName") + .IsUnique() + .HasFilter("[BusMessageTypeName] IS NOT NULL"); + + b.HasIndex("Code") + .IsUnique() + .HasFilter("[Code] IS NOT NULL"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Documents", (string)null); + + b.HasData( + new + { + Id = 10001, + BusEnabled = false, + DocumentFormat = 0, + DuplicateInterval = 0, + Name = "Aggregation Document", + PromotedProperties = "{}" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DocumentTrail", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("Code") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("StateAfter") + .HasColumnType("nvarchar(max)"); + + b.Property("StateBefore") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("CreatedOn"); + + b.HasIndex("DocumentId"); + + b.ToTable("DocumentTrail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Inactive") + .HasColumnType("bit"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("UrlName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("UrlName") + .IsUnique(); + + b.ToTable("ApiGateways", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.Property("ApiGatewayId") + .HasColumnType("int"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.HasKey("ApiGatewayId", "PartnerId", "SubscriptionId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("ApiGatewayPartners", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("Inactive") + .HasColumnType("bit"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.ToTable("BusGateways", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BusGatewayId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("MatchExpression") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("BusGatewayId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("BusGatewayRoutes", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.GlobalAdapterValuesSet", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("Values") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("GlobalAdapterValuesSets", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Notifier", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("Inactive") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("RunOnBadResult") + .HasColumnType("bit"); + + b.Property("RunOnFailedResult") + .HasColumnType("bit"); + + b.Property("RunOnSubscriptions") + .HasColumnType("nvarchar(max)"); + + b.Property("RunOnSuccessfulResult") + .HasColumnType("bit"); + + b.HasKey("Id"); + + b.ToTable("Notifiers", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OnHoldXchange", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BadData") + .HasColumnType("bit"); + + b.Property("Data") + .HasColumnType("nvarchar(max)"); + + b.Property("FileName") + .HasColumnType("nvarchar(max)"); + + b.Property("References") + .HasMaxLength(1024) + .HasColumnType("nvarchar(1024)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("OnHoldXchanges", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AdapterProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.ToTable("Partners", (string)null); + + b.HasData( + new + { + Id = 1, + Name = "SYSTEM" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.ReceiveAttempt", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ErrorMessage") + .HasMaxLength(4000) + .HasColumnType("nvarchar(4000)"); + + b.Property("ExchangeIds") + .HasColumnType("nvarchar(max)"); + + b.Property("FinishedOn") + .HasColumnType("datetime2"); + + b.Property("Outcome") + .HasColumnType("int"); + + b.Property("StartedOn") + .HasColumnType("datetime2"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("SubscriptionId", "StartedOn"); + + b.ToTable("ReceiveAttempts", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryAlertOverride", b => + { + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("GroupId") + .HasColumnType("uniqueidentifier"); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("AlertHandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("AlertMode") + .HasColumnType("tinyint"); + + b.HasKey("SubscriptionId", "GroupId"); + + b.ToTable("RetryAlertOverrides", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryGroupUsage", b => + { + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("GroupId") + .HasColumnType("uniqueidentifier"); + + b.Property("AttemptsUsed") + .HasColumnType("int"); + + b.Property("ExhaustedNotifiedOn") + .HasColumnType("datetime2"); + + b.Property("LastAttemptOn") + .HasColumnType("datetime2"); + + b.HasKey("SubscriptionId", "GroupId"); + + b.ToTable("RetryGroupUsages", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryPolicy", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("AlertHandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Groups") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.ToTable("RetryPolicies", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Setting", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Value") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Settings", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AggregateOn") + .HasColumnType("datetime2"); + + b.Property("AggregationForId") + .HasColumnType("int"); + + b.Property("AggregationTarget") + .HasColumnType("tinyint"); + + b.Property("CategoryId") + .HasColumnType("int"); + + b.Property("ConsecutiveFailures") + .HasColumnType("int"); + + b.Property("CustomRetryPolicy") + .HasColumnType("nvarchar(max)"); + + b.Property("DocumentFilter") + .HasColumnType("nvarchar(max)"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("Inactive") + .HasColumnType("bit"); + + b.Property("IsRunning") + .HasColumnType("bit"); + + b.Property("LastException") + .HasColumnType("nvarchar(max)"); + + b.Property("MapperId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("MapperProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("MatchExpression") + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("PausedOn") + .HasColumnType("datetime2"); + + b.Property("ReceiveOn") + .HasColumnType("datetime2"); + + b.Property("ReceiverId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ReceiverProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("int"); + + b.Property("RetryPolicyId") + .HasColumnType("int"); + + b.Property("Temporary") + .HasColumnType("bit"); + + b.Property("Type") + .HasColumnType("tinyint"); + + b.Property("ValidatorId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ValidatorProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("WorkGroupId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("AggregationForId"); + + b.HasIndex("CategoryId"); + + b.HasIndex("DocumentId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("ResponseSubscriptionId"); + + b.HasIndex("RetryPolicyId"); + + b.HasIndex("WorkGroupId"); + + b.ToTable("Subscriptions", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Code") + .HasColumnType("nvarchar(450)"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique() + .HasFilter("[Code] IS NOT NULL"); + + b.ToTable("SubscriptionCategory"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionTrail", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("Code") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("StateAfter") + .HasColumnType("nvarchar(max)"); + + b.Property("StateBefore") + .HasColumnType("nvarchar(max)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("CreatedOn"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("SubscriptionTrail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.WorkGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BusMessageName") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("Options") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("CorrelationId") + .HasColumnType("nvarchar(max)"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("InputContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("InputHash") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("InputName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("InputSize") + .HasColumnType("int"); + + b.Property("ManualRetry") + .HasColumnType("bit"); + + b.Property("MapperId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("MapperProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("References") + .HasMaxLength(1024) + .HasColumnType("nvarchar(1024)"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("int"); + + b.Property("RetryFor") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("StartedOn") + .HasColumnType("datetime2"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.HasIndex("InputHash"); + + b.HasIndex("RetryFor"); + + b.HasIndex("StartedOn"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("Xchanges", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AggregatedOn") + .HasColumnType("datetime2"); + + b.Property("AggregationXchangeId") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("AggregationXchangeId"); + + b.ToTable("XchangeAggregations", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("DeliveredOn") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("DeliveredOn"); + + b.ToTable("XchangeDeliveries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeNotification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Exception") + .HasColumnType("nvarchar(max)"); + + b.Property("FinishedOn") + .HasColumnType("datetime2"); + + b.Property("NotifierId") + .HasColumnType("int"); + + b.Property("NotifierName") + .HasColumnType("nvarchar(max)"); + + b.Property("Success") + .HasColumnType("bit"); + + b.Property("XchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.ToTable("XchangeNotifications", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("Hits") + .HasMaxLength(2000) + .IsUnicode(false) + .HasColumnType("varchar(2000)"); + + b.Property("Properties") + .HasColumnType("nvarchar(max)"); + + b.Property("PropertiesRaw") + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("PropertiesRaw"); + + b.ToTable("XchangePromotedProperties", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AttemptNumber") + .HasColumnType("int"); + + b.Property("Exception") + .HasColumnType("nvarchar(max)"); + + b.Property("FinishedOn") + .HasColumnType("datetime2"); + + b.Property("OutputBad") + .HasColumnType("bit"); + + b.Property("OutputContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("OutputHash") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("OutputName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("OutputSize") + .HasColumnType("int"); + + b.Property("ResponseBad") + .HasColumnType("bit"); + + b.Property("ResponseContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ResponseHash") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("ResponseName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("ResponseSize") + .HasColumnType("int"); + + b.Property("ResponseXchangeId") + .HasColumnType("nvarchar(max)"); + + b.Property("RetryBlockedReason") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("RetryGroupId") + .HasColumnType("uniqueidentifier"); + + b.Property("Success") + .HasColumnType("bit"); + + b.HasKey("Id"); + + b.HasIndex("RetryGroupId"); + + b.ToTable("XchangeResults", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.RunFlagUpdater+RunningResult", b => + { + b.Property("IsRunning") + .HasColumnType("bit"); + + b.ToTable((string)null); + + b.ToView(null, (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.JobExecution", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Context") + .HasColumnType("nvarchar(max)") + .HasColumnName("context"); + + b.Property("DurationMs") + .HasColumnType("bigint") + .HasColumnName("duration_ms"); + + b.Property("EndTimeUtc") + .HasColumnType("datetime2") + .HasColumnName("end_time_utc"); + + b.Property("Error") + .HasColumnType("nvarchar(max)") + .HasColumnName("error"); + + b.Property("FireInstanceId") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("fire_instance_id"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_name"); + + b.Property("JobTypeName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_type_name"); + + b.Property("Node") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("node"); + + b.Property("StartTimeUtc") + .HasColumnType("datetime2") + .HasColumnName("start_time_utc"); + + b.Property("Success") + .HasColumnType("bit") + .HasColumnName("success"); + + b.HasKey("Id"); + + b.HasIndex("FireInstanceId") + .IsUnique() + .HasDatabaseName("idx_je_fire_instance_id"); + + b.HasIndex("StartTimeUtc") + .HasDatabaseName("idx_je_start_time"); + + b.HasIndex("Success") + .HasDatabaseName("idx_je_success"); + + b.HasIndex("JobGroup", "JobName", "StartTimeUtc") + .HasDatabaseName("idx_je_group_name_start"); + + b.ToTable("job_executions", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("BlobData") + .HasColumnType("varbinary(max)") + .HasColumnName("blob_data"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_blob_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCalendar", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("CalendarName") + .HasColumnType("nvarchar(450)") + .HasColumnName("calendar_name"); + + b.Property("Calendar") + .IsRequired() + .HasColumnType("varbinary(max)") + .HasColumnName("calendar"); + + b.HasKey("SchedulerName", "CalendarName"); + + b.ToTable("QRTZ_calendars", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("CronExpression") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("cron_expression"); + + b.Property("TimeZoneId") + .HasColumnType("nvarchar(450)") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_cron_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzFiredTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("EntryId") + .HasColumnType("nvarchar(450)") + .HasColumnName("entry_id"); + + b.Property("FiredTime") + .HasColumnType("bigint") + .HasColumnName("fired_time"); + + b.Property("InstanceName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("instance_name"); + + b.Property("IsNonConcurrent") + .HasColumnType("bit") + .HasColumnName("is_nonconcurrent"); + + b.Property("JobGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("job_group"); + + b.Property("JobName") + .HasColumnType("nvarchar(450)") + .HasColumnName("job_name"); + + b.Property("Priority") + .HasColumnType("int") + .HasColumnName("priority"); + + b.Property("RequestsRecovery") + .HasColumnType("bit") + .HasColumnName("requests_recovery"); + + b.Property("ScheduledTime") + .HasColumnType("bigint") + .HasColumnName("sched_time"); + + b.Property("State") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("state"); + + b.Property("TriggerGroup") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("TriggerName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.HasKey("SchedulerName", "EntryId"); + + b.HasIndex("InstanceName") + .HasDatabaseName("idx_QRTZ_ft_trig_inst_name"); + + b.HasIndex("JobGroup") + .HasDatabaseName("idx_QRTZ_ft_job_group"); + + b.HasIndex("JobName") + .HasDatabaseName("idx_QRTZ_ft_job_name"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_QRTZ_ft_job_req_recovery"); + + b.HasIndex("TriggerGroup") + .HasDatabaseName("idx_QRTZ_ft_trig_group"); + + b.HasIndex("TriggerName") + .HasDatabaseName("idx_QRTZ_ft_trig_name"); + + b.HasIndex("SchedulerName", "TriggerName", "TriggerGroup") + .HasDatabaseName("idx_QRTZ_ft_trig_nm_gp"); + + b.ToTable("QRTZ_fired_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("JobName") + .HasColumnType("nvarchar(450)") + .HasColumnName("job_name"); + + b.Property("JobGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("job_group"); + + b.Property("Description") + .HasColumnType("nvarchar(450)") + .HasColumnName("description"); + + b.Property("IsDurable") + .HasColumnType("bit") + .HasColumnName("is_durable"); + + b.Property("IsNonConcurrent") + .HasColumnType("bit") + .HasColumnName("is_nonconcurrent"); + + b.Property("IsUpdateData") + .HasColumnType("bit") + .HasColumnName("is_update_data"); + + b.Property("JobClassName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_class_name"); + + b.Property("JobData") + .HasColumnType("varbinary(max)") + .HasColumnName("job_data"); + + b.Property("RequestsRecovery") + .HasColumnType("bit") + .HasColumnName("requests_recovery"); + + b.HasKey("SchedulerName", "JobName", "JobGroup"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_j_req_recovery"); + + b.ToTable("QRTZ_job_details", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzLock", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("LockName") + .HasColumnType("nvarchar(450)") + .HasColumnName("lock_name"); + + b.HasKey("SchedulerName", "LockName"); + + b.ToTable("QRTZ_locks", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzPausedTriggerGroup", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.HasKey("SchedulerName", "TriggerGroup"); + + b.ToTable("QRTZ_paused_trigger_grps", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSchedulerState", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("InstanceName") + .HasColumnType("nvarchar(450)") + .HasColumnName("instance_name"); + + b.Property("CheckInInterval") + .HasColumnType("bigint") + .HasColumnName("checkin_interval"); + + b.Property("LastCheckInTime") + .HasColumnType("bigint") + .HasColumnName("last_checkin_time"); + + b.HasKey("SchedulerName", "InstanceName"); + + b.ToTable("QRTZ_scheduler_state", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("BooleanProperty1") + .HasColumnType("bit") + .HasColumnName("bool_prop_1"); + + b.Property("BooleanProperty2") + .HasColumnType("bit") + .HasColumnName("bool_prop_2"); + + b.Property("DecimalProperty1") + .HasColumnType("numeric") + .HasColumnName("dec_prop_1"); + + b.Property("DecimalProperty2") + .HasColumnType("numeric") + .HasColumnName("dec_prop_2"); + + b.Property("IntegerProperty1") + .HasColumnType("int") + .HasColumnName("int_prop_1"); + + b.Property("IntegerProperty2") + .HasColumnType("int") + .HasColumnName("int_prop_2"); + + b.Property("LongProperty1") + .HasColumnType("bigint") + .HasColumnName("long_prop_1"); + + b.Property("LongProperty2") + .HasColumnType("bigint") + .HasColumnName("long_prop_2"); + + b.Property("StringProperty1") + .HasColumnType("nvarchar(450)") + .HasColumnName("str_prop_1"); + + b.Property("StringProperty2") + .HasColumnType("nvarchar(450)") + .HasColumnName("str_prop_2"); + + b.Property("StringProperty3") + .HasColumnType("nvarchar(450)") + .HasColumnName("str_prop_3"); + + b.Property("TimeZoneId") + .HasColumnType("nvarchar(450)") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_simprop_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("RepeatCount") + .HasColumnType("bigint") + .HasColumnName("repeat_count"); + + b.Property("RepeatInterval") + .HasColumnType("bigint") + .HasColumnName("repeat_interval"); + + b.Property("TimesTriggered") + .HasColumnType("bigint") + .HasColumnName("times_triggered"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_simple_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("CalendarName") + .HasColumnType("nvarchar(450)") + .HasColumnName("calendar_name"); + + b.Property("Description") + .HasColumnType("nvarchar(450)") + .HasColumnName("description"); + + b.Property("EndTime") + .HasColumnType("bigint") + .HasColumnName("end_time"); + + b.Property("JobData") + .HasColumnType("varbinary(max)") + .HasColumnName("job_data"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_name"); + + b.Property("MisfireInstruction") + .HasColumnType("int") + .HasColumnName("misfire_instr"); + + b.Property("NextFireTime") + .HasColumnType("bigint") + .HasColumnName("next_fire_time"); + + b.Property("PreviousFireTime") + .HasColumnType("bigint") + .HasColumnName("prev_fire_time"); + + b.Property("Priority") + .HasColumnType("int") + .HasColumnName("priority"); + + b.Property("StartTime") + .HasColumnType("bigint") + .HasColumnName("start_time"); + + b.Property("TriggerState") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_state"); + + b.Property("TriggerType") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_type"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.HasIndex("NextFireTime") + .HasDatabaseName("idx_t_next_fire_time"); + + b.HasIndex("TriggerState") + .HasDatabaseName("idx_t_state"); + + b.HasIndex("NextFireTime", "TriggerState") + .HasDatabaseName("idx_t_nft_st"); + + b.HasIndex("SchedulerName", "JobName", "JobGroup"); + + b.ToTable("QRTZ_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.AccountRoleLink", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Accounts.Role", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DocumentTrail", b => + { + b.HasOne("SW.Bitween.Domain.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Document"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.ApiGateway", "ApiGateway") + .WithMany("Partners") + .HasForeignKey("ApiGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("ApiGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.BusGateway", "BusGateway") + .WithMany("Routes") + .HasForeignKey("BusGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("BusGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.OwnsMany("SW.Bitween.Domain.ApiCredential", "ApiCredentials", b1 => + { + b1.Property("PartnerId") + .HasColumnType("int"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b1.Property("Id")); + + b1.Property("Key") + .IsRequired() + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b1.Property("Name") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b1.HasKey("PartnerId", "Id"); + + b1.HasIndex("Key") + .IsUnique(); + + b1.ToTable("PartnerApiCredentials", (string)null); + + b1.WithOwner() + .HasForeignKey("PartnerId"); + + b1.HasData( + new + { + PartnerId = 1, + Id = 1, + Key = "7facc758283844b49cc4ffd26a75b1de", + Name = "default" + }); + }); + + b.Navigation("ApiCredentials"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.HasOne("SW.Bitween.Domain.Subscription", null) + .WithMany() + .HasForeignKey("AggregationForId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("FK_Subscriptions_AggFor"); + + b.HasOne("SW.Bitween.Domain.SubscriptionCategory", "Category") + .WithMany() + .HasForeignKey("CategoryId"); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", null) + .WithMany("Subscriptions") + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Subscription", null) + .WithMany() + .HasForeignKey("ResponseSubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("FK_Subscriptions_RespSub"); + + b.HasOne("SW.Bitween.Domain.RetryPolicy", "RetryPolicy") + .WithMany() + .HasForeignKey("RetryPolicyId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("SW.Bitween.Domain.WorkGroup", "WorkGroup") + .WithMany() + .HasForeignKey("WorkGroupId"); + + b.OwnsMany("SW.Bitween.Domain.Schedule", "Schedules", b1 => + { + b1.Property("SubscriptionId") + .HasColumnType("int"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b1.Property("Id")); + + b1.Property("Backwards") + .HasColumnType("bit"); + + b1.Property("On") + .HasColumnType("bigint"); + + b1.Property("Recurrence") + .HasColumnType("tinyint"); + + b1.HasKey("SubscriptionId", "Id"); + + b1.ToTable("SubscriptionSchedules", (string)null); + + b1.WithOwner() + .HasForeignKey("SubscriptionId"); + }); + + b.Navigation("Category"); + + b.Navigation("RetryPolicy"); + + b.Navigation("Schedules"); + + b.Navigation("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionTrail", b => + { + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeAggregation", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeDelivery", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangePromotedProperties", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeResult", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("BlobTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("CronTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimplePropertyTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimpleTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzJobDetail", "JobDetail") + .WithMany("Triggers") + .HasForeignKey("SchedulerName", "JobName", "JobGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("JobDetail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Navigation("Partners"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Navigation("Routes"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Navigation("Subscriptions"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Navigation("Triggers"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Navigation("BlobTriggers"); + + b.Navigation("CronTriggers"); + + b.Navigation("SimplePropertyTriggers"); + + b.Navigation("SimpleTriggers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/SW.Bitween.MsSql/Migrations/20260824093631_AddReceiveAttempts.cs b/SW.Bitween.MsSql/Migrations/20260824093631_AddReceiveAttempts.cs new file mode 100644 index 00000000..044c3a9b --- /dev/null +++ b/SW.Bitween.MsSql/Migrations/20260824093631_AddReceiveAttempts.cs @@ -0,0 +1,45 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace SW.Bitween.MsSql.Migrations +{ + /// + public partial class AddReceiveAttempts : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "ReceiveAttempts", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + SubscriptionId = table.Column(type: "int", nullable: false), + StartedOn = table.Column(type: "datetime2", nullable: false), + FinishedOn = table.Column(type: "datetime2", nullable: false), + Outcome = table.Column(type: "int", nullable: false), + ErrorMessage = table.Column(type: "nvarchar(4000)", maxLength: 4000, nullable: true), + ExchangeIds = table.Column(type: "nvarchar(max)", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_ReceiveAttempts", x => x.Id); + }); + + migrationBuilder.CreateIndex( + name: "IX_ReceiveAttempts_SubscriptionId_StartedOn", + table: "ReceiveAttempts", + columns: new[] { "SubscriptionId", "StartedOn" }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "ReceiveAttempts"); + } + } +} diff --git a/SW.Bitween.MsSql/Migrations/BitweenDbContextModelSnapshot.cs b/SW.Bitween.MsSql/Migrations/BitweenDbContextModelSnapshot.cs index 8b393eb8..de43d8c9 100644 --- a/SW.Bitween.MsSql/Migrations/BitweenDbContextModelSnapshot.cs +++ b/SW.Bitween.MsSql/Migrations/BitweenDbContextModelSnapshot.cs @@ -57,6 +57,12 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("EmailProvider") .HasColumnType("tinyint"); + b.Property("FailedLoginCount") + .HasColumnType("int"); + + b.Property("LockoutEnd") + .HasColumnType("datetime2"); + b.Property("LoginMethods") .HasColumnType("tinyint"); @@ -92,6 +98,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) DisplayName = "Admin", Email = "admin@Bitween.systems", EmailProvider = (byte)0, + FailedLoginCount = 0, LoginMethods = (byte)2, Password = "$SWHASH$V1$10000$VQCi48eitH4Ml5juvBMOFZrMdQwBbhuIQVXe6RR7qJdDF2bJ", Role = 0 @@ -215,9 +222,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) .IsUnicode(false) .HasColumnType("varchar(50)"); - b.Property("GroupAttemptCounts") - .HasColumnType("nvarchar(max)"); - b.Property("On") .HasColumnType("datetime2"); @@ -342,6 +346,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("CreatedOn") .HasColumnType("datetime2"); + b.Property("Inactive") + .HasColumnType("bit"); + b.Property("ModifiedBy") .HasColumnType("nvarchar(max)"); @@ -415,6 +422,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("DocumentId") .HasColumnType("int"); + b.Property("Inactive") + .HasColumnType("bit"); + b.Property("ModifiedBy") .HasColumnType("nvarchar(max)"); @@ -595,6 +605,86 @@ protected override void BuildModel(ModelBuilder modelBuilder) }); }); + modelBuilder.Entity("SW.Bitween.Domain.ReceiveAttempt", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ErrorMessage") + .HasMaxLength(4000) + .HasColumnType("nvarchar(4000)"); + + b.Property("ExchangeIds") + .HasColumnType("nvarchar(max)"); + + b.Property("FinishedOn") + .HasColumnType("datetime2"); + + b.Property("Outcome") + .HasColumnType("int"); + + b.Property("StartedOn") + .HasColumnType("datetime2"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("SubscriptionId", "StartedOn"); + + b.ToTable("ReceiveAttempts", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryAlertOverride", b => + { + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("GroupId") + .HasColumnType("uniqueidentifier"); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("AlertHandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("AlertMode") + .HasColumnType("tinyint"); + + b.HasKey("SubscriptionId", "GroupId"); + + b.ToTable("RetryAlertOverrides", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryGroupUsage", b => + { + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("GroupId") + .HasColumnType("uniqueidentifier"); + + b.Property("AttemptsUsed") + .HasColumnType("int"); + + b.Property("ExhaustedNotifiedOn") + .HasColumnType("datetime2"); + + b.Property("LastAttemptOn") + .HasColumnType("datetime2"); + + b.HasKey("SubscriptionId", "GroupId"); + + b.ToTable("RetryGroupUsages", (string)null); + }); + modelBuilder.Entity("SW.Bitween.Domain.RetryPolicy", b => { b.Property("Id") @@ -603,6 +693,14 @@ protected override void BuildModel(ModelBuilder modelBuilder) SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + b.Property("AlertHandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("AlertHandlerProperties") + .HasColumnType("nvarchar(max)"); + b.Property("CreatedBy") .HasColumnType("nvarchar(max)"); @@ -890,9 +988,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("DocumentId") .HasColumnType("int"); - b.Property("GroupAttemptCounts") - .HasColumnType("nvarchar(max)"); - b.Property("HandlerId") .HasMaxLength(200) .IsUnicode(false) @@ -919,6 +1014,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("InputSize") .HasColumnType("int"); + b.Property("ManualRetry") + .HasColumnType("bit"); + b.Property("MapperId") .HasMaxLength(200) .IsUnicode(false) @@ -1022,7 +1120,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("FinishedOn") .HasColumnType("datetime2"); - b.Property("NotifierId") + b.Property("NotifierId") .HasColumnType("int"); b.Property("NotifierName") @@ -1073,6 +1171,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) .IsUnicode(false) .HasColumnType("varchar(50)"); + b.Property("AttemptNumber") + .HasColumnType("int"); + b.Property("Exception") .HasColumnType("nvarchar(max)"); @@ -1122,11 +1223,20 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("ResponseXchangeId") .HasColumnType("nvarchar(max)"); + b.Property("RetryBlockedReason") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("RetryGroupId") + .HasColumnType("uniqueidentifier"); + b.Property("Success") .HasColumnType("bit"); b.HasKey("Id"); + b.HasIndex("RetryGroupId"); + b.ToTable("XchangeResults", (string)null); }); diff --git a/SW.Bitween.MySql/Migrations/20260811081221_SharedRetryGroupTotals.Designer.cs b/SW.Bitween.MySql/Migrations/20260811081221_SharedRetryGroupTotals.Designer.cs new file mode 100644 index 00000000..3ed0e858 --- /dev/null +++ b/SW.Bitween.MySql/Migrations/20260811081221_SharedRetryGroupTotals.Designer.cs @@ -0,0 +1,1899 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using SW.Bitween.MySql; + +#nullable disable + +namespace SW.Bitween.MySql.Migrations +{ + [DbContext(typeof(BitweenDbContext))] + [Migration("20260811081221_SharedRetryGroupTotals")] + partial class SharedRetryGroupTotals + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.23") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Deleted") + .HasColumnType("tinyint(1)"); + + b.Property("Disabled") + .HasColumnType("tinyint(1)"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("Email") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("EmailProvider") + .HasColumnType("tinyint unsigned"); + + b.Property("LoginMethods") + .HasColumnType("tinyint unsigned"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Password") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("Phone") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("varchar(20)"); + + b.Property("Role") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique(); + + b.ToTable("Accounts", (string)null); + + b.HasData( + new + { + Id = 9999, + CreatedOn = new DateTime(2021, 12, 31, 22, 0, 0, 0, DateTimeKind.Utc), + Deleted = false, + Disabled = false, + DisplayName = "Admin", + Email = "admin@Bitween.systems", + EmailProvider = (byte)0, + LoginMethods = (byte)2, + Password = "$SWHASH$V1$10000$VQCi48eitH4Ml5juvBMOFZrMdQwBbhuIQVXe6RR7qJdDF2bJ", + Role = 0 + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AccountId") + .HasColumnType("int"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("LoginMethod") + .HasColumnType("tinyint unsigned"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.ToTable("RefreshTokens", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DelayedRetry", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("On") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("On"); + + b.ToTable("DelayedRetries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Document", b => + { + b.Property("Id") + .HasColumnType("int"); + + b.Property("BusEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("BusMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("DisregardsUnfilteredMessages") + .HasColumnType("tinyint(1)"); + + b.Property("DocumentFormat") + .HasColumnType("int"); + + b.Property("DuplicateInterval") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("PromotedProperties") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("BusMessageTypeName") + .IsUnique(); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Documents", (string)null); + + b.HasData( + new + { + Id = 10001, + BusEnabled = false, + DocumentFormat = 0, + DuplicateInterval = 0, + Name = "Aggregation Document", + PromotedProperties = "{}" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DocumentTrail", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Code") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("StateAfter") + .HasColumnType("longtext"); + + b.Property("StateBefore") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("CreatedOn"); + + b.HasIndex("DocumentId"); + + b.ToTable("DocumentTrail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("UrlName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("UrlName") + .IsUnique(); + + b.ToTable("ApiGateways", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.Property("ApiGatewayId") + .HasColumnType("int"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.HasKey("ApiGatewayId", "PartnerId", "SubscriptionId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("ApiGatewayPartners", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.ToTable("BusGateways", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("BusGatewayId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("MatchExpression") + .HasColumnType("longtext"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("BusGatewayId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("BusGatewayRoutes", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.GlobalAdapterValuesSet", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("Values") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("GlobalAdapterValuesSets", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Notifier", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("longtext"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("RunOnBadResult") + .HasColumnType("tinyint(1)"); + + b.Property("RunOnFailedResult") + .HasColumnType("tinyint(1)"); + + b.Property("RunOnSubscriptions") + .HasColumnType("longtext"); + + b.Property("RunOnSuccessfulResult") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.ToTable("Notifiers", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OnHoldXchange", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("BadData") + .HasColumnType("tinyint(1)"); + + b.Property("Data") + .HasColumnType("longtext"); + + b.Property("FileName") + .HasColumnType("longtext"); + + b.Property("References") + .HasMaxLength(1024) + .HasColumnType("varchar(1024)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("OnHoldXchanges", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AdapterProperties") + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.ToTable("Partners", (string)null); + + b.HasData( + new + { + Id = 1, + Name = "SYSTEM" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryGroupUsage", b => + { + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("GroupId") + .HasColumnType("char(36)"); + + b.Property("AttemptsUsed") + .HasColumnType("int"); + + b.Property("LastAttemptOn") + .HasColumnType("datetime(6)"); + + b.HasKey("SubscriptionId", "GroupId"); + + b.ToTable("RetryGroupUsages", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryPolicy", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Groups") + .HasColumnType("longtext"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.ToTable("RetryPolicies", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AggregateOn") + .HasColumnType("datetime(6)"); + + b.Property("AggregationForId") + .HasColumnType("int"); + + b.Property("AggregationTarget") + .HasColumnType("tinyint unsigned"); + + b.Property("CategoryId") + .HasColumnType("int"); + + b.Property("ConsecutiveFailures") + .HasColumnType("int"); + + b.Property("CustomRetryPolicy") + .HasColumnType("longtext"); + + b.Property("DocumentFilter") + .HasColumnType("longtext"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("longtext"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("IsRunning") + .HasColumnType("tinyint(1)"); + + b.Property("LastException") + .HasColumnType("longtext"); + + b.Property("MapperId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("MapperProperties") + .HasColumnType("longtext"); + + b.Property("MatchExpression") + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("PausedOn") + .HasColumnType("datetime(6)"); + + b.Property("ReceiveOn") + .HasColumnType("datetime(6)"); + + b.Property("ReceiverId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ReceiverProperties") + .HasColumnType("longtext"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("int"); + + b.Property("RetryPolicyId") + .HasColumnType("int"); + + b.Property("Temporary") + .HasColumnType("tinyint(1)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("ValidatorId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ValidatorProperties") + .HasColumnType("longtext"); + + b.Property("WorkGroupId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("AggregationForId"); + + b.HasIndex("CategoryId"); + + b.HasIndex("DocumentId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("ResponseSubscriptionId"); + + b.HasIndex("RetryPolicyId"); + + b.HasIndex("WorkGroupId"); + + b.ToTable("Subscriptions", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Code") + .HasColumnType("varchar(255)"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Description") + .HasColumnType("longtext"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.ToTable("SubscriptionCategory"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionTrail", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Code") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("StateAfter") + .HasColumnType("longtext"); + + b.Property("StateBefore") + .HasColumnType("longtext"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("CreatedOn"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("SubscriptionTrail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.WorkGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("BusMessageName") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("Options") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("CorrelationId") + .HasColumnType("longtext"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("longtext"); + + b.Property("InputContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("InputHash") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("InputName") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("InputSize") + .HasColumnType("int"); + + b.Property("MapperId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("MapperProperties") + .HasColumnType("longtext"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("References") + .HasMaxLength(1024) + .HasColumnType("varchar(1024)"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("int"); + + b.Property("RetryFor") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("StartedOn") + .HasColumnType("datetime(6)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.HasIndex("InputHash"); + + b.HasIndex("RetryFor"); + + b.HasIndex("StartedOn"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("Xchanges", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AggregatedOn") + .HasColumnType("datetime(6)"); + + b.Property("AggregationXchangeId") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("AggregationXchangeId"); + + b.ToTable("XchangeAggregations", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("DeliveredOn") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("DeliveredOn"); + + b.ToTable("XchangeDeliveries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeNotification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Exception") + .HasColumnType("longtext"); + + b.Property("FinishedOn") + .HasColumnType("datetime(6)"); + + b.Property("NotifierId") + .HasColumnType("int"); + + b.Property("NotifierName") + .HasColumnType("longtext"); + + b.Property("Success") + .HasColumnType("tinyint(1)"); + + b.Property("XchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.ToTable("XchangeNotifications", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("Hits") + .HasMaxLength(2000) + .IsUnicode(false) + .HasColumnType("varchar(2000)"); + + b.Property("Properties") + .HasColumnType("longtext"); + + b.Property("PropertiesRaw") + .HasColumnType("varchar(255)"); + + b.HasKey("Id"); + + b.HasIndex("PropertiesRaw"); + + b.ToTable("XchangePromotedProperties", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("Exception") + .HasColumnType("longtext"); + + b.Property("FinishedOn") + .HasColumnType("datetime(6)"); + + b.Property("OutputBad") + .HasColumnType("tinyint(1)"); + + b.Property("OutputContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("OutputHash") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("OutputName") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("OutputSize") + .HasColumnType("int"); + + b.Property("ResponseBad") + .HasColumnType("tinyint(1)"); + + b.Property("ResponseContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ResponseHash") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("ResponseName") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("ResponseSize") + .HasColumnType("int"); + + b.Property("ResponseXchangeId") + .HasColumnType("longtext"); + + b.Property("Success") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.ToTable("XchangeResults", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.RunFlagUpdater+RunningResult", b => + { + b.Property("IsRunning") + .HasColumnType("tinyint(1)"); + + b.ToTable((string)null); + + b.ToView(null, (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.JobExecution", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Context") + .HasColumnType("longtext") + .HasColumnName("context"); + + b.Property("DurationMs") + .HasColumnType("bigint") + .HasColumnName("duration_ms"); + + b.Property("EndTimeUtc") + .HasColumnType("datetime(6)") + .HasColumnName("end_time_utc"); + + b.Property("Error") + .HasColumnType("longtext") + .HasColumnName("error"); + + b.Property("FireInstanceId") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("fire_instance_id"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_name"); + + b.Property("JobTypeName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_type_name"); + + b.Property("Node") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("node"); + + b.Property("StartTimeUtc") + .HasColumnType("datetime(6)") + .HasColumnName("start_time_utc"); + + b.Property("Success") + .HasColumnType("tinyint(1)") + .HasColumnName("success"); + + b.HasKey("Id"); + + b.HasIndex("FireInstanceId") + .IsUnique() + .HasDatabaseName("idx_je_fire_instance_id"); + + b.HasIndex("StartTimeUtc") + .HasDatabaseName("idx_je_start_time"); + + b.HasIndex("Success") + .HasDatabaseName("idx_je_success"); + + b.HasIndex("JobGroup", "JobName", "StartTimeUtc") + .HasDatabaseName("idx_je_group_name_start"); + + b.ToTable("job_executions", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("BlobData") + .HasColumnType("longblob") + .HasColumnName("blob_data"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_blob_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCalendar", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("CalendarName") + .HasColumnType("varchar(200)") + .HasColumnName("calendar_name"); + + b.Property("Calendar") + .IsRequired() + .HasColumnType("longblob") + .HasColumnName("calendar"); + + b.HasKey("SchedulerName", "CalendarName"); + + b.ToTable("QRTZ_calendars", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("CronExpression") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("cron_expression"); + + b.Property("TimeZoneId") + .HasColumnType("varchar(200)") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_cron_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzFiredTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("EntryId") + .HasColumnType("varchar(200)") + .HasColumnName("entry_id"); + + b.Property("FiredTime") + .HasColumnType("bigint") + .HasColumnName("fired_time"); + + b.Property("InstanceName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("instance_name"); + + b.Property("IsNonConcurrent") + .HasColumnType("tinyint(1)") + .HasColumnName("is_nonconcurrent"); + + b.Property("JobGroup") + .HasColumnType("varchar(200)") + .HasColumnName("job_group"); + + b.Property("JobName") + .HasColumnType("varchar(200)") + .HasColumnName("job_name"); + + b.Property("Priority") + .HasColumnType("int") + .HasColumnName("priority"); + + b.Property("RequestsRecovery") + .HasColumnType("tinyint(1)") + .HasColumnName("requests_recovery"); + + b.Property("ScheduledTime") + .HasColumnType("bigint") + .HasColumnName("sched_time"); + + b.Property("State") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("state"); + + b.Property("TriggerGroup") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("TriggerName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.HasKey("SchedulerName", "EntryId"); + + b.HasIndex("InstanceName") + .HasDatabaseName("idx_QRTZ_ft_trig_inst_name"); + + b.HasIndex("JobGroup") + .HasDatabaseName("idx_QRTZ_ft_job_group"); + + b.HasIndex("JobName") + .HasDatabaseName("idx_QRTZ_ft_job_name"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_QRTZ_ft_job_req_recovery"); + + b.HasIndex("TriggerGroup") + .HasDatabaseName("idx_QRTZ_ft_trig_group"); + + b.HasIndex("TriggerName") + .HasDatabaseName("idx_QRTZ_ft_trig_name"); + + b.HasIndex("SchedulerName", "TriggerName", "TriggerGroup") + .HasDatabaseName("idx_QRTZ_ft_trig_nm_gp"); + + b.ToTable("QRTZ_fired_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("JobName") + .HasColumnType("varchar(200)") + .HasColumnName("job_name"); + + b.Property("JobGroup") + .HasColumnType("varchar(200)") + .HasColumnName("job_group"); + + b.Property("Description") + .HasColumnType("varchar(200)") + .HasColumnName("description"); + + b.Property("IsDurable") + .HasColumnType("tinyint(1)") + .HasColumnName("is_durable"); + + b.Property("IsNonConcurrent") + .HasColumnType("tinyint(1)") + .HasColumnName("is_nonconcurrent"); + + b.Property("IsUpdateData") + .HasColumnType("tinyint(1)") + .HasColumnName("is_update_data"); + + b.Property("JobClassName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_class_name"); + + b.Property("JobData") + .HasColumnType("longblob") + .HasColumnName("job_data"); + + b.Property("RequestsRecovery") + .HasColumnType("tinyint(1)") + .HasColumnName("requests_recovery"); + + b.HasKey("SchedulerName", "JobName", "JobGroup"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_j_req_recovery"); + + b.ToTable("QRTZ_job_details", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzLock", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("LockName") + .HasColumnType("varchar(200)") + .HasColumnName("lock_name"); + + b.HasKey("SchedulerName", "LockName"); + + b.ToTable("QRTZ_locks", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzPausedTriggerGroup", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.HasKey("SchedulerName", "TriggerGroup"); + + b.ToTable("QRTZ_paused_trigger_grps", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSchedulerState", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("InstanceName") + .HasColumnType("varchar(200)") + .HasColumnName("instance_name"); + + b.Property("CheckInInterval") + .HasColumnType("bigint") + .HasColumnName("checkin_interval"); + + b.Property("LastCheckInTime") + .HasColumnType("bigint") + .HasColumnName("last_checkin_time"); + + b.HasKey("SchedulerName", "InstanceName"); + + b.ToTable("QRTZ_scheduler_state", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("BooleanProperty1") + .HasColumnType("tinyint(1)") + .HasColumnName("bool_prop_1"); + + b.Property("BooleanProperty2") + .HasColumnType("tinyint(1)") + .HasColumnName("bool_prop_2"); + + b.Property("DecimalProperty1") + .HasColumnType("numeric") + .HasColumnName("dec_prop_1"); + + b.Property("DecimalProperty2") + .HasColumnType("numeric") + .HasColumnName("dec_prop_2"); + + b.Property("IntegerProperty1") + .HasColumnType("int") + .HasColumnName("int_prop_1"); + + b.Property("IntegerProperty2") + .HasColumnType("int") + .HasColumnName("int_prop_2"); + + b.Property("LongProperty1") + .HasColumnType("bigint") + .HasColumnName("long_prop_1"); + + b.Property("LongProperty2") + .HasColumnType("bigint") + .HasColumnName("long_prop_2"); + + b.Property("StringProperty1") + .HasColumnType("varchar(200)") + .HasColumnName("str_prop_1"); + + b.Property("StringProperty2") + .HasColumnType("varchar(200)") + .HasColumnName("str_prop_2"); + + b.Property("StringProperty3") + .HasColumnType("varchar(200)") + .HasColumnName("str_prop_3"); + + b.Property("TimeZoneId") + .HasColumnType("varchar(200)") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_simprop_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("RepeatCount") + .HasColumnType("bigint") + .HasColumnName("repeat_count"); + + b.Property("RepeatInterval") + .HasColumnType("bigint") + .HasColumnName("repeat_interval"); + + b.Property("TimesTriggered") + .HasColumnType("bigint") + .HasColumnName("times_triggered"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_simple_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("CalendarName") + .HasColumnType("varchar(200)") + .HasColumnName("calendar_name"); + + b.Property("Description") + .HasColumnType("varchar(200)") + .HasColumnName("description"); + + b.Property("EndTime") + .HasColumnType("bigint") + .HasColumnName("end_time"); + + b.Property("JobData") + .HasColumnType("longblob") + .HasColumnName("job_data"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_name"); + + b.Property("MisfireInstruction") + .HasColumnType("int") + .HasColumnName("misfire_instr"); + + b.Property("NextFireTime") + .HasColumnType("bigint") + .HasColumnName("next_fire_time"); + + b.Property("PreviousFireTime") + .HasColumnType("bigint") + .HasColumnName("prev_fire_time"); + + b.Property("Priority") + .HasColumnType("int") + .HasColumnName("priority"); + + b.Property("StartTime") + .HasColumnType("bigint") + .HasColumnName("start_time"); + + b.Property("TriggerState") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("trigger_state"); + + b.Property("TriggerType") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("trigger_type"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.HasIndex("NextFireTime") + .HasDatabaseName("idx_t_next_fire_time"); + + b.HasIndex("TriggerState") + .HasDatabaseName("idx_t_state"); + + b.HasIndex("NextFireTime", "TriggerState") + .HasDatabaseName("idx_t_nft_st"); + + b.HasIndex("SchedulerName", "JobName", "JobGroup"); + + b.ToTable("QRTZ_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DocumentTrail", b => + { + b.HasOne("SW.Bitween.Domain.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Document"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.ApiGateway", "ApiGateway") + .WithMany("Partners") + .HasForeignKey("ApiGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("ApiGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.BusGateway", "BusGateway") + .WithMany("Routes") + .HasForeignKey("BusGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("BusGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.OwnsMany("SW.Bitween.Domain.ApiCredential", "ApiCredentials", b1 => + { + b1.Property("PartnerId") + .HasColumnType("int"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b1.Property("Id")); + + b1.Property("Key") + .IsRequired() + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b1.Property("Name") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b1.HasKey("PartnerId", "Id"); + + b1.HasIndex("Key") + .IsUnique(); + + b1.ToTable("PartnerApiCredentials", (string)null); + + b1.WithOwner() + .HasForeignKey("PartnerId"); + + b1.HasData( + new + { + PartnerId = 1, + Id = 1, + Key = "7facc758283844b49cc4ffd26a75b1de", + Name = "default" + }); + }); + + b.Navigation("ApiCredentials"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.HasOne("SW.Bitween.Domain.Subscription", null) + .WithMany() + .HasForeignKey("AggregationForId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("FK_Subscriptions_AggFor"); + + b.HasOne("SW.Bitween.Domain.SubscriptionCategory", "Category") + .WithMany() + .HasForeignKey("CategoryId"); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", null) + .WithMany("Subscriptions") + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Subscription", null) + .WithMany() + .HasForeignKey("ResponseSubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("FK_Subscriptions_RespSub"); + + b.HasOne("SW.Bitween.Domain.RetryPolicy", "RetryPolicy") + .WithMany() + .HasForeignKey("RetryPolicyId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("SW.Bitween.Domain.WorkGroup", "WorkGroup") + .WithMany() + .HasForeignKey("WorkGroupId"); + + b.OwnsMany("SW.Bitween.Domain.Schedule", "Schedules", b1 => + { + b1.Property("SubscriptionId") + .HasColumnType("int"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b1.Property("Id")); + + b1.Property("Backwards") + .HasColumnType("tinyint(1)"); + + b1.Property("On") + .HasColumnType("bigint"); + + b1.Property("Recurrence") + .HasColumnType("tinyint unsigned"); + + b1.HasKey("SubscriptionId", "Id"); + + b1.ToTable("SubscriptionSchedules", (string)null); + + b1.WithOwner() + .HasForeignKey("SubscriptionId"); + }); + + b.Navigation("Category"); + + b.Navigation("RetryPolicy"); + + b.Navigation("Schedules"); + + b.Navigation("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionTrail", b => + { + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeAggregation", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeDelivery", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangePromotedProperties", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeResult", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("BlobTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("CronTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimplePropertyTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimpleTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzJobDetail", "JobDetail") + .WithMany("Triggers") + .HasForeignKey("SchedulerName", "JobName", "JobGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("JobDetail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Navigation("Partners"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Navigation("Routes"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Navigation("Subscriptions"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Navigation("Triggers"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Navigation("BlobTriggers"); + + b.Navigation("CronTriggers"); + + b.Navigation("SimplePropertyTriggers"); + + b.Navigation("SimpleTriggers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/SW.Bitween.MySql/Migrations/20260811081221_SharedRetryGroupTotals.cs b/SW.Bitween.MySql/Migrations/20260811081221_SharedRetryGroupTotals.cs new file mode 100644 index 00000000..3bda1da3 --- /dev/null +++ b/SW.Bitween.MySql/Migrations/20260811081221_SharedRetryGroupTotals.cs @@ -0,0 +1,59 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace SW.Bitween.MySql.Migrations +{ + /// + public partial class SharedRetryGroupTotals : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "GroupAttemptCounts", + table: "Xchanges"); + + migrationBuilder.DropColumn( + name: "GroupAttemptCounts", + table: "DelayedRetries"); + + migrationBuilder.CreateTable( + name: "RetryGroupUsages", + columns: table => new + { + SubscriptionId = table.Column(type: "int", nullable: false), + GroupId = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + AttemptsUsed = table.Column(type: "int", nullable: false), + LastAttemptOn = table.Column(type: "datetime(6)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_RetryGroupUsages", x => new { x.SubscriptionId, x.GroupId }); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "RetryGroupUsages"); + + migrationBuilder.AddColumn( + name: "GroupAttemptCounts", + table: "Xchanges", + type: "longtext", + nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.AddColumn( + name: "GroupAttemptCounts", + table: "DelayedRetries", + type: "longtext", + nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"); + } + } +} diff --git a/SW.Bitween.MySql/Migrations/20260811092323_RetryBlockedReason.Designer.cs b/SW.Bitween.MySql/Migrations/20260811092323_RetryBlockedReason.Designer.cs new file mode 100644 index 00000000..53cb83ff --- /dev/null +++ b/SW.Bitween.MySql/Migrations/20260811092323_RetryBlockedReason.Designer.cs @@ -0,0 +1,1903 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using SW.Bitween.MySql; + +#nullable disable + +namespace SW.Bitween.MySql.Migrations +{ + [DbContext(typeof(BitweenDbContext))] + [Migration("20260811092323_RetryBlockedReason")] + partial class RetryBlockedReason + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.23") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Deleted") + .HasColumnType("tinyint(1)"); + + b.Property("Disabled") + .HasColumnType("tinyint(1)"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("Email") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("EmailProvider") + .HasColumnType("tinyint unsigned"); + + b.Property("LoginMethods") + .HasColumnType("tinyint unsigned"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Password") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("Phone") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("varchar(20)"); + + b.Property("Role") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique(); + + b.ToTable("Accounts", (string)null); + + b.HasData( + new + { + Id = 9999, + CreatedOn = new DateTime(2021, 12, 31, 22, 0, 0, 0, DateTimeKind.Utc), + Deleted = false, + Disabled = false, + DisplayName = "Admin", + Email = "admin@Bitween.systems", + EmailProvider = (byte)0, + LoginMethods = (byte)2, + Password = "$SWHASH$V1$10000$VQCi48eitH4Ml5juvBMOFZrMdQwBbhuIQVXe6RR7qJdDF2bJ", + Role = 0 + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AccountId") + .HasColumnType("int"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("LoginMethod") + .HasColumnType("tinyint unsigned"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.ToTable("RefreshTokens", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DelayedRetry", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("On") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("On"); + + b.ToTable("DelayedRetries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Document", b => + { + b.Property("Id") + .HasColumnType("int"); + + b.Property("BusEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("BusMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("DisregardsUnfilteredMessages") + .HasColumnType("tinyint(1)"); + + b.Property("DocumentFormat") + .HasColumnType("int"); + + b.Property("DuplicateInterval") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("PromotedProperties") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("BusMessageTypeName") + .IsUnique(); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Documents", (string)null); + + b.HasData( + new + { + Id = 10001, + BusEnabled = false, + DocumentFormat = 0, + DuplicateInterval = 0, + Name = "Aggregation Document", + PromotedProperties = "{}" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DocumentTrail", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Code") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("StateAfter") + .HasColumnType("longtext"); + + b.Property("StateBefore") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("CreatedOn"); + + b.HasIndex("DocumentId"); + + b.ToTable("DocumentTrail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("UrlName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("UrlName") + .IsUnique(); + + b.ToTable("ApiGateways", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.Property("ApiGatewayId") + .HasColumnType("int"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.HasKey("ApiGatewayId", "PartnerId", "SubscriptionId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("ApiGatewayPartners", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.ToTable("BusGateways", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("BusGatewayId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("MatchExpression") + .HasColumnType("longtext"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("BusGatewayId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("BusGatewayRoutes", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.GlobalAdapterValuesSet", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("Values") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("GlobalAdapterValuesSets", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Notifier", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("longtext"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("RunOnBadResult") + .HasColumnType("tinyint(1)"); + + b.Property("RunOnFailedResult") + .HasColumnType("tinyint(1)"); + + b.Property("RunOnSubscriptions") + .HasColumnType("longtext"); + + b.Property("RunOnSuccessfulResult") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.ToTable("Notifiers", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OnHoldXchange", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("BadData") + .HasColumnType("tinyint(1)"); + + b.Property("Data") + .HasColumnType("longtext"); + + b.Property("FileName") + .HasColumnType("longtext"); + + b.Property("References") + .HasMaxLength(1024) + .HasColumnType("varchar(1024)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("OnHoldXchanges", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AdapterProperties") + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.ToTable("Partners", (string)null); + + b.HasData( + new + { + Id = 1, + Name = "SYSTEM" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryGroupUsage", b => + { + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("GroupId") + .HasColumnType("char(36)"); + + b.Property("AttemptsUsed") + .HasColumnType("int"); + + b.Property("LastAttemptOn") + .HasColumnType("datetime(6)"); + + b.HasKey("SubscriptionId", "GroupId"); + + b.ToTable("RetryGroupUsages", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryPolicy", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Groups") + .HasColumnType("longtext"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.ToTable("RetryPolicies", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AggregateOn") + .HasColumnType("datetime(6)"); + + b.Property("AggregationForId") + .HasColumnType("int"); + + b.Property("AggregationTarget") + .HasColumnType("tinyint unsigned"); + + b.Property("CategoryId") + .HasColumnType("int"); + + b.Property("ConsecutiveFailures") + .HasColumnType("int"); + + b.Property("CustomRetryPolicy") + .HasColumnType("longtext"); + + b.Property("DocumentFilter") + .HasColumnType("longtext"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("longtext"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("IsRunning") + .HasColumnType("tinyint(1)"); + + b.Property("LastException") + .HasColumnType("longtext"); + + b.Property("MapperId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("MapperProperties") + .HasColumnType("longtext"); + + b.Property("MatchExpression") + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("PausedOn") + .HasColumnType("datetime(6)"); + + b.Property("ReceiveOn") + .HasColumnType("datetime(6)"); + + b.Property("ReceiverId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ReceiverProperties") + .HasColumnType("longtext"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("int"); + + b.Property("RetryPolicyId") + .HasColumnType("int"); + + b.Property("Temporary") + .HasColumnType("tinyint(1)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("ValidatorId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ValidatorProperties") + .HasColumnType("longtext"); + + b.Property("WorkGroupId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("AggregationForId"); + + b.HasIndex("CategoryId"); + + b.HasIndex("DocumentId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("ResponseSubscriptionId"); + + b.HasIndex("RetryPolicyId"); + + b.HasIndex("WorkGroupId"); + + b.ToTable("Subscriptions", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Code") + .HasColumnType("varchar(255)"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Description") + .HasColumnType("longtext"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.ToTable("SubscriptionCategory"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionTrail", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Code") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("StateAfter") + .HasColumnType("longtext"); + + b.Property("StateBefore") + .HasColumnType("longtext"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("CreatedOn"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("SubscriptionTrail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.WorkGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("BusMessageName") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("Options") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("CorrelationId") + .HasColumnType("longtext"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("longtext"); + + b.Property("InputContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("InputHash") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("InputName") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("InputSize") + .HasColumnType("int"); + + b.Property("MapperId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("MapperProperties") + .HasColumnType("longtext"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("References") + .HasMaxLength(1024) + .HasColumnType("varchar(1024)"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("int"); + + b.Property("RetryFor") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("StartedOn") + .HasColumnType("datetime(6)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.HasIndex("InputHash"); + + b.HasIndex("RetryFor"); + + b.HasIndex("StartedOn"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("Xchanges", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AggregatedOn") + .HasColumnType("datetime(6)"); + + b.Property("AggregationXchangeId") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("AggregationXchangeId"); + + b.ToTable("XchangeAggregations", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("DeliveredOn") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("DeliveredOn"); + + b.ToTable("XchangeDeliveries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeNotification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Exception") + .HasColumnType("longtext"); + + b.Property("FinishedOn") + .HasColumnType("datetime(6)"); + + b.Property("NotifierId") + .HasColumnType("int"); + + b.Property("NotifierName") + .HasColumnType("longtext"); + + b.Property("Success") + .HasColumnType("tinyint(1)"); + + b.Property("XchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.ToTable("XchangeNotifications", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("Hits") + .HasMaxLength(2000) + .IsUnicode(false) + .HasColumnType("varchar(2000)"); + + b.Property("Properties") + .HasColumnType("longtext"); + + b.Property("PropertiesRaw") + .HasColumnType("varchar(255)"); + + b.HasKey("Id"); + + b.HasIndex("PropertiesRaw"); + + b.ToTable("XchangePromotedProperties", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("Exception") + .HasColumnType("longtext"); + + b.Property("FinishedOn") + .HasColumnType("datetime(6)"); + + b.Property("OutputBad") + .HasColumnType("tinyint(1)"); + + b.Property("OutputContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("OutputHash") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("OutputName") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("OutputSize") + .HasColumnType("int"); + + b.Property("ResponseBad") + .HasColumnType("tinyint(1)"); + + b.Property("ResponseContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ResponseHash") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("ResponseName") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("ResponseSize") + .HasColumnType("int"); + + b.Property("ResponseXchangeId") + .HasColumnType("longtext"); + + b.Property("RetryBlockedReason") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("Success") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.ToTable("XchangeResults", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.RunFlagUpdater+RunningResult", b => + { + b.Property("IsRunning") + .HasColumnType("tinyint(1)"); + + b.ToTable((string)null); + + b.ToView(null, (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.JobExecution", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Context") + .HasColumnType("longtext") + .HasColumnName("context"); + + b.Property("DurationMs") + .HasColumnType("bigint") + .HasColumnName("duration_ms"); + + b.Property("EndTimeUtc") + .HasColumnType("datetime(6)") + .HasColumnName("end_time_utc"); + + b.Property("Error") + .HasColumnType("longtext") + .HasColumnName("error"); + + b.Property("FireInstanceId") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("fire_instance_id"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_name"); + + b.Property("JobTypeName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_type_name"); + + b.Property("Node") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("node"); + + b.Property("StartTimeUtc") + .HasColumnType("datetime(6)") + .HasColumnName("start_time_utc"); + + b.Property("Success") + .HasColumnType("tinyint(1)") + .HasColumnName("success"); + + b.HasKey("Id"); + + b.HasIndex("FireInstanceId") + .IsUnique() + .HasDatabaseName("idx_je_fire_instance_id"); + + b.HasIndex("StartTimeUtc") + .HasDatabaseName("idx_je_start_time"); + + b.HasIndex("Success") + .HasDatabaseName("idx_je_success"); + + b.HasIndex("JobGroup", "JobName", "StartTimeUtc") + .HasDatabaseName("idx_je_group_name_start"); + + b.ToTable("job_executions", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("BlobData") + .HasColumnType("longblob") + .HasColumnName("blob_data"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_blob_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCalendar", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("CalendarName") + .HasColumnType("varchar(200)") + .HasColumnName("calendar_name"); + + b.Property("Calendar") + .IsRequired() + .HasColumnType("longblob") + .HasColumnName("calendar"); + + b.HasKey("SchedulerName", "CalendarName"); + + b.ToTable("QRTZ_calendars", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("CronExpression") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("cron_expression"); + + b.Property("TimeZoneId") + .HasColumnType("varchar(200)") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_cron_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzFiredTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("EntryId") + .HasColumnType("varchar(200)") + .HasColumnName("entry_id"); + + b.Property("FiredTime") + .HasColumnType("bigint") + .HasColumnName("fired_time"); + + b.Property("InstanceName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("instance_name"); + + b.Property("IsNonConcurrent") + .HasColumnType("tinyint(1)") + .HasColumnName("is_nonconcurrent"); + + b.Property("JobGroup") + .HasColumnType("varchar(200)") + .HasColumnName("job_group"); + + b.Property("JobName") + .HasColumnType("varchar(200)") + .HasColumnName("job_name"); + + b.Property("Priority") + .HasColumnType("int") + .HasColumnName("priority"); + + b.Property("RequestsRecovery") + .HasColumnType("tinyint(1)") + .HasColumnName("requests_recovery"); + + b.Property("ScheduledTime") + .HasColumnType("bigint") + .HasColumnName("sched_time"); + + b.Property("State") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("state"); + + b.Property("TriggerGroup") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("TriggerName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.HasKey("SchedulerName", "EntryId"); + + b.HasIndex("InstanceName") + .HasDatabaseName("idx_QRTZ_ft_trig_inst_name"); + + b.HasIndex("JobGroup") + .HasDatabaseName("idx_QRTZ_ft_job_group"); + + b.HasIndex("JobName") + .HasDatabaseName("idx_QRTZ_ft_job_name"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_QRTZ_ft_job_req_recovery"); + + b.HasIndex("TriggerGroup") + .HasDatabaseName("idx_QRTZ_ft_trig_group"); + + b.HasIndex("TriggerName") + .HasDatabaseName("idx_QRTZ_ft_trig_name"); + + b.HasIndex("SchedulerName", "TriggerName", "TriggerGroup") + .HasDatabaseName("idx_QRTZ_ft_trig_nm_gp"); + + b.ToTable("QRTZ_fired_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("JobName") + .HasColumnType("varchar(200)") + .HasColumnName("job_name"); + + b.Property("JobGroup") + .HasColumnType("varchar(200)") + .HasColumnName("job_group"); + + b.Property("Description") + .HasColumnType("varchar(200)") + .HasColumnName("description"); + + b.Property("IsDurable") + .HasColumnType("tinyint(1)") + .HasColumnName("is_durable"); + + b.Property("IsNonConcurrent") + .HasColumnType("tinyint(1)") + .HasColumnName("is_nonconcurrent"); + + b.Property("IsUpdateData") + .HasColumnType("tinyint(1)") + .HasColumnName("is_update_data"); + + b.Property("JobClassName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_class_name"); + + b.Property("JobData") + .HasColumnType("longblob") + .HasColumnName("job_data"); + + b.Property("RequestsRecovery") + .HasColumnType("tinyint(1)") + .HasColumnName("requests_recovery"); + + b.HasKey("SchedulerName", "JobName", "JobGroup"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_j_req_recovery"); + + b.ToTable("QRTZ_job_details", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzLock", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("LockName") + .HasColumnType("varchar(200)") + .HasColumnName("lock_name"); + + b.HasKey("SchedulerName", "LockName"); + + b.ToTable("QRTZ_locks", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzPausedTriggerGroup", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.HasKey("SchedulerName", "TriggerGroup"); + + b.ToTable("QRTZ_paused_trigger_grps", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSchedulerState", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("InstanceName") + .HasColumnType("varchar(200)") + .HasColumnName("instance_name"); + + b.Property("CheckInInterval") + .HasColumnType("bigint") + .HasColumnName("checkin_interval"); + + b.Property("LastCheckInTime") + .HasColumnType("bigint") + .HasColumnName("last_checkin_time"); + + b.HasKey("SchedulerName", "InstanceName"); + + b.ToTable("QRTZ_scheduler_state", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("BooleanProperty1") + .HasColumnType("tinyint(1)") + .HasColumnName("bool_prop_1"); + + b.Property("BooleanProperty2") + .HasColumnType("tinyint(1)") + .HasColumnName("bool_prop_2"); + + b.Property("DecimalProperty1") + .HasColumnType("numeric") + .HasColumnName("dec_prop_1"); + + b.Property("DecimalProperty2") + .HasColumnType("numeric") + .HasColumnName("dec_prop_2"); + + b.Property("IntegerProperty1") + .HasColumnType("int") + .HasColumnName("int_prop_1"); + + b.Property("IntegerProperty2") + .HasColumnType("int") + .HasColumnName("int_prop_2"); + + b.Property("LongProperty1") + .HasColumnType("bigint") + .HasColumnName("long_prop_1"); + + b.Property("LongProperty2") + .HasColumnType("bigint") + .HasColumnName("long_prop_2"); + + b.Property("StringProperty1") + .HasColumnType("varchar(200)") + .HasColumnName("str_prop_1"); + + b.Property("StringProperty2") + .HasColumnType("varchar(200)") + .HasColumnName("str_prop_2"); + + b.Property("StringProperty3") + .HasColumnType("varchar(200)") + .HasColumnName("str_prop_3"); + + b.Property("TimeZoneId") + .HasColumnType("varchar(200)") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_simprop_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("RepeatCount") + .HasColumnType("bigint") + .HasColumnName("repeat_count"); + + b.Property("RepeatInterval") + .HasColumnType("bigint") + .HasColumnName("repeat_interval"); + + b.Property("TimesTriggered") + .HasColumnType("bigint") + .HasColumnName("times_triggered"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_simple_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("CalendarName") + .HasColumnType("varchar(200)") + .HasColumnName("calendar_name"); + + b.Property("Description") + .HasColumnType("varchar(200)") + .HasColumnName("description"); + + b.Property("EndTime") + .HasColumnType("bigint") + .HasColumnName("end_time"); + + b.Property("JobData") + .HasColumnType("longblob") + .HasColumnName("job_data"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_name"); + + b.Property("MisfireInstruction") + .HasColumnType("int") + .HasColumnName("misfire_instr"); + + b.Property("NextFireTime") + .HasColumnType("bigint") + .HasColumnName("next_fire_time"); + + b.Property("PreviousFireTime") + .HasColumnType("bigint") + .HasColumnName("prev_fire_time"); + + b.Property("Priority") + .HasColumnType("int") + .HasColumnName("priority"); + + b.Property("StartTime") + .HasColumnType("bigint") + .HasColumnName("start_time"); + + b.Property("TriggerState") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("trigger_state"); + + b.Property("TriggerType") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("trigger_type"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.HasIndex("NextFireTime") + .HasDatabaseName("idx_t_next_fire_time"); + + b.HasIndex("TriggerState") + .HasDatabaseName("idx_t_state"); + + b.HasIndex("NextFireTime", "TriggerState") + .HasDatabaseName("idx_t_nft_st"); + + b.HasIndex("SchedulerName", "JobName", "JobGroup"); + + b.ToTable("QRTZ_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DocumentTrail", b => + { + b.HasOne("SW.Bitween.Domain.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Document"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.ApiGateway", "ApiGateway") + .WithMany("Partners") + .HasForeignKey("ApiGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("ApiGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.BusGateway", "BusGateway") + .WithMany("Routes") + .HasForeignKey("BusGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("BusGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.OwnsMany("SW.Bitween.Domain.ApiCredential", "ApiCredentials", b1 => + { + b1.Property("PartnerId") + .HasColumnType("int"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b1.Property("Id")); + + b1.Property("Key") + .IsRequired() + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b1.Property("Name") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b1.HasKey("PartnerId", "Id"); + + b1.HasIndex("Key") + .IsUnique(); + + b1.ToTable("PartnerApiCredentials", (string)null); + + b1.WithOwner() + .HasForeignKey("PartnerId"); + + b1.HasData( + new + { + PartnerId = 1, + Id = 1, + Key = "7facc758283844b49cc4ffd26a75b1de", + Name = "default" + }); + }); + + b.Navigation("ApiCredentials"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.HasOne("SW.Bitween.Domain.Subscription", null) + .WithMany() + .HasForeignKey("AggregationForId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("FK_Subscriptions_AggFor"); + + b.HasOne("SW.Bitween.Domain.SubscriptionCategory", "Category") + .WithMany() + .HasForeignKey("CategoryId"); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", null) + .WithMany("Subscriptions") + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Subscription", null) + .WithMany() + .HasForeignKey("ResponseSubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("FK_Subscriptions_RespSub"); + + b.HasOne("SW.Bitween.Domain.RetryPolicy", "RetryPolicy") + .WithMany() + .HasForeignKey("RetryPolicyId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("SW.Bitween.Domain.WorkGroup", "WorkGroup") + .WithMany() + .HasForeignKey("WorkGroupId"); + + b.OwnsMany("SW.Bitween.Domain.Schedule", "Schedules", b1 => + { + b1.Property("SubscriptionId") + .HasColumnType("int"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b1.Property("Id")); + + b1.Property("Backwards") + .HasColumnType("tinyint(1)"); + + b1.Property("On") + .HasColumnType("bigint"); + + b1.Property("Recurrence") + .HasColumnType("tinyint unsigned"); + + b1.HasKey("SubscriptionId", "Id"); + + b1.ToTable("SubscriptionSchedules", (string)null); + + b1.WithOwner() + .HasForeignKey("SubscriptionId"); + }); + + b.Navigation("Category"); + + b.Navigation("RetryPolicy"); + + b.Navigation("Schedules"); + + b.Navigation("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionTrail", b => + { + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeAggregation", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeDelivery", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangePromotedProperties", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeResult", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("BlobTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("CronTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimplePropertyTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimpleTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzJobDetail", "JobDetail") + .WithMany("Triggers") + .HasForeignKey("SchedulerName", "JobName", "JobGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("JobDetail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Navigation("Partners"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Navigation("Routes"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Navigation("Subscriptions"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Navigation("Triggers"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Navigation("BlobTriggers"); + + b.Navigation("CronTriggers"); + + b.Navigation("SimplePropertyTriggers"); + + b.Navigation("SimpleTriggers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/SW.Bitween.MySql/Migrations/20260811092323_RetryBlockedReason.cs b/SW.Bitween.MySql/Migrations/20260811092323_RetryBlockedReason.cs new file mode 100644 index 00000000..81d72732 --- /dev/null +++ b/SW.Bitween.MySql/Migrations/20260811092323_RetryBlockedReason.cs @@ -0,0 +1,30 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace SW.Bitween.MySql.Migrations +{ + /// + public partial class RetryBlockedReason : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "RetryBlockedReason", + table: "XchangeResults", + type: "varchar(500)", + maxLength: 500, + nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "RetryBlockedReason", + table: "XchangeResults"); + } + } +} diff --git a/SW.Bitween.MySql/Migrations/20260812092701_AddAccountLockout.Designer.cs b/SW.Bitween.MySql/Migrations/20260812092701_AddAccountLockout.Designer.cs new file mode 100644 index 00000000..b7948513 --- /dev/null +++ b/SW.Bitween.MySql/Migrations/20260812092701_AddAccountLockout.Designer.cs @@ -0,0 +1,1893 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using SW.Bitween.MySql; + +#nullable disable + +namespace SW.Bitween.MySql.Migrations +{ + [DbContext(typeof(BitweenDbContext))] + [Migration("20260812092701_AddAccountLockout")] + partial class AddAccountLockout + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.23") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Deleted") + .HasColumnType("tinyint(1)"); + + b.Property("Disabled") + .HasColumnType("tinyint(1)"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("Email") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("EmailProvider") + .HasColumnType("tinyint unsigned"); + + b.Property("FailedLoginCount") + .HasColumnType("int"); + + b.Property("LockoutEnd") + .HasColumnType("datetime(6)"); + + b.Property("LoginMethods") + .HasColumnType("tinyint unsigned"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Password") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("Phone") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("varchar(20)"); + + b.Property("Role") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique(); + + b.ToTable("Accounts", (string)null); + + b.HasData( + new + { + Id = 9999, + CreatedOn = new DateTime(2021, 12, 31, 22, 0, 0, 0, DateTimeKind.Utc), + Deleted = false, + Disabled = false, + DisplayName = "Admin", + Email = "admin@Bitween.systems", + EmailProvider = (byte)0, + FailedLoginCount = 0, + LoginMethods = (byte)2, + Password = "$SWHASH$V1$10000$VQCi48eitH4Ml5juvBMOFZrMdQwBbhuIQVXe6RR7qJdDF2bJ", + Role = 0 + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AccountId") + .HasColumnType("int"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("LoginMethod") + .HasColumnType("tinyint unsigned"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.ToTable("RefreshTokens", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DelayedRetry", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("GroupAttemptCounts") + .HasColumnType("longtext"); + + b.Property("On") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("On"); + + b.ToTable("DelayedRetries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Document", b => + { + b.Property("Id") + .HasColumnType("int"); + + b.Property("BusEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("BusMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("DisregardsUnfilteredMessages") + .HasColumnType("tinyint(1)"); + + b.Property("DocumentFormat") + .HasColumnType("int"); + + b.Property("DuplicateInterval") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("PromotedProperties") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("BusMessageTypeName") + .IsUnique(); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Documents", (string)null); + + b.HasData( + new + { + Id = 10001, + BusEnabled = false, + DocumentFormat = 0, + DuplicateInterval = 0, + Name = "Aggregation Document", + PromotedProperties = "{}" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DocumentTrail", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Code") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("StateAfter") + .HasColumnType("longtext"); + + b.Property("StateBefore") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("CreatedOn"); + + b.HasIndex("DocumentId"); + + b.ToTable("DocumentTrail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("UrlName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("UrlName") + .IsUnique(); + + b.ToTable("ApiGateways", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.Property("ApiGatewayId") + .HasColumnType("int"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.HasKey("ApiGatewayId", "PartnerId", "SubscriptionId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("ApiGatewayPartners", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.ToTable("BusGateways", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("BusGatewayId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("MatchExpression") + .HasColumnType("longtext"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("BusGatewayId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("BusGatewayRoutes", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.GlobalAdapterValuesSet", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("Values") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("GlobalAdapterValuesSets", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Notifier", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("longtext"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("RunOnBadResult") + .HasColumnType("tinyint(1)"); + + b.Property("RunOnFailedResult") + .HasColumnType("tinyint(1)"); + + b.Property("RunOnSubscriptions") + .HasColumnType("longtext"); + + b.Property("RunOnSuccessfulResult") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.ToTable("Notifiers", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OnHoldXchange", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("BadData") + .HasColumnType("tinyint(1)"); + + b.Property("Data") + .HasColumnType("longtext"); + + b.Property("FileName") + .HasColumnType("longtext"); + + b.Property("References") + .HasMaxLength(1024) + .HasColumnType("varchar(1024)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("OnHoldXchanges", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AdapterProperties") + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.ToTable("Partners", (string)null); + + b.HasData( + new + { + Id = 1, + Name = "SYSTEM" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryPolicy", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Groups") + .HasColumnType("longtext"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.ToTable("RetryPolicies", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AggregateOn") + .HasColumnType("datetime(6)"); + + b.Property("AggregationForId") + .HasColumnType("int"); + + b.Property("AggregationTarget") + .HasColumnType("tinyint unsigned"); + + b.Property("CategoryId") + .HasColumnType("int"); + + b.Property("ConsecutiveFailures") + .HasColumnType("int"); + + b.Property("CustomRetryPolicy") + .HasColumnType("longtext"); + + b.Property("DocumentFilter") + .HasColumnType("longtext"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("longtext"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("IsRunning") + .HasColumnType("tinyint(1)"); + + b.Property("LastException") + .HasColumnType("longtext"); + + b.Property("MapperId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("MapperProperties") + .HasColumnType("longtext"); + + b.Property("MatchExpression") + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("PausedOn") + .HasColumnType("datetime(6)"); + + b.Property("ReceiveOn") + .HasColumnType("datetime(6)"); + + b.Property("ReceiverId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ReceiverProperties") + .HasColumnType("longtext"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("int"); + + b.Property("RetryPolicyId") + .HasColumnType("int"); + + b.Property("Temporary") + .HasColumnType("tinyint(1)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("ValidatorId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ValidatorProperties") + .HasColumnType("longtext"); + + b.Property("WorkGroupId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("AggregationForId"); + + b.HasIndex("CategoryId"); + + b.HasIndex("DocumentId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("ResponseSubscriptionId"); + + b.HasIndex("RetryPolicyId"); + + b.HasIndex("WorkGroupId"); + + b.ToTable("Subscriptions", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Code") + .HasColumnType("varchar(255)"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Description") + .HasColumnType("longtext"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.ToTable("SubscriptionCategory"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionTrail", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Code") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("StateAfter") + .HasColumnType("longtext"); + + b.Property("StateBefore") + .HasColumnType("longtext"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("CreatedOn"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("SubscriptionTrail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.WorkGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("BusMessageName") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("Options") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("CorrelationId") + .HasColumnType("longtext"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("GroupAttemptCounts") + .HasColumnType("longtext"); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("longtext"); + + b.Property("InputContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("InputHash") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("InputName") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("InputSize") + .HasColumnType("int"); + + b.Property("MapperId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("MapperProperties") + .HasColumnType("longtext"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("References") + .HasMaxLength(1024) + .HasColumnType("varchar(1024)"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("int"); + + b.Property("RetryFor") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("StartedOn") + .HasColumnType("datetime(6)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.HasIndex("InputHash"); + + b.HasIndex("RetryFor"); + + b.HasIndex("StartedOn"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("Xchanges", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AggregatedOn") + .HasColumnType("datetime(6)"); + + b.Property("AggregationXchangeId") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("AggregationXchangeId"); + + b.ToTable("XchangeAggregations", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("DeliveredOn") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("DeliveredOn"); + + b.ToTable("XchangeDeliveries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeNotification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Exception") + .HasColumnType("longtext"); + + b.Property("FinishedOn") + .HasColumnType("datetime(6)"); + + b.Property("NotifierId") + .HasColumnType("int"); + + b.Property("NotifierName") + .HasColumnType("longtext"); + + b.Property("Success") + .HasColumnType("tinyint(1)"); + + b.Property("XchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.ToTable("XchangeNotifications", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("Hits") + .HasMaxLength(2000) + .IsUnicode(false) + .HasColumnType("varchar(2000)"); + + b.Property("Properties") + .HasColumnType("longtext"); + + b.Property("PropertiesRaw") + .HasColumnType("varchar(255)"); + + b.HasKey("Id"); + + b.HasIndex("PropertiesRaw"); + + b.ToTable("XchangePromotedProperties", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("Exception") + .HasColumnType("longtext"); + + b.Property("FinishedOn") + .HasColumnType("datetime(6)"); + + b.Property("OutputBad") + .HasColumnType("tinyint(1)"); + + b.Property("OutputContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("OutputHash") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("OutputName") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("OutputSize") + .HasColumnType("int"); + + b.Property("ResponseBad") + .HasColumnType("tinyint(1)"); + + b.Property("ResponseContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ResponseHash") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("ResponseName") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("ResponseSize") + .HasColumnType("int"); + + b.Property("ResponseXchangeId") + .HasColumnType("longtext"); + + b.Property("Success") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.ToTable("XchangeResults", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.RunFlagUpdater+RunningResult", b => + { + b.Property("IsRunning") + .HasColumnType("tinyint(1)"); + + b.ToTable((string)null); + + b.ToView(null, (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.JobExecution", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Context") + .HasColumnType("longtext") + .HasColumnName("context"); + + b.Property("DurationMs") + .HasColumnType("bigint") + .HasColumnName("duration_ms"); + + b.Property("EndTimeUtc") + .HasColumnType("datetime(6)") + .HasColumnName("end_time_utc"); + + b.Property("Error") + .HasColumnType("longtext") + .HasColumnName("error"); + + b.Property("FireInstanceId") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("fire_instance_id"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_name"); + + b.Property("JobTypeName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_type_name"); + + b.Property("Node") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("node"); + + b.Property("StartTimeUtc") + .HasColumnType("datetime(6)") + .HasColumnName("start_time_utc"); + + b.Property("Success") + .HasColumnType("tinyint(1)") + .HasColumnName("success"); + + b.HasKey("Id"); + + b.HasIndex("FireInstanceId") + .IsUnique() + .HasDatabaseName("idx_je_fire_instance_id"); + + b.HasIndex("StartTimeUtc") + .HasDatabaseName("idx_je_start_time"); + + b.HasIndex("Success") + .HasDatabaseName("idx_je_success"); + + b.HasIndex("JobGroup", "JobName", "StartTimeUtc") + .HasDatabaseName("idx_je_group_name_start"); + + b.ToTable("job_executions", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("BlobData") + .HasColumnType("longblob") + .HasColumnName("blob_data"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_blob_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCalendar", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("CalendarName") + .HasColumnType("varchar(200)") + .HasColumnName("calendar_name"); + + b.Property("Calendar") + .IsRequired() + .HasColumnType("longblob") + .HasColumnName("calendar"); + + b.HasKey("SchedulerName", "CalendarName"); + + b.ToTable("QRTZ_calendars", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("CronExpression") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("cron_expression"); + + b.Property("TimeZoneId") + .HasColumnType("varchar(200)") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_cron_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzFiredTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("EntryId") + .HasColumnType("varchar(200)") + .HasColumnName("entry_id"); + + b.Property("FiredTime") + .HasColumnType("bigint") + .HasColumnName("fired_time"); + + b.Property("InstanceName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("instance_name"); + + b.Property("IsNonConcurrent") + .HasColumnType("tinyint(1)") + .HasColumnName("is_nonconcurrent"); + + b.Property("JobGroup") + .HasColumnType("varchar(200)") + .HasColumnName("job_group"); + + b.Property("JobName") + .HasColumnType("varchar(200)") + .HasColumnName("job_name"); + + b.Property("Priority") + .HasColumnType("int") + .HasColumnName("priority"); + + b.Property("RequestsRecovery") + .HasColumnType("tinyint(1)") + .HasColumnName("requests_recovery"); + + b.Property("ScheduledTime") + .HasColumnType("bigint") + .HasColumnName("sched_time"); + + b.Property("State") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("state"); + + b.Property("TriggerGroup") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("TriggerName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.HasKey("SchedulerName", "EntryId"); + + b.HasIndex("InstanceName") + .HasDatabaseName("idx_QRTZ_ft_trig_inst_name"); + + b.HasIndex("JobGroup") + .HasDatabaseName("idx_QRTZ_ft_job_group"); + + b.HasIndex("JobName") + .HasDatabaseName("idx_QRTZ_ft_job_name"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_QRTZ_ft_job_req_recovery"); + + b.HasIndex("TriggerGroup") + .HasDatabaseName("idx_QRTZ_ft_trig_group"); + + b.HasIndex("TriggerName") + .HasDatabaseName("idx_QRTZ_ft_trig_name"); + + b.HasIndex("SchedulerName", "TriggerName", "TriggerGroup") + .HasDatabaseName("idx_QRTZ_ft_trig_nm_gp"); + + b.ToTable("QRTZ_fired_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("JobName") + .HasColumnType("varchar(200)") + .HasColumnName("job_name"); + + b.Property("JobGroup") + .HasColumnType("varchar(200)") + .HasColumnName("job_group"); + + b.Property("Description") + .HasColumnType("varchar(200)") + .HasColumnName("description"); + + b.Property("IsDurable") + .HasColumnType("tinyint(1)") + .HasColumnName("is_durable"); + + b.Property("IsNonConcurrent") + .HasColumnType("tinyint(1)") + .HasColumnName("is_nonconcurrent"); + + b.Property("IsUpdateData") + .HasColumnType("tinyint(1)") + .HasColumnName("is_update_data"); + + b.Property("JobClassName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_class_name"); + + b.Property("JobData") + .HasColumnType("longblob") + .HasColumnName("job_data"); + + b.Property("RequestsRecovery") + .HasColumnType("tinyint(1)") + .HasColumnName("requests_recovery"); + + b.HasKey("SchedulerName", "JobName", "JobGroup"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_j_req_recovery"); + + b.ToTable("QRTZ_job_details", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzLock", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("LockName") + .HasColumnType("varchar(200)") + .HasColumnName("lock_name"); + + b.HasKey("SchedulerName", "LockName"); + + b.ToTable("QRTZ_locks", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzPausedTriggerGroup", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.HasKey("SchedulerName", "TriggerGroup"); + + b.ToTable("QRTZ_paused_trigger_grps", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSchedulerState", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("InstanceName") + .HasColumnType("varchar(200)") + .HasColumnName("instance_name"); + + b.Property("CheckInInterval") + .HasColumnType("bigint") + .HasColumnName("checkin_interval"); + + b.Property("LastCheckInTime") + .HasColumnType("bigint") + .HasColumnName("last_checkin_time"); + + b.HasKey("SchedulerName", "InstanceName"); + + b.ToTable("QRTZ_scheduler_state", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("BooleanProperty1") + .HasColumnType("tinyint(1)") + .HasColumnName("bool_prop_1"); + + b.Property("BooleanProperty2") + .HasColumnType("tinyint(1)") + .HasColumnName("bool_prop_2"); + + b.Property("DecimalProperty1") + .HasColumnType("numeric") + .HasColumnName("dec_prop_1"); + + b.Property("DecimalProperty2") + .HasColumnType("numeric") + .HasColumnName("dec_prop_2"); + + b.Property("IntegerProperty1") + .HasColumnType("int") + .HasColumnName("int_prop_1"); + + b.Property("IntegerProperty2") + .HasColumnType("int") + .HasColumnName("int_prop_2"); + + b.Property("LongProperty1") + .HasColumnType("bigint") + .HasColumnName("long_prop_1"); + + b.Property("LongProperty2") + .HasColumnType("bigint") + .HasColumnName("long_prop_2"); + + b.Property("StringProperty1") + .HasColumnType("varchar(200)") + .HasColumnName("str_prop_1"); + + b.Property("StringProperty2") + .HasColumnType("varchar(200)") + .HasColumnName("str_prop_2"); + + b.Property("StringProperty3") + .HasColumnType("varchar(200)") + .HasColumnName("str_prop_3"); + + b.Property("TimeZoneId") + .HasColumnType("varchar(200)") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_simprop_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("RepeatCount") + .HasColumnType("bigint") + .HasColumnName("repeat_count"); + + b.Property("RepeatInterval") + .HasColumnType("bigint") + .HasColumnName("repeat_interval"); + + b.Property("TimesTriggered") + .HasColumnType("bigint") + .HasColumnName("times_triggered"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_simple_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("CalendarName") + .HasColumnType("varchar(200)") + .HasColumnName("calendar_name"); + + b.Property("Description") + .HasColumnType("varchar(200)") + .HasColumnName("description"); + + b.Property("EndTime") + .HasColumnType("bigint") + .HasColumnName("end_time"); + + b.Property("JobData") + .HasColumnType("longblob") + .HasColumnName("job_data"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_name"); + + b.Property("MisfireInstruction") + .HasColumnType("int") + .HasColumnName("misfire_instr"); + + b.Property("NextFireTime") + .HasColumnType("bigint") + .HasColumnName("next_fire_time"); + + b.Property("PreviousFireTime") + .HasColumnType("bigint") + .HasColumnName("prev_fire_time"); + + b.Property("Priority") + .HasColumnType("int") + .HasColumnName("priority"); + + b.Property("StartTime") + .HasColumnType("bigint") + .HasColumnName("start_time"); + + b.Property("TriggerState") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("trigger_state"); + + b.Property("TriggerType") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("trigger_type"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.HasIndex("NextFireTime") + .HasDatabaseName("idx_t_next_fire_time"); + + b.HasIndex("TriggerState") + .HasDatabaseName("idx_t_state"); + + b.HasIndex("NextFireTime", "TriggerState") + .HasDatabaseName("idx_t_nft_st"); + + b.HasIndex("SchedulerName", "JobName", "JobGroup"); + + b.ToTable("QRTZ_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DocumentTrail", b => + { + b.HasOne("SW.Bitween.Domain.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Document"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.ApiGateway", "ApiGateway") + .WithMany("Partners") + .HasForeignKey("ApiGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("ApiGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.BusGateway", "BusGateway") + .WithMany("Routes") + .HasForeignKey("BusGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("BusGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.OwnsMany("SW.Bitween.Domain.ApiCredential", "ApiCredentials", b1 => + { + b1.Property("PartnerId") + .HasColumnType("int"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b1.Property("Id")); + + b1.Property("Key") + .IsRequired() + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b1.Property("Name") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b1.HasKey("PartnerId", "Id"); + + b1.HasIndex("Key") + .IsUnique(); + + b1.ToTable("PartnerApiCredentials", (string)null); + + b1.WithOwner() + .HasForeignKey("PartnerId"); + + b1.HasData( + new + { + PartnerId = 1, + Id = 1, + Key = "7facc758283844b49cc4ffd26a75b1de", + Name = "default" + }); + }); + + b.Navigation("ApiCredentials"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.HasOne("SW.Bitween.Domain.Subscription", null) + .WithMany() + .HasForeignKey("AggregationForId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("FK_Subscriptions_AggFor"); + + b.HasOne("SW.Bitween.Domain.SubscriptionCategory", "Category") + .WithMany() + .HasForeignKey("CategoryId"); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", null) + .WithMany("Subscriptions") + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Subscription", null) + .WithMany() + .HasForeignKey("ResponseSubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("FK_Subscriptions_RespSub"); + + b.HasOne("SW.Bitween.Domain.RetryPolicy", "RetryPolicy") + .WithMany() + .HasForeignKey("RetryPolicyId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("SW.Bitween.Domain.WorkGroup", "WorkGroup") + .WithMany() + .HasForeignKey("WorkGroupId"); + + b.OwnsMany("SW.Bitween.Domain.Schedule", "Schedules", b1 => + { + b1.Property("SubscriptionId") + .HasColumnType("int"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b1.Property("Id")); + + b1.Property("Backwards") + .HasColumnType("tinyint(1)"); + + b1.Property("On") + .HasColumnType("bigint"); + + b1.Property("Recurrence") + .HasColumnType("tinyint unsigned"); + + b1.HasKey("SubscriptionId", "Id"); + + b1.ToTable("SubscriptionSchedules", (string)null); + + b1.WithOwner() + .HasForeignKey("SubscriptionId"); + }); + + b.Navigation("Category"); + + b.Navigation("RetryPolicy"); + + b.Navigation("Schedules"); + + b.Navigation("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionTrail", b => + { + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeAggregation", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeDelivery", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangePromotedProperties", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeResult", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("BlobTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("CronTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimplePropertyTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimpleTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzJobDetail", "JobDetail") + .WithMany("Triggers") + .HasForeignKey("SchedulerName", "JobName", "JobGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("JobDetail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Navigation("Partners"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Navigation("Routes"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Navigation("Subscriptions"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Navigation("Triggers"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Navigation("BlobTriggers"); + + b.Navigation("CronTriggers"); + + b.Navigation("SimplePropertyTriggers"); + + b.Navigation("SimpleTriggers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/SW.Bitween.MySql/Migrations/20260812092701_AddAccountLockout.cs b/SW.Bitween.MySql/Migrations/20260812092701_AddAccountLockout.cs new file mode 100644 index 00000000..0fabaec5 --- /dev/null +++ b/SW.Bitween.MySql/Migrations/20260812092701_AddAccountLockout.cs @@ -0,0 +1,47 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace SW.Bitween.MySql.Migrations +{ + /// + public partial class AddAccountLockout : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "FailedLoginCount", + table: "Accounts", + type: "int", + nullable: false, + defaultValue: 0); + + migrationBuilder.AddColumn( + name: "LockoutEnd", + table: "Accounts", + type: "datetime(6)", + nullable: true); + + migrationBuilder.UpdateData( + table: "Accounts", + keyColumn: "Id", + keyValue: 9999, + columns: new[] { "FailedLoginCount", "LockoutEnd" }, + values: new object[] { 0, null }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "FailedLoginCount", + table: "Accounts"); + + migrationBuilder.DropColumn( + name: "LockoutEnd", + table: "Accounts"); + } + } +} diff --git a/SW.Bitween.MySql/Migrations/20260817103452_RetryBudgetAlerts.Designer.cs b/SW.Bitween.MySql/Migrations/20260817103452_RetryBudgetAlerts.Designer.cs new file mode 100644 index 00000000..087fa89c --- /dev/null +++ b/SW.Bitween.MySql/Migrations/20260817103452_RetryBudgetAlerts.Designer.cs @@ -0,0 +1,1953 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using SW.Bitween.MySql; + +#nullable disable + +namespace SW.Bitween.MySql.Migrations +{ + [DbContext(typeof(BitweenDbContext))] + [Migration("20260817103452_RetryBudgetAlerts")] + partial class RetryBudgetAlerts + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.23") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Deleted") + .HasColumnType("tinyint(1)"); + + b.Property("Disabled") + .HasColumnType("tinyint(1)"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("Email") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("EmailProvider") + .HasColumnType("tinyint unsigned"); + + b.Property("FailedLoginCount") + .HasColumnType("int"); + + b.Property("LockoutEnd") + .HasColumnType("datetime(6)"); + + b.Property("LoginMethods") + .HasColumnType("tinyint unsigned"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Password") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("Phone") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("varchar(20)"); + + b.Property("Role") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique(); + + b.ToTable("Accounts", (string)null); + + b.HasData( + new + { + Id = 9999, + CreatedOn = new DateTime(2021, 12, 31, 22, 0, 0, 0, DateTimeKind.Utc), + Deleted = false, + Disabled = false, + DisplayName = "Admin", + Email = "admin@Bitween.systems", + EmailProvider = (byte)0, + FailedLoginCount = 0, + LoginMethods = (byte)2, + Password = "$SWHASH$V1$10000$VQCi48eitH4Ml5juvBMOFZrMdQwBbhuIQVXe6RR7qJdDF2bJ", + Role = 0 + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AccountId") + .HasColumnType("int"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("LoginMethod") + .HasColumnType("tinyint unsigned"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.ToTable("RefreshTokens", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DelayedRetry", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("On") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("On"); + + b.ToTable("DelayedRetries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Document", b => + { + b.Property("Id") + .HasColumnType("int"); + + b.Property("BusEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("BusMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("DisregardsUnfilteredMessages") + .HasColumnType("tinyint(1)"); + + b.Property("DocumentFormat") + .HasColumnType("int"); + + b.Property("DuplicateInterval") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("PromotedProperties") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("BusMessageTypeName") + .IsUnique(); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Documents", (string)null); + + b.HasData( + new + { + Id = 10001, + BusEnabled = false, + DocumentFormat = 0, + DuplicateInterval = 0, + Name = "Aggregation Document", + PromotedProperties = "{}" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DocumentTrail", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Code") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("StateAfter") + .HasColumnType("longtext"); + + b.Property("StateBefore") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("CreatedOn"); + + b.HasIndex("DocumentId"); + + b.ToTable("DocumentTrail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("UrlName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("UrlName") + .IsUnique(); + + b.ToTable("ApiGateways", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.Property("ApiGatewayId") + .HasColumnType("int"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.HasKey("ApiGatewayId", "PartnerId", "SubscriptionId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("ApiGatewayPartners", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.ToTable("BusGateways", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("BusGatewayId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("MatchExpression") + .HasColumnType("longtext"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("BusGatewayId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("BusGatewayRoutes", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.GlobalAdapterValuesSet", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("Values") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("GlobalAdapterValuesSets", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Notifier", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("longtext"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("RunOnBadResult") + .HasColumnType("tinyint(1)"); + + b.Property("RunOnFailedResult") + .HasColumnType("tinyint(1)"); + + b.Property("RunOnSubscriptions") + .HasColumnType("longtext"); + + b.Property("RunOnSuccessfulResult") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.ToTable("Notifiers", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OnHoldXchange", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("BadData") + .HasColumnType("tinyint(1)"); + + b.Property("Data") + .HasColumnType("longtext"); + + b.Property("FileName") + .HasColumnType("longtext"); + + b.Property("References") + .HasMaxLength(1024) + .HasColumnType("varchar(1024)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("OnHoldXchanges", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AdapterProperties") + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.ToTable("Partners", (string)null); + + b.HasData( + new + { + Id = 1, + Name = "SYSTEM" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryAlertOverride", b => + { + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("GroupId") + .HasColumnType("char(36)"); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("AlertHandlerProperties") + .HasColumnType("longtext"); + + b.Property("AlertMode") + .HasColumnType("tinyint unsigned"); + + b.HasKey("SubscriptionId", "GroupId"); + + b.ToTable("RetryAlertOverrides", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryGroupUsage", b => + { + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("GroupId") + .HasColumnType("char(36)"); + + b.Property("AttemptsUsed") + .HasColumnType("int"); + + b.Property("ExhaustedNotifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("LastAttemptOn") + .HasColumnType("datetime(6)"); + + b.HasKey("SubscriptionId", "GroupId"); + + b.ToTable("RetryGroupUsages", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryPolicy", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("AlertHandlerProperties") + .HasColumnType("longtext"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Groups") + .HasColumnType("longtext"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.ToTable("RetryPolicies", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AggregateOn") + .HasColumnType("datetime(6)"); + + b.Property("AggregationForId") + .HasColumnType("int"); + + b.Property("AggregationTarget") + .HasColumnType("tinyint unsigned"); + + b.Property("CategoryId") + .HasColumnType("int"); + + b.Property("ConsecutiveFailures") + .HasColumnType("int"); + + b.Property("CustomRetryPolicy") + .HasColumnType("longtext"); + + b.Property("DocumentFilter") + .HasColumnType("longtext"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("longtext"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("IsRunning") + .HasColumnType("tinyint(1)"); + + b.Property("LastException") + .HasColumnType("longtext"); + + b.Property("MapperId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("MapperProperties") + .HasColumnType("longtext"); + + b.Property("MatchExpression") + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("PausedOn") + .HasColumnType("datetime(6)"); + + b.Property("ReceiveOn") + .HasColumnType("datetime(6)"); + + b.Property("ReceiverId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ReceiverProperties") + .HasColumnType("longtext"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("int"); + + b.Property("RetryPolicyId") + .HasColumnType("int"); + + b.Property("Temporary") + .HasColumnType("tinyint(1)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("ValidatorId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ValidatorProperties") + .HasColumnType("longtext"); + + b.Property("WorkGroupId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("AggregationForId"); + + b.HasIndex("CategoryId"); + + b.HasIndex("DocumentId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("ResponseSubscriptionId"); + + b.HasIndex("RetryPolicyId"); + + b.HasIndex("WorkGroupId"); + + b.ToTable("Subscriptions", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Code") + .HasColumnType("varchar(255)"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Description") + .HasColumnType("longtext"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.ToTable("SubscriptionCategory"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionTrail", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Code") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("StateAfter") + .HasColumnType("longtext"); + + b.Property("StateBefore") + .HasColumnType("longtext"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("CreatedOn"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("SubscriptionTrail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.WorkGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("BusMessageName") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("Options") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("CorrelationId") + .HasColumnType("longtext"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("longtext"); + + b.Property("InputContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("InputHash") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("InputName") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("InputSize") + .HasColumnType("int"); + + b.Property("MapperId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("MapperProperties") + .HasColumnType("longtext"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("References") + .HasMaxLength(1024) + .HasColumnType("varchar(1024)"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("int"); + + b.Property("RetryFor") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("StartedOn") + .HasColumnType("datetime(6)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.HasIndex("InputHash"); + + b.HasIndex("RetryFor"); + + b.HasIndex("StartedOn"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("Xchanges", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AggregatedOn") + .HasColumnType("datetime(6)"); + + b.Property("AggregationXchangeId") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("AggregationXchangeId"); + + b.ToTable("XchangeAggregations", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("DeliveredOn") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("DeliveredOn"); + + b.ToTable("XchangeDeliveries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeNotification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Exception") + .HasColumnType("longtext"); + + b.Property("FinishedOn") + .HasColumnType("datetime(6)"); + + b.Property("NotifierId") + .HasColumnType("int"); + + b.Property("NotifierName") + .HasColumnType("longtext"); + + b.Property("Success") + .HasColumnType("tinyint(1)"); + + b.Property("XchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.ToTable("XchangeNotifications", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("Hits") + .HasMaxLength(2000) + .IsUnicode(false) + .HasColumnType("varchar(2000)"); + + b.Property("Properties") + .HasColumnType("longtext"); + + b.Property("PropertiesRaw") + .HasColumnType("varchar(255)"); + + b.HasKey("Id"); + + b.HasIndex("PropertiesRaw"); + + b.ToTable("XchangePromotedProperties", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AttemptNumber") + .HasColumnType("int"); + + b.Property("Exception") + .HasColumnType("longtext"); + + b.Property("FinishedOn") + .HasColumnType("datetime(6)"); + + b.Property("OutputBad") + .HasColumnType("tinyint(1)"); + + b.Property("OutputContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("OutputHash") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("OutputName") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("OutputSize") + .HasColumnType("int"); + + b.Property("ResponseBad") + .HasColumnType("tinyint(1)"); + + b.Property("ResponseContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ResponseHash") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("ResponseName") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("ResponseSize") + .HasColumnType("int"); + + b.Property("ResponseXchangeId") + .HasColumnType("longtext"); + + b.Property("RetryBlockedReason") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("RetryGroupId") + .HasColumnType("char(36)"); + + b.Property("Success") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.HasIndex("RetryGroupId"); + + b.ToTable("XchangeResults", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.RunFlagUpdater+RunningResult", b => + { + b.Property("IsRunning") + .HasColumnType("tinyint(1)"); + + b.ToTable((string)null); + + b.ToView(null, (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.JobExecution", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Context") + .HasColumnType("longtext") + .HasColumnName("context"); + + b.Property("DurationMs") + .HasColumnType("bigint") + .HasColumnName("duration_ms"); + + b.Property("EndTimeUtc") + .HasColumnType("datetime(6)") + .HasColumnName("end_time_utc"); + + b.Property("Error") + .HasColumnType("longtext") + .HasColumnName("error"); + + b.Property("FireInstanceId") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("fire_instance_id"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_name"); + + b.Property("JobTypeName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_type_name"); + + b.Property("Node") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("node"); + + b.Property("StartTimeUtc") + .HasColumnType("datetime(6)") + .HasColumnName("start_time_utc"); + + b.Property("Success") + .HasColumnType("tinyint(1)") + .HasColumnName("success"); + + b.HasKey("Id"); + + b.HasIndex("FireInstanceId") + .IsUnique() + .HasDatabaseName("idx_je_fire_instance_id"); + + b.HasIndex("StartTimeUtc") + .HasDatabaseName("idx_je_start_time"); + + b.HasIndex("Success") + .HasDatabaseName("idx_je_success"); + + b.HasIndex("JobGroup", "JobName", "StartTimeUtc") + .HasDatabaseName("idx_je_group_name_start"); + + b.ToTable("job_executions", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("BlobData") + .HasColumnType("longblob") + .HasColumnName("blob_data"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_blob_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCalendar", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("CalendarName") + .HasColumnType("varchar(200)") + .HasColumnName("calendar_name"); + + b.Property("Calendar") + .IsRequired() + .HasColumnType("longblob") + .HasColumnName("calendar"); + + b.HasKey("SchedulerName", "CalendarName"); + + b.ToTable("QRTZ_calendars", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("CronExpression") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("cron_expression"); + + b.Property("TimeZoneId") + .HasColumnType("varchar(200)") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_cron_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzFiredTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("EntryId") + .HasColumnType("varchar(200)") + .HasColumnName("entry_id"); + + b.Property("FiredTime") + .HasColumnType("bigint") + .HasColumnName("fired_time"); + + b.Property("InstanceName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("instance_name"); + + b.Property("IsNonConcurrent") + .HasColumnType("tinyint(1)") + .HasColumnName("is_nonconcurrent"); + + b.Property("JobGroup") + .HasColumnType("varchar(200)") + .HasColumnName("job_group"); + + b.Property("JobName") + .HasColumnType("varchar(200)") + .HasColumnName("job_name"); + + b.Property("Priority") + .HasColumnType("int") + .HasColumnName("priority"); + + b.Property("RequestsRecovery") + .HasColumnType("tinyint(1)") + .HasColumnName("requests_recovery"); + + b.Property("ScheduledTime") + .HasColumnType("bigint") + .HasColumnName("sched_time"); + + b.Property("State") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("state"); + + b.Property("TriggerGroup") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("TriggerName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.HasKey("SchedulerName", "EntryId"); + + b.HasIndex("InstanceName") + .HasDatabaseName("idx_QRTZ_ft_trig_inst_name"); + + b.HasIndex("JobGroup") + .HasDatabaseName("idx_QRTZ_ft_job_group"); + + b.HasIndex("JobName") + .HasDatabaseName("idx_QRTZ_ft_job_name"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_QRTZ_ft_job_req_recovery"); + + b.HasIndex("TriggerGroup") + .HasDatabaseName("idx_QRTZ_ft_trig_group"); + + b.HasIndex("TriggerName") + .HasDatabaseName("idx_QRTZ_ft_trig_name"); + + b.HasIndex("SchedulerName", "TriggerName", "TriggerGroup") + .HasDatabaseName("idx_QRTZ_ft_trig_nm_gp"); + + b.ToTable("QRTZ_fired_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("JobName") + .HasColumnType("varchar(200)") + .HasColumnName("job_name"); + + b.Property("JobGroup") + .HasColumnType("varchar(200)") + .HasColumnName("job_group"); + + b.Property("Description") + .HasColumnType("varchar(200)") + .HasColumnName("description"); + + b.Property("IsDurable") + .HasColumnType("tinyint(1)") + .HasColumnName("is_durable"); + + b.Property("IsNonConcurrent") + .HasColumnType("tinyint(1)") + .HasColumnName("is_nonconcurrent"); + + b.Property("IsUpdateData") + .HasColumnType("tinyint(1)") + .HasColumnName("is_update_data"); + + b.Property("JobClassName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_class_name"); + + b.Property("JobData") + .HasColumnType("longblob") + .HasColumnName("job_data"); + + b.Property("RequestsRecovery") + .HasColumnType("tinyint(1)") + .HasColumnName("requests_recovery"); + + b.HasKey("SchedulerName", "JobName", "JobGroup"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_j_req_recovery"); + + b.ToTable("QRTZ_job_details", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzLock", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("LockName") + .HasColumnType("varchar(200)") + .HasColumnName("lock_name"); + + b.HasKey("SchedulerName", "LockName"); + + b.ToTable("QRTZ_locks", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzPausedTriggerGroup", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.HasKey("SchedulerName", "TriggerGroup"); + + b.ToTable("QRTZ_paused_trigger_grps", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSchedulerState", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("InstanceName") + .HasColumnType("varchar(200)") + .HasColumnName("instance_name"); + + b.Property("CheckInInterval") + .HasColumnType("bigint") + .HasColumnName("checkin_interval"); + + b.Property("LastCheckInTime") + .HasColumnType("bigint") + .HasColumnName("last_checkin_time"); + + b.HasKey("SchedulerName", "InstanceName"); + + b.ToTable("QRTZ_scheduler_state", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("BooleanProperty1") + .HasColumnType("tinyint(1)") + .HasColumnName("bool_prop_1"); + + b.Property("BooleanProperty2") + .HasColumnType("tinyint(1)") + .HasColumnName("bool_prop_2"); + + b.Property("DecimalProperty1") + .HasColumnType("numeric") + .HasColumnName("dec_prop_1"); + + b.Property("DecimalProperty2") + .HasColumnType("numeric") + .HasColumnName("dec_prop_2"); + + b.Property("IntegerProperty1") + .HasColumnType("int") + .HasColumnName("int_prop_1"); + + b.Property("IntegerProperty2") + .HasColumnType("int") + .HasColumnName("int_prop_2"); + + b.Property("LongProperty1") + .HasColumnType("bigint") + .HasColumnName("long_prop_1"); + + b.Property("LongProperty2") + .HasColumnType("bigint") + .HasColumnName("long_prop_2"); + + b.Property("StringProperty1") + .HasColumnType("varchar(200)") + .HasColumnName("str_prop_1"); + + b.Property("StringProperty2") + .HasColumnType("varchar(200)") + .HasColumnName("str_prop_2"); + + b.Property("StringProperty3") + .HasColumnType("varchar(200)") + .HasColumnName("str_prop_3"); + + b.Property("TimeZoneId") + .HasColumnType("varchar(200)") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_simprop_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("RepeatCount") + .HasColumnType("bigint") + .HasColumnName("repeat_count"); + + b.Property("RepeatInterval") + .HasColumnType("bigint") + .HasColumnName("repeat_interval"); + + b.Property("TimesTriggered") + .HasColumnType("bigint") + .HasColumnName("times_triggered"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_simple_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("CalendarName") + .HasColumnType("varchar(200)") + .HasColumnName("calendar_name"); + + b.Property("Description") + .HasColumnType("varchar(200)") + .HasColumnName("description"); + + b.Property("EndTime") + .HasColumnType("bigint") + .HasColumnName("end_time"); + + b.Property("JobData") + .HasColumnType("longblob") + .HasColumnName("job_data"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_name"); + + b.Property("MisfireInstruction") + .HasColumnType("int") + .HasColumnName("misfire_instr"); + + b.Property("NextFireTime") + .HasColumnType("bigint") + .HasColumnName("next_fire_time"); + + b.Property("PreviousFireTime") + .HasColumnType("bigint") + .HasColumnName("prev_fire_time"); + + b.Property("Priority") + .HasColumnType("int") + .HasColumnName("priority"); + + b.Property("StartTime") + .HasColumnType("bigint") + .HasColumnName("start_time"); + + b.Property("TriggerState") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("trigger_state"); + + b.Property("TriggerType") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("trigger_type"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.HasIndex("NextFireTime") + .HasDatabaseName("idx_t_next_fire_time"); + + b.HasIndex("TriggerState") + .HasDatabaseName("idx_t_state"); + + b.HasIndex("NextFireTime", "TriggerState") + .HasDatabaseName("idx_t_nft_st"); + + b.HasIndex("SchedulerName", "JobName", "JobGroup"); + + b.ToTable("QRTZ_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DocumentTrail", b => + { + b.HasOne("SW.Bitween.Domain.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Document"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.ApiGateway", "ApiGateway") + .WithMany("Partners") + .HasForeignKey("ApiGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("ApiGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.BusGateway", "BusGateway") + .WithMany("Routes") + .HasForeignKey("BusGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("BusGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.OwnsMany("SW.Bitween.Domain.ApiCredential", "ApiCredentials", b1 => + { + b1.Property("PartnerId") + .HasColumnType("int"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b1.Property("Id")); + + b1.Property("Key") + .IsRequired() + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b1.Property("Name") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b1.HasKey("PartnerId", "Id"); + + b1.HasIndex("Key") + .IsUnique(); + + b1.ToTable("PartnerApiCredentials", (string)null); + + b1.WithOwner() + .HasForeignKey("PartnerId"); + + b1.HasData( + new + { + PartnerId = 1, + Id = 1, + Key = "7facc758283844b49cc4ffd26a75b1de", + Name = "default" + }); + }); + + b.Navigation("ApiCredentials"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.HasOne("SW.Bitween.Domain.Subscription", null) + .WithMany() + .HasForeignKey("AggregationForId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("FK_Subscriptions_AggFor"); + + b.HasOne("SW.Bitween.Domain.SubscriptionCategory", "Category") + .WithMany() + .HasForeignKey("CategoryId"); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", null) + .WithMany("Subscriptions") + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Subscription", null) + .WithMany() + .HasForeignKey("ResponseSubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("FK_Subscriptions_RespSub"); + + b.HasOne("SW.Bitween.Domain.RetryPolicy", "RetryPolicy") + .WithMany() + .HasForeignKey("RetryPolicyId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("SW.Bitween.Domain.WorkGroup", "WorkGroup") + .WithMany() + .HasForeignKey("WorkGroupId"); + + b.OwnsMany("SW.Bitween.Domain.Schedule", "Schedules", b1 => + { + b1.Property("SubscriptionId") + .HasColumnType("int"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b1.Property("Id")); + + b1.Property("Backwards") + .HasColumnType("tinyint(1)"); + + b1.Property("On") + .HasColumnType("bigint"); + + b1.Property("Recurrence") + .HasColumnType("tinyint unsigned"); + + b1.HasKey("SubscriptionId", "Id"); + + b1.ToTable("SubscriptionSchedules", (string)null); + + b1.WithOwner() + .HasForeignKey("SubscriptionId"); + }); + + b.Navigation("Category"); + + b.Navigation("RetryPolicy"); + + b.Navigation("Schedules"); + + b.Navigation("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionTrail", b => + { + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeAggregation", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeDelivery", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangePromotedProperties", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeResult", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("BlobTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("CronTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimplePropertyTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimpleTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzJobDetail", "JobDetail") + .WithMany("Triggers") + .HasForeignKey("SchedulerName", "JobName", "JobGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("JobDetail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Navigation("Partners"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Navigation("Routes"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Navigation("Subscriptions"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Navigation("Triggers"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Navigation("BlobTriggers"); + + b.Navigation("CronTriggers"); + + b.Navigation("SimplePropertyTriggers"); + + b.Navigation("SimpleTriggers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/SW.Bitween.MySql/Migrations/20260817103452_RetryBudgetAlerts.cs b/SW.Bitween.MySql/Migrations/20260817103452_RetryBudgetAlerts.cs new file mode 100644 index 00000000..278a5f02 --- /dev/null +++ b/SW.Bitween.MySql/Migrations/20260817103452_RetryBudgetAlerts.cs @@ -0,0 +1,128 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace SW.Bitween.MySql.Migrations +{ + /// + public partial class RetryBudgetAlerts : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "AttemptNumber", + table: "XchangeResults", + type: "int", + nullable: true); + + migrationBuilder.AddColumn( + name: "RetryGroupId", + table: "XchangeResults", + type: "char(36)", + nullable: true, + collation: "ascii_general_ci"); + + migrationBuilder.AlterColumn( + name: "NotifierId", + table: "XchangeNotifications", + type: "int", + nullable: true, + oldClrType: typeof(int), + oldType: "int"); + + migrationBuilder.AddColumn( + name: "AlertHandlerId", + table: "RetryPolicies", + type: "varchar(200)", + unicode: false, + maxLength: 200, + nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.AddColumn( + name: "AlertHandlerProperties", + table: "RetryPolicies", + type: "longtext", + nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.AddColumn( + name: "ExhaustedNotifiedOn", + table: "RetryGroupUsages", + type: "datetime(6)", + nullable: true); + + migrationBuilder.CreateTable( + name: "RetryAlertOverrides", + columns: table => new + { + SubscriptionId = table.Column(type: "int", nullable: false), + GroupId = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + AlertMode = table.Column(type: "tinyint unsigned", nullable: false), + AlertHandlerId = table.Column(type: "varchar(200)", unicode: false, maxLength: 200, nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"), + AlertHandlerProperties = table.Column(type: "longtext", nullable: true) + .Annotation("MySql:CharSet", "utf8mb4") + }, + constraints: table => + { + table.PrimaryKey("PK_RetryAlertOverrides", x => new { x.SubscriptionId, x.GroupId }); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateIndex( + name: "IX_XchangeResults_RetryGroupId", + table: "XchangeResults", + column: "RetryGroupId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "RetryAlertOverrides"); + + migrationBuilder.DropIndex( + name: "IX_XchangeResults_RetryGroupId", + table: "XchangeResults"); + + migrationBuilder.DropColumn( + name: "AttemptNumber", + table: "XchangeResults"); + + migrationBuilder.DropColumn( + name: "RetryGroupId", + table: "XchangeResults"); + + migrationBuilder.DropColumn( + name: "AlertHandlerId", + table: "RetryPolicies"); + + migrationBuilder.DropColumn( + name: "AlertHandlerProperties", + table: "RetryPolicies"); + + migrationBuilder.DropColumn( + name: "ExhaustedNotifiedOn", + table: "RetryGroupUsages"); + + // These rows are the alert's own delivery log, and they are the reason the column was + // made nullable. Rolling the feature back leaves nowhere to put them, and the column + // cannot go back to NOT NULL while they are here, so they go with the feature. + migrationBuilder.Sql( + "DELETE FROM `XchangeNotifications` WHERE `NotifierId` IS NULL;"); + + migrationBuilder.AlterColumn( + name: "NotifierId", + table: "XchangeNotifications", + type: "int", + nullable: false, + defaultValue: 0, + oldClrType: typeof(int), + oldType: "int", + oldNullable: true); + } + } +} diff --git a/SW.Bitween.MySql/Migrations/20260819100452_ManualRetryFlag.Designer.cs b/SW.Bitween.MySql/Migrations/20260819100452_ManualRetryFlag.Designer.cs new file mode 100644 index 00000000..61ddbe0f --- /dev/null +++ b/SW.Bitween.MySql/Migrations/20260819100452_ManualRetryFlag.Designer.cs @@ -0,0 +1,1956 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using SW.Bitween.MySql; + +#nullable disable + +namespace SW.Bitween.MySql.Migrations +{ + [DbContext(typeof(BitweenDbContext))] + [Migration("20260819100452_ManualRetryFlag")] + partial class ManualRetryFlag + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.23") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Deleted") + .HasColumnType("tinyint(1)"); + + b.Property("Disabled") + .HasColumnType("tinyint(1)"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("Email") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("EmailProvider") + .HasColumnType("tinyint unsigned"); + + b.Property("FailedLoginCount") + .HasColumnType("int"); + + b.Property("LockoutEnd") + .HasColumnType("datetime(6)"); + + b.Property("LoginMethods") + .HasColumnType("tinyint unsigned"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Password") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("Phone") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("varchar(20)"); + + b.Property("Role") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique(); + + b.ToTable("Accounts", (string)null); + + b.HasData( + new + { + Id = 9999, + CreatedOn = new DateTime(2021, 12, 31, 22, 0, 0, 0, DateTimeKind.Utc), + Deleted = false, + Disabled = false, + DisplayName = "Admin", + Email = "admin@Bitween.systems", + EmailProvider = (byte)0, + FailedLoginCount = 0, + LoginMethods = (byte)2, + Password = "$SWHASH$V1$10000$VQCi48eitH4Ml5juvBMOFZrMdQwBbhuIQVXe6RR7qJdDF2bJ", + Role = 0 + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AccountId") + .HasColumnType("int"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("LoginMethod") + .HasColumnType("tinyint unsigned"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.ToTable("RefreshTokens", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DelayedRetry", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("On") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("On"); + + b.ToTable("DelayedRetries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Document", b => + { + b.Property("Id") + .HasColumnType("int"); + + b.Property("BusEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("BusMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("DisregardsUnfilteredMessages") + .HasColumnType("tinyint(1)"); + + b.Property("DocumentFormat") + .HasColumnType("int"); + + b.Property("DuplicateInterval") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("PromotedProperties") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("BusMessageTypeName") + .IsUnique(); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Documents", (string)null); + + b.HasData( + new + { + Id = 10001, + BusEnabled = false, + DocumentFormat = 0, + DuplicateInterval = 0, + Name = "Aggregation Document", + PromotedProperties = "{}" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DocumentTrail", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Code") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("StateAfter") + .HasColumnType("longtext"); + + b.Property("StateBefore") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("CreatedOn"); + + b.HasIndex("DocumentId"); + + b.ToTable("DocumentTrail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("UrlName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("UrlName") + .IsUnique(); + + b.ToTable("ApiGateways", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.Property("ApiGatewayId") + .HasColumnType("int"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.HasKey("ApiGatewayId", "PartnerId", "SubscriptionId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("ApiGatewayPartners", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.ToTable("BusGateways", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("BusGatewayId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("MatchExpression") + .HasColumnType("longtext"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("BusGatewayId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("BusGatewayRoutes", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.GlobalAdapterValuesSet", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("Values") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("GlobalAdapterValuesSets", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Notifier", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("longtext"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("RunOnBadResult") + .HasColumnType("tinyint(1)"); + + b.Property("RunOnFailedResult") + .HasColumnType("tinyint(1)"); + + b.Property("RunOnSubscriptions") + .HasColumnType("longtext"); + + b.Property("RunOnSuccessfulResult") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.ToTable("Notifiers", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OnHoldXchange", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("BadData") + .HasColumnType("tinyint(1)"); + + b.Property("Data") + .HasColumnType("longtext"); + + b.Property("FileName") + .HasColumnType("longtext"); + + b.Property("References") + .HasMaxLength(1024) + .HasColumnType("varchar(1024)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("OnHoldXchanges", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AdapterProperties") + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.ToTable("Partners", (string)null); + + b.HasData( + new + { + Id = 1, + Name = "SYSTEM" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryAlertOverride", b => + { + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("GroupId") + .HasColumnType("char(36)"); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("AlertHandlerProperties") + .HasColumnType("longtext"); + + b.Property("AlertMode") + .HasColumnType("tinyint unsigned"); + + b.HasKey("SubscriptionId", "GroupId"); + + b.ToTable("RetryAlertOverrides", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryGroupUsage", b => + { + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("GroupId") + .HasColumnType("char(36)"); + + b.Property("AttemptsUsed") + .HasColumnType("int"); + + b.Property("ExhaustedNotifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("LastAttemptOn") + .HasColumnType("datetime(6)"); + + b.HasKey("SubscriptionId", "GroupId"); + + b.ToTable("RetryGroupUsages", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryPolicy", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("AlertHandlerProperties") + .HasColumnType("longtext"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Groups") + .HasColumnType("longtext"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.ToTable("RetryPolicies", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AggregateOn") + .HasColumnType("datetime(6)"); + + b.Property("AggregationForId") + .HasColumnType("int"); + + b.Property("AggregationTarget") + .HasColumnType("tinyint unsigned"); + + b.Property("CategoryId") + .HasColumnType("int"); + + b.Property("ConsecutiveFailures") + .HasColumnType("int"); + + b.Property("CustomRetryPolicy") + .HasColumnType("longtext"); + + b.Property("DocumentFilter") + .HasColumnType("longtext"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("longtext"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("IsRunning") + .HasColumnType("tinyint(1)"); + + b.Property("LastException") + .HasColumnType("longtext"); + + b.Property("MapperId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("MapperProperties") + .HasColumnType("longtext"); + + b.Property("MatchExpression") + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("PausedOn") + .HasColumnType("datetime(6)"); + + b.Property("ReceiveOn") + .HasColumnType("datetime(6)"); + + b.Property("ReceiverId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ReceiverProperties") + .HasColumnType("longtext"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("int"); + + b.Property("RetryPolicyId") + .HasColumnType("int"); + + b.Property("Temporary") + .HasColumnType("tinyint(1)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("ValidatorId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ValidatorProperties") + .HasColumnType("longtext"); + + b.Property("WorkGroupId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("AggregationForId"); + + b.HasIndex("CategoryId"); + + b.HasIndex("DocumentId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("ResponseSubscriptionId"); + + b.HasIndex("RetryPolicyId"); + + b.HasIndex("WorkGroupId"); + + b.ToTable("Subscriptions", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Code") + .HasColumnType("varchar(255)"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Description") + .HasColumnType("longtext"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.ToTable("SubscriptionCategory"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionTrail", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Code") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("StateAfter") + .HasColumnType("longtext"); + + b.Property("StateBefore") + .HasColumnType("longtext"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("CreatedOn"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("SubscriptionTrail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.WorkGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("BusMessageName") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("Options") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("CorrelationId") + .HasColumnType("longtext"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("longtext"); + + b.Property("InputContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("InputHash") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("InputName") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("InputSize") + .HasColumnType("int"); + + b.Property("ManualRetry") + .HasColumnType("tinyint(1)"); + + b.Property("MapperId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("MapperProperties") + .HasColumnType("longtext"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("References") + .HasMaxLength(1024) + .HasColumnType("varchar(1024)"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("int"); + + b.Property("RetryFor") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("StartedOn") + .HasColumnType("datetime(6)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.HasIndex("InputHash"); + + b.HasIndex("RetryFor"); + + b.HasIndex("StartedOn"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("Xchanges", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AggregatedOn") + .HasColumnType("datetime(6)"); + + b.Property("AggregationXchangeId") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("AggregationXchangeId"); + + b.ToTable("XchangeAggregations", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("DeliveredOn") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("DeliveredOn"); + + b.ToTable("XchangeDeliveries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeNotification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Exception") + .HasColumnType("longtext"); + + b.Property("FinishedOn") + .HasColumnType("datetime(6)"); + + b.Property("NotifierId") + .HasColumnType("int"); + + b.Property("NotifierName") + .HasColumnType("longtext"); + + b.Property("Success") + .HasColumnType("tinyint(1)"); + + b.Property("XchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.ToTable("XchangeNotifications", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("Hits") + .HasMaxLength(2000) + .IsUnicode(false) + .HasColumnType("varchar(2000)"); + + b.Property("Properties") + .HasColumnType("longtext"); + + b.Property("PropertiesRaw") + .HasColumnType("varchar(255)"); + + b.HasKey("Id"); + + b.HasIndex("PropertiesRaw"); + + b.ToTable("XchangePromotedProperties", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AttemptNumber") + .HasColumnType("int"); + + b.Property("Exception") + .HasColumnType("longtext"); + + b.Property("FinishedOn") + .HasColumnType("datetime(6)"); + + b.Property("OutputBad") + .HasColumnType("tinyint(1)"); + + b.Property("OutputContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("OutputHash") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("OutputName") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("OutputSize") + .HasColumnType("int"); + + b.Property("ResponseBad") + .HasColumnType("tinyint(1)"); + + b.Property("ResponseContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ResponseHash") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("ResponseName") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("ResponseSize") + .HasColumnType("int"); + + b.Property("ResponseXchangeId") + .HasColumnType("longtext"); + + b.Property("RetryBlockedReason") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("RetryGroupId") + .HasColumnType("char(36)"); + + b.Property("Success") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.HasIndex("RetryGroupId"); + + b.ToTable("XchangeResults", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.RunFlagUpdater+RunningResult", b => + { + b.Property("IsRunning") + .HasColumnType("tinyint(1)"); + + b.ToTable((string)null); + + b.ToView(null, (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.JobExecution", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Context") + .HasColumnType("longtext") + .HasColumnName("context"); + + b.Property("DurationMs") + .HasColumnType("bigint") + .HasColumnName("duration_ms"); + + b.Property("EndTimeUtc") + .HasColumnType("datetime(6)") + .HasColumnName("end_time_utc"); + + b.Property("Error") + .HasColumnType("longtext") + .HasColumnName("error"); + + b.Property("FireInstanceId") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("fire_instance_id"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_name"); + + b.Property("JobTypeName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_type_name"); + + b.Property("Node") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("node"); + + b.Property("StartTimeUtc") + .HasColumnType("datetime(6)") + .HasColumnName("start_time_utc"); + + b.Property("Success") + .HasColumnType("tinyint(1)") + .HasColumnName("success"); + + b.HasKey("Id"); + + b.HasIndex("FireInstanceId") + .IsUnique() + .HasDatabaseName("idx_je_fire_instance_id"); + + b.HasIndex("StartTimeUtc") + .HasDatabaseName("idx_je_start_time"); + + b.HasIndex("Success") + .HasDatabaseName("idx_je_success"); + + b.HasIndex("JobGroup", "JobName", "StartTimeUtc") + .HasDatabaseName("idx_je_group_name_start"); + + b.ToTable("job_executions", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("BlobData") + .HasColumnType("longblob") + .HasColumnName("blob_data"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_blob_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCalendar", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("CalendarName") + .HasColumnType("varchar(200)") + .HasColumnName("calendar_name"); + + b.Property("Calendar") + .IsRequired() + .HasColumnType("longblob") + .HasColumnName("calendar"); + + b.HasKey("SchedulerName", "CalendarName"); + + b.ToTable("QRTZ_calendars", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("CronExpression") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("cron_expression"); + + b.Property("TimeZoneId") + .HasColumnType("varchar(200)") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_cron_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzFiredTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("EntryId") + .HasColumnType("varchar(200)") + .HasColumnName("entry_id"); + + b.Property("FiredTime") + .HasColumnType("bigint") + .HasColumnName("fired_time"); + + b.Property("InstanceName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("instance_name"); + + b.Property("IsNonConcurrent") + .HasColumnType("tinyint(1)") + .HasColumnName("is_nonconcurrent"); + + b.Property("JobGroup") + .HasColumnType("varchar(200)") + .HasColumnName("job_group"); + + b.Property("JobName") + .HasColumnType("varchar(200)") + .HasColumnName("job_name"); + + b.Property("Priority") + .HasColumnType("int") + .HasColumnName("priority"); + + b.Property("RequestsRecovery") + .HasColumnType("tinyint(1)") + .HasColumnName("requests_recovery"); + + b.Property("ScheduledTime") + .HasColumnType("bigint") + .HasColumnName("sched_time"); + + b.Property("State") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("state"); + + b.Property("TriggerGroup") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("TriggerName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.HasKey("SchedulerName", "EntryId"); + + b.HasIndex("InstanceName") + .HasDatabaseName("idx_QRTZ_ft_trig_inst_name"); + + b.HasIndex("JobGroup") + .HasDatabaseName("idx_QRTZ_ft_job_group"); + + b.HasIndex("JobName") + .HasDatabaseName("idx_QRTZ_ft_job_name"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_QRTZ_ft_job_req_recovery"); + + b.HasIndex("TriggerGroup") + .HasDatabaseName("idx_QRTZ_ft_trig_group"); + + b.HasIndex("TriggerName") + .HasDatabaseName("idx_QRTZ_ft_trig_name"); + + b.HasIndex("SchedulerName", "TriggerName", "TriggerGroup") + .HasDatabaseName("idx_QRTZ_ft_trig_nm_gp"); + + b.ToTable("QRTZ_fired_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("JobName") + .HasColumnType("varchar(200)") + .HasColumnName("job_name"); + + b.Property("JobGroup") + .HasColumnType("varchar(200)") + .HasColumnName("job_group"); + + b.Property("Description") + .HasColumnType("varchar(200)") + .HasColumnName("description"); + + b.Property("IsDurable") + .HasColumnType("tinyint(1)") + .HasColumnName("is_durable"); + + b.Property("IsNonConcurrent") + .HasColumnType("tinyint(1)") + .HasColumnName("is_nonconcurrent"); + + b.Property("IsUpdateData") + .HasColumnType("tinyint(1)") + .HasColumnName("is_update_data"); + + b.Property("JobClassName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_class_name"); + + b.Property("JobData") + .HasColumnType("longblob") + .HasColumnName("job_data"); + + b.Property("RequestsRecovery") + .HasColumnType("tinyint(1)") + .HasColumnName("requests_recovery"); + + b.HasKey("SchedulerName", "JobName", "JobGroup"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_j_req_recovery"); + + b.ToTable("QRTZ_job_details", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzLock", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("LockName") + .HasColumnType("varchar(200)") + .HasColumnName("lock_name"); + + b.HasKey("SchedulerName", "LockName"); + + b.ToTable("QRTZ_locks", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzPausedTriggerGroup", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.HasKey("SchedulerName", "TriggerGroup"); + + b.ToTable("QRTZ_paused_trigger_grps", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSchedulerState", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("InstanceName") + .HasColumnType("varchar(200)") + .HasColumnName("instance_name"); + + b.Property("CheckInInterval") + .HasColumnType("bigint") + .HasColumnName("checkin_interval"); + + b.Property("LastCheckInTime") + .HasColumnType("bigint") + .HasColumnName("last_checkin_time"); + + b.HasKey("SchedulerName", "InstanceName"); + + b.ToTable("QRTZ_scheduler_state", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("BooleanProperty1") + .HasColumnType("tinyint(1)") + .HasColumnName("bool_prop_1"); + + b.Property("BooleanProperty2") + .HasColumnType("tinyint(1)") + .HasColumnName("bool_prop_2"); + + b.Property("DecimalProperty1") + .HasColumnType("numeric") + .HasColumnName("dec_prop_1"); + + b.Property("DecimalProperty2") + .HasColumnType("numeric") + .HasColumnName("dec_prop_2"); + + b.Property("IntegerProperty1") + .HasColumnType("int") + .HasColumnName("int_prop_1"); + + b.Property("IntegerProperty2") + .HasColumnType("int") + .HasColumnName("int_prop_2"); + + b.Property("LongProperty1") + .HasColumnType("bigint") + .HasColumnName("long_prop_1"); + + b.Property("LongProperty2") + .HasColumnType("bigint") + .HasColumnName("long_prop_2"); + + b.Property("StringProperty1") + .HasColumnType("varchar(200)") + .HasColumnName("str_prop_1"); + + b.Property("StringProperty2") + .HasColumnType("varchar(200)") + .HasColumnName("str_prop_2"); + + b.Property("StringProperty3") + .HasColumnType("varchar(200)") + .HasColumnName("str_prop_3"); + + b.Property("TimeZoneId") + .HasColumnType("varchar(200)") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_simprop_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("RepeatCount") + .HasColumnType("bigint") + .HasColumnName("repeat_count"); + + b.Property("RepeatInterval") + .HasColumnType("bigint") + .HasColumnName("repeat_interval"); + + b.Property("TimesTriggered") + .HasColumnType("bigint") + .HasColumnName("times_triggered"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_simple_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("CalendarName") + .HasColumnType("varchar(200)") + .HasColumnName("calendar_name"); + + b.Property("Description") + .HasColumnType("varchar(200)") + .HasColumnName("description"); + + b.Property("EndTime") + .HasColumnType("bigint") + .HasColumnName("end_time"); + + b.Property("JobData") + .HasColumnType("longblob") + .HasColumnName("job_data"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_name"); + + b.Property("MisfireInstruction") + .HasColumnType("int") + .HasColumnName("misfire_instr"); + + b.Property("NextFireTime") + .HasColumnType("bigint") + .HasColumnName("next_fire_time"); + + b.Property("PreviousFireTime") + .HasColumnType("bigint") + .HasColumnName("prev_fire_time"); + + b.Property("Priority") + .HasColumnType("int") + .HasColumnName("priority"); + + b.Property("StartTime") + .HasColumnType("bigint") + .HasColumnName("start_time"); + + b.Property("TriggerState") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("trigger_state"); + + b.Property("TriggerType") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("trigger_type"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.HasIndex("NextFireTime") + .HasDatabaseName("idx_t_next_fire_time"); + + b.HasIndex("TriggerState") + .HasDatabaseName("idx_t_state"); + + b.HasIndex("NextFireTime", "TriggerState") + .HasDatabaseName("idx_t_nft_st"); + + b.HasIndex("SchedulerName", "JobName", "JobGroup"); + + b.ToTable("QRTZ_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DocumentTrail", b => + { + b.HasOne("SW.Bitween.Domain.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Document"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.ApiGateway", "ApiGateway") + .WithMany("Partners") + .HasForeignKey("ApiGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("ApiGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.BusGateway", "BusGateway") + .WithMany("Routes") + .HasForeignKey("BusGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("BusGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.OwnsMany("SW.Bitween.Domain.ApiCredential", "ApiCredentials", b1 => + { + b1.Property("PartnerId") + .HasColumnType("int"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b1.Property("Id")); + + b1.Property("Key") + .IsRequired() + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b1.Property("Name") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b1.HasKey("PartnerId", "Id"); + + b1.HasIndex("Key") + .IsUnique(); + + b1.ToTable("PartnerApiCredentials", (string)null); + + b1.WithOwner() + .HasForeignKey("PartnerId"); + + b1.HasData( + new + { + PartnerId = 1, + Id = 1, + Key = "7facc758283844b49cc4ffd26a75b1de", + Name = "default" + }); + }); + + b.Navigation("ApiCredentials"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.HasOne("SW.Bitween.Domain.Subscription", null) + .WithMany() + .HasForeignKey("AggregationForId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("FK_Subscriptions_AggFor"); + + b.HasOne("SW.Bitween.Domain.SubscriptionCategory", "Category") + .WithMany() + .HasForeignKey("CategoryId"); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", null) + .WithMany("Subscriptions") + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Subscription", null) + .WithMany() + .HasForeignKey("ResponseSubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("FK_Subscriptions_RespSub"); + + b.HasOne("SW.Bitween.Domain.RetryPolicy", "RetryPolicy") + .WithMany() + .HasForeignKey("RetryPolicyId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("SW.Bitween.Domain.WorkGroup", "WorkGroup") + .WithMany() + .HasForeignKey("WorkGroupId"); + + b.OwnsMany("SW.Bitween.Domain.Schedule", "Schedules", b1 => + { + b1.Property("SubscriptionId") + .HasColumnType("int"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b1.Property("Id")); + + b1.Property("Backwards") + .HasColumnType("tinyint(1)"); + + b1.Property("On") + .HasColumnType("bigint"); + + b1.Property("Recurrence") + .HasColumnType("tinyint unsigned"); + + b1.HasKey("SubscriptionId", "Id"); + + b1.ToTable("SubscriptionSchedules", (string)null); + + b1.WithOwner() + .HasForeignKey("SubscriptionId"); + }); + + b.Navigation("Category"); + + b.Navigation("RetryPolicy"); + + b.Navigation("Schedules"); + + b.Navigation("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionTrail", b => + { + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeAggregation", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeDelivery", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangePromotedProperties", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeResult", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("BlobTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("CronTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimplePropertyTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimpleTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzJobDetail", "JobDetail") + .WithMany("Triggers") + .HasForeignKey("SchedulerName", "JobName", "JobGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("JobDetail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Navigation("Partners"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Navigation("Routes"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Navigation("Subscriptions"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Navigation("Triggers"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Navigation("BlobTriggers"); + + b.Navigation("CronTriggers"); + + b.Navigation("SimplePropertyTriggers"); + + b.Navigation("SimpleTriggers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/SW.Bitween.MySql/Migrations/20260819100452_ManualRetryFlag.cs b/SW.Bitween.MySql/Migrations/20260819100452_ManualRetryFlag.cs new file mode 100644 index 00000000..2be75b27 --- /dev/null +++ b/SW.Bitween.MySql/Migrations/20260819100452_ManualRetryFlag.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace SW.Bitween.MySql.Migrations +{ + /// + public partial class ManualRetryFlag : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "ManualRetry", + table: "Xchanges", + type: "tinyint(1)", + nullable: false, + defaultValue: false); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "ManualRetry", + table: "Xchanges"); + } + } +} diff --git a/SW.Bitween.MySql/Migrations/20260823104220_GatewayInactiveFlag.Designer.cs b/SW.Bitween.MySql/Migrations/20260823104220_GatewayInactiveFlag.Designer.cs new file mode 100644 index 00000000..217041ea --- /dev/null +++ b/SW.Bitween.MySql/Migrations/20260823104220_GatewayInactiveFlag.Designer.cs @@ -0,0 +1,2097 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using SW.Bitween.MySql; + +#nullable disable + +namespace SW.Bitween.MySql.Migrations +{ + [DbContext(typeof(BitweenDbContext))] + [Migration("20260823104220_GatewayInactiveFlag")] + partial class GatewayInactiveFlag + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.23") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Deleted") + .HasColumnType("tinyint(1)"); + + b.Property("Disabled") + .HasColumnType("tinyint(1)"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("Email") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("EmailProvider") + .HasColumnType("tinyint unsigned"); + + b.Property("FailedLoginCount") + .HasColumnType("int"); + + b.Property("LockoutEnd") + .HasColumnType("datetime(6)"); + + b.Property("LoginMethods") + .HasColumnType("tinyint unsigned"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Password") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("Role") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique(); + + b.ToTable("Accounts", (string)null); + + b.HasData( + new + { + Id = 9999, + CreatedOn = new DateTime(2021, 12, 31, 22, 0, 0, 0, DateTimeKind.Utc), + Deleted = false, + Disabled = false, + DisplayName = "Admin", + Email = "admin@Bitween.systems", + EmailProvider = (byte)0, + FailedLoginCount = 0, + LoginMethods = (byte)2, + Password = "$SWHASH$V1$10000$VQCi48eitH4Ml5juvBMOFZrMdQwBbhuIQVXe6RR7qJdDF2bJ", + Role = 0 + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.AccountRoleLink", b => + { + b.Property("AccountId") + .HasColumnType("int"); + + b.Property("RoleId") + .HasColumnType("int"); + + b.HasKey("AccountId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AccountRoles", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AccountId") + .HasColumnType("int"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("LoginMethod") + .HasColumnType("tinyint unsigned"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.ToTable("RefreshTokens", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("IsSystem") + .HasColumnType("tinyint(1)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("Permissions") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Roles", (string)null); + + b.HasData( + new + { + Id = 1, + CreatedOn = new DateTime(2021, 12, 31, 22, 0, 0, 0, DateTimeKind.Utc), + Description = "Full access to everything, including members, roles and settings.", + IsSystem = true, + Name = "Administrator", + Permissions = "[]" + }, + new + { + Id = 2, + CreatedOn = new DateTime(2021, 12, 31, 22, 0, 0, 0, DateTimeKind.Utc), + Description = "Runs and configures integrations. Can't manage members, roles or settings.", + IsSystem = true, + Name = "Member", + Permissions = "[]" + }, + new + { + Id = 3, + CreatedOn = new DateTime(2021, 12, 31, 22, 0, 0, 0, DateTimeKind.Utc), + Description = "Read-only access to integrations, exchanges and configuration.", + IsSystem = true, + Name = "Viewer", + Permissions = "[]" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DelayedRetry", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("On") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("On"); + + b.ToTable("DelayedRetries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Document", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("BusEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("BusMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("Code") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("DisregardsUnfilteredMessages") + .HasColumnType("tinyint(1)"); + + b.Property("DocumentFormat") + .HasColumnType("int"); + + b.Property("DuplicateInterval") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("PromotedProperties") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("BusMessageTypeName") + .IsUnique(); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Documents", (string)null); + + b.HasData( + new + { + Id = 10001, + BusEnabled = false, + DocumentFormat = 0, + DuplicateInterval = 0, + Name = "Aggregation Document", + PromotedProperties = "{}" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DocumentTrail", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Code") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("StateAfter") + .HasColumnType("longtext"); + + b.Property("StateBefore") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("CreatedOn"); + + b.HasIndex("DocumentId"); + + b.ToTable("DocumentTrail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("UrlName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("UrlName") + .IsUnique(); + + b.ToTable("ApiGateways", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.Property("ApiGatewayId") + .HasColumnType("int"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.HasKey("ApiGatewayId", "PartnerId", "SubscriptionId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("ApiGatewayPartners", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.ToTable("BusGateways", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("BusGatewayId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("MatchExpression") + .HasColumnType("longtext"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("BusGatewayId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("BusGatewayRoutes", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.GlobalAdapterValuesSet", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("Values") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("GlobalAdapterValuesSets", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Notifier", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("longtext"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("RunOnBadResult") + .HasColumnType("tinyint(1)"); + + b.Property("RunOnFailedResult") + .HasColumnType("tinyint(1)"); + + b.Property("RunOnSubscriptions") + .HasColumnType("longtext"); + + b.Property("RunOnSuccessfulResult") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.ToTable("Notifiers", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OnHoldXchange", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("BadData") + .HasColumnType("tinyint(1)"); + + b.Property("Data") + .HasColumnType("longtext"); + + b.Property("FileName") + .HasColumnType("longtext"); + + b.Property("References") + .HasMaxLength(1024) + .HasColumnType("varchar(1024)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("OnHoldXchanges", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AdapterProperties") + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.ToTable("Partners", (string)null); + + b.HasData( + new + { + Id = 1, + Name = "SYSTEM" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryAlertOverride", b => + { + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("GroupId") + .HasColumnType("char(36)"); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("AlertHandlerProperties") + .HasColumnType("longtext"); + + b.Property("AlertMode") + .HasColumnType("tinyint unsigned"); + + b.HasKey("SubscriptionId", "GroupId"); + + b.ToTable("RetryAlertOverrides", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryGroupUsage", b => + { + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("GroupId") + .HasColumnType("char(36)"); + + b.Property("AttemptsUsed") + .HasColumnType("int"); + + b.Property("ExhaustedNotifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("LastAttemptOn") + .HasColumnType("datetime(6)"); + + b.HasKey("SubscriptionId", "GroupId"); + + b.ToTable("RetryGroupUsages", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryPolicy", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("AlertHandlerProperties") + .HasColumnType("longtext"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Groups") + .HasColumnType("longtext"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.ToTable("RetryPolicies", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Setting", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Value") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("Settings", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AggregateOn") + .HasColumnType("datetime(6)"); + + b.Property("AggregationForId") + .HasColumnType("int"); + + b.Property("AggregationTarget") + .HasColumnType("tinyint unsigned"); + + b.Property("CategoryId") + .HasColumnType("int"); + + b.Property("ConsecutiveFailures") + .HasColumnType("int"); + + b.Property("CustomRetryPolicy") + .HasColumnType("longtext"); + + b.Property("DocumentFilter") + .HasColumnType("longtext"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("longtext"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("IsRunning") + .HasColumnType("tinyint(1)"); + + b.Property("LastException") + .HasColumnType("longtext"); + + b.Property("MapperId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("MapperProperties") + .HasColumnType("longtext"); + + b.Property("MatchExpression") + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("PausedOn") + .HasColumnType("datetime(6)"); + + b.Property("ReceiveOn") + .HasColumnType("datetime(6)"); + + b.Property("ReceiverId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ReceiverProperties") + .HasColumnType("longtext"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("int"); + + b.Property("RetryPolicyId") + .HasColumnType("int"); + + b.Property("Temporary") + .HasColumnType("tinyint(1)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("ValidatorId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ValidatorProperties") + .HasColumnType("longtext"); + + b.Property("WorkGroupId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("AggregationForId"); + + b.HasIndex("CategoryId"); + + b.HasIndex("DocumentId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("ResponseSubscriptionId"); + + b.HasIndex("RetryPolicyId"); + + b.HasIndex("WorkGroupId"); + + b.ToTable("Subscriptions", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Code") + .HasColumnType("varchar(255)"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Description") + .HasColumnType("longtext"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.ToTable("SubscriptionCategory"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionTrail", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Code") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("StateAfter") + .HasColumnType("longtext"); + + b.Property("StateBefore") + .HasColumnType("longtext"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("CreatedOn"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("SubscriptionTrail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.WorkGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("BusMessageName") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("Options") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("CorrelationId") + .HasColumnType("longtext"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("longtext"); + + b.Property("InputContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("InputHash") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("InputName") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("InputSize") + .HasColumnType("int"); + + b.Property("ManualRetry") + .HasColumnType("tinyint(1)"); + + b.Property("MapperId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("MapperProperties") + .HasColumnType("longtext"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("References") + .HasMaxLength(1024) + .HasColumnType("varchar(1024)"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("int"); + + b.Property("RetryFor") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("StartedOn") + .HasColumnType("datetime(6)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.HasIndex("InputHash"); + + b.HasIndex("RetryFor"); + + b.HasIndex("StartedOn"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("Xchanges", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AggregatedOn") + .HasColumnType("datetime(6)"); + + b.Property("AggregationXchangeId") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("AggregationXchangeId"); + + b.ToTable("XchangeAggregations", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("DeliveredOn") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("DeliveredOn"); + + b.ToTable("XchangeDeliveries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeNotification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Exception") + .HasColumnType("longtext"); + + b.Property("FinishedOn") + .HasColumnType("datetime(6)"); + + b.Property("NotifierId") + .HasColumnType("int"); + + b.Property("NotifierName") + .HasColumnType("longtext"); + + b.Property("Success") + .HasColumnType("tinyint(1)"); + + b.Property("XchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.ToTable("XchangeNotifications", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("Hits") + .HasMaxLength(2000) + .IsUnicode(false) + .HasColumnType("varchar(2000)"); + + b.Property("Properties") + .HasColumnType("longtext"); + + b.Property("PropertiesRaw") + .HasColumnType("varchar(255)"); + + b.HasKey("Id"); + + b.HasIndex("PropertiesRaw"); + + b.ToTable("XchangePromotedProperties", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AttemptNumber") + .HasColumnType("int"); + + b.Property("Exception") + .HasColumnType("longtext"); + + b.Property("FinishedOn") + .HasColumnType("datetime(6)"); + + b.Property("OutputBad") + .HasColumnType("tinyint(1)"); + + b.Property("OutputContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("OutputHash") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("OutputName") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("OutputSize") + .HasColumnType("int"); + + b.Property("ResponseBad") + .HasColumnType("tinyint(1)"); + + b.Property("ResponseContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ResponseHash") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("ResponseName") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("ResponseSize") + .HasColumnType("int"); + + b.Property("ResponseXchangeId") + .HasColumnType("longtext"); + + b.Property("RetryBlockedReason") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("RetryGroupId") + .HasColumnType("char(36)"); + + b.Property("Success") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.HasIndex("RetryGroupId"); + + b.ToTable("XchangeResults", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.RunFlagUpdater+RunningResult", b => + { + b.Property("IsRunning") + .HasColumnType("tinyint(1)"); + + b.ToTable((string)null); + + b.ToView(null, (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.JobExecution", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Context") + .HasColumnType("longtext") + .HasColumnName("context"); + + b.Property("DurationMs") + .HasColumnType("bigint") + .HasColumnName("duration_ms"); + + b.Property("EndTimeUtc") + .HasColumnType("datetime(6)") + .HasColumnName("end_time_utc"); + + b.Property("Error") + .HasColumnType("longtext") + .HasColumnName("error"); + + b.Property("FireInstanceId") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("fire_instance_id"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_name"); + + b.Property("JobTypeName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_type_name"); + + b.Property("Node") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("node"); + + b.Property("StartTimeUtc") + .HasColumnType("datetime(6)") + .HasColumnName("start_time_utc"); + + b.Property("Success") + .HasColumnType("tinyint(1)") + .HasColumnName("success"); + + b.HasKey("Id"); + + b.HasIndex("FireInstanceId") + .IsUnique() + .HasDatabaseName("idx_je_fire_instance_id"); + + b.HasIndex("StartTimeUtc") + .HasDatabaseName("idx_je_start_time"); + + b.HasIndex("Success") + .HasDatabaseName("idx_je_success"); + + b.HasIndex("JobGroup", "JobName", "StartTimeUtc") + .HasDatabaseName("idx_je_group_name_start"); + + b.ToTable("job_executions", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("BlobData") + .HasColumnType("longblob") + .HasColumnName("blob_data"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_blob_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCalendar", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("CalendarName") + .HasColumnType("varchar(200)") + .HasColumnName("calendar_name"); + + b.Property("Calendar") + .IsRequired() + .HasColumnType("longblob") + .HasColumnName("calendar"); + + b.HasKey("SchedulerName", "CalendarName"); + + b.ToTable("QRTZ_calendars", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("CronExpression") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("cron_expression"); + + b.Property("TimeZoneId") + .HasColumnType("varchar(200)") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_cron_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzFiredTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("EntryId") + .HasColumnType("varchar(200)") + .HasColumnName("entry_id"); + + b.Property("FiredTime") + .HasColumnType("bigint") + .HasColumnName("fired_time"); + + b.Property("InstanceName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("instance_name"); + + b.Property("IsNonConcurrent") + .HasColumnType("tinyint(1)") + .HasColumnName("is_nonconcurrent"); + + b.Property("JobGroup") + .HasColumnType("varchar(200)") + .HasColumnName("job_group"); + + b.Property("JobName") + .HasColumnType("varchar(200)") + .HasColumnName("job_name"); + + b.Property("Priority") + .HasColumnType("int") + .HasColumnName("priority"); + + b.Property("RequestsRecovery") + .HasColumnType("tinyint(1)") + .HasColumnName("requests_recovery"); + + b.Property("ScheduledTime") + .HasColumnType("bigint") + .HasColumnName("sched_time"); + + b.Property("State") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("state"); + + b.Property("TriggerGroup") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("TriggerName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.HasKey("SchedulerName", "EntryId"); + + b.HasIndex("InstanceName") + .HasDatabaseName("idx_QRTZ_ft_trig_inst_name"); + + b.HasIndex("JobGroup") + .HasDatabaseName("idx_QRTZ_ft_job_group"); + + b.HasIndex("JobName") + .HasDatabaseName("idx_QRTZ_ft_job_name"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_QRTZ_ft_job_req_recovery"); + + b.HasIndex("TriggerGroup") + .HasDatabaseName("idx_QRTZ_ft_trig_group"); + + b.HasIndex("TriggerName") + .HasDatabaseName("idx_QRTZ_ft_trig_name"); + + b.HasIndex("SchedulerName", "TriggerName", "TriggerGroup") + .HasDatabaseName("idx_QRTZ_ft_trig_nm_gp"); + + b.ToTable("QRTZ_fired_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("JobName") + .HasColumnType("varchar(200)") + .HasColumnName("job_name"); + + b.Property("JobGroup") + .HasColumnType("varchar(200)") + .HasColumnName("job_group"); + + b.Property("Description") + .HasColumnType("varchar(200)") + .HasColumnName("description"); + + b.Property("IsDurable") + .HasColumnType("tinyint(1)") + .HasColumnName("is_durable"); + + b.Property("IsNonConcurrent") + .HasColumnType("tinyint(1)") + .HasColumnName("is_nonconcurrent"); + + b.Property("IsUpdateData") + .HasColumnType("tinyint(1)") + .HasColumnName("is_update_data"); + + b.Property("JobClassName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_class_name"); + + b.Property("JobData") + .HasColumnType("longblob") + .HasColumnName("job_data"); + + b.Property("RequestsRecovery") + .HasColumnType("tinyint(1)") + .HasColumnName("requests_recovery"); + + b.HasKey("SchedulerName", "JobName", "JobGroup"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_j_req_recovery"); + + b.ToTable("QRTZ_job_details", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzLock", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("LockName") + .HasColumnType("varchar(200)") + .HasColumnName("lock_name"); + + b.HasKey("SchedulerName", "LockName"); + + b.ToTable("QRTZ_locks", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzPausedTriggerGroup", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.HasKey("SchedulerName", "TriggerGroup"); + + b.ToTable("QRTZ_paused_trigger_grps", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSchedulerState", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("InstanceName") + .HasColumnType("varchar(200)") + .HasColumnName("instance_name"); + + b.Property("CheckInInterval") + .HasColumnType("bigint") + .HasColumnName("checkin_interval"); + + b.Property("LastCheckInTime") + .HasColumnType("bigint") + .HasColumnName("last_checkin_time"); + + b.HasKey("SchedulerName", "InstanceName"); + + b.ToTable("QRTZ_scheduler_state", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("BooleanProperty1") + .HasColumnType("tinyint(1)") + .HasColumnName("bool_prop_1"); + + b.Property("BooleanProperty2") + .HasColumnType("tinyint(1)") + .HasColumnName("bool_prop_2"); + + b.Property("DecimalProperty1") + .HasColumnType("numeric") + .HasColumnName("dec_prop_1"); + + b.Property("DecimalProperty2") + .HasColumnType("numeric") + .HasColumnName("dec_prop_2"); + + b.Property("IntegerProperty1") + .HasColumnType("int") + .HasColumnName("int_prop_1"); + + b.Property("IntegerProperty2") + .HasColumnType("int") + .HasColumnName("int_prop_2"); + + b.Property("LongProperty1") + .HasColumnType("bigint") + .HasColumnName("long_prop_1"); + + b.Property("LongProperty2") + .HasColumnType("bigint") + .HasColumnName("long_prop_2"); + + b.Property("StringProperty1") + .HasColumnType("varchar(200)") + .HasColumnName("str_prop_1"); + + b.Property("StringProperty2") + .HasColumnType("varchar(200)") + .HasColumnName("str_prop_2"); + + b.Property("StringProperty3") + .HasColumnType("varchar(200)") + .HasColumnName("str_prop_3"); + + b.Property("TimeZoneId") + .HasColumnType("varchar(200)") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_simprop_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("RepeatCount") + .HasColumnType("bigint") + .HasColumnName("repeat_count"); + + b.Property("RepeatInterval") + .HasColumnType("bigint") + .HasColumnName("repeat_interval"); + + b.Property("TimesTriggered") + .HasColumnType("bigint") + .HasColumnName("times_triggered"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_simple_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("CalendarName") + .HasColumnType("varchar(200)") + .HasColumnName("calendar_name"); + + b.Property("Description") + .HasColumnType("varchar(200)") + .HasColumnName("description"); + + b.Property("EndTime") + .HasColumnType("bigint") + .HasColumnName("end_time"); + + b.Property("JobData") + .HasColumnType("longblob") + .HasColumnName("job_data"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_name"); + + b.Property("MisfireInstruction") + .HasColumnType("int") + .HasColumnName("misfire_instr"); + + b.Property("NextFireTime") + .HasColumnType("bigint") + .HasColumnName("next_fire_time"); + + b.Property("PreviousFireTime") + .HasColumnType("bigint") + .HasColumnName("prev_fire_time"); + + b.Property("Priority") + .HasColumnType("int") + .HasColumnName("priority"); + + b.Property("StartTime") + .HasColumnType("bigint") + .HasColumnName("start_time"); + + b.Property("TriggerState") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("trigger_state"); + + b.Property("TriggerType") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("trigger_type"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.HasIndex("NextFireTime") + .HasDatabaseName("idx_t_next_fire_time"); + + b.HasIndex("TriggerState") + .HasDatabaseName("idx_t_state"); + + b.HasIndex("NextFireTime", "TriggerState") + .HasDatabaseName("idx_t_nft_st"); + + b.HasIndex("SchedulerName", "JobName", "JobGroup"); + + b.ToTable("QRTZ_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.AccountRoleLink", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Accounts.Role", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DocumentTrail", b => + { + b.HasOne("SW.Bitween.Domain.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Document"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.ApiGateway", "ApiGateway") + .WithMany("Partners") + .HasForeignKey("ApiGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("ApiGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.BusGateway", "BusGateway") + .WithMany("Routes") + .HasForeignKey("BusGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("BusGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.OwnsMany("SW.Bitween.Domain.ApiCredential", "ApiCredentials", b1 => + { + b1.Property("PartnerId") + .HasColumnType("int"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b1.Property("Id")); + + b1.Property("Key") + .IsRequired() + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b1.Property("Name") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b1.HasKey("PartnerId", "Id"); + + b1.HasIndex("Key") + .IsUnique(); + + b1.ToTable("PartnerApiCredentials", (string)null); + + b1.WithOwner() + .HasForeignKey("PartnerId"); + + b1.HasData( + new + { + PartnerId = 1, + Id = 1, + Key = "7facc758283844b49cc4ffd26a75b1de", + Name = "default" + }); + }); + + b.Navigation("ApiCredentials"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.HasOne("SW.Bitween.Domain.Subscription", null) + .WithMany() + .HasForeignKey("AggregationForId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("FK_Subscriptions_AggFor"); + + b.HasOne("SW.Bitween.Domain.SubscriptionCategory", "Category") + .WithMany() + .HasForeignKey("CategoryId"); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", null) + .WithMany("Subscriptions") + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Subscription", null) + .WithMany() + .HasForeignKey("ResponseSubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("FK_Subscriptions_RespSub"); + + b.HasOne("SW.Bitween.Domain.RetryPolicy", "RetryPolicy") + .WithMany() + .HasForeignKey("RetryPolicyId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("SW.Bitween.Domain.WorkGroup", "WorkGroup") + .WithMany() + .HasForeignKey("WorkGroupId"); + + b.OwnsMany("SW.Bitween.Domain.Schedule", "Schedules", b1 => + { + b1.Property("SubscriptionId") + .HasColumnType("int"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b1.Property("Id")); + + b1.Property("Backwards") + .HasColumnType("tinyint(1)"); + + b1.Property("On") + .HasColumnType("bigint"); + + b1.Property("Recurrence") + .HasColumnType("tinyint unsigned"); + + b1.HasKey("SubscriptionId", "Id"); + + b1.ToTable("SubscriptionSchedules", (string)null); + + b1.WithOwner() + .HasForeignKey("SubscriptionId"); + }); + + b.Navigation("Category"); + + b.Navigation("RetryPolicy"); + + b.Navigation("Schedules"); + + b.Navigation("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionTrail", b => + { + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeAggregation", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeDelivery", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangePromotedProperties", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeResult", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("BlobTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("CronTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimplePropertyTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimpleTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzJobDetail", "JobDetail") + .WithMany("Triggers") + .HasForeignKey("SchedulerName", "JobName", "JobGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("JobDetail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Navigation("Partners"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Navigation("Routes"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Navigation("Subscriptions"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Navigation("Triggers"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Navigation("BlobTriggers"); + + b.Navigation("CronTriggers"); + + b.Navigation("SimplePropertyTriggers"); + + b.Navigation("SimpleTriggers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/SW.Bitween.MySql/Migrations/20260823104220_GatewayInactiveFlag.cs b/SW.Bitween.MySql/Migrations/20260823104220_GatewayInactiveFlag.cs new file mode 100644 index 00000000..30574d06 --- /dev/null +++ b/SW.Bitween.MySql/Migrations/20260823104220_GatewayInactiveFlag.cs @@ -0,0 +1,40 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace SW.Bitween.MySql.Migrations +{ + /// + public partial class GatewayInactiveFlag : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Inactive", + table: "BusGateways", + type: "tinyint(1)", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "Inactive", + table: "ApiGateways", + type: "tinyint(1)", + nullable: false, + defaultValue: false); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Inactive", + table: "BusGateways"); + + migrationBuilder.DropColumn( + name: "Inactive", + table: "ApiGateways"); + } + } +} diff --git a/SW.Bitween.MySql/Migrations/20260824093618_AddReceiveAttempts.Designer.cs b/SW.Bitween.MySql/Migrations/20260824093618_AddReceiveAttempts.Designer.cs new file mode 100644 index 00000000..e8d70e04 --- /dev/null +++ b/SW.Bitween.MySql/Migrations/20260824093618_AddReceiveAttempts.Designer.cs @@ -0,0 +1,2131 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using SW.Bitween.MySql; + +#nullable disable + +namespace SW.Bitween.MySql.Migrations +{ + [DbContext(typeof(BitweenDbContext))] + [Migration("20260824093618_AddReceiveAttempts")] + partial class AddReceiveAttempts + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.23") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Deleted") + .HasColumnType("tinyint(1)"); + + b.Property("Disabled") + .HasColumnType("tinyint(1)"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("Email") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("EmailProvider") + .HasColumnType("tinyint unsigned"); + + b.Property("FailedLoginCount") + .HasColumnType("int"); + + b.Property("LockoutEnd") + .HasColumnType("datetime(6)"); + + b.Property("LoginMethods") + .HasColumnType("tinyint unsigned"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Password") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("Role") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique(); + + b.ToTable("Accounts", (string)null); + + b.HasData( + new + { + Id = 9999, + CreatedOn = new DateTime(2021, 12, 31, 22, 0, 0, 0, DateTimeKind.Utc), + Deleted = false, + Disabled = false, + DisplayName = "Admin", + Email = "admin@Bitween.systems", + EmailProvider = (byte)0, + FailedLoginCount = 0, + LoginMethods = (byte)2, + Password = "$SWHASH$V1$10000$VQCi48eitH4Ml5juvBMOFZrMdQwBbhuIQVXe6RR7qJdDF2bJ", + Role = 0 + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.AccountRoleLink", b => + { + b.Property("AccountId") + .HasColumnType("int"); + + b.Property("RoleId") + .HasColumnType("int"); + + b.HasKey("AccountId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AccountRoles", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AccountId") + .HasColumnType("int"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("LoginMethod") + .HasColumnType("tinyint unsigned"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.ToTable("RefreshTokens", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("IsSystem") + .HasColumnType("tinyint(1)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("Permissions") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Roles", (string)null); + + b.HasData( + new + { + Id = 1, + CreatedOn = new DateTime(2021, 12, 31, 22, 0, 0, 0, DateTimeKind.Utc), + Description = "Full access to everything, including members, roles and settings.", + IsSystem = true, + Name = "Administrator", + Permissions = "[]" + }, + new + { + Id = 2, + CreatedOn = new DateTime(2021, 12, 31, 22, 0, 0, 0, DateTimeKind.Utc), + Description = "Runs and configures integrations. Can't manage members, roles or settings.", + IsSystem = true, + Name = "Member", + Permissions = "[]" + }, + new + { + Id = 3, + CreatedOn = new DateTime(2021, 12, 31, 22, 0, 0, 0, DateTimeKind.Utc), + Description = "Read-only access to integrations, exchanges and configuration.", + IsSystem = true, + Name = "Viewer", + Permissions = "[]" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DelayedRetry", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("On") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("On"); + + b.ToTable("DelayedRetries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Document", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("BusEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("BusMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("Code") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("DisregardsUnfilteredMessages") + .HasColumnType("tinyint(1)"); + + b.Property("DocumentFormat") + .HasColumnType("int"); + + b.Property("DuplicateInterval") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("PromotedProperties") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("BusMessageTypeName") + .IsUnique(); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Documents", (string)null); + + b.HasData( + new + { + Id = 10001, + BusEnabled = false, + DocumentFormat = 0, + DuplicateInterval = 0, + Name = "Aggregation Document", + PromotedProperties = "{}" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DocumentTrail", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Code") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("StateAfter") + .HasColumnType("longtext"); + + b.Property("StateBefore") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("CreatedOn"); + + b.HasIndex("DocumentId"); + + b.ToTable("DocumentTrail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("UrlName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("UrlName") + .IsUnique(); + + b.ToTable("ApiGateways", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.Property("ApiGatewayId") + .HasColumnType("int"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.HasKey("ApiGatewayId", "PartnerId", "SubscriptionId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("ApiGatewayPartners", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.ToTable("BusGateways", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("BusGatewayId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("MatchExpression") + .HasColumnType("longtext"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("BusGatewayId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("BusGatewayRoutes", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.GlobalAdapterValuesSet", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("Values") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("GlobalAdapterValuesSets", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Notifier", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("longtext"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("RunOnBadResult") + .HasColumnType("tinyint(1)"); + + b.Property("RunOnFailedResult") + .HasColumnType("tinyint(1)"); + + b.Property("RunOnSubscriptions") + .HasColumnType("longtext"); + + b.Property("RunOnSuccessfulResult") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.ToTable("Notifiers", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OnHoldXchange", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("BadData") + .HasColumnType("tinyint(1)"); + + b.Property("Data") + .HasColumnType("longtext"); + + b.Property("FileName") + .HasColumnType("longtext"); + + b.Property("References") + .HasMaxLength(1024) + .HasColumnType("varchar(1024)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("OnHoldXchanges", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AdapterProperties") + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.ToTable("Partners", (string)null); + + b.HasData( + new + { + Id = 1, + Name = "SYSTEM" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.ReceiveAttempt", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("ErrorMessage") + .HasMaxLength(4000) + .HasColumnType("varchar(4000)"); + + b.Property("ExchangeIds") + .HasColumnType("longtext"); + + b.Property("FinishedOn") + .HasColumnType("datetime(6)"); + + b.Property("Outcome") + .HasColumnType("int"); + + b.Property("StartedOn") + .HasColumnType("datetime(6)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("SubscriptionId", "StartedOn"); + + b.ToTable("ReceiveAttempts", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryAlertOverride", b => + { + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("GroupId") + .HasColumnType("char(36)"); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("AlertHandlerProperties") + .HasColumnType("longtext"); + + b.Property("AlertMode") + .HasColumnType("tinyint unsigned"); + + b.HasKey("SubscriptionId", "GroupId"); + + b.ToTable("RetryAlertOverrides", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryGroupUsage", b => + { + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("GroupId") + .HasColumnType("char(36)"); + + b.Property("AttemptsUsed") + .HasColumnType("int"); + + b.Property("ExhaustedNotifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("LastAttemptOn") + .HasColumnType("datetime(6)"); + + b.HasKey("SubscriptionId", "GroupId"); + + b.ToTable("RetryGroupUsages", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryPolicy", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("AlertHandlerProperties") + .HasColumnType("longtext"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Groups") + .HasColumnType("longtext"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.ToTable("RetryPolicies", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Setting", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Value") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("Settings", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AggregateOn") + .HasColumnType("datetime(6)"); + + b.Property("AggregationForId") + .HasColumnType("int"); + + b.Property("AggregationTarget") + .HasColumnType("tinyint unsigned"); + + b.Property("CategoryId") + .HasColumnType("int"); + + b.Property("ConsecutiveFailures") + .HasColumnType("int"); + + b.Property("CustomRetryPolicy") + .HasColumnType("longtext"); + + b.Property("DocumentFilter") + .HasColumnType("longtext"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("longtext"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("IsRunning") + .HasColumnType("tinyint(1)"); + + b.Property("LastException") + .HasColumnType("longtext"); + + b.Property("MapperId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("MapperProperties") + .HasColumnType("longtext"); + + b.Property("MatchExpression") + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("PausedOn") + .HasColumnType("datetime(6)"); + + b.Property("ReceiveOn") + .HasColumnType("datetime(6)"); + + b.Property("ReceiverId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ReceiverProperties") + .HasColumnType("longtext"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("int"); + + b.Property("RetryPolicyId") + .HasColumnType("int"); + + b.Property("Temporary") + .HasColumnType("tinyint(1)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("ValidatorId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ValidatorProperties") + .HasColumnType("longtext"); + + b.Property("WorkGroupId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("AggregationForId"); + + b.HasIndex("CategoryId"); + + b.HasIndex("DocumentId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("ResponseSubscriptionId"); + + b.HasIndex("RetryPolicyId"); + + b.HasIndex("WorkGroupId"); + + b.ToTable("Subscriptions", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Code") + .HasColumnType("varchar(255)"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Description") + .HasColumnType("longtext"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.ToTable("SubscriptionCategory"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionTrail", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Code") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("StateAfter") + .HasColumnType("longtext"); + + b.Property("StateBefore") + .HasColumnType("longtext"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("CreatedOn"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("SubscriptionTrail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.WorkGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("BusMessageName") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("Options") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("CorrelationId") + .HasColumnType("longtext"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("longtext"); + + b.Property("InputContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("InputHash") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("InputName") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("InputSize") + .HasColumnType("int"); + + b.Property("ManualRetry") + .HasColumnType("tinyint(1)"); + + b.Property("MapperId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("MapperProperties") + .HasColumnType("longtext"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("References") + .HasMaxLength(1024) + .HasColumnType("varchar(1024)"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("int"); + + b.Property("RetryFor") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("StartedOn") + .HasColumnType("datetime(6)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.HasIndex("InputHash"); + + b.HasIndex("RetryFor"); + + b.HasIndex("StartedOn"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("Xchanges", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AggregatedOn") + .HasColumnType("datetime(6)"); + + b.Property("AggregationXchangeId") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("AggregationXchangeId"); + + b.ToTable("XchangeAggregations", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("DeliveredOn") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("DeliveredOn"); + + b.ToTable("XchangeDeliveries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeNotification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Exception") + .HasColumnType("longtext"); + + b.Property("FinishedOn") + .HasColumnType("datetime(6)"); + + b.Property("NotifierId") + .HasColumnType("int"); + + b.Property("NotifierName") + .HasColumnType("longtext"); + + b.Property("Success") + .HasColumnType("tinyint(1)"); + + b.Property("XchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.ToTable("XchangeNotifications", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("Hits") + .HasMaxLength(2000) + .IsUnicode(false) + .HasColumnType("varchar(2000)"); + + b.Property("Properties") + .HasColumnType("longtext"); + + b.Property("PropertiesRaw") + .HasColumnType("varchar(255)"); + + b.HasKey("Id"); + + b.HasIndex("PropertiesRaw"); + + b.ToTable("XchangePromotedProperties", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AttemptNumber") + .HasColumnType("int"); + + b.Property("Exception") + .HasColumnType("longtext"); + + b.Property("FinishedOn") + .HasColumnType("datetime(6)"); + + b.Property("OutputBad") + .HasColumnType("tinyint(1)"); + + b.Property("OutputContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("OutputHash") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("OutputName") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("OutputSize") + .HasColumnType("int"); + + b.Property("ResponseBad") + .HasColumnType("tinyint(1)"); + + b.Property("ResponseContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ResponseHash") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("ResponseName") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("ResponseSize") + .HasColumnType("int"); + + b.Property("ResponseXchangeId") + .HasColumnType("longtext"); + + b.Property("RetryBlockedReason") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("RetryGroupId") + .HasColumnType("char(36)"); + + b.Property("Success") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.HasIndex("RetryGroupId"); + + b.ToTable("XchangeResults", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.RunFlagUpdater+RunningResult", b => + { + b.Property("IsRunning") + .HasColumnType("tinyint(1)"); + + b.ToTable((string)null); + + b.ToView(null, (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.JobExecution", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Context") + .HasColumnType("longtext") + .HasColumnName("context"); + + b.Property("DurationMs") + .HasColumnType("bigint") + .HasColumnName("duration_ms"); + + b.Property("EndTimeUtc") + .HasColumnType("datetime(6)") + .HasColumnName("end_time_utc"); + + b.Property("Error") + .HasColumnType("longtext") + .HasColumnName("error"); + + b.Property("FireInstanceId") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("fire_instance_id"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_name"); + + b.Property("JobTypeName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_type_name"); + + b.Property("Node") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("node"); + + b.Property("StartTimeUtc") + .HasColumnType("datetime(6)") + .HasColumnName("start_time_utc"); + + b.Property("Success") + .HasColumnType("tinyint(1)") + .HasColumnName("success"); + + b.HasKey("Id"); + + b.HasIndex("FireInstanceId") + .IsUnique() + .HasDatabaseName("idx_je_fire_instance_id"); + + b.HasIndex("StartTimeUtc") + .HasDatabaseName("idx_je_start_time"); + + b.HasIndex("Success") + .HasDatabaseName("idx_je_success"); + + b.HasIndex("JobGroup", "JobName", "StartTimeUtc") + .HasDatabaseName("idx_je_group_name_start"); + + b.ToTable("job_executions", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("BlobData") + .HasColumnType("longblob") + .HasColumnName("blob_data"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_blob_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCalendar", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("CalendarName") + .HasColumnType("varchar(200)") + .HasColumnName("calendar_name"); + + b.Property("Calendar") + .IsRequired() + .HasColumnType("longblob") + .HasColumnName("calendar"); + + b.HasKey("SchedulerName", "CalendarName"); + + b.ToTable("QRTZ_calendars", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("CronExpression") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("cron_expression"); + + b.Property("TimeZoneId") + .HasColumnType("varchar(200)") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_cron_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzFiredTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("EntryId") + .HasColumnType("varchar(200)") + .HasColumnName("entry_id"); + + b.Property("FiredTime") + .HasColumnType("bigint") + .HasColumnName("fired_time"); + + b.Property("InstanceName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("instance_name"); + + b.Property("IsNonConcurrent") + .HasColumnType("tinyint(1)") + .HasColumnName("is_nonconcurrent"); + + b.Property("JobGroup") + .HasColumnType("varchar(200)") + .HasColumnName("job_group"); + + b.Property("JobName") + .HasColumnType("varchar(200)") + .HasColumnName("job_name"); + + b.Property("Priority") + .HasColumnType("int") + .HasColumnName("priority"); + + b.Property("RequestsRecovery") + .HasColumnType("tinyint(1)") + .HasColumnName("requests_recovery"); + + b.Property("ScheduledTime") + .HasColumnType("bigint") + .HasColumnName("sched_time"); + + b.Property("State") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("state"); + + b.Property("TriggerGroup") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("TriggerName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.HasKey("SchedulerName", "EntryId"); + + b.HasIndex("InstanceName") + .HasDatabaseName("idx_QRTZ_ft_trig_inst_name"); + + b.HasIndex("JobGroup") + .HasDatabaseName("idx_QRTZ_ft_job_group"); + + b.HasIndex("JobName") + .HasDatabaseName("idx_QRTZ_ft_job_name"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_QRTZ_ft_job_req_recovery"); + + b.HasIndex("TriggerGroup") + .HasDatabaseName("idx_QRTZ_ft_trig_group"); + + b.HasIndex("TriggerName") + .HasDatabaseName("idx_QRTZ_ft_trig_name"); + + b.HasIndex("SchedulerName", "TriggerName", "TriggerGroup") + .HasDatabaseName("idx_QRTZ_ft_trig_nm_gp"); + + b.ToTable("QRTZ_fired_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("JobName") + .HasColumnType("varchar(200)") + .HasColumnName("job_name"); + + b.Property("JobGroup") + .HasColumnType("varchar(200)") + .HasColumnName("job_group"); + + b.Property("Description") + .HasColumnType("varchar(200)") + .HasColumnName("description"); + + b.Property("IsDurable") + .HasColumnType("tinyint(1)") + .HasColumnName("is_durable"); + + b.Property("IsNonConcurrent") + .HasColumnType("tinyint(1)") + .HasColumnName("is_nonconcurrent"); + + b.Property("IsUpdateData") + .HasColumnType("tinyint(1)") + .HasColumnName("is_update_data"); + + b.Property("JobClassName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_class_name"); + + b.Property("JobData") + .HasColumnType("longblob") + .HasColumnName("job_data"); + + b.Property("RequestsRecovery") + .HasColumnType("tinyint(1)") + .HasColumnName("requests_recovery"); + + b.HasKey("SchedulerName", "JobName", "JobGroup"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_j_req_recovery"); + + b.ToTable("QRTZ_job_details", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzLock", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("LockName") + .HasColumnType("varchar(200)") + .HasColumnName("lock_name"); + + b.HasKey("SchedulerName", "LockName"); + + b.ToTable("QRTZ_locks", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzPausedTriggerGroup", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.HasKey("SchedulerName", "TriggerGroup"); + + b.ToTable("QRTZ_paused_trigger_grps", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSchedulerState", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("InstanceName") + .HasColumnType("varchar(200)") + .HasColumnName("instance_name"); + + b.Property("CheckInInterval") + .HasColumnType("bigint") + .HasColumnName("checkin_interval"); + + b.Property("LastCheckInTime") + .HasColumnType("bigint") + .HasColumnName("last_checkin_time"); + + b.HasKey("SchedulerName", "InstanceName"); + + b.ToTable("QRTZ_scheduler_state", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("BooleanProperty1") + .HasColumnType("tinyint(1)") + .HasColumnName("bool_prop_1"); + + b.Property("BooleanProperty2") + .HasColumnType("tinyint(1)") + .HasColumnName("bool_prop_2"); + + b.Property("DecimalProperty1") + .HasColumnType("numeric") + .HasColumnName("dec_prop_1"); + + b.Property("DecimalProperty2") + .HasColumnType("numeric") + .HasColumnName("dec_prop_2"); + + b.Property("IntegerProperty1") + .HasColumnType("int") + .HasColumnName("int_prop_1"); + + b.Property("IntegerProperty2") + .HasColumnType("int") + .HasColumnName("int_prop_2"); + + b.Property("LongProperty1") + .HasColumnType("bigint") + .HasColumnName("long_prop_1"); + + b.Property("LongProperty2") + .HasColumnType("bigint") + .HasColumnName("long_prop_2"); + + b.Property("StringProperty1") + .HasColumnType("varchar(200)") + .HasColumnName("str_prop_1"); + + b.Property("StringProperty2") + .HasColumnType("varchar(200)") + .HasColumnName("str_prop_2"); + + b.Property("StringProperty3") + .HasColumnType("varchar(200)") + .HasColumnName("str_prop_3"); + + b.Property("TimeZoneId") + .HasColumnType("varchar(200)") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_simprop_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("RepeatCount") + .HasColumnType("bigint") + .HasColumnName("repeat_count"); + + b.Property("RepeatInterval") + .HasColumnType("bigint") + .HasColumnName("repeat_interval"); + + b.Property("TimesTriggered") + .HasColumnType("bigint") + .HasColumnName("times_triggered"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_simple_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("CalendarName") + .HasColumnType("varchar(200)") + .HasColumnName("calendar_name"); + + b.Property("Description") + .HasColumnType("varchar(200)") + .HasColumnName("description"); + + b.Property("EndTime") + .HasColumnType("bigint") + .HasColumnName("end_time"); + + b.Property("JobData") + .HasColumnType("longblob") + .HasColumnName("job_data"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_name"); + + b.Property("MisfireInstruction") + .HasColumnType("int") + .HasColumnName("misfire_instr"); + + b.Property("NextFireTime") + .HasColumnType("bigint") + .HasColumnName("next_fire_time"); + + b.Property("PreviousFireTime") + .HasColumnType("bigint") + .HasColumnName("prev_fire_time"); + + b.Property("Priority") + .HasColumnType("int") + .HasColumnName("priority"); + + b.Property("StartTime") + .HasColumnType("bigint") + .HasColumnName("start_time"); + + b.Property("TriggerState") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("trigger_state"); + + b.Property("TriggerType") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("trigger_type"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.HasIndex("NextFireTime") + .HasDatabaseName("idx_t_next_fire_time"); + + b.HasIndex("TriggerState") + .HasDatabaseName("idx_t_state"); + + b.HasIndex("NextFireTime", "TriggerState") + .HasDatabaseName("idx_t_nft_st"); + + b.HasIndex("SchedulerName", "JobName", "JobGroup"); + + b.ToTable("QRTZ_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.AccountRoleLink", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Accounts.Role", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DocumentTrail", b => + { + b.HasOne("SW.Bitween.Domain.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Document"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.ApiGateway", "ApiGateway") + .WithMany("Partners") + .HasForeignKey("ApiGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("ApiGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.BusGateway", "BusGateway") + .WithMany("Routes") + .HasForeignKey("BusGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("BusGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.OwnsMany("SW.Bitween.Domain.ApiCredential", "ApiCredentials", b1 => + { + b1.Property("PartnerId") + .HasColumnType("int"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b1.Property("Id")); + + b1.Property("Key") + .IsRequired() + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b1.Property("Name") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b1.HasKey("PartnerId", "Id"); + + b1.HasIndex("Key") + .IsUnique(); + + b1.ToTable("PartnerApiCredentials", (string)null); + + b1.WithOwner() + .HasForeignKey("PartnerId"); + + b1.HasData( + new + { + PartnerId = 1, + Id = 1, + Key = "7facc758283844b49cc4ffd26a75b1de", + Name = "default" + }); + }); + + b.Navigation("ApiCredentials"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.HasOne("SW.Bitween.Domain.Subscription", null) + .WithMany() + .HasForeignKey("AggregationForId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("FK_Subscriptions_AggFor"); + + b.HasOne("SW.Bitween.Domain.SubscriptionCategory", "Category") + .WithMany() + .HasForeignKey("CategoryId"); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", null) + .WithMany("Subscriptions") + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Subscription", null) + .WithMany() + .HasForeignKey("ResponseSubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("FK_Subscriptions_RespSub"); + + b.HasOne("SW.Bitween.Domain.RetryPolicy", "RetryPolicy") + .WithMany() + .HasForeignKey("RetryPolicyId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("SW.Bitween.Domain.WorkGroup", "WorkGroup") + .WithMany() + .HasForeignKey("WorkGroupId"); + + b.OwnsMany("SW.Bitween.Domain.Schedule", "Schedules", b1 => + { + b1.Property("SubscriptionId") + .HasColumnType("int"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b1.Property("Id")); + + b1.Property("Backwards") + .HasColumnType("tinyint(1)"); + + b1.Property("On") + .HasColumnType("bigint"); + + b1.Property("Recurrence") + .HasColumnType("tinyint unsigned"); + + b1.HasKey("SubscriptionId", "Id"); + + b1.ToTable("SubscriptionSchedules", (string)null); + + b1.WithOwner() + .HasForeignKey("SubscriptionId"); + }); + + b.Navigation("Category"); + + b.Navigation("RetryPolicy"); + + b.Navigation("Schedules"); + + b.Navigation("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionTrail", b => + { + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeAggregation", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeDelivery", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangePromotedProperties", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeResult", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("BlobTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("CronTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimplePropertyTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimpleTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzJobDetail", "JobDetail") + .WithMany("Triggers") + .HasForeignKey("SchedulerName", "JobName", "JobGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("JobDetail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Navigation("Partners"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Navigation("Routes"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Navigation("Subscriptions"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Navigation("Triggers"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Navigation("BlobTriggers"); + + b.Navigation("CronTriggers"); + + b.Navigation("SimplePropertyTriggers"); + + b.Navigation("SimpleTriggers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/SW.Bitween.MySql/Migrations/20260824093618_AddReceiveAttempts.cs b/SW.Bitween.MySql/Migrations/20260824093618_AddReceiveAttempts.cs new file mode 100644 index 00000000..9402ea68 --- /dev/null +++ b/SW.Bitween.MySql/Migrations/20260824093618_AddReceiveAttempts.cs @@ -0,0 +1,49 @@ +using System; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace SW.Bitween.MySql.Migrations +{ + /// + public partial class AddReceiveAttempts : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "ReceiveAttempts", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn), + SubscriptionId = table.Column(type: "int", nullable: false), + StartedOn = table.Column(type: "datetime(6)", nullable: false), + FinishedOn = table.Column(type: "datetime(6)", nullable: false), + Outcome = table.Column(type: "int", nullable: false), + ErrorMessage = table.Column(type: "varchar(4000)", maxLength: 4000, nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"), + ExchangeIds = table.Column(type: "longtext", nullable: true) + .Annotation("MySql:CharSet", "utf8mb4") + }, + constraints: table => + { + table.PrimaryKey("PK_ReceiveAttempts", x => x.Id); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateIndex( + name: "IX_ReceiveAttempts_SubscriptionId_StartedOn", + table: "ReceiveAttempts", + columns: new[] { "SubscriptionId", "StartedOn" }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "ReceiveAttempts"); + } + } +} diff --git a/SW.Bitween.MySql/Migrations/BitweenDbContextModelSnapshot.cs b/SW.Bitween.MySql/Migrations/BitweenDbContextModelSnapshot.cs index 66cd9ef7..78a9a898 100644 --- a/SW.Bitween.MySql/Migrations/BitweenDbContextModelSnapshot.cs +++ b/SW.Bitween.MySql/Migrations/BitweenDbContextModelSnapshot.cs @@ -55,6 +55,12 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("EmailProvider") .HasColumnType("tinyint unsigned"); + b.Property("FailedLoginCount") + .HasColumnType("int"); + + b.Property("LockoutEnd") + .HasColumnType("datetime(6)"); + b.Property("LoginMethods") .HasColumnType("tinyint unsigned"); @@ -89,6 +95,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) DisplayName = "Admin", Email = "admin@Bitween.systems", EmailProvider = (byte)0, + FailedLoginCount = 0, LoginMethods = (byte)2, Password = "$SWHASH$V1$10000$VQCi48eitH4Ml5juvBMOFZrMdQwBbhuIQVXe6RR7qJdDF2bJ", Role = 0 @@ -212,9 +219,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) .IsUnicode(false) .HasColumnType("varchar(50)"); - b.Property("GroupAttemptCounts") - .HasColumnType("longtext"); - b.Property("On") .HasColumnType("datetime(6)"); @@ -336,6 +340,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("CreatedOn") .HasColumnType("datetime(6)"); + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + b.Property("ModifiedBy") .HasColumnType("longtext"); @@ -409,6 +416,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("DocumentId") .HasColumnType("int"); + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + b.Property("ModifiedBy") .HasColumnType("longtext"); @@ -589,6 +599,86 @@ protected override void BuildModel(ModelBuilder modelBuilder) }); }); + modelBuilder.Entity("SW.Bitween.Domain.ReceiveAttempt", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("ErrorMessage") + .HasMaxLength(4000) + .HasColumnType("varchar(4000)"); + + b.Property("ExchangeIds") + .HasColumnType("longtext"); + + b.Property("FinishedOn") + .HasColumnType("datetime(6)"); + + b.Property("Outcome") + .HasColumnType("int"); + + b.Property("StartedOn") + .HasColumnType("datetime(6)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("SubscriptionId", "StartedOn"); + + b.ToTable("ReceiveAttempts", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryAlertOverride", b => + { + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("GroupId") + .HasColumnType("char(36)"); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("AlertHandlerProperties") + .HasColumnType("longtext"); + + b.Property("AlertMode") + .HasColumnType("tinyint unsigned"); + + b.HasKey("SubscriptionId", "GroupId"); + + b.ToTable("RetryAlertOverrides", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryGroupUsage", b => + { + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("GroupId") + .HasColumnType("char(36)"); + + b.Property("AttemptsUsed") + .HasColumnType("int"); + + b.Property("ExhaustedNotifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("LastAttemptOn") + .HasColumnType("datetime(6)"); + + b.HasKey("SubscriptionId", "GroupId"); + + b.ToTable("RetryGroupUsages", (string)null); + }); + modelBuilder.Entity("SW.Bitween.Domain.RetryPolicy", b => { b.Property("Id") @@ -597,6 +687,14 @@ protected override void BuildModel(ModelBuilder modelBuilder) MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + b.Property("AlertHandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("AlertHandlerProperties") + .HasColumnType("longtext"); + b.Property("CreatedBy") .HasColumnType("longtext"); @@ -883,9 +981,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("DocumentId") .HasColumnType("int"); - b.Property("GroupAttemptCounts") - .HasColumnType("longtext"); - b.Property("HandlerId") .HasMaxLength(200) .IsUnicode(false) @@ -912,6 +1007,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("InputSize") .HasColumnType("int"); + b.Property("ManualRetry") + .HasColumnType("tinyint(1)"); + b.Property("MapperId") .HasMaxLength(200) .IsUnicode(false) @@ -1015,7 +1113,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("FinishedOn") .HasColumnType("datetime(6)"); - b.Property("NotifierId") + b.Property("NotifierId") .HasColumnType("int"); b.Property("NotifierName") @@ -1066,6 +1164,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) .IsUnicode(false) .HasColumnType("varchar(50)"); + b.Property("AttemptNumber") + .HasColumnType("int"); + b.Property("Exception") .HasColumnType("longtext"); @@ -1115,11 +1216,20 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("ResponseXchangeId") .HasColumnType("longtext"); + b.Property("RetryBlockedReason") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("RetryGroupId") + .HasColumnType("char(36)"); + b.Property("Success") .HasColumnType("tinyint(1)"); b.HasKey("Id"); + b.HasIndex("RetryGroupId"); + b.ToTable("XchangeResults", (string)null); }); diff --git a/SW.Bitween.NativeAdapters/JsonMapper/ScribanJsonHelper.cs b/SW.Bitween.NativeAdapters/JsonMapper/ScribanJsonHelper.cs index eb369695..b535a8db 100644 --- a/SW.Bitween.NativeAdapters/JsonMapper/ScribanJsonHelper.cs +++ b/SW.Bitween.NativeAdapters/JsonMapper/ScribanJsonHelper.cs @@ -13,6 +13,37 @@ public static class ScribanJsonHelper /// Renders a Scriban template against the provided input JSON and returns the mapped output JSON. /// ` public static string Render(string scribanTemplate, string inputJson) + { + var rendered = RenderText(scribanTemplate, inputJson); + + // 6. Strip trailing commas that may appear after the last field/element + rendered = Regex.Replace(rendered, @",(\s*[}\]])", "$1"); + + // 7. Parse rendered output — root may be an object OR an array + JToken renderedToken; + try + { + renderedToken = JToken.Parse(rendered); + } + catch (JsonException ex) + { + throw new InvalidOperationException($"Template produced invalid JSON: {ex.Message}\n\nRendered:\n{rendered}"); + } + + // 8. Expand dotted keys into nested objects recursively at all depths + return ExpandDottedKeys(renderedToken).ToString(Formatting.Indented); + } + + /// + /// Renders a Scriban template against the provided input JSON and returns the text as-is, + /// without requiring the result to be JSON. + /// + /// + /// For templates whose output is prose rather than a payload — an email subject or body, say. + /// builds on this and adds the JSON validation and dotted-key expansion + /// that a mapper needs and a sentence does not. + /// + public static string RenderText(string scribanTemplate, string inputJson) { // 1. Parse input JSON — handle both root object and root array var rootToken = JToken.Parse(inputJson); @@ -71,24 +102,7 @@ public static string Render(string scribanTemplate, string inputJson) throw new InvalidOperationException($"Template parse error: {errors}"); } - var rendered = template.Render(context); - - // 6. Strip trailing commas that may appear after the last field/element - rendered = Regex.Replace(rendered, @",(\s*[}\]])", "$1"); - - // 7. Parse rendered output — root may be an object OR an array - JToken renderedToken; - try - { - renderedToken = JToken.Parse(rendered); - } - catch (JsonException ex) - { - throw new InvalidOperationException($"Template produced invalid JSON: {ex.Message}\n\nRendered:\n{rendered}"); - } - - // 8. Expand dotted keys into nested objects recursively at all depths - return ExpandDottedKeys(renderedToken).ToString(Formatting.Indented); + return template.Render(context); } // ─── Helpers ────────────────────────────────────────────────────────────── diff --git a/SW.Bitween.NativeAdapters/ServiceCollectionExtensions.cs b/SW.Bitween.NativeAdapters/ServiceCollectionExtensions.cs index fb0d0750..2202564e 100644 --- a/SW.Bitween.NativeAdapters/ServiceCollectionExtensions.cs +++ b/SW.Bitween.NativeAdapters/ServiceCollectionExtensions.cs @@ -8,6 +8,7 @@ using SW.Bitween.NativeAdapters.RebexPop3Receiver; using SW.Bitween.NativeAdapters.S3Receiver; using SW.Bitween.NativeAdapters.S3UploadHandler; +using SW.Bitween.NativeAdapters.SmtpHandler; namespace SW.Bitween.NativeAdapters; @@ -48,6 +49,9 @@ public static void AddNativeAdapters(this IServiceCollection serviceCollection, serviceCollection.AddScoped(); serviceCollection.AddScoped(); + serviceCollection.AddScoped(); + serviceCollection.AddScoped(); + serviceCollection.AddScoped(); serviceCollection.AddScoped(); diff --git a/SW.Bitween.NativeAdapters/SmtpHandler/NativeSmtpHandler.cs b/SW.Bitween.NativeAdapters/SmtpHandler/NativeSmtpHandler.cs new file mode 100644 index 00000000..6cc74e84 --- /dev/null +++ b/SW.Bitween.NativeAdapters/SmtpHandler/NativeSmtpHandler.cs @@ -0,0 +1,165 @@ +using System.Collections.Generic; +using System.Linq; +using System.Net.Security; +using System.Security.Cryptography.X509Certificates; +using MailKit.Net.Smtp; +using MailKit.Security; +using MimeKit; +using MimeKit.Text; +using Newtonsoft.Json.Linq; +using SW.Bitween.NativeAdapters.JsonMapper; +using SW.PrimitiveTypes; + +namespace SW.Bitween.NativeAdapters.SmtpHandler; + +/// +/// Sends the payload as an email, with the subject and body written as templates over it. +/// +/// +/// Built for cases where the recipient is a person rather than a system — a retry budget running +/// out, a notifier on a failed exchange — which is why the subject and body are templated instead +/// of the payload being emailed raw. A JSON blob in an inbox tells nobody anything. +/// +public class NativeSmtpHandler : INativeInfolinkHandler +{ + private SmtpHandlerInput _options = new(); + + public string Name => "NativeSmtpHandler"; + + public Type StartupValuesType => typeof(SmtpHandlerInput); + + public void InitializeStartupValues(IDictionary settings) + { + _options = settings.ConvertTo(); + } + + public async Task Handle(XchangeFile xchangeFile) + { + var subject = Fill(_options.Subject, xchangeFile.Data); + var body = Fill(_options.Body, xchangeFile.Data); + + var message = new MimeMessage(); + message.From.Add(new MailboxAddress(_options.FromName ?? string.Empty, _options.From)); + message.Subject = subject; + message.Body = new TextPart(_options.IsHtml ? TextFormat.Html : TextFormat.Plain) { Text = body }; + + AddAddresses(message.To, _options.To); + AddAddresses(message.Cc, _options.Cc); + AddAddresses(message.Bcc, _options.Bcc); + + if (message.To.Count == 0 && message.Cc.Count == 0 && message.Bcc.Count == 0) + throw new InvalidOperationException("No recipients were configured for the SMTP handler."); + + using var client = new SmtpClient(); + + // Revocation stays switched on, so a certificate the CA has actually revoked is still refused. + // What the callback softens is the other outcome: the lookup needs the CA's OCSP or CRL server + // to be reachable, which the corporate networks Bitween runs inside routinely block, and + // MailKit's default treats "could not find out" exactly like "revoked". That rejected a + // perfectly good Gmail certificate — one OpenSSL accepts on the same machine — and the alert + // never went out. Soft-failing an undeterminable status is what browsers and mail clients do; + // every other defect, revocation included, still fails. + client.CheckCertificateRevocation = true; + client.ServerCertificateValidationCallback = (_, _, chain, errors) => + IsCertificateAcceptable(errors, + chain?.ChainStatus.Select(s => s.Status) ?? Enumerable.Empty()); + + // Named rather than left to Auto: on any port but 465, Auto means "encrypt if the server + // offers it", so a server that does not offer STARTTLS — or an offer stripped in transit — + // silently continues in the clear. StartTls demands it and fails if it is not there. 465 is + // the implicit-TLS port, where the handshake happens before any of that is negotiable. + var security = _options.UseTls + ? _options.Port == 465 ? SecureSocketOptions.SslOnConnect : SecureSocketOptions.StartTls + : SecureSocketOptions.None; + + await client.ConnectAsync(_options.Host, _options.Port, security); + + // A relay that accepts unauthenticated mail from inside the network is a normal setup, so + // only authenticate when a password was actually supplied. + if (!string.IsNullOrWhiteSpace(_options.Password)) + { + // Refusing beats sending the credential over a connection anyone on the path can read. + if (!client.IsSecure) + throw new InvalidOperationException( + "The SMTP handler will not send a password over an unencrypted connection. " + + "Set UseTls to true, or clear the password if the relay does not need one."); + + await client.AuthenticateAsync( + string.IsNullOrWhiteSpace(_options.Username) ? _options.From : _options.Username, + _options.Password); + } + + await client.SendAsync(message); + await client.DisconnectAsync(true); + + return new XchangeFile(subject, xchangeFile.Filename); + } + + private static void AddAddresses(InternetAddressList list, string? addresses) + { + if (string.IsNullOrWhiteSpace(addresses)) return; + + foreach (var address in addresses.Split(',', StringSplitOptions.RemoveEmptyEntries + | StringSplitOptions.TrimEntries)) + list.Add(MailboxAddress.Parse(address)); + } + + /// + /// Renders a template against the payload, or returns it unchanged when the payload is not JSON. + /// + /// + /// Non-JSON payloads are normal for a pipeline handler — a CSV or a flat file on its way out — + /// and those have no fields to substitute. A broken template still throws, so a typo in a + /// placeholder is not quietly emailed as literal text. + /// + internal static string Fill(string template, string payload) + { + if (string.IsNullOrEmpty(template) || !LooksLikeJson(payload)) return template; + + return ScribanJsonHelper.RenderText(template, payload); + } + + /// + /// Whether a server certificate should be accepted, given what validation found wrong with it. + /// + /// + /// Only one defect is tolerated: a revocation status that could not be established, because the + /// CA's OCSP or CRL server was unreachable. A certificate the CA has revoked, an untrusted root, + /// a wrong hostname and an expired certificate are all still refused — as is any chain flag not + /// named here, so a defect nobody thought about fails closed rather than slipping through. + /// + internal static bool IsCertificateAcceptable(SslPolicyErrors errors, + IEnumerable chainStatus) + { + if (errors == SslPolicyErrors.None) return true; + + // A missing certificate or the wrong name on one is not a revocation question at all, and the + // chain flags say nothing about either. + if (errors != SslPolicyErrors.RemoteCertificateChainErrors) return false; + + const X509ChainStatusFlags undeterminable = + X509ChainStatusFlags.RevocationStatusUnknown | X509ChainStatusFlags.OfflineRevocation; + + // Masked rather than compared: one entry can carry several flags at once, and "revoked" set + // alongside "could not check" has to fail. + return chainStatus.All(status => (status & ~undeterminable) == X509ChainStatusFlags.NoError); + } + + private static bool LooksLikeJson(string payload) + { + if (string.IsNullOrWhiteSpace(payload)) return false; + + var trimmed = payload.TrimStart(); + if (trimmed[0] is not ('{' or '[')) return false; + + try + { + JToken.Parse(payload); + return true; + } + catch + { + return false; + } + } +} diff --git a/SW.Bitween.NativeAdapters/SmtpHandler/SmtpHandlerInput.cs b/SW.Bitween.NativeAdapters/SmtpHandler/SmtpHandlerInput.cs new file mode 100644 index 00000000..b190827b --- /dev/null +++ b/SW.Bitween.NativeAdapters/SmtpHandler/SmtpHandlerInput.cs @@ -0,0 +1,59 @@ +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; + +namespace SW.Bitween.NativeAdapters.SmtpHandler; + +public class SmtpHandlerInput +{ + [Required] + [Description("SMTP server hostname.")] + public string Host { get; set; } = string.Empty; + + [DefaultValue(587)] + [Description("SMTP server port. 587 for STARTTLS, 465 for implicit SSL, 25 for an unencrypted relay.")] + public int Port { get; set; } = 587; + + [Description("SMTP username. Leave empty to authenticate as the From address, or for a relay that needs no credentials.")] + public string? Username { get; set; } + + [Secure] + [Description("SMTP password. Leave empty for a relay that needs no credentials.")] + public string? Password { get; set; } + + [DefaultValue(true)] + [Description("Encrypt the connection, choosing STARTTLS or SSL to match the port. Turn off only for an internal relay with no TLS.")] + public bool UseTls { get; set; } = true; + + [Required] + [Description("Address the message is sent from.")] + public string From { get; set; } = string.Empty; + + [Description("Display name shown beside the From address, e.g. Bitween Alerts.")] + public string? FromName { get; set; } + + [Required] + [Description("Recipients, separated by commas.")] + public string To { get; set; } = string.Empty; + + [Description("Carbon-copy recipients, separated by commas.")] + public string? Cc { get; set; } + + [Description("Blind carbon-copy recipients, separated by commas.")] + public string? Bcc { get; set; } + + [Required] + [Description( + "Subject line. Placeholders in the incoming payload are substituted, e.g. " + + "'Retries stopped for {{ SubscriptionName }}'.")] + public string Subject { get; set; } = string.Empty; + + [Required] + [Description( + "Message body. Uses the same template syntax as the JSON mapper, so payload fields can be " + + "referenced directly, e.g. '{{ GroupName }} used all {{ MaxAttemptsTotal }} retries.'")] + public string Body { get; set; } = string.Empty; + + [DefaultValue(true)] + [Description("Send the body as HTML. Turn off to send it as plain text.")] + public bool IsHtml { get; set; } = true; +} diff --git a/SW.Bitween.PgSql/BitweenDbContext.cs b/SW.Bitween.PgSql/BitweenDbContext.cs index 44f45cc3..b0456c75 100644 --- a/SW.Bitween.PgSql/BitweenDbContext.cs +++ b/SW.Bitween.PgSql/BitweenDbContext.cs @@ -263,6 +263,10 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) b.Property(p => p.ResponseName).HasMaxLength(200); b.Property(p => p.ResponseContentType).HasMaxLength(200); b.Property(p => p.OutputContentType).HasMaxLength(200); + b.Property(p => p.RetryBlockedReason).HasMaxLength(500); + b.Property(p => p.RetryGroupId); + b.Property(p => p.AttemptNumber); + b.HasIndex(p => p.RetryGroupId); b.HasOne().WithOne().HasForeignKey(p => p.Id).OnDelete(DeleteBehavior.Cascade); }); @@ -339,7 +343,8 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) CreatedOn = defaultCreatedOn.ToUniversalTime(), Disabled = false, Password = defaultPasswordHash, - Role = AccountRole.Admin + Role = AccountRole.Admin, + FailedLoginCount = 0 }); }); @@ -390,6 +395,8 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) b.HasKey(p => p.Id); b.Property(p => p.Id).ValueGeneratedOnAdd(); b.Property(p => p.Name).IsRequired().HasMaxLength(200); + b.Property(p => p.AlertHandlerId).HasMaxLength(200); + b.Property(p => p.AlertHandlerProperties).StoreAsJson(); b.Property(p => p.Groups).HasConversion( groups => JsonSerializer.Serialize(groups, _polymorphicOpts), json => JsonSerializer.Deserialize>(json, _polymorphicOpts)!, @@ -415,13 +422,29 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) { b.HasKey(p => p.Id); b.Property(p => p.Id).HasMaxLength(50); - b.Property(p => p.GroupAttemptCounts).HasColumnType("jsonb"); b.HasIndex(p => p.On); }); - modelBuilder.Entity(b => + modelBuilder.Entity(b => + { + b.Property(p => p.Id).ValueGeneratedOnAdd(); + b.HasIndex(p => new { p.SubscriptionId, p.StartedOn }); + }); + + modelBuilder.Entity(b => + { + b.HasKey(p => new { p.SubscriptionId, p.GroupId }); + b.Property(p => p.AttemptsUsed); + b.Property(p => p.LastAttemptOn); + b.Property(p => p.ExhaustedNotifiedOn); + }); + + modelBuilder.Entity(b => { - b.Property(p => p.GroupAttemptCounts).HasColumnType("jsonb"); + b.HasKey(p => new { p.SubscriptionId, p.GroupId }); + b.Property(p => p.AlertMode).HasConversion(); + b.Property(p => p.AlertHandlerId).HasMaxLength(200); + b.Property(p => p.AlertHandlerProperties).StoreAsJson(); }); modelBuilder.UseSchedulerPostgreSql(Schema); diff --git a/SW.Bitween.PgSql/Migrations/20260811081200_SharedRetryGroupTotals.Designer.cs b/SW.Bitween.PgSql/Migrations/20260811081200_SharedRetryGroupTotals.Designer.cs new file mode 100644 index 00000000..7ad3ef75 --- /dev/null +++ b/SW.Bitween.PgSql/Migrations/20260811081200_SharedRetryGroupTotals.Designer.cs @@ -0,0 +1,2175 @@ +// +using System; +using System.Collections.Generic; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using SW.Bitween.Model; +using SW.Bitween.PgSql; + +#nullable disable + +namespace SW.Bitween.PgSql.Migrations +{ + [DbContext(typeof(BitweenDbContext))] + [Migration("20260811081200_SharedRetryGroupTotals")] + partial class SharedRetryGroupTotals + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("infolink") + .HasAnnotation("ProductVersion", "8.0.23") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Deleted") + .HasColumnType("boolean") + .HasColumnName("deleted"); + + b.Property("Disabled") + .HasColumnType("boolean") + .HasColumnName("disabled"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("display_name"); + + b.Property("Email") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("character varying(200)") + .HasColumnName("email"); + + b.Property("EmailProvider") + .HasColumnType("smallint") + .HasColumnName("email_provider"); + + b.Property("LoginMethods") + .HasColumnType("smallint") + .HasColumnName("login_methods"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Password") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("character varying(500)") + .HasColumnName("password"); + + b.Property("Phone") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("character varying(20)") + .HasColumnName("phone"); + + b.Property("Role") + .HasColumnType("integer") + .HasColumnName("role"); + + b.HasKey("Id") + .HasName("pk_accounts"); + + b.HasIndex("Email") + .IsUnique() + .HasDatabaseName("ix_accounts_email"); + + b.ToTable("Accounts", "infolink"); + + b.HasData( + new + { + Id = 9999, + CreatedOn = new DateTime(2021, 12, 31, 22, 0, 0, 0, DateTimeKind.Utc), + Deleted = false, + Disabled = false, + DisplayName = "Admin", + Email = "admin@Bitween.systems", + EmailProvider = (byte)0, + LoginMethods = (byte)2, + Password = "$SWHASH$V1$10000$VQCi48eitH4Ml5juvBMOFZrMdQwBbhuIQVXe6RR7qJdDF2bJ", + Role = 0 + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("AccountId") + .HasColumnType("integer") + .HasColumnName("account_id"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("LoginMethod") + .HasColumnType("smallint") + .HasColumnName("login_method"); + + b.HasKey("Id") + .HasName("pk_refresh_tokens"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_refresh_tokens_account_id"); + + b.ToTable("RefreshTokens", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DelayedRetry", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("On") + .HasColumnType("timestamp with time zone") + .HasColumnName("on"); + + b.HasKey("Id") + .HasName("pk_delayed_retry"); + + b.HasIndex("On") + .HasDatabaseName("ix_delayed_retry_on"); + + b.ToTable("delayed_retry", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Document", b => + { + b.Property("Id") + .HasColumnType("integer") + .HasColumnName("id"); + + b.Property("BusEnabled") + .HasColumnType("boolean") + .HasColumnName("bus_enabled"); + + b.Property("BusMessageTypeName") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("bus_message_type_name"); + + b.Property("DisregardsUnfilteredMessages") + .HasColumnType("boolean") + .HasColumnName("disregards_unfiltered_messages"); + + b.Property("DocumentFormat") + .HasColumnType("integer") + .HasColumnName("document_format"); + + b.Property("DuplicateInterval") + .HasColumnType("integer") + .HasColumnName("duplicate_interval"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("PromotedProperties") + .HasColumnType("jsonb") + .HasColumnName("promoted_properties"); + + b.HasKey("Id") + .HasName("pk_document"); + + b.HasIndex("BusMessageTypeName") + .IsUnique() + .HasDatabaseName("ix_document_bus_message_type_name"); + + b.HasIndex("Name") + .IsUnique() + .HasDatabaseName("ix_document_name"); + + b.ToTable("document", "infolink"); + + b.HasData( + new + { + Id = 10001, + BusEnabled = false, + DocumentFormat = 0, + DuplicateInterval = 0, + Name = "Aggregation Document", + PromotedProperties = "{}" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DocumentTrail", b => + { + b.Property("Id") + .HasColumnType("text") + .HasColumnName("id"); + + b.Property("Code") + .HasColumnType("integer") + .HasColumnName("code"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("DocumentId") + .HasColumnType("integer") + .HasColumnName("document_id"); + + b.Property("StateAfter") + .HasColumnType("text") + .HasColumnName("state_after"); + + b.Property("StateBefore") + .HasColumnType("text") + .HasColumnName("state_before"); + + b.HasKey("Id") + .HasName("pk_document_trail"); + + b.HasIndex("CreatedOn") + .HasDatabaseName("ix_document_trail_created_on"); + + b.HasIndex("DocumentId") + .HasDatabaseName("ix_document_trail_document_id"); + + b.ToTable("document_trail", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.Property("UrlName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("url_name"); + + b.HasKey("Id") + .HasName("pk_api_gateway"); + + b.HasIndex("UrlName") + .IsUnique() + .HasDatabaseName("ix_api_gateway_url_name"); + + b.ToTable("api_gateway", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.Property("ApiGatewayId") + .HasColumnType("integer") + .HasColumnName("api_gateway_id"); + + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.HasKey("ApiGatewayId", "PartnerId", "SubscriptionId") + .HasName("pk_api_gateway_partner"); + + b.HasIndex("PartnerId") + .HasDatabaseName("ix_api_gateway_partner_partner_id"); + + b.HasIndex("SubscriptionId") + .HasDatabaseName("ix_api_gateway_partner_subscription_id"); + + b.ToTable("api_gateway_partner", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("DocumentId") + .HasColumnType("integer") + .HasColumnName("document_id"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("pk_bus_gateway"); + + b.HasIndex("DocumentId") + .HasDatabaseName("ix_bus_gateway_document_id"); + + b.ToTable("bus_gateway", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BusGatewayId") + .HasColumnType("integer") + .HasColumnName("bus_gateway_id"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("MatchExpression") + .HasColumnType("text") + .HasColumnName("match_expression"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.HasKey("Id") + .HasName("pk_bus_gateway_route"); + + b.HasIndex("BusGatewayId") + .HasDatabaseName("ix_bus_gateway_route_bus_gateway_id"); + + b.HasIndex("PartnerId") + .HasDatabaseName("ix_bus_gateway_route_partner_id"); + + b.HasIndex("SubscriptionId") + .HasDatabaseName("ix_bus_gateway_route_subscription_id"); + + b.ToTable("bus_gateway_route", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.GlobalAdapterValuesSet", b => + { + b.Property("Id") + .HasColumnType("text") + .HasColumnName("id"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property>("Values") + .HasColumnType("jsonb") + .HasColumnName("values"); + + b.HasKey("Id") + .HasName("pk_global_adapter_values_set"); + + b.ToTable("global_adapter_values_set", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Notifier", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("character varying(200)") + .HasColumnName("handler_id"); + + b.Property("HandlerProperties") + .HasColumnType("text") + .HasColumnName("handler_properties"); + + b.Property("Inactive") + .HasColumnType("boolean") + .HasColumnName("inactive"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("RunOnBadResult") + .HasColumnType("boolean") + .HasColumnName("run_on_bad_result"); + + b.Property("RunOnFailedResult") + .HasColumnType("boolean") + .HasColumnName("run_on_failed_result"); + + b.Property("RunOnSubscriptions") + .HasColumnType("integer[]") + .HasColumnName("run_on_subscriptions"); + + b.Property("RunOnSuccessfulResult") + .HasColumnType("boolean") + .HasColumnName("run_on_successful_result"); + + b.HasKey("Id") + .HasName("pk_notifier"); + + b.ToTable("notifier", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OnHoldXchange", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BadData") + .HasColumnType("boolean") + .HasColumnName("bad_data"); + + b.Property("Data") + .HasColumnType("text") + .HasColumnName("data"); + + b.Property("FileName") + .HasColumnType("text") + .HasColumnName("file_name"); + + b.Property("References") + .HasColumnType("text[]") + .HasColumnName("references"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.HasKey("Id") + .HasName("pk_on_hold_xchange"); + + b.HasIndex("SubscriptionId") + .HasDatabaseName("ix_on_hold_xchange_subscription_id"); + + b.ToTable("on_hold_xchange", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property>("AdapterProperties") + .HasColumnType("jsonb") + .HasColumnName("adapter_properties"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("pk_partner"); + + b.ToTable("partner", "infolink"); + + b.HasData( + new + { + Id = 1, + Name = "SYSTEM" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryGroupUsage", b => + { + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.Property("GroupId") + .HasColumnType("uuid") + .HasColumnName("group_id"); + + b.Property("AttemptsUsed") + .HasColumnType("integer") + .HasColumnName("attempts_used"); + + b.Property("LastAttemptOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_attempt_on"); + + b.HasKey("SubscriptionId", "GroupId") + .HasName("pk_retry_group_usage"); + + b.ToTable("retry_group_usage", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryPolicy", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Groups") + .HasColumnType("text") + .HasColumnName("groups"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("pk_retry_policy"); + + b.ToTable("retry_policy", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AggregateOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("aggregate_on"); + + b.Property("AggregationForId") + .HasColumnType("integer") + .HasColumnName("aggregation_for_id"); + + b.Property("AggregationTarget") + .HasColumnType("smallint") + .HasColumnName("aggregation_target"); + + b.Property("CategoryId") + .HasColumnType("integer") + .HasColumnName("category_id"); + + b.Property("ConsecutiveFailures") + .HasColumnType("integer") + .HasColumnName("consecutive_failures"); + + b.Property("CustomRetryPolicy") + .HasColumnType("text") + .HasColumnName("custom_retry_policy"); + + b.Property>("DocumentFilter") + .HasColumnType("jsonb") + .HasColumnName("document_filter"); + + b.Property("DocumentId") + .HasColumnType("integer") + .HasColumnName("document_id"); + + b.Property("HandlerId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("handler_id"); + + b.Property>("HandlerProperties") + .HasColumnType("jsonb") + .HasColumnName("handler_properties"); + + b.Property("Inactive") + .HasColumnType("boolean") + .HasColumnName("inactive"); + + b.Property("IsRunning") + .HasColumnType("boolean") + .HasColumnName("is_running"); + + b.Property("LastException") + .HasColumnType("text") + .HasColumnName("last_exception"); + + b.Property("MapperId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("mapper_id"); + + b.Property>("MapperProperties") + .HasColumnType("jsonb") + .HasColumnName("mapper_properties"); + + b.Property("MatchExpression") + .HasColumnType("text") + .HasColumnName("match_expression"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.Property("PausedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("paused_on"); + + b.Property("ReceiveOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("receive_on"); + + b.Property("ReceiverId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("receiver_id"); + + b.Property>("ReceiverProperties") + .HasColumnType("jsonb") + .HasColumnName("receiver_properties"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("response_message_type_name"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("integer") + .HasColumnName("response_subscription_id"); + + b.Property("RetryPolicyId") + .HasColumnType("integer") + .HasColumnName("retry_policy_id"); + + b.Property("Temporary") + .HasColumnType("boolean") + .HasColumnName("temporary"); + + b.Property("Type") + .HasColumnType("smallint") + .HasColumnName("type"); + + b.Property("ValidatorId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("validator_id"); + + b.Property>("ValidatorProperties") + .HasColumnType("jsonb") + .HasColumnName("validator_properties"); + + b.Property("WorkGroupId") + .HasColumnType("integer") + .HasColumnName("work_group_id"); + + b.HasKey("Id") + .HasName("pk_subscription"); + + b.HasIndex("AggregationForId") + .HasDatabaseName("ix_subscription_aggregation_for_id"); + + b.HasIndex("CategoryId") + .HasDatabaseName("ix_subscription_category_id"); + + b.HasIndex("DocumentId") + .HasDatabaseName("ix_subscription_document_id"); + + b.HasIndex("PartnerId") + .HasDatabaseName("ix_subscription_partner_id"); + + b.HasIndex("ResponseSubscriptionId") + .HasDatabaseName("ix_subscription_response_subscription_id"); + + b.HasIndex("RetryPolicyId") + .HasDatabaseName("ix_subscription_retry_policy_id"); + + b.HasIndex("WorkGroupId") + .HasDatabaseName("ix_subscription_work_group_id"); + + b.ToTable("subscription", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Code") + .HasColumnType("text") + .HasColumnName("code"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.HasKey("Id") + .HasName("pk_subscription_category"); + + b.HasIndex("Code") + .IsUnique() + .HasDatabaseName("ix_subscription_category_code"); + + b.ToTable("subscription_category", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionTrail", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("Code") + .HasColumnType("integer") + .HasColumnName("code"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("StateAfter") + .HasColumnType("text") + .HasColumnName("state_after"); + + b.Property("StateBefore") + .HasColumnType("text") + .HasColumnName("state_before"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.HasKey("Id") + .HasName("pk_subscription_trail"); + + b.HasIndex("CreatedOn") + .HasDatabaseName("ix_subscription_trail_created_on"); + + b.HasIndex("SubscriptionId") + .HasDatabaseName("ix_subscription_trail_subscription_id"); + + b.ToTable("subscription_trail", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.WorkGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BusMessageName") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("character varying(100)") + .HasColumnName("bus_message_name"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("Options") + .HasColumnType("jsonb") + .HasColumnName("options"); + + b.HasKey("Id") + .HasName("pk_work_group"); + + b.ToTable("work_group", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("CorrelationId") + .HasColumnType("text") + .HasColumnName("correlation_id"); + + b.Property("DocumentId") + .HasColumnType("integer") + .HasColumnName("document_id"); + + b.Property("HandlerId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("handler_id"); + + b.Property>("HandlerProperties") + .HasColumnType("jsonb") + .HasColumnName("handler_properties"); + + b.Property("InputContentType") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("input_content_type"); + + b.Property("InputHash") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("input_hash"); + + b.Property("InputName") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("input_name"); + + b.Property("InputSize") + .HasColumnType("integer") + .HasColumnName("input_size"); + + b.Property("MapperId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("mapper_id"); + + b.Property>("MapperProperties") + .HasColumnType("jsonb") + .HasColumnName("mapper_properties"); + + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.Property("References") + .HasColumnType("text[]") + .HasColumnName("references"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("response_message_type_name"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("integer") + .HasColumnName("response_subscription_id"); + + b.Property("RetryFor") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("retry_for"); + + b.Property("StartedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("started_on"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.HasKey("Id") + .HasName("pk_xchange"); + + b.HasIndex("DocumentId") + .HasDatabaseName("ix_xchange_document_id"); + + b.HasIndex("InputHash") + .HasDatabaseName("ix_xchange_input_hash"); + + b.HasIndex("RetryFor") + .HasDatabaseName("ix_xchange_retry_for"); + + b.HasIndex("StartedOn") + .HasDatabaseName("ix_xchange_started_on"); + + b.HasIndex("SubscriptionId") + .HasDatabaseName("ix_xchange_subscription_id"); + + b.ToTable("xchange", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("AggregatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("aggregated_on"); + + b.Property("AggregationXchangeId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("aggregation_xchange_id"); + + b.HasKey("Id") + .HasName("pk_xchange_aggregation"); + + b.HasIndex("AggregationXchangeId") + .HasDatabaseName("ix_xchange_aggregation_aggregation_xchange_id"); + + b.ToTable("xchange_aggregation", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("DeliveredOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("delivered_on"); + + b.HasKey("Id") + .HasName("pk_xchange_delivery"); + + b.HasIndex("DeliveredOn") + .HasDatabaseName("ix_xchange_delivery_delivered_on"); + + b.ToTable("xchange_delivery", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeNotification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Exception") + .HasColumnType("text") + .HasColumnName("exception"); + + b.Property("FinishedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("finished_on"); + + b.Property("NotifierId") + .HasColumnType("integer") + .HasColumnName("notifier_id"); + + b.Property("NotifierName") + .HasColumnType("text") + .HasColumnName("notifier_name"); + + b.Property("Success") + .HasColumnType("boolean") + .HasColumnName("success"); + + b.Property("XchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("character varying(50)") + .HasColumnName("xchange_id"); + + b.HasKey("Id") + .HasName("pk_xchange_notification"); + + b.ToTable("xchange_notification", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("Hits") + .HasColumnType("integer[]") + .HasColumnName("hits"); + + b.Property>("Properties") + .HasColumnType("jsonb") + .HasColumnName("properties"); + + b.Property("PropertiesRaw") + .HasColumnType("text") + .HasColumnName("properties_raw"); + + b.HasKey("Id") + .HasName("pk_xchange_promoted_properties"); + + b.HasIndex("PropertiesRaw") + .HasDatabaseName("ix_xchange_promoted_properties_properties_raw"); + + b.ToTable("xchange_promoted_properties", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("Exception") + .HasColumnType("text") + .HasColumnName("exception"); + + b.Property("FinishedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("finished_on"); + + b.Property("OutputBad") + .HasColumnType("boolean") + .HasColumnName("output_bad"); + + b.Property("OutputContentType") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("output_content_type"); + + b.Property("OutputHash") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("output_hash"); + + b.Property("OutputName") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("output_name"); + + b.Property("OutputSize") + .HasColumnType("integer") + .HasColumnName("output_size"); + + b.Property("ResponseBad") + .HasColumnType("boolean") + .HasColumnName("response_bad"); + + b.Property("ResponseContentType") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("response_content_type"); + + b.Property("ResponseHash") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("response_hash"); + + b.Property("ResponseName") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("response_name"); + + b.Property("ResponseSize") + .HasColumnType("integer") + .HasColumnName("response_size"); + + b.Property("ResponseXchangeId") + .HasColumnType("text") + .HasColumnName("response_xchange_id"); + + b.Property("Success") + .HasColumnType("boolean") + .HasColumnName("success"); + + b.HasKey("Id") + .HasName("pk_xchange_result"); + + b.ToTable("xchange_result", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.RunFlagUpdater+RunningResult", b => + { + b.Property("IsRunning") + .HasColumnType("boolean") + .HasColumnName("is_running"); + + b.ToTable("running_result", "infolink"); + + b.ToView(null, (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.JobExecution", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Context") + .HasColumnType("text") + .HasColumnName("context"); + + b.Property("DurationMs") + .HasColumnType("bigint") + .HasColumnName("duration_ms"); + + b.Property("EndTimeUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("end_time_utc"); + + b.Property("Error") + .HasColumnType("text") + .HasColumnName("error"); + + b.Property("FireInstanceId") + .IsRequired() + .HasColumnType("text") + .HasColumnName("fire_instance_id"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property("JobTypeName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_type_name"); + + b.Property("Node") + .IsRequired() + .HasColumnType("text") + .HasColumnName("node"); + + b.Property("StartTimeUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("start_time_utc"); + + b.Property("Success") + .HasColumnType("boolean") + .HasColumnName("success"); + + b.HasKey("Id") + .HasName("pk_job_executions"); + + b.HasIndex("FireInstanceId") + .IsUnique() + .HasDatabaseName("idx_je_fire_instance_id"); + + b.HasIndex("StartTimeUtc") + .HasDatabaseName("idx_je_start_time"); + + b.HasIndex("Success") + .HasDatabaseName("idx_je_success"); + + b.HasIndex("JobGroup", "JobName", "StartTimeUtc") + .HasDatabaseName("idx_je_group_name_start"); + + b.ToTable("job_executions", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("BlobData") + .HasColumnType("bytea") + .HasColumnName("blob_data"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup") + .HasName("pk_qrtz_blob_triggers"); + + b.ToTable("qrtz_blob_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCalendar", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("CalendarName") + .HasColumnType("text") + .HasColumnName("calendar_name"); + + b.Property("Calendar") + .IsRequired() + .HasColumnType("bytea") + .HasColumnName("calendar"); + + b.HasKey("SchedulerName", "CalendarName") + .HasName("pk_qrtz_calendars"); + + b.ToTable("qrtz_calendars", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("CronExpression") + .IsRequired() + .HasColumnType("text") + .HasColumnName("cron_expression"); + + b.Property("TimeZoneId") + .HasColumnType("text") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup") + .HasName("pk_qrtz_cron_triggers"); + + b.ToTable("qrtz_cron_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzFiredTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("EntryId") + .HasColumnType("text") + .HasColumnName("entry_id"); + + b.Property("FiredTime") + .HasColumnType("bigint") + .HasColumnName("fired_time"); + + b.Property("InstanceName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("instance_name"); + + b.Property("IsNonConcurrent") + .HasColumnType("bool") + .HasColumnName("is_nonconcurrent"); + + b.Property("JobGroup") + .HasColumnType("text") + .HasColumnName("job_group"); + + b.Property("JobName") + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property("Priority") + .HasColumnType("integer") + .HasColumnName("priority"); + + b.Property("RequestsRecovery") + .HasColumnType("bool") + .HasColumnName("requests_recovery"); + + b.Property("ScheduledTime") + .HasColumnType("bigint") + .HasColumnName("sched_time"); + + b.Property("State") + .IsRequired() + .HasColumnType("text") + .HasColumnName("state"); + + b.Property("TriggerGroup") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("TriggerName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.HasKey("SchedulerName", "EntryId") + .HasName("pk_qrtz_fired_triggers"); + + b.HasIndex("InstanceName") + .HasDatabaseName("idx_qrtz_ft_trig_inst_name"); + + b.HasIndex("JobGroup") + .HasDatabaseName("idx_qrtz_ft_job_group"); + + b.HasIndex("JobName") + .HasDatabaseName("idx_qrtz_ft_job_name"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_qrtz_ft_job_req_recovery"); + + b.HasIndex("TriggerGroup") + .HasDatabaseName("idx_qrtz_ft_trig_group"); + + b.HasIndex("TriggerName") + .HasDatabaseName("idx_qrtz_ft_trig_name"); + + b.HasIndex("SchedulerName", "TriggerName", "TriggerGroup") + .HasDatabaseName("idx_qrtz_ft_trig_nm_gp"); + + b.ToTable("qrtz_fired_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("JobName") + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property("JobGroup") + .HasColumnType("text") + .HasColumnName("job_group"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("IsDurable") + .HasColumnType("bool") + .HasColumnName("is_durable"); + + b.Property("IsNonConcurrent") + .HasColumnType("bool") + .HasColumnName("is_nonconcurrent"); + + b.Property("IsUpdateData") + .HasColumnType("bool") + .HasColumnName("is_update_data"); + + b.Property("JobClassName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_class_name"); + + b.Property("JobData") + .HasColumnType("bytea") + .HasColumnName("job_data"); + + b.Property("RequestsRecovery") + .HasColumnType("bool") + .HasColumnName("requests_recovery"); + + b.HasKey("SchedulerName", "JobName", "JobGroup") + .HasName("pk_qrtz_job_details"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_j_req_recovery"); + + b.ToTable("qrtz_job_details", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzLock", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("LockName") + .HasColumnType("text") + .HasColumnName("lock_name"); + + b.HasKey("SchedulerName", "LockName") + .HasName("pk_qrtz_locks"); + + b.ToTable("qrtz_locks", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzPausedTriggerGroup", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.HasKey("SchedulerName", "TriggerGroup") + .HasName("pk_qrtz_paused_trigger_grps"); + + b.ToTable("qrtz_paused_trigger_grps", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSchedulerState", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("InstanceName") + .HasColumnType("text") + .HasColumnName("instance_name"); + + b.Property("CheckInInterval") + .HasColumnType("bigint") + .HasColumnName("checkin_interval"); + + b.Property("LastCheckInTime") + .HasColumnType("bigint") + .HasColumnName("last_checkin_time"); + + b.HasKey("SchedulerName", "InstanceName") + .HasName("pk_qrtz_scheduler_state"); + + b.ToTable("qrtz_scheduler_state", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("BooleanProperty1") + .HasColumnType("bool") + .HasColumnName("bool_prop_1"); + + b.Property("BooleanProperty2") + .HasColumnType("bool") + .HasColumnName("bool_prop_2"); + + b.Property("DecimalProperty1") + .HasColumnType("numeric") + .HasColumnName("dec_prop_1"); + + b.Property("DecimalProperty2") + .HasColumnType("numeric") + .HasColumnName("dec_prop_2"); + + b.Property("IntegerProperty1") + .HasColumnType("integer") + .HasColumnName("int_prop_1"); + + b.Property("IntegerProperty2") + .HasColumnType("integer") + .HasColumnName("int_prop_2"); + + b.Property("LongProperty1") + .HasColumnType("bigint") + .HasColumnName("long_prop_1"); + + b.Property("LongProperty2") + .HasColumnType("bigint") + .HasColumnName("long_prop_2"); + + b.Property("StringProperty1") + .HasColumnType("text") + .HasColumnName("str_prop_1"); + + b.Property("StringProperty2") + .HasColumnType("text") + .HasColumnName("str_prop_2"); + + b.Property("StringProperty3") + .HasColumnType("text") + .HasColumnName("str_prop_3"); + + b.Property("TimeZoneId") + .HasColumnType("text") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup") + .HasName("pk_qrtz_simprop_triggers"); + + b.ToTable("qrtz_simprop_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("RepeatCount") + .HasColumnType("bigint") + .HasColumnName("repeat_count"); + + b.Property("RepeatInterval") + .HasColumnType("bigint") + .HasColumnName("repeat_interval"); + + b.Property("TimesTriggered") + .HasColumnType("bigint") + .HasColumnName("times_triggered"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup") + .HasName("pk_qrtz_simple_triggers"); + + b.ToTable("qrtz_simple_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("CalendarName") + .HasColumnType("text") + .HasColumnName("calendar_name"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("EndTime") + .HasColumnType("bigint") + .HasColumnName("end_time"); + + b.Property("JobData") + .HasColumnType("bytea") + .HasColumnName("job_data"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property("MisfireInstruction") + .HasColumnType("integer") + .HasColumnName("misfire_instr"); + + b.Property("NextFireTime") + .HasColumnType("bigint") + .HasColumnName("next_fire_time"); + + b.Property("PreviousFireTime") + .HasColumnType("bigint") + .HasColumnName("prev_fire_time"); + + b.Property("Priority") + .HasColumnType("integer") + .HasColumnName("priority"); + + b.Property("StartTime") + .HasColumnType("bigint") + .HasColumnName("start_time"); + + b.Property("TriggerState") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_state"); + + b.Property("TriggerType") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_type"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup") + .HasName("pk_qrtz_triggers"); + + b.HasIndex("NextFireTime") + .HasDatabaseName("idx_t_next_fire_time"); + + b.HasIndex("TriggerState") + .HasDatabaseName("idx_t_state"); + + b.HasIndex("NextFireTime", "TriggerState") + .HasDatabaseName("idx_t_nft_st"); + + b.HasIndex("SchedulerName", "JobName", "JobGroup") + .HasDatabaseName("ix_qrtz_triggers_sched_name_job_name_job_group"); + + b.ToTable("qrtz_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_refresh_tokens_accounts_account_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DocumentTrail", b => + { + b.HasOne("SW.Bitween.Domain.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_document_trail_document_document_id"); + + b.Navigation("Document"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.ApiGateway", "ApiGateway") + .WithMany("Partners") + .HasForeignKey("ApiGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_api_gateway_partner_api_gateway_api_gateway_id"); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_api_gateway_partner_partner_partner_id"); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_api_gateway_partner_subscription_subscription_id"); + + b.Navigation("ApiGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_bus_gateway_document_document_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.BusGateway", "BusGateway") + .WithMany("Routes") + .HasForeignKey("BusGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_bus_gateway_route_bus_gateway_bus_gateway_id"); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_bus_gateway_route_partner_partner_id"); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_bus_gateway_route_subscription_subscription_id"); + + b.Navigation("BusGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.OwnsMany("SW.Bitween.Domain.ApiCredential", "ApiCredentials", b1 => + { + b1.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("Key") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("key"); + + b1.Property("Name") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("name"); + + b1.HasKey("PartnerId", "Id") + .HasName("pk_partner_api_credential"); + + b1.HasIndex("Key") + .IsUnique() + .HasDatabaseName("ix_partner_api_credential_key"); + + b1.ToTable("partner_api_credential", "infolink"); + + b1.WithOwner() + .HasForeignKey("PartnerId") + .HasConstraintName("fk_partner_api_credential_partner_partner_id"); + + b1.HasData( + new + { + PartnerId = 1, + Id = 1, + Key = "7facc758283844b49cc4ffd26a75b1de", + Name = "default" + }); + }); + + b.Navigation("ApiCredentials"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.HasOne("SW.Bitween.Domain.Subscription", null) + .WithMany() + .HasForeignKey("AggregationForId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_subscription_aggregation_for"); + + b.HasOne("SW.Bitween.Domain.SubscriptionCategory", "Category") + .WithMany() + .HasForeignKey("CategoryId") + .HasConstraintName("fk_subscription_subscription_category_category_id"); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_subscription_document_document_id"); + + b.HasOne("SW.Bitween.Domain.Partner", null) + .WithMany("Subscriptions") + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_subscription_partner_partner_id"); + + b.HasOne("SW.Bitween.Domain.Subscription", null) + .WithMany() + .HasForeignKey("ResponseSubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_subscription_response_subscriber"); + + b.HasOne("SW.Bitween.Domain.RetryPolicy", "RetryPolicy") + .WithMany() + .HasForeignKey("RetryPolicyId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_subscription_retry_policy_retry_policy_id"); + + b.HasOne("SW.Bitween.Domain.WorkGroup", "WorkGroup") + .WithMany() + .HasForeignKey("WorkGroupId") + .HasConstraintName("fk_subscription_work_group_work_group_id"); + + b.OwnsMany("SW.Bitween.Domain.Schedule", "Schedules", b1 => + { + b1.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("Backwards") + .HasColumnType("boolean") + .HasColumnName("backwards"); + + b1.Property("On") + .HasColumnType("bigint") + .HasColumnName("on"); + + b1.Property("Recurrence") + .HasColumnType("smallint") + .HasColumnName("recurrence"); + + b1.HasKey("SubscriptionId", "Id") + .HasName("pk_subscription_schedule"); + + b1.ToTable("subscription_schedule", "infolink"); + + b1.WithOwner() + .HasForeignKey("SubscriptionId") + .HasConstraintName("fk_subscription_schedule_subscription_subscription_id"); + }); + + b.Navigation("Category"); + + b.Navigation("RetryPolicy"); + + b.Navigation("Schedules"); + + b.Navigation("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionTrail", b => + { + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_subscription_trail_subscription_subscription_id"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_xchange_document_document_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeAggregation", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_xchange_aggregation_xchange_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeDelivery", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_xchange_delivery_xchange_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangePromotedProperties", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_xchange_promoted_properties_xchange_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeResult", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_xchange_result_xchange_id"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("BlobTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_qrtz_blob_triggers_qrtz_triggers_sched_name_trigger_name_tr"); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("CronTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_qrtz_cron_triggers_qrtz_triggers_sched_name_trigger_name_tr"); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimplePropertyTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_qrtz_simprop_triggers_qrtz_triggers_sched_name_trigger_name"); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimpleTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_qrtz_simple_triggers_qrtz_triggers_sched_name_trigger_name_"); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzJobDetail", "JobDetail") + .WithMany("Triggers") + .HasForeignKey("SchedulerName", "JobName", "JobGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_qrtz_triggers_qrtz_job_details_sched_name_job_name_job_group"); + + b.Navigation("JobDetail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Navigation("Partners"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Navigation("Routes"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Navigation("Subscriptions"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Navigation("Triggers"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Navigation("BlobTriggers"); + + b.Navigation("CronTriggers"); + + b.Navigation("SimplePropertyTriggers"); + + b.Navigation("SimpleTriggers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/SW.Bitween.PgSql/Migrations/20260811081200_SharedRetryGroupTotals.cs b/SW.Bitween.PgSql/Migrations/20260811081200_SharedRetryGroupTotals.cs new file mode 100644 index 00000000..2bad0745 --- /dev/null +++ b/SW.Bitween.PgSql/Migrations/20260811081200_SharedRetryGroupTotals.cs @@ -0,0 +1,63 @@ +using System; +using System.Collections.Generic; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace SW.Bitween.PgSql.Migrations +{ + /// + public partial class SharedRetryGroupTotals : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "group_attempt_counts", + schema: "infolink", + table: "xchange"); + + migrationBuilder.DropColumn( + name: "group_attempt_counts", + schema: "infolink", + table: "delayed_retry"); + + migrationBuilder.CreateTable( + name: "retry_group_usage", + schema: "infolink", + columns: table => new + { + subscription_id = table.Column(type: "integer", nullable: false), + group_id = table.Column(type: "uuid", nullable: false), + attempts_used = table.Column(type: "integer", nullable: false), + last_attempt_on = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_retry_group_usage", x => new { x.subscription_id, x.group_id }); + }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "retry_group_usage", + schema: "infolink"); + + migrationBuilder.AddColumn>( + name: "group_attempt_counts", + schema: "infolink", + table: "xchange", + type: "jsonb", + nullable: true); + + migrationBuilder.AddColumn>( + name: "group_attempt_counts", + schema: "infolink", + table: "delayed_retry", + type: "jsonb", + nullable: true); + } + } +} diff --git a/SW.Bitween.PgSql/Migrations/20260811092318_RetryBlockedReason.Designer.cs b/SW.Bitween.PgSql/Migrations/20260811092318_RetryBlockedReason.Designer.cs new file mode 100644 index 00000000..e7f65c75 --- /dev/null +++ b/SW.Bitween.PgSql/Migrations/20260811092318_RetryBlockedReason.Designer.cs @@ -0,0 +1,2180 @@ +// +using System; +using System.Collections.Generic; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using SW.Bitween.Model; +using SW.Bitween.PgSql; + +#nullable disable + +namespace SW.Bitween.PgSql.Migrations +{ + [DbContext(typeof(BitweenDbContext))] + [Migration("20260811092318_RetryBlockedReason")] + partial class RetryBlockedReason + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("infolink") + .HasAnnotation("ProductVersion", "8.0.23") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Deleted") + .HasColumnType("boolean") + .HasColumnName("deleted"); + + b.Property("Disabled") + .HasColumnType("boolean") + .HasColumnName("disabled"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("display_name"); + + b.Property("Email") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("character varying(200)") + .HasColumnName("email"); + + b.Property("EmailProvider") + .HasColumnType("smallint") + .HasColumnName("email_provider"); + + b.Property("LoginMethods") + .HasColumnType("smallint") + .HasColumnName("login_methods"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Password") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("character varying(500)") + .HasColumnName("password"); + + b.Property("Phone") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("character varying(20)") + .HasColumnName("phone"); + + b.Property("Role") + .HasColumnType("integer") + .HasColumnName("role"); + + b.HasKey("Id") + .HasName("pk_accounts"); + + b.HasIndex("Email") + .IsUnique() + .HasDatabaseName("ix_accounts_email"); + + b.ToTable("Accounts", "infolink"); + + b.HasData( + new + { + Id = 9999, + CreatedOn = new DateTime(2021, 12, 31, 22, 0, 0, 0, DateTimeKind.Utc), + Deleted = false, + Disabled = false, + DisplayName = "Admin", + Email = "admin@Bitween.systems", + EmailProvider = (byte)0, + LoginMethods = (byte)2, + Password = "$SWHASH$V1$10000$VQCi48eitH4Ml5juvBMOFZrMdQwBbhuIQVXe6RR7qJdDF2bJ", + Role = 0 + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("AccountId") + .HasColumnType("integer") + .HasColumnName("account_id"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("LoginMethod") + .HasColumnType("smallint") + .HasColumnName("login_method"); + + b.HasKey("Id") + .HasName("pk_refresh_tokens"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_refresh_tokens_account_id"); + + b.ToTable("RefreshTokens", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DelayedRetry", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("On") + .HasColumnType("timestamp with time zone") + .HasColumnName("on"); + + b.HasKey("Id") + .HasName("pk_delayed_retry"); + + b.HasIndex("On") + .HasDatabaseName("ix_delayed_retry_on"); + + b.ToTable("delayed_retry", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Document", b => + { + b.Property("Id") + .HasColumnType("integer") + .HasColumnName("id"); + + b.Property("BusEnabled") + .HasColumnType("boolean") + .HasColumnName("bus_enabled"); + + b.Property("BusMessageTypeName") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("bus_message_type_name"); + + b.Property("DisregardsUnfilteredMessages") + .HasColumnType("boolean") + .HasColumnName("disregards_unfiltered_messages"); + + b.Property("DocumentFormat") + .HasColumnType("integer") + .HasColumnName("document_format"); + + b.Property("DuplicateInterval") + .HasColumnType("integer") + .HasColumnName("duplicate_interval"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("PromotedProperties") + .HasColumnType("jsonb") + .HasColumnName("promoted_properties"); + + b.HasKey("Id") + .HasName("pk_document"); + + b.HasIndex("BusMessageTypeName") + .IsUnique() + .HasDatabaseName("ix_document_bus_message_type_name"); + + b.HasIndex("Name") + .IsUnique() + .HasDatabaseName("ix_document_name"); + + b.ToTable("document", "infolink"); + + b.HasData( + new + { + Id = 10001, + BusEnabled = false, + DocumentFormat = 0, + DuplicateInterval = 0, + Name = "Aggregation Document", + PromotedProperties = "{}" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DocumentTrail", b => + { + b.Property("Id") + .HasColumnType("text") + .HasColumnName("id"); + + b.Property("Code") + .HasColumnType("integer") + .HasColumnName("code"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("DocumentId") + .HasColumnType("integer") + .HasColumnName("document_id"); + + b.Property("StateAfter") + .HasColumnType("text") + .HasColumnName("state_after"); + + b.Property("StateBefore") + .HasColumnType("text") + .HasColumnName("state_before"); + + b.HasKey("Id") + .HasName("pk_document_trail"); + + b.HasIndex("CreatedOn") + .HasDatabaseName("ix_document_trail_created_on"); + + b.HasIndex("DocumentId") + .HasDatabaseName("ix_document_trail_document_id"); + + b.ToTable("document_trail", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.Property("UrlName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("url_name"); + + b.HasKey("Id") + .HasName("pk_api_gateway"); + + b.HasIndex("UrlName") + .IsUnique() + .HasDatabaseName("ix_api_gateway_url_name"); + + b.ToTable("api_gateway", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.Property("ApiGatewayId") + .HasColumnType("integer") + .HasColumnName("api_gateway_id"); + + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.HasKey("ApiGatewayId", "PartnerId", "SubscriptionId") + .HasName("pk_api_gateway_partner"); + + b.HasIndex("PartnerId") + .HasDatabaseName("ix_api_gateway_partner_partner_id"); + + b.HasIndex("SubscriptionId") + .HasDatabaseName("ix_api_gateway_partner_subscription_id"); + + b.ToTable("api_gateway_partner", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("DocumentId") + .HasColumnType("integer") + .HasColumnName("document_id"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("pk_bus_gateway"); + + b.HasIndex("DocumentId") + .HasDatabaseName("ix_bus_gateway_document_id"); + + b.ToTable("bus_gateway", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BusGatewayId") + .HasColumnType("integer") + .HasColumnName("bus_gateway_id"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("MatchExpression") + .HasColumnType("text") + .HasColumnName("match_expression"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.HasKey("Id") + .HasName("pk_bus_gateway_route"); + + b.HasIndex("BusGatewayId") + .HasDatabaseName("ix_bus_gateway_route_bus_gateway_id"); + + b.HasIndex("PartnerId") + .HasDatabaseName("ix_bus_gateway_route_partner_id"); + + b.HasIndex("SubscriptionId") + .HasDatabaseName("ix_bus_gateway_route_subscription_id"); + + b.ToTable("bus_gateway_route", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.GlobalAdapterValuesSet", b => + { + b.Property("Id") + .HasColumnType("text") + .HasColumnName("id"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property>("Values") + .HasColumnType("jsonb") + .HasColumnName("values"); + + b.HasKey("Id") + .HasName("pk_global_adapter_values_set"); + + b.ToTable("global_adapter_values_set", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Notifier", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("character varying(200)") + .HasColumnName("handler_id"); + + b.Property("HandlerProperties") + .HasColumnType("text") + .HasColumnName("handler_properties"); + + b.Property("Inactive") + .HasColumnType("boolean") + .HasColumnName("inactive"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("RunOnBadResult") + .HasColumnType("boolean") + .HasColumnName("run_on_bad_result"); + + b.Property("RunOnFailedResult") + .HasColumnType("boolean") + .HasColumnName("run_on_failed_result"); + + b.Property("RunOnSubscriptions") + .HasColumnType("integer[]") + .HasColumnName("run_on_subscriptions"); + + b.Property("RunOnSuccessfulResult") + .HasColumnType("boolean") + .HasColumnName("run_on_successful_result"); + + b.HasKey("Id") + .HasName("pk_notifier"); + + b.ToTable("notifier", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OnHoldXchange", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BadData") + .HasColumnType("boolean") + .HasColumnName("bad_data"); + + b.Property("Data") + .HasColumnType("text") + .HasColumnName("data"); + + b.Property("FileName") + .HasColumnType("text") + .HasColumnName("file_name"); + + b.Property("References") + .HasColumnType("text[]") + .HasColumnName("references"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.HasKey("Id") + .HasName("pk_on_hold_xchange"); + + b.HasIndex("SubscriptionId") + .HasDatabaseName("ix_on_hold_xchange_subscription_id"); + + b.ToTable("on_hold_xchange", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property>("AdapterProperties") + .HasColumnType("jsonb") + .HasColumnName("adapter_properties"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("pk_partner"); + + b.ToTable("partner", "infolink"); + + b.HasData( + new + { + Id = 1, + Name = "SYSTEM" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryGroupUsage", b => + { + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.Property("GroupId") + .HasColumnType("uuid") + .HasColumnName("group_id"); + + b.Property("AttemptsUsed") + .HasColumnType("integer") + .HasColumnName("attempts_used"); + + b.Property("LastAttemptOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_attempt_on"); + + b.HasKey("SubscriptionId", "GroupId") + .HasName("pk_retry_group_usage"); + + b.ToTable("retry_group_usage", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryPolicy", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Groups") + .HasColumnType("text") + .HasColumnName("groups"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("pk_retry_policy"); + + b.ToTable("retry_policy", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AggregateOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("aggregate_on"); + + b.Property("AggregationForId") + .HasColumnType("integer") + .HasColumnName("aggregation_for_id"); + + b.Property("AggregationTarget") + .HasColumnType("smallint") + .HasColumnName("aggregation_target"); + + b.Property("CategoryId") + .HasColumnType("integer") + .HasColumnName("category_id"); + + b.Property("ConsecutiveFailures") + .HasColumnType("integer") + .HasColumnName("consecutive_failures"); + + b.Property("CustomRetryPolicy") + .HasColumnType("text") + .HasColumnName("custom_retry_policy"); + + b.Property>("DocumentFilter") + .HasColumnType("jsonb") + .HasColumnName("document_filter"); + + b.Property("DocumentId") + .HasColumnType("integer") + .HasColumnName("document_id"); + + b.Property("HandlerId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("handler_id"); + + b.Property>("HandlerProperties") + .HasColumnType("jsonb") + .HasColumnName("handler_properties"); + + b.Property("Inactive") + .HasColumnType("boolean") + .HasColumnName("inactive"); + + b.Property("IsRunning") + .HasColumnType("boolean") + .HasColumnName("is_running"); + + b.Property("LastException") + .HasColumnType("text") + .HasColumnName("last_exception"); + + b.Property("MapperId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("mapper_id"); + + b.Property>("MapperProperties") + .HasColumnType("jsonb") + .HasColumnName("mapper_properties"); + + b.Property("MatchExpression") + .HasColumnType("text") + .HasColumnName("match_expression"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.Property("PausedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("paused_on"); + + b.Property("ReceiveOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("receive_on"); + + b.Property("ReceiverId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("receiver_id"); + + b.Property>("ReceiverProperties") + .HasColumnType("jsonb") + .HasColumnName("receiver_properties"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("response_message_type_name"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("integer") + .HasColumnName("response_subscription_id"); + + b.Property("RetryPolicyId") + .HasColumnType("integer") + .HasColumnName("retry_policy_id"); + + b.Property("Temporary") + .HasColumnType("boolean") + .HasColumnName("temporary"); + + b.Property("Type") + .HasColumnType("smallint") + .HasColumnName("type"); + + b.Property("ValidatorId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("validator_id"); + + b.Property>("ValidatorProperties") + .HasColumnType("jsonb") + .HasColumnName("validator_properties"); + + b.Property("WorkGroupId") + .HasColumnType("integer") + .HasColumnName("work_group_id"); + + b.HasKey("Id") + .HasName("pk_subscription"); + + b.HasIndex("AggregationForId") + .HasDatabaseName("ix_subscription_aggregation_for_id"); + + b.HasIndex("CategoryId") + .HasDatabaseName("ix_subscription_category_id"); + + b.HasIndex("DocumentId") + .HasDatabaseName("ix_subscription_document_id"); + + b.HasIndex("PartnerId") + .HasDatabaseName("ix_subscription_partner_id"); + + b.HasIndex("ResponseSubscriptionId") + .HasDatabaseName("ix_subscription_response_subscription_id"); + + b.HasIndex("RetryPolicyId") + .HasDatabaseName("ix_subscription_retry_policy_id"); + + b.HasIndex("WorkGroupId") + .HasDatabaseName("ix_subscription_work_group_id"); + + b.ToTable("subscription", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Code") + .HasColumnType("text") + .HasColumnName("code"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.HasKey("Id") + .HasName("pk_subscription_category"); + + b.HasIndex("Code") + .IsUnique() + .HasDatabaseName("ix_subscription_category_code"); + + b.ToTable("subscription_category", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionTrail", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("Code") + .HasColumnType("integer") + .HasColumnName("code"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("StateAfter") + .HasColumnType("text") + .HasColumnName("state_after"); + + b.Property("StateBefore") + .HasColumnType("text") + .HasColumnName("state_before"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.HasKey("Id") + .HasName("pk_subscription_trail"); + + b.HasIndex("CreatedOn") + .HasDatabaseName("ix_subscription_trail_created_on"); + + b.HasIndex("SubscriptionId") + .HasDatabaseName("ix_subscription_trail_subscription_id"); + + b.ToTable("subscription_trail", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.WorkGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BusMessageName") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("character varying(100)") + .HasColumnName("bus_message_name"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("Options") + .HasColumnType("jsonb") + .HasColumnName("options"); + + b.HasKey("Id") + .HasName("pk_work_group"); + + b.ToTable("work_group", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("CorrelationId") + .HasColumnType("text") + .HasColumnName("correlation_id"); + + b.Property("DocumentId") + .HasColumnType("integer") + .HasColumnName("document_id"); + + b.Property("HandlerId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("handler_id"); + + b.Property>("HandlerProperties") + .HasColumnType("jsonb") + .HasColumnName("handler_properties"); + + b.Property("InputContentType") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("input_content_type"); + + b.Property("InputHash") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("input_hash"); + + b.Property("InputName") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("input_name"); + + b.Property("InputSize") + .HasColumnType("integer") + .HasColumnName("input_size"); + + b.Property("MapperId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("mapper_id"); + + b.Property>("MapperProperties") + .HasColumnType("jsonb") + .HasColumnName("mapper_properties"); + + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.Property("References") + .HasColumnType("text[]") + .HasColumnName("references"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("response_message_type_name"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("integer") + .HasColumnName("response_subscription_id"); + + b.Property("RetryFor") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("retry_for"); + + b.Property("StartedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("started_on"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.HasKey("Id") + .HasName("pk_xchange"); + + b.HasIndex("DocumentId") + .HasDatabaseName("ix_xchange_document_id"); + + b.HasIndex("InputHash") + .HasDatabaseName("ix_xchange_input_hash"); + + b.HasIndex("RetryFor") + .HasDatabaseName("ix_xchange_retry_for"); + + b.HasIndex("StartedOn") + .HasDatabaseName("ix_xchange_started_on"); + + b.HasIndex("SubscriptionId") + .HasDatabaseName("ix_xchange_subscription_id"); + + b.ToTable("xchange", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("AggregatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("aggregated_on"); + + b.Property("AggregationXchangeId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("aggregation_xchange_id"); + + b.HasKey("Id") + .HasName("pk_xchange_aggregation"); + + b.HasIndex("AggregationXchangeId") + .HasDatabaseName("ix_xchange_aggregation_aggregation_xchange_id"); + + b.ToTable("xchange_aggregation", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("DeliveredOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("delivered_on"); + + b.HasKey("Id") + .HasName("pk_xchange_delivery"); + + b.HasIndex("DeliveredOn") + .HasDatabaseName("ix_xchange_delivery_delivered_on"); + + b.ToTable("xchange_delivery", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeNotification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Exception") + .HasColumnType("text") + .HasColumnName("exception"); + + b.Property("FinishedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("finished_on"); + + b.Property("NotifierId") + .HasColumnType("integer") + .HasColumnName("notifier_id"); + + b.Property("NotifierName") + .HasColumnType("text") + .HasColumnName("notifier_name"); + + b.Property("Success") + .HasColumnType("boolean") + .HasColumnName("success"); + + b.Property("XchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("character varying(50)") + .HasColumnName("xchange_id"); + + b.HasKey("Id") + .HasName("pk_xchange_notification"); + + b.ToTable("xchange_notification", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("Hits") + .HasColumnType("integer[]") + .HasColumnName("hits"); + + b.Property>("Properties") + .HasColumnType("jsonb") + .HasColumnName("properties"); + + b.Property("PropertiesRaw") + .HasColumnType("text") + .HasColumnName("properties_raw"); + + b.HasKey("Id") + .HasName("pk_xchange_promoted_properties"); + + b.HasIndex("PropertiesRaw") + .HasDatabaseName("ix_xchange_promoted_properties_properties_raw"); + + b.ToTable("xchange_promoted_properties", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("Exception") + .HasColumnType("text") + .HasColumnName("exception"); + + b.Property("FinishedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("finished_on"); + + b.Property("OutputBad") + .HasColumnType("boolean") + .HasColumnName("output_bad"); + + b.Property("OutputContentType") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("output_content_type"); + + b.Property("OutputHash") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("output_hash"); + + b.Property("OutputName") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("output_name"); + + b.Property("OutputSize") + .HasColumnType("integer") + .HasColumnName("output_size"); + + b.Property("ResponseBad") + .HasColumnType("boolean") + .HasColumnName("response_bad"); + + b.Property("ResponseContentType") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("response_content_type"); + + b.Property("ResponseHash") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("response_hash"); + + b.Property("ResponseName") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("response_name"); + + b.Property("ResponseSize") + .HasColumnType("integer") + .HasColumnName("response_size"); + + b.Property("ResponseXchangeId") + .HasColumnType("text") + .HasColumnName("response_xchange_id"); + + b.Property("RetryBlockedReason") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("retry_blocked_reason"); + + b.Property("Success") + .HasColumnType("boolean") + .HasColumnName("success"); + + b.HasKey("Id") + .HasName("pk_xchange_result"); + + b.ToTable("xchange_result", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.RunFlagUpdater+RunningResult", b => + { + b.Property("IsRunning") + .HasColumnType("boolean") + .HasColumnName("is_running"); + + b.ToTable("running_result", "infolink"); + + b.ToView(null, (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.JobExecution", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Context") + .HasColumnType("text") + .HasColumnName("context"); + + b.Property("DurationMs") + .HasColumnType("bigint") + .HasColumnName("duration_ms"); + + b.Property("EndTimeUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("end_time_utc"); + + b.Property("Error") + .HasColumnType("text") + .HasColumnName("error"); + + b.Property("FireInstanceId") + .IsRequired() + .HasColumnType("text") + .HasColumnName("fire_instance_id"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property("JobTypeName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_type_name"); + + b.Property("Node") + .IsRequired() + .HasColumnType("text") + .HasColumnName("node"); + + b.Property("StartTimeUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("start_time_utc"); + + b.Property("Success") + .HasColumnType("boolean") + .HasColumnName("success"); + + b.HasKey("Id") + .HasName("pk_job_executions"); + + b.HasIndex("FireInstanceId") + .IsUnique() + .HasDatabaseName("idx_je_fire_instance_id"); + + b.HasIndex("StartTimeUtc") + .HasDatabaseName("idx_je_start_time"); + + b.HasIndex("Success") + .HasDatabaseName("idx_je_success"); + + b.HasIndex("JobGroup", "JobName", "StartTimeUtc") + .HasDatabaseName("idx_je_group_name_start"); + + b.ToTable("job_executions", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("BlobData") + .HasColumnType("bytea") + .HasColumnName("blob_data"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup") + .HasName("pk_qrtz_blob_triggers"); + + b.ToTable("qrtz_blob_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCalendar", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("CalendarName") + .HasColumnType("text") + .HasColumnName("calendar_name"); + + b.Property("Calendar") + .IsRequired() + .HasColumnType("bytea") + .HasColumnName("calendar"); + + b.HasKey("SchedulerName", "CalendarName") + .HasName("pk_qrtz_calendars"); + + b.ToTable("qrtz_calendars", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("CronExpression") + .IsRequired() + .HasColumnType("text") + .HasColumnName("cron_expression"); + + b.Property("TimeZoneId") + .HasColumnType("text") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup") + .HasName("pk_qrtz_cron_triggers"); + + b.ToTable("qrtz_cron_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzFiredTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("EntryId") + .HasColumnType("text") + .HasColumnName("entry_id"); + + b.Property("FiredTime") + .HasColumnType("bigint") + .HasColumnName("fired_time"); + + b.Property("InstanceName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("instance_name"); + + b.Property("IsNonConcurrent") + .HasColumnType("bool") + .HasColumnName("is_nonconcurrent"); + + b.Property("JobGroup") + .HasColumnType("text") + .HasColumnName("job_group"); + + b.Property("JobName") + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property("Priority") + .HasColumnType("integer") + .HasColumnName("priority"); + + b.Property("RequestsRecovery") + .HasColumnType("bool") + .HasColumnName("requests_recovery"); + + b.Property("ScheduledTime") + .HasColumnType("bigint") + .HasColumnName("sched_time"); + + b.Property("State") + .IsRequired() + .HasColumnType("text") + .HasColumnName("state"); + + b.Property("TriggerGroup") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("TriggerName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.HasKey("SchedulerName", "EntryId") + .HasName("pk_qrtz_fired_triggers"); + + b.HasIndex("InstanceName") + .HasDatabaseName("idx_qrtz_ft_trig_inst_name"); + + b.HasIndex("JobGroup") + .HasDatabaseName("idx_qrtz_ft_job_group"); + + b.HasIndex("JobName") + .HasDatabaseName("idx_qrtz_ft_job_name"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_qrtz_ft_job_req_recovery"); + + b.HasIndex("TriggerGroup") + .HasDatabaseName("idx_qrtz_ft_trig_group"); + + b.HasIndex("TriggerName") + .HasDatabaseName("idx_qrtz_ft_trig_name"); + + b.HasIndex("SchedulerName", "TriggerName", "TriggerGroup") + .HasDatabaseName("idx_qrtz_ft_trig_nm_gp"); + + b.ToTable("qrtz_fired_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("JobName") + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property("JobGroup") + .HasColumnType("text") + .HasColumnName("job_group"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("IsDurable") + .HasColumnType("bool") + .HasColumnName("is_durable"); + + b.Property("IsNonConcurrent") + .HasColumnType("bool") + .HasColumnName("is_nonconcurrent"); + + b.Property("IsUpdateData") + .HasColumnType("bool") + .HasColumnName("is_update_data"); + + b.Property("JobClassName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_class_name"); + + b.Property("JobData") + .HasColumnType("bytea") + .HasColumnName("job_data"); + + b.Property("RequestsRecovery") + .HasColumnType("bool") + .HasColumnName("requests_recovery"); + + b.HasKey("SchedulerName", "JobName", "JobGroup") + .HasName("pk_qrtz_job_details"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_j_req_recovery"); + + b.ToTable("qrtz_job_details", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzLock", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("LockName") + .HasColumnType("text") + .HasColumnName("lock_name"); + + b.HasKey("SchedulerName", "LockName") + .HasName("pk_qrtz_locks"); + + b.ToTable("qrtz_locks", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzPausedTriggerGroup", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.HasKey("SchedulerName", "TriggerGroup") + .HasName("pk_qrtz_paused_trigger_grps"); + + b.ToTable("qrtz_paused_trigger_grps", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSchedulerState", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("InstanceName") + .HasColumnType("text") + .HasColumnName("instance_name"); + + b.Property("CheckInInterval") + .HasColumnType("bigint") + .HasColumnName("checkin_interval"); + + b.Property("LastCheckInTime") + .HasColumnType("bigint") + .HasColumnName("last_checkin_time"); + + b.HasKey("SchedulerName", "InstanceName") + .HasName("pk_qrtz_scheduler_state"); + + b.ToTable("qrtz_scheduler_state", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("BooleanProperty1") + .HasColumnType("bool") + .HasColumnName("bool_prop_1"); + + b.Property("BooleanProperty2") + .HasColumnType("bool") + .HasColumnName("bool_prop_2"); + + b.Property("DecimalProperty1") + .HasColumnType("numeric") + .HasColumnName("dec_prop_1"); + + b.Property("DecimalProperty2") + .HasColumnType("numeric") + .HasColumnName("dec_prop_2"); + + b.Property("IntegerProperty1") + .HasColumnType("integer") + .HasColumnName("int_prop_1"); + + b.Property("IntegerProperty2") + .HasColumnType("integer") + .HasColumnName("int_prop_2"); + + b.Property("LongProperty1") + .HasColumnType("bigint") + .HasColumnName("long_prop_1"); + + b.Property("LongProperty2") + .HasColumnType("bigint") + .HasColumnName("long_prop_2"); + + b.Property("StringProperty1") + .HasColumnType("text") + .HasColumnName("str_prop_1"); + + b.Property("StringProperty2") + .HasColumnType("text") + .HasColumnName("str_prop_2"); + + b.Property("StringProperty3") + .HasColumnType("text") + .HasColumnName("str_prop_3"); + + b.Property("TimeZoneId") + .HasColumnType("text") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup") + .HasName("pk_qrtz_simprop_triggers"); + + b.ToTable("qrtz_simprop_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("RepeatCount") + .HasColumnType("bigint") + .HasColumnName("repeat_count"); + + b.Property("RepeatInterval") + .HasColumnType("bigint") + .HasColumnName("repeat_interval"); + + b.Property("TimesTriggered") + .HasColumnType("bigint") + .HasColumnName("times_triggered"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup") + .HasName("pk_qrtz_simple_triggers"); + + b.ToTable("qrtz_simple_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("CalendarName") + .HasColumnType("text") + .HasColumnName("calendar_name"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("EndTime") + .HasColumnType("bigint") + .HasColumnName("end_time"); + + b.Property("JobData") + .HasColumnType("bytea") + .HasColumnName("job_data"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property("MisfireInstruction") + .HasColumnType("integer") + .HasColumnName("misfire_instr"); + + b.Property("NextFireTime") + .HasColumnType("bigint") + .HasColumnName("next_fire_time"); + + b.Property("PreviousFireTime") + .HasColumnType("bigint") + .HasColumnName("prev_fire_time"); + + b.Property("Priority") + .HasColumnType("integer") + .HasColumnName("priority"); + + b.Property("StartTime") + .HasColumnType("bigint") + .HasColumnName("start_time"); + + b.Property("TriggerState") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_state"); + + b.Property("TriggerType") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_type"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup") + .HasName("pk_qrtz_triggers"); + + b.HasIndex("NextFireTime") + .HasDatabaseName("idx_t_next_fire_time"); + + b.HasIndex("TriggerState") + .HasDatabaseName("idx_t_state"); + + b.HasIndex("NextFireTime", "TriggerState") + .HasDatabaseName("idx_t_nft_st"); + + b.HasIndex("SchedulerName", "JobName", "JobGroup") + .HasDatabaseName("ix_qrtz_triggers_sched_name_job_name_job_group"); + + b.ToTable("qrtz_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_refresh_tokens_accounts_account_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DocumentTrail", b => + { + b.HasOne("SW.Bitween.Domain.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_document_trail_document_document_id"); + + b.Navigation("Document"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.ApiGateway", "ApiGateway") + .WithMany("Partners") + .HasForeignKey("ApiGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_api_gateway_partner_api_gateway_api_gateway_id"); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_api_gateway_partner_partner_partner_id"); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_api_gateway_partner_subscription_subscription_id"); + + b.Navigation("ApiGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_bus_gateway_document_document_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.BusGateway", "BusGateway") + .WithMany("Routes") + .HasForeignKey("BusGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_bus_gateway_route_bus_gateway_bus_gateway_id"); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_bus_gateway_route_partner_partner_id"); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_bus_gateway_route_subscription_subscription_id"); + + b.Navigation("BusGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.OwnsMany("SW.Bitween.Domain.ApiCredential", "ApiCredentials", b1 => + { + b1.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("Key") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("key"); + + b1.Property("Name") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("name"); + + b1.HasKey("PartnerId", "Id") + .HasName("pk_partner_api_credential"); + + b1.HasIndex("Key") + .IsUnique() + .HasDatabaseName("ix_partner_api_credential_key"); + + b1.ToTable("partner_api_credential", "infolink"); + + b1.WithOwner() + .HasForeignKey("PartnerId") + .HasConstraintName("fk_partner_api_credential_partner_partner_id"); + + b1.HasData( + new + { + PartnerId = 1, + Id = 1, + Key = "7facc758283844b49cc4ffd26a75b1de", + Name = "default" + }); + }); + + b.Navigation("ApiCredentials"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.HasOne("SW.Bitween.Domain.Subscription", null) + .WithMany() + .HasForeignKey("AggregationForId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_subscription_aggregation_for"); + + b.HasOne("SW.Bitween.Domain.SubscriptionCategory", "Category") + .WithMany() + .HasForeignKey("CategoryId") + .HasConstraintName("fk_subscription_subscription_category_category_id"); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_subscription_document_document_id"); + + b.HasOne("SW.Bitween.Domain.Partner", null) + .WithMany("Subscriptions") + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_subscription_partner_partner_id"); + + b.HasOne("SW.Bitween.Domain.Subscription", null) + .WithMany() + .HasForeignKey("ResponseSubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_subscription_response_subscriber"); + + b.HasOne("SW.Bitween.Domain.RetryPolicy", "RetryPolicy") + .WithMany() + .HasForeignKey("RetryPolicyId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_subscription_retry_policy_retry_policy_id"); + + b.HasOne("SW.Bitween.Domain.WorkGroup", "WorkGroup") + .WithMany() + .HasForeignKey("WorkGroupId") + .HasConstraintName("fk_subscription_work_group_work_group_id"); + + b.OwnsMany("SW.Bitween.Domain.Schedule", "Schedules", b1 => + { + b1.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("Backwards") + .HasColumnType("boolean") + .HasColumnName("backwards"); + + b1.Property("On") + .HasColumnType("bigint") + .HasColumnName("on"); + + b1.Property("Recurrence") + .HasColumnType("smallint") + .HasColumnName("recurrence"); + + b1.HasKey("SubscriptionId", "Id") + .HasName("pk_subscription_schedule"); + + b1.ToTable("subscription_schedule", "infolink"); + + b1.WithOwner() + .HasForeignKey("SubscriptionId") + .HasConstraintName("fk_subscription_schedule_subscription_subscription_id"); + }); + + b.Navigation("Category"); + + b.Navigation("RetryPolicy"); + + b.Navigation("Schedules"); + + b.Navigation("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionTrail", b => + { + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_subscription_trail_subscription_subscription_id"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_xchange_document_document_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeAggregation", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_xchange_aggregation_xchange_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeDelivery", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_xchange_delivery_xchange_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangePromotedProperties", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_xchange_promoted_properties_xchange_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeResult", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_xchange_result_xchange_id"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("BlobTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_qrtz_blob_triggers_qrtz_triggers_sched_name_trigger_name_tr"); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("CronTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_qrtz_cron_triggers_qrtz_triggers_sched_name_trigger_name_tr"); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimplePropertyTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_qrtz_simprop_triggers_qrtz_triggers_sched_name_trigger_name"); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimpleTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_qrtz_simple_triggers_qrtz_triggers_sched_name_trigger_name_"); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzJobDetail", "JobDetail") + .WithMany("Triggers") + .HasForeignKey("SchedulerName", "JobName", "JobGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_qrtz_triggers_qrtz_job_details_sched_name_job_name_job_group"); + + b.Navigation("JobDetail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Navigation("Partners"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Navigation("Routes"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Navigation("Subscriptions"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Navigation("Triggers"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Navigation("BlobTriggers"); + + b.Navigation("CronTriggers"); + + b.Navigation("SimplePropertyTriggers"); + + b.Navigation("SimpleTriggers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/SW.Bitween.PgSql/Migrations/20260811092318_RetryBlockedReason.cs b/SW.Bitween.PgSql/Migrations/20260811092318_RetryBlockedReason.cs new file mode 100644 index 00000000..e33c8ce0 --- /dev/null +++ b/SW.Bitween.PgSql/Migrations/20260811092318_RetryBlockedReason.cs @@ -0,0 +1,31 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace SW.Bitween.PgSql.Migrations +{ + /// + public partial class RetryBlockedReason : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "retry_blocked_reason", + schema: "infolink", + table: "xchange_result", + type: "character varying(500)", + maxLength: 500, + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "retry_blocked_reason", + schema: "infolink", + table: "xchange_result"); + } + } +} diff --git a/SW.Bitween.PgSql/Migrations/20260812092613_AddAccountLockout.Designer.cs b/SW.Bitween.PgSql/Migrations/20260812092613_AddAccountLockout.Designer.cs new file mode 100644 index 00000000..cf14dea6 --- /dev/null +++ b/SW.Bitween.PgSql/Migrations/20260812092613_AddAccountLockout.Designer.cs @@ -0,0 +1,2168 @@ +// +using System; +using System.Collections.Generic; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using SW.Bitween.Model; +using SW.Bitween.PgSql; + +#nullable disable + +namespace SW.Bitween.PgSql.Migrations +{ + [DbContext(typeof(BitweenDbContext))] + [Migration("20260812092613_AddAccountLockout")] + partial class AddAccountLockout + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("infolink") + .HasAnnotation("ProductVersion", "8.0.23") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Deleted") + .HasColumnType("boolean") + .HasColumnName("deleted"); + + b.Property("Disabled") + .HasColumnType("boolean") + .HasColumnName("disabled"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("display_name"); + + b.Property("Email") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("character varying(200)") + .HasColumnName("email"); + + b.Property("EmailProvider") + .HasColumnType("smallint") + .HasColumnName("email_provider"); + + b.Property("FailedLoginCount") + .HasColumnType("integer") + .HasColumnName("failed_login_count"); + + b.Property("LockoutEnd") + .HasColumnType("timestamp with time zone") + .HasColumnName("lockout_end"); + + b.Property("LoginMethods") + .HasColumnType("smallint") + .HasColumnName("login_methods"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Password") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("character varying(500)") + .HasColumnName("password"); + + b.Property("Phone") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("character varying(20)") + .HasColumnName("phone"); + + b.Property("Role") + .HasColumnType("integer") + .HasColumnName("role"); + + b.HasKey("Id") + .HasName("pk_accounts"); + + b.HasIndex("Email") + .IsUnique() + .HasDatabaseName("ix_accounts_email"); + + b.ToTable("Accounts", "infolink"); + + b.HasData( + new + { + Id = 9999, + CreatedOn = new DateTime(2021, 12, 31, 22, 0, 0, 0, DateTimeKind.Utc), + Deleted = false, + Disabled = false, + DisplayName = "Admin", + Email = "admin@Bitween.systems", + EmailProvider = (byte)0, + FailedLoginCount = 0, + LoginMethods = (byte)2, + Password = "$SWHASH$V1$10000$VQCi48eitH4Ml5juvBMOFZrMdQwBbhuIQVXe6RR7qJdDF2bJ", + Role = 0 + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("AccountId") + .HasColumnType("integer") + .HasColumnName("account_id"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("LoginMethod") + .HasColumnType("smallint") + .HasColumnName("login_method"); + + b.HasKey("Id") + .HasName("pk_refresh_tokens"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_refresh_tokens_account_id"); + + b.ToTable("RefreshTokens", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DelayedRetry", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property>("GroupAttemptCounts") + .HasColumnType("jsonb") + .HasColumnName("group_attempt_counts"); + + b.Property("On") + .HasColumnType("timestamp with time zone") + .HasColumnName("on"); + + b.HasKey("Id") + .HasName("pk_delayed_retry"); + + b.HasIndex("On") + .HasDatabaseName("ix_delayed_retry_on"); + + b.ToTable("delayed_retry", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Document", b => + { + b.Property("Id") + .HasColumnType("integer") + .HasColumnName("id"); + + b.Property("BusEnabled") + .HasColumnType("boolean") + .HasColumnName("bus_enabled"); + + b.Property("BusMessageTypeName") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("bus_message_type_name"); + + b.Property("DisregardsUnfilteredMessages") + .HasColumnType("boolean") + .HasColumnName("disregards_unfiltered_messages"); + + b.Property("DocumentFormat") + .HasColumnType("integer") + .HasColumnName("document_format"); + + b.Property("DuplicateInterval") + .HasColumnType("integer") + .HasColumnName("duplicate_interval"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("PromotedProperties") + .HasColumnType("jsonb") + .HasColumnName("promoted_properties"); + + b.HasKey("Id") + .HasName("pk_document"); + + b.HasIndex("BusMessageTypeName") + .IsUnique() + .HasDatabaseName("ix_document_bus_message_type_name"); + + b.HasIndex("Name") + .IsUnique() + .HasDatabaseName("ix_document_name"); + + b.ToTable("document", "infolink"); + + b.HasData( + new + { + Id = 10001, + BusEnabled = false, + DocumentFormat = 0, + DuplicateInterval = 0, + Name = "Aggregation Document", + PromotedProperties = "{}" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DocumentTrail", b => + { + b.Property("Id") + .HasColumnType("text") + .HasColumnName("id"); + + b.Property("Code") + .HasColumnType("integer") + .HasColumnName("code"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("DocumentId") + .HasColumnType("integer") + .HasColumnName("document_id"); + + b.Property("StateAfter") + .HasColumnType("text") + .HasColumnName("state_after"); + + b.Property("StateBefore") + .HasColumnType("text") + .HasColumnName("state_before"); + + b.HasKey("Id") + .HasName("pk_document_trail"); + + b.HasIndex("CreatedOn") + .HasDatabaseName("ix_document_trail_created_on"); + + b.HasIndex("DocumentId") + .HasDatabaseName("ix_document_trail_document_id"); + + b.ToTable("document_trail", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.Property("UrlName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("url_name"); + + b.HasKey("Id") + .HasName("pk_api_gateway"); + + b.HasIndex("UrlName") + .IsUnique() + .HasDatabaseName("ix_api_gateway_url_name"); + + b.ToTable("api_gateway", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.Property("ApiGatewayId") + .HasColumnType("integer") + .HasColumnName("api_gateway_id"); + + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.HasKey("ApiGatewayId", "PartnerId", "SubscriptionId") + .HasName("pk_api_gateway_partner"); + + b.HasIndex("PartnerId") + .HasDatabaseName("ix_api_gateway_partner_partner_id"); + + b.HasIndex("SubscriptionId") + .HasDatabaseName("ix_api_gateway_partner_subscription_id"); + + b.ToTable("api_gateway_partner", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("DocumentId") + .HasColumnType("integer") + .HasColumnName("document_id"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("pk_bus_gateway"); + + b.HasIndex("DocumentId") + .HasDatabaseName("ix_bus_gateway_document_id"); + + b.ToTable("bus_gateway", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BusGatewayId") + .HasColumnType("integer") + .HasColumnName("bus_gateway_id"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("MatchExpression") + .HasColumnType("text") + .HasColumnName("match_expression"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.HasKey("Id") + .HasName("pk_bus_gateway_route"); + + b.HasIndex("BusGatewayId") + .HasDatabaseName("ix_bus_gateway_route_bus_gateway_id"); + + b.HasIndex("PartnerId") + .HasDatabaseName("ix_bus_gateway_route_partner_id"); + + b.HasIndex("SubscriptionId") + .HasDatabaseName("ix_bus_gateway_route_subscription_id"); + + b.ToTable("bus_gateway_route", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.GlobalAdapterValuesSet", b => + { + b.Property("Id") + .HasColumnType("text") + .HasColumnName("id"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property>("Values") + .HasColumnType("jsonb") + .HasColumnName("values"); + + b.HasKey("Id") + .HasName("pk_global_adapter_values_set"); + + b.ToTable("global_adapter_values_set", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Notifier", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("character varying(200)") + .HasColumnName("handler_id"); + + b.Property("HandlerProperties") + .HasColumnType("text") + .HasColumnName("handler_properties"); + + b.Property("Inactive") + .HasColumnType("boolean") + .HasColumnName("inactive"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("RunOnBadResult") + .HasColumnType("boolean") + .HasColumnName("run_on_bad_result"); + + b.Property("RunOnFailedResult") + .HasColumnType("boolean") + .HasColumnName("run_on_failed_result"); + + b.Property("RunOnSubscriptions") + .HasColumnType("integer[]") + .HasColumnName("run_on_subscriptions"); + + b.Property("RunOnSuccessfulResult") + .HasColumnType("boolean") + .HasColumnName("run_on_successful_result"); + + b.HasKey("Id") + .HasName("pk_notifier"); + + b.ToTable("notifier", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OnHoldXchange", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BadData") + .HasColumnType("boolean") + .HasColumnName("bad_data"); + + b.Property("Data") + .HasColumnType("text") + .HasColumnName("data"); + + b.Property("FileName") + .HasColumnType("text") + .HasColumnName("file_name"); + + b.Property("References") + .HasColumnType("text[]") + .HasColumnName("references"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.HasKey("Id") + .HasName("pk_on_hold_xchange"); + + b.HasIndex("SubscriptionId") + .HasDatabaseName("ix_on_hold_xchange_subscription_id"); + + b.ToTable("on_hold_xchange", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property>("AdapterProperties") + .HasColumnType("jsonb") + .HasColumnName("adapter_properties"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("pk_partner"); + + b.ToTable("partner", "infolink"); + + b.HasData( + new + { + Id = 1, + Name = "SYSTEM" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryPolicy", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Groups") + .HasColumnType("text") + .HasColumnName("groups"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("pk_retry_policy"); + + b.ToTable("retry_policy", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AggregateOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("aggregate_on"); + + b.Property("AggregationForId") + .HasColumnType("integer") + .HasColumnName("aggregation_for_id"); + + b.Property("AggregationTarget") + .HasColumnType("smallint") + .HasColumnName("aggregation_target"); + + b.Property("CategoryId") + .HasColumnType("integer") + .HasColumnName("category_id"); + + b.Property("ConsecutiveFailures") + .HasColumnType("integer") + .HasColumnName("consecutive_failures"); + + b.Property("CustomRetryPolicy") + .HasColumnType("text") + .HasColumnName("custom_retry_policy"); + + b.Property>("DocumentFilter") + .HasColumnType("jsonb") + .HasColumnName("document_filter"); + + b.Property("DocumentId") + .HasColumnType("integer") + .HasColumnName("document_id"); + + b.Property("HandlerId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("handler_id"); + + b.Property>("HandlerProperties") + .HasColumnType("jsonb") + .HasColumnName("handler_properties"); + + b.Property("Inactive") + .HasColumnType("boolean") + .HasColumnName("inactive"); + + b.Property("IsRunning") + .HasColumnType("boolean") + .HasColumnName("is_running"); + + b.Property("LastException") + .HasColumnType("text") + .HasColumnName("last_exception"); + + b.Property("MapperId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("mapper_id"); + + b.Property>("MapperProperties") + .HasColumnType("jsonb") + .HasColumnName("mapper_properties"); + + b.Property("MatchExpression") + .HasColumnType("text") + .HasColumnName("match_expression"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.Property("PausedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("paused_on"); + + b.Property("ReceiveOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("receive_on"); + + b.Property("ReceiverId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("receiver_id"); + + b.Property>("ReceiverProperties") + .HasColumnType("jsonb") + .HasColumnName("receiver_properties"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("response_message_type_name"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("integer") + .HasColumnName("response_subscription_id"); + + b.Property("RetryPolicyId") + .HasColumnType("integer") + .HasColumnName("retry_policy_id"); + + b.Property("Temporary") + .HasColumnType("boolean") + .HasColumnName("temporary"); + + b.Property("Type") + .HasColumnType("smallint") + .HasColumnName("type"); + + b.Property("ValidatorId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("validator_id"); + + b.Property>("ValidatorProperties") + .HasColumnType("jsonb") + .HasColumnName("validator_properties"); + + b.Property("WorkGroupId") + .HasColumnType("integer") + .HasColumnName("work_group_id"); + + b.HasKey("Id") + .HasName("pk_subscription"); + + b.HasIndex("AggregationForId") + .HasDatabaseName("ix_subscription_aggregation_for_id"); + + b.HasIndex("CategoryId") + .HasDatabaseName("ix_subscription_category_id"); + + b.HasIndex("DocumentId") + .HasDatabaseName("ix_subscription_document_id"); + + b.HasIndex("PartnerId") + .HasDatabaseName("ix_subscription_partner_id"); + + b.HasIndex("ResponseSubscriptionId") + .HasDatabaseName("ix_subscription_response_subscription_id"); + + b.HasIndex("RetryPolicyId") + .HasDatabaseName("ix_subscription_retry_policy_id"); + + b.HasIndex("WorkGroupId") + .HasDatabaseName("ix_subscription_work_group_id"); + + b.ToTable("subscription", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Code") + .HasColumnType("text") + .HasColumnName("code"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.HasKey("Id") + .HasName("pk_subscription_category"); + + b.HasIndex("Code") + .IsUnique() + .HasDatabaseName("ix_subscription_category_code"); + + b.ToTable("subscription_category", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionTrail", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("Code") + .HasColumnType("integer") + .HasColumnName("code"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("StateAfter") + .HasColumnType("text") + .HasColumnName("state_after"); + + b.Property("StateBefore") + .HasColumnType("text") + .HasColumnName("state_before"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.HasKey("Id") + .HasName("pk_subscription_trail"); + + b.HasIndex("CreatedOn") + .HasDatabaseName("ix_subscription_trail_created_on"); + + b.HasIndex("SubscriptionId") + .HasDatabaseName("ix_subscription_trail_subscription_id"); + + b.ToTable("subscription_trail", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.WorkGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BusMessageName") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("character varying(100)") + .HasColumnName("bus_message_name"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("Options") + .HasColumnType("jsonb") + .HasColumnName("options"); + + b.HasKey("Id") + .HasName("pk_work_group"); + + b.ToTable("work_group", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("CorrelationId") + .HasColumnType("text") + .HasColumnName("correlation_id"); + + b.Property("DocumentId") + .HasColumnType("integer") + .HasColumnName("document_id"); + + b.Property>("GroupAttemptCounts") + .HasColumnType("jsonb") + .HasColumnName("group_attempt_counts"); + + b.Property("HandlerId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("handler_id"); + + b.Property>("HandlerProperties") + .HasColumnType("jsonb") + .HasColumnName("handler_properties"); + + b.Property("InputContentType") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("input_content_type"); + + b.Property("InputHash") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("input_hash"); + + b.Property("InputName") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("input_name"); + + b.Property("InputSize") + .HasColumnType("integer") + .HasColumnName("input_size"); + + b.Property("MapperId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("mapper_id"); + + b.Property>("MapperProperties") + .HasColumnType("jsonb") + .HasColumnName("mapper_properties"); + + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.Property("References") + .HasColumnType("text[]") + .HasColumnName("references"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("response_message_type_name"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("integer") + .HasColumnName("response_subscription_id"); + + b.Property("RetryFor") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("retry_for"); + + b.Property("StartedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("started_on"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.HasKey("Id") + .HasName("pk_xchange"); + + b.HasIndex("DocumentId") + .HasDatabaseName("ix_xchange_document_id"); + + b.HasIndex("InputHash") + .HasDatabaseName("ix_xchange_input_hash"); + + b.HasIndex("RetryFor") + .HasDatabaseName("ix_xchange_retry_for"); + + b.HasIndex("StartedOn") + .HasDatabaseName("ix_xchange_started_on"); + + b.HasIndex("SubscriptionId") + .HasDatabaseName("ix_xchange_subscription_id"); + + b.ToTable("xchange", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("AggregatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("aggregated_on"); + + b.Property("AggregationXchangeId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("aggregation_xchange_id"); + + b.HasKey("Id") + .HasName("pk_xchange_aggregation"); + + b.HasIndex("AggregationXchangeId") + .HasDatabaseName("ix_xchange_aggregation_aggregation_xchange_id"); + + b.ToTable("xchange_aggregation", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("DeliveredOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("delivered_on"); + + b.HasKey("Id") + .HasName("pk_xchange_delivery"); + + b.HasIndex("DeliveredOn") + .HasDatabaseName("ix_xchange_delivery_delivered_on"); + + b.ToTable("xchange_delivery", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeNotification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Exception") + .HasColumnType("text") + .HasColumnName("exception"); + + b.Property("FinishedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("finished_on"); + + b.Property("NotifierId") + .HasColumnType("integer") + .HasColumnName("notifier_id"); + + b.Property("NotifierName") + .HasColumnType("text") + .HasColumnName("notifier_name"); + + b.Property("Success") + .HasColumnType("boolean") + .HasColumnName("success"); + + b.Property("XchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("character varying(50)") + .HasColumnName("xchange_id"); + + b.HasKey("Id") + .HasName("pk_xchange_notification"); + + b.ToTable("xchange_notification", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("Hits") + .HasColumnType("integer[]") + .HasColumnName("hits"); + + b.Property>("Properties") + .HasColumnType("jsonb") + .HasColumnName("properties"); + + b.Property("PropertiesRaw") + .HasColumnType("text") + .HasColumnName("properties_raw"); + + b.HasKey("Id") + .HasName("pk_xchange_promoted_properties"); + + b.HasIndex("PropertiesRaw") + .HasDatabaseName("ix_xchange_promoted_properties_properties_raw"); + + b.ToTable("xchange_promoted_properties", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("Exception") + .HasColumnType("text") + .HasColumnName("exception"); + + b.Property("FinishedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("finished_on"); + + b.Property("OutputBad") + .HasColumnType("boolean") + .HasColumnName("output_bad"); + + b.Property("OutputContentType") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("output_content_type"); + + b.Property("OutputHash") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("output_hash"); + + b.Property("OutputName") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("output_name"); + + b.Property("OutputSize") + .HasColumnType("integer") + .HasColumnName("output_size"); + + b.Property("ResponseBad") + .HasColumnType("boolean") + .HasColumnName("response_bad"); + + b.Property("ResponseContentType") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("response_content_type"); + + b.Property("ResponseHash") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("response_hash"); + + b.Property("ResponseName") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("response_name"); + + b.Property("ResponseSize") + .HasColumnType("integer") + .HasColumnName("response_size"); + + b.Property("ResponseXchangeId") + .HasColumnType("text") + .HasColumnName("response_xchange_id"); + + b.Property("Success") + .HasColumnType("boolean") + .HasColumnName("success"); + + b.HasKey("Id") + .HasName("pk_xchange_result"); + + b.ToTable("xchange_result", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.RunFlagUpdater+RunningResult", b => + { + b.Property("IsRunning") + .HasColumnType("boolean") + .HasColumnName("is_running"); + + b.ToTable("running_result", "infolink"); + + b.ToView(null, (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.JobExecution", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Context") + .HasColumnType("text") + .HasColumnName("context"); + + b.Property("DurationMs") + .HasColumnType("bigint") + .HasColumnName("duration_ms"); + + b.Property("EndTimeUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("end_time_utc"); + + b.Property("Error") + .HasColumnType("text") + .HasColumnName("error"); + + b.Property("FireInstanceId") + .IsRequired() + .HasColumnType("text") + .HasColumnName("fire_instance_id"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property("JobTypeName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_type_name"); + + b.Property("Node") + .IsRequired() + .HasColumnType("text") + .HasColumnName("node"); + + b.Property("StartTimeUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("start_time_utc"); + + b.Property("Success") + .HasColumnType("boolean") + .HasColumnName("success"); + + b.HasKey("Id") + .HasName("pk_job_executions"); + + b.HasIndex("FireInstanceId") + .IsUnique() + .HasDatabaseName("idx_je_fire_instance_id"); + + b.HasIndex("StartTimeUtc") + .HasDatabaseName("idx_je_start_time"); + + b.HasIndex("Success") + .HasDatabaseName("idx_je_success"); + + b.HasIndex("JobGroup", "JobName", "StartTimeUtc") + .HasDatabaseName("idx_je_group_name_start"); + + b.ToTable("job_executions", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("BlobData") + .HasColumnType("bytea") + .HasColumnName("blob_data"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup") + .HasName("pk_qrtz_blob_triggers"); + + b.ToTable("qrtz_blob_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCalendar", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("CalendarName") + .HasColumnType("text") + .HasColumnName("calendar_name"); + + b.Property("Calendar") + .IsRequired() + .HasColumnType("bytea") + .HasColumnName("calendar"); + + b.HasKey("SchedulerName", "CalendarName") + .HasName("pk_qrtz_calendars"); + + b.ToTable("qrtz_calendars", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("CronExpression") + .IsRequired() + .HasColumnType("text") + .HasColumnName("cron_expression"); + + b.Property("TimeZoneId") + .HasColumnType("text") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup") + .HasName("pk_qrtz_cron_triggers"); + + b.ToTable("qrtz_cron_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzFiredTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("EntryId") + .HasColumnType("text") + .HasColumnName("entry_id"); + + b.Property("FiredTime") + .HasColumnType("bigint") + .HasColumnName("fired_time"); + + b.Property("InstanceName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("instance_name"); + + b.Property("IsNonConcurrent") + .HasColumnType("bool") + .HasColumnName("is_nonconcurrent"); + + b.Property("JobGroup") + .HasColumnType("text") + .HasColumnName("job_group"); + + b.Property("JobName") + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property("Priority") + .HasColumnType("integer") + .HasColumnName("priority"); + + b.Property("RequestsRecovery") + .HasColumnType("bool") + .HasColumnName("requests_recovery"); + + b.Property("ScheduledTime") + .HasColumnType("bigint") + .HasColumnName("sched_time"); + + b.Property("State") + .IsRequired() + .HasColumnType("text") + .HasColumnName("state"); + + b.Property("TriggerGroup") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("TriggerName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.HasKey("SchedulerName", "EntryId") + .HasName("pk_qrtz_fired_triggers"); + + b.HasIndex("InstanceName") + .HasDatabaseName("idx_qrtz_ft_trig_inst_name"); + + b.HasIndex("JobGroup") + .HasDatabaseName("idx_qrtz_ft_job_group"); + + b.HasIndex("JobName") + .HasDatabaseName("idx_qrtz_ft_job_name"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_qrtz_ft_job_req_recovery"); + + b.HasIndex("TriggerGroup") + .HasDatabaseName("idx_qrtz_ft_trig_group"); + + b.HasIndex("TriggerName") + .HasDatabaseName("idx_qrtz_ft_trig_name"); + + b.HasIndex("SchedulerName", "TriggerName", "TriggerGroup") + .HasDatabaseName("idx_qrtz_ft_trig_nm_gp"); + + b.ToTable("qrtz_fired_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("JobName") + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property("JobGroup") + .HasColumnType("text") + .HasColumnName("job_group"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("IsDurable") + .HasColumnType("bool") + .HasColumnName("is_durable"); + + b.Property("IsNonConcurrent") + .HasColumnType("bool") + .HasColumnName("is_nonconcurrent"); + + b.Property("IsUpdateData") + .HasColumnType("bool") + .HasColumnName("is_update_data"); + + b.Property("JobClassName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_class_name"); + + b.Property("JobData") + .HasColumnType("bytea") + .HasColumnName("job_data"); + + b.Property("RequestsRecovery") + .HasColumnType("bool") + .HasColumnName("requests_recovery"); + + b.HasKey("SchedulerName", "JobName", "JobGroup") + .HasName("pk_qrtz_job_details"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_j_req_recovery"); + + b.ToTable("qrtz_job_details", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzLock", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("LockName") + .HasColumnType("text") + .HasColumnName("lock_name"); + + b.HasKey("SchedulerName", "LockName") + .HasName("pk_qrtz_locks"); + + b.ToTable("qrtz_locks", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzPausedTriggerGroup", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.HasKey("SchedulerName", "TriggerGroup") + .HasName("pk_qrtz_paused_trigger_grps"); + + b.ToTable("qrtz_paused_trigger_grps", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSchedulerState", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("InstanceName") + .HasColumnType("text") + .HasColumnName("instance_name"); + + b.Property("CheckInInterval") + .HasColumnType("bigint") + .HasColumnName("checkin_interval"); + + b.Property("LastCheckInTime") + .HasColumnType("bigint") + .HasColumnName("last_checkin_time"); + + b.HasKey("SchedulerName", "InstanceName") + .HasName("pk_qrtz_scheduler_state"); + + b.ToTable("qrtz_scheduler_state", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("BooleanProperty1") + .HasColumnType("bool") + .HasColumnName("bool_prop_1"); + + b.Property("BooleanProperty2") + .HasColumnType("bool") + .HasColumnName("bool_prop_2"); + + b.Property("DecimalProperty1") + .HasColumnType("numeric") + .HasColumnName("dec_prop_1"); + + b.Property("DecimalProperty2") + .HasColumnType("numeric") + .HasColumnName("dec_prop_2"); + + b.Property("IntegerProperty1") + .HasColumnType("integer") + .HasColumnName("int_prop_1"); + + b.Property("IntegerProperty2") + .HasColumnType("integer") + .HasColumnName("int_prop_2"); + + b.Property("LongProperty1") + .HasColumnType("bigint") + .HasColumnName("long_prop_1"); + + b.Property("LongProperty2") + .HasColumnType("bigint") + .HasColumnName("long_prop_2"); + + b.Property("StringProperty1") + .HasColumnType("text") + .HasColumnName("str_prop_1"); + + b.Property("StringProperty2") + .HasColumnType("text") + .HasColumnName("str_prop_2"); + + b.Property("StringProperty3") + .HasColumnType("text") + .HasColumnName("str_prop_3"); + + b.Property("TimeZoneId") + .HasColumnType("text") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup") + .HasName("pk_qrtz_simprop_triggers"); + + b.ToTable("qrtz_simprop_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("RepeatCount") + .HasColumnType("bigint") + .HasColumnName("repeat_count"); + + b.Property("RepeatInterval") + .HasColumnType("bigint") + .HasColumnName("repeat_interval"); + + b.Property("TimesTriggered") + .HasColumnType("bigint") + .HasColumnName("times_triggered"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup") + .HasName("pk_qrtz_simple_triggers"); + + b.ToTable("qrtz_simple_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("CalendarName") + .HasColumnType("text") + .HasColumnName("calendar_name"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("EndTime") + .HasColumnType("bigint") + .HasColumnName("end_time"); + + b.Property("JobData") + .HasColumnType("bytea") + .HasColumnName("job_data"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property("MisfireInstruction") + .HasColumnType("integer") + .HasColumnName("misfire_instr"); + + b.Property("NextFireTime") + .HasColumnType("bigint") + .HasColumnName("next_fire_time"); + + b.Property("PreviousFireTime") + .HasColumnType("bigint") + .HasColumnName("prev_fire_time"); + + b.Property("Priority") + .HasColumnType("integer") + .HasColumnName("priority"); + + b.Property("StartTime") + .HasColumnType("bigint") + .HasColumnName("start_time"); + + b.Property("TriggerState") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_state"); + + b.Property("TriggerType") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_type"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup") + .HasName("pk_qrtz_triggers"); + + b.HasIndex("NextFireTime") + .HasDatabaseName("idx_t_next_fire_time"); + + b.HasIndex("TriggerState") + .HasDatabaseName("idx_t_state"); + + b.HasIndex("NextFireTime", "TriggerState") + .HasDatabaseName("idx_t_nft_st"); + + b.HasIndex("SchedulerName", "JobName", "JobGroup") + .HasDatabaseName("ix_qrtz_triggers_sched_name_job_name_job_group"); + + b.ToTable("qrtz_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_refresh_tokens_accounts_account_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DocumentTrail", b => + { + b.HasOne("SW.Bitween.Domain.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_document_trail_document_document_id"); + + b.Navigation("Document"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.ApiGateway", "ApiGateway") + .WithMany("Partners") + .HasForeignKey("ApiGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_api_gateway_partner_api_gateway_api_gateway_id"); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_api_gateway_partner_partner_partner_id"); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_api_gateway_partner_subscription_subscription_id"); + + b.Navigation("ApiGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_bus_gateway_document_document_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.BusGateway", "BusGateway") + .WithMany("Routes") + .HasForeignKey("BusGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_bus_gateway_route_bus_gateway_bus_gateway_id"); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_bus_gateway_route_partner_partner_id"); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_bus_gateway_route_subscription_subscription_id"); + + b.Navigation("BusGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.OwnsMany("SW.Bitween.Domain.ApiCredential", "ApiCredentials", b1 => + { + b1.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("Key") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("key"); + + b1.Property("Name") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("name"); + + b1.HasKey("PartnerId", "Id") + .HasName("pk_partner_api_credential"); + + b1.HasIndex("Key") + .IsUnique() + .HasDatabaseName("ix_partner_api_credential_key"); + + b1.ToTable("partner_api_credential", "infolink"); + + b1.WithOwner() + .HasForeignKey("PartnerId") + .HasConstraintName("fk_partner_api_credential_partner_partner_id"); + + b1.HasData( + new + { + PartnerId = 1, + Id = 1, + Key = "7facc758283844b49cc4ffd26a75b1de", + Name = "default" + }); + }); + + b.Navigation("ApiCredentials"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.HasOne("SW.Bitween.Domain.Subscription", null) + .WithMany() + .HasForeignKey("AggregationForId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_subscription_aggregation_for"); + + b.HasOne("SW.Bitween.Domain.SubscriptionCategory", "Category") + .WithMany() + .HasForeignKey("CategoryId") + .HasConstraintName("fk_subscription_subscription_category_category_id"); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_subscription_document_document_id"); + + b.HasOne("SW.Bitween.Domain.Partner", null) + .WithMany("Subscriptions") + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_subscription_partner_partner_id"); + + b.HasOne("SW.Bitween.Domain.Subscription", null) + .WithMany() + .HasForeignKey("ResponseSubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_subscription_response_subscriber"); + + b.HasOne("SW.Bitween.Domain.RetryPolicy", "RetryPolicy") + .WithMany() + .HasForeignKey("RetryPolicyId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_subscription_retry_policy_retry_policy_id"); + + b.HasOne("SW.Bitween.Domain.WorkGroup", "WorkGroup") + .WithMany() + .HasForeignKey("WorkGroupId") + .HasConstraintName("fk_subscription_work_group_work_group_id"); + + b.OwnsMany("SW.Bitween.Domain.Schedule", "Schedules", b1 => + { + b1.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("Backwards") + .HasColumnType("boolean") + .HasColumnName("backwards"); + + b1.Property("On") + .HasColumnType("bigint") + .HasColumnName("on"); + + b1.Property("Recurrence") + .HasColumnType("smallint") + .HasColumnName("recurrence"); + + b1.HasKey("SubscriptionId", "Id") + .HasName("pk_subscription_schedule"); + + b1.ToTable("subscription_schedule", "infolink"); + + b1.WithOwner() + .HasForeignKey("SubscriptionId") + .HasConstraintName("fk_subscription_schedule_subscription_subscription_id"); + }); + + b.Navigation("Category"); + + b.Navigation("RetryPolicy"); + + b.Navigation("Schedules"); + + b.Navigation("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionTrail", b => + { + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_subscription_trail_subscription_subscription_id"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_xchange_document_document_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeAggregation", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_xchange_aggregation_xchange_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeDelivery", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_xchange_delivery_xchange_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangePromotedProperties", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_xchange_promoted_properties_xchange_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeResult", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_xchange_result_xchange_id"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("BlobTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_qrtz_blob_triggers_qrtz_triggers_sched_name_trigger_name_tr"); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("CronTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_qrtz_cron_triggers_qrtz_triggers_sched_name_trigger_name_tr"); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimplePropertyTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_qrtz_simprop_triggers_qrtz_triggers_sched_name_trigger_name"); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimpleTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_qrtz_simple_triggers_qrtz_triggers_sched_name_trigger_name_"); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzJobDetail", "JobDetail") + .WithMany("Triggers") + .HasForeignKey("SchedulerName", "JobName", "JobGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_qrtz_triggers_qrtz_job_details_sched_name_job_name_job_group"); + + b.Navigation("JobDetail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Navigation("Partners"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Navigation("Routes"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Navigation("Subscriptions"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Navigation("Triggers"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Navigation("BlobTriggers"); + + b.Navigation("CronTriggers"); + + b.Navigation("SimplePropertyTriggers"); + + b.Navigation("SimpleTriggers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/SW.Bitween.PgSql/Migrations/20260812092613_AddAccountLockout.cs b/SW.Bitween.PgSql/Migrations/20260812092613_AddAccountLockout.cs new file mode 100644 index 00000000..3577dd72 --- /dev/null +++ b/SW.Bitween.PgSql/Migrations/20260812092613_AddAccountLockout.cs @@ -0,0 +1,52 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace SW.Bitween.PgSql.Migrations +{ + /// + public partial class AddAccountLockout : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "failed_login_count", + schema: "infolink", + table: "Accounts", + type: "integer", + nullable: false, + defaultValue: 0); + + migrationBuilder.AddColumn( + name: "lockout_end", + schema: "infolink", + table: "Accounts", + type: "timestamp with time zone", + nullable: true); + + migrationBuilder.UpdateData( + schema: "infolink", + table: "Accounts", + keyColumn: "id", + keyValue: 9999, + columns: new[] { "failed_login_count", "lockout_end" }, + values: new object[] { 0, null }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "failed_login_count", + schema: "infolink", + table: "Accounts"); + + migrationBuilder.DropColumn( + name: "lockout_end", + schema: "infolink", + table: "Accounts"); + } + } +} diff --git a/SW.Bitween.PgSql/Migrations/20260817103433_RetryBudgetAlerts.Designer.cs b/SW.Bitween.PgSql/Migrations/20260817103433_RetryBudgetAlerts.Designer.cs new file mode 100644 index 00000000..f8b2d158 --- /dev/null +++ b/SW.Bitween.PgSql/Migrations/20260817103433_RetryBudgetAlerts.Designer.cs @@ -0,0 +1,2242 @@ +// +using System; +using System.Collections.Generic; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using SW.Bitween.Model; +using SW.Bitween.PgSql; + +#nullable disable + +namespace SW.Bitween.PgSql.Migrations +{ + [DbContext(typeof(BitweenDbContext))] + [Migration("20260817103433_RetryBudgetAlerts")] + partial class RetryBudgetAlerts + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("infolink") + .HasAnnotation("ProductVersion", "8.0.23") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Deleted") + .HasColumnType("boolean") + .HasColumnName("deleted"); + + b.Property("Disabled") + .HasColumnType("boolean") + .HasColumnName("disabled"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("display_name"); + + b.Property("Email") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("character varying(200)") + .HasColumnName("email"); + + b.Property("EmailProvider") + .HasColumnType("smallint") + .HasColumnName("email_provider"); + + b.Property("FailedLoginCount") + .HasColumnType("integer") + .HasColumnName("failed_login_count"); + + b.Property("LockoutEnd") + .HasColumnType("timestamp with time zone") + .HasColumnName("lockout_end"); + + b.Property("LoginMethods") + .HasColumnType("smallint") + .HasColumnName("login_methods"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Password") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("character varying(500)") + .HasColumnName("password"); + + b.Property("Phone") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("character varying(20)") + .HasColumnName("phone"); + + b.Property("Role") + .HasColumnType("integer") + .HasColumnName("role"); + + b.HasKey("Id") + .HasName("pk_accounts"); + + b.HasIndex("Email") + .IsUnique() + .HasDatabaseName("ix_accounts_email"); + + b.ToTable("Accounts", "infolink"); + + b.HasData( + new + { + Id = 9999, + CreatedOn = new DateTime(2021, 12, 31, 22, 0, 0, 0, DateTimeKind.Utc), + Deleted = false, + Disabled = false, + DisplayName = "Admin", + Email = "admin@Bitween.systems", + EmailProvider = (byte)0, + FailedLoginCount = 0, + LoginMethods = (byte)2, + Password = "$SWHASH$V1$10000$VQCi48eitH4Ml5juvBMOFZrMdQwBbhuIQVXe6RR7qJdDF2bJ", + Role = 0 + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("AccountId") + .HasColumnType("integer") + .HasColumnName("account_id"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("LoginMethod") + .HasColumnType("smallint") + .HasColumnName("login_method"); + + b.HasKey("Id") + .HasName("pk_refresh_tokens"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_refresh_tokens_account_id"); + + b.ToTable("RefreshTokens", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DelayedRetry", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("On") + .HasColumnType("timestamp with time zone") + .HasColumnName("on"); + + b.HasKey("Id") + .HasName("pk_delayed_retry"); + + b.HasIndex("On") + .HasDatabaseName("ix_delayed_retry_on"); + + b.ToTable("delayed_retry", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Document", b => + { + b.Property("Id") + .HasColumnType("integer") + .HasColumnName("id"); + + b.Property("BusEnabled") + .HasColumnType("boolean") + .HasColumnName("bus_enabled"); + + b.Property("BusMessageTypeName") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("bus_message_type_name"); + + b.Property("DisregardsUnfilteredMessages") + .HasColumnType("boolean") + .HasColumnName("disregards_unfiltered_messages"); + + b.Property("DocumentFormat") + .HasColumnType("integer") + .HasColumnName("document_format"); + + b.Property("DuplicateInterval") + .HasColumnType("integer") + .HasColumnName("duplicate_interval"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("PromotedProperties") + .HasColumnType("jsonb") + .HasColumnName("promoted_properties"); + + b.HasKey("Id") + .HasName("pk_document"); + + b.HasIndex("BusMessageTypeName") + .IsUnique() + .HasDatabaseName("ix_document_bus_message_type_name"); + + b.HasIndex("Name") + .IsUnique() + .HasDatabaseName("ix_document_name"); + + b.ToTable("document", "infolink"); + + b.HasData( + new + { + Id = 10001, + BusEnabled = false, + DocumentFormat = 0, + DuplicateInterval = 0, + Name = "Aggregation Document", + PromotedProperties = "{}" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DocumentTrail", b => + { + b.Property("Id") + .HasColumnType("text") + .HasColumnName("id"); + + b.Property("Code") + .HasColumnType("integer") + .HasColumnName("code"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("DocumentId") + .HasColumnType("integer") + .HasColumnName("document_id"); + + b.Property("StateAfter") + .HasColumnType("text") + .HasColumnName("state_after"); + + b.Property("StateBefore") + .HasColumnType("text") + .HasColumnName("state_before"); + + b.HasKey("Id") + .HasName("pk_document_trail"); + + b.HasIndex("CreatedOn") + .HasDatabaseName("ix_document_trail_created_on"); + + b.HasIndex("DocumentId") + .HasDatabaseName("ix_document_trail_document_id"); + + b.ToTable("document_trail", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.Property("UrlName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("url_name"); + + b.HasKey("Id") + .HasName("pk_api_gateway"); + + b.HasIndex("UrlName") + .IsUnique() + .HasDatabaseName("ix_api_gateway_url_name"); + + b.ToTable("api_gateway", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.Property("ApiGatewayId") + .HasColumnType("integer") + .HasColumnName("api_gateway_id"); + + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.HasKey("ApiGatewayId", "PartnerId", "SubscriptionId") + .HasName("pk_api_gateway_partner"); + + b.HasIndex("PartnerId") + .HasDatabaseName("ix_api_gateway_partner_partner_id"); + + b.HasIndex("SubscriptionId") + .HasDatabaseName("ix_api_gateway_partner_subscription_id"); + + b.ToTable("api_gateway_partner", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("DocumentId") + .HasColumnType("integer") + .HasColumnName("document_id"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("pk_bus_gateway"); + + b.HasIndex("DocumentId") + .HasDatabaseName("ix_bus_gateway_document_id"); + + b.ToTable("bus_gateway", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BusGatewayId") + .HasColumnType("integer") + .HasColumnName("bus_gateway_id"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("MatchExpression") + .HasColumnType("text") + .HasColumnName("match_expression"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.HasKey("Id") + .HasName("pk_bus_gateway_route"); + + b.HasIndex("BusGatewayId") + .HasDatabaseName("ix_bus_gateway_route_bus_gateway_id"); + + b.HasIndex("PartnerId") + .HasDatabaseName("ix_bus_gateway_route_partner_id"); + + b.HasIndex("SubscriptionId") + .HasDatabaseName("ix_bus_gateway_route_subscription_id"); + + b.ToTable("bus_gateway_route", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.GlobalAdapterValuesSet", b => + { + b.Property("Id") + .HasColumnType("text") + .HasColumnName("id"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property>("Values") + .HasColumnType("jsonb") + .HasColumnName("values"); + + b.HasKey("Id") + .HasName("pk_global_adapter_values_set"); + + b.ToTable("global_adapter_values_set", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Notifier", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("character varying(200)") + .HasColumnName("handler_id"); + + b.Property("HandlerProperties") + .HasColumnType("text") + .HasColumnName("handler_properties"); + + b.Property("Inactive") + .HasColumnType("boolean") + .HasColumnName("inactive"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("RunOnBadResult") + .HasColumnType("boolean") + .HasColumnName("run_on_bad_result"); + + b.Property("RunOnFailedResult") + .HasColumnType("boolean") + .HasColumnName("run_on_failed_result"); + + b.Property("RunOnSubscriptions") + .HasColumnType("integer[]") + .HasColumnName("run_on_subscriptions"); + + b.Property("RunOnSuccessfulResult") + .HasColumnType("boolean") + .HasColumnName("run_on_successful_result"); + + b.HasKey("Id") + .HasName("pk_notifier"); + + b.ToTable("notifier", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OnHoldXchange", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BadData") + .HasColumnType("boolean") + .HasColumnName("bad_data"); + + b.Property("Data") + .HasColumnType("text") + .HasColumnName("data"); + + b.Property("FileName") + .HasColumnType("text") + .HasColumnName("file_name"); + + b.Property("References") + .HasColumnType("text[]") + .HasColumnName("references"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.HasKey("Id") + .HasName("pk_on_hold_xchange"); + + b.HasIndex("SubscriptionId") + .HasDatabaseName("ix_on_hold_xchange_subscription_id"); + + b.ToTable("on_hold_xchange", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property>("AdapterProperties") + .HasColumnType("jsonb") + .HasColumnName("adapter_properties"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("pk_partner"); + + b.ToTable("partner", "infolink"); + + b.HasData( + new + { + Id = 1, + Name = "SYSTEM" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryAlertOverride", b => + { + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.Property("GroupId") + .HasColumnType("uuid") + .HasColumnName("group_id"); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("alert_handler_id"); + + b.Property("AlertHandlerProperties") + .HasColumnType("text") + .HasColumnName("alert_handler_properties"); + + b.Property("AlertMode") + .HasColumnType("smallint") + .HasColumnName("alert_mode"); + + b.HasKey("SubscriptionId", "GroupId") + .HasName("pk_retry_alert_override"); + + b.ToTable("retry_alert_override", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryGroupUsage", b => + { + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.Property("GroupId") + .HasColumnType("uuid") + .HasColumnName("group_id"); + + b.Property("AttemptsUsed") + .HasColumnType("integer") + .HasColumnName("attempts_used"); + + b.Property("ExhaustedNotifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("exhausted_notified_on"); + + b.Property("LastAttemptOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_attempt_on"); + + b.HasKey("SubscriptionId", "GroupId") + .HasName("pk_retry_group_usage"); + + b.ToTable("retry_group_usage", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryPolicy", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("alert_handler_id"); + + b.Property("AlertHandlerProperties") + .HasColumnType("text") + .HasColumnName("alert_handler_properties"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Groups") + .HasColumnType("text") + .HasColumnName("groups"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("pk_retry_policy"); + + b.ToTable("retry_policy", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AggregateOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("aggregate_on"); + + b.Property("AggregationForId") + .HasColumnType("integer") + .HasColumnName("aggregation_for_id"); + + b.Property("AggregationTarget") + .HasColumnType("smallint") + .HasColumnName("aggregation_target"); + + b.Property("CategoryId") + .HasColumnType("integer") + .HasColumnName("category_id"); + + b.Property("ConsecutiveFailures") + .HasColumnType("integer") + .HasColumnName("consecutive_failures"); + + b.Property("CustomRetryPolicy") + .HasColumnType("text") + .HasColumnName("custom_retry_policy"); + + b.Property>("DocumentFilter") + .HasColumnType("jsonb") + .HasColumnName("document_filter"); + + b.Property("DocumentId") + .HasColumnType("integer") + .HasColumnName("document_id"); + + b.Property("HandlerId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("handler_id"); + + b.Property>("HandlerProperties") + .HasColumnType("jsonb") + .HasColumnName("handler_properties"); + + b.Property("Inactive") + .HasColumnType("boolean") + .HasColumnName("inactive"); + + b.Property("IsRunning") + .HasColumnType("boolean") + .HasColumnName("is_running"); + + b.Property("LastException") + .HasColumnType("text") + .HasColumnName("last_exception"); + + b.Property("MapperId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("mapper_id"); + + b.Property>("MapperProperties") + .HasColumnType("jsonb") + .HasColumnName("mapper_properties"); + + b.Property("MatchExpression") + .HasColumnType("text") + .HasColumnName("match_expression"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.Property("PausedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("paused_on"); + + b.Property("ReceiveOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("receive_on"); + + b.Property("ReceiverId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("receiver_id"); + + b.Property>("ReceiverProperties") + .HasColumnType("jsonb") + .HasColumnName("receiver_properties"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("response_message_type_name"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("integer") + .HasColumnName("response_subscription_id"); + + b.Property("RetryPolicyId") + .HasColumnType("integer") + .HasColumnName("retry_policy_id"); + + b.Property("Temporary") + .HasColumnType("boolean") + .HasColumnName("temporary"); + + b.Property("Type") + .HasColumnType("smallint") + .HasColumnName("type"); + + b.Property("ValidatorId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("validator_id"); + + b.Property>("ValidatorProperties") + .HasColumnType("jsonb") + .HasColumnName("validator_properties"); + + b.Property("WorkGroupId") + .HasColumnType("integer") + .HasColumnName("work_group_id"); + + b.HasKey("Id") + .HasName("pk_subscription"); + + b.HasIndex("AggregationForId") + .HasDatabaseName("ix_subscription_aggregation_for_id"); + + b.HasIndex("CategoryId") + .HasDatabaseName("ix_subscription_category_id"); + + b.HasIndex("DocumentId") + .HasDatabaseName("ix_subscription_document_id"); + + b.HasIndex("PartnerId") + .HasDatabaseName("ix_subscription_partner_id"); + + b.HasIndex("ResponseSubscriptionId") + .HasDatabaseName("ix_subscription_response_subscription_id"); + + b.HasIndex("RetryPolicyId") + .HasDatabaseName("ix_subscription_retry_policy_id"); + + b.HasIndex("WorkGroupId") + .HasDatabaseName("ix_subscription_work_group_id"); + + b.ToTable("subscription", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Code") + .HasColumnType("text") + .HasColumnName("code"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.HasKey("Id") + .HasName("pk_subscription_category"); + + b.HasIndex("Code") + .IsUnique() + .HasDatabaseName("ix_subscription_category_code"); + + b.ToTable("subscription_category", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionTrail", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("Code") + .HasColumnType("integer") + .HasColumnName("code"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("StateAfter") + .HasColumnType("text") + .HasColumnName("state_after"); + + b.Property("StateBefore") + .HasColumnType("text") + .HasColumnName("state_before"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.HasKey("Id") + .HasName("pk_subscription_trail"); + + b.HasIndex("CreatedOn") + .HasDatabaseName("ix_subscription_trail_created_on"); + + b.HasIndex("SubscriptionId") + .HasDatabaseName("ix_subscription_trail_subscription_id"); + + b.ToTable("subscription_trail", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.WorkGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BusMessageName") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("character varying(100)") + .HasColumnName("bus_message_name"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("Options") + .HasColumnType("jsonb") + .HasColumnName("options"); + + b.HasKey("Id") + .HasName("pk_work_group"); + + b.ToTable("work_group", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("CorrelationId") + .HasColumnType("text") + .HasColumnName("correlation_id"); + + b.Property("DocumentId") + .HasColumnType("integer") + .HasColumnName("document_id"); + + b.Property("HandlerId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("handler_id"); + + b.Property>("HandlerProperties") + .HasColumnType("jsonb") + .HasColumnName("handler_properties"); + + b.Property("InputContentType") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("input_content_type"); + + b.Property("InputHash") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("input_hash"); + + b.Property("InputName") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("input_name"); + + b.Property("InputSize") + .HasColumnType("integer") + .HasColumnName("input_size"); + + b.Property("MapperId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("mapper_id"); + + b.Property>("MapperProperties") + .HasColumnType("jsonb") + .HasColumnName("mapper_properties"); + + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.Property("References") + .HasColumnType("text[]") + .HasColumnName("references"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("response_message_type_name"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("integer") + .HasColumnName("response_subscription_id"); + + b.Property("RetryFor") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("retry_for"); + + b.Property("StartedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("started_on"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.HasKey("Id") + .HasName("pk_xchange"); + + b.HasIndex("DocumentId") + .HasDatabaseName("ix_xchange_document_id"); + + b.HasIndex("InputHash") + .HasDatabaseName("ix_xchange_input_hash"); + + b.HasIndex("RetryFor") + .HasDatabaseName("ix_xchange_retry_for"); + + b.HasIndex("StartedOn") + .HasDatabaseName("ix_xchange_started_on"); + + b.HasIndex("SubscriptionId") + .HasDatabaseName("ix_xchange_subscription_id"); + + b.ToTable("xchange", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("AggregatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("aggregated_on"); + + b.Property("AggregationXchangeId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("aggregation_xchange_id"); + + b.HasKey("Id") + .HasName("pk_xchange_aggregation"); + + b.HasIndex("AggregationXchangeId") + .HasDatabaseName("ix_xchange_aggregation_aggregation_xchange_id"); + + b.ToTable("xchange_aggregation", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("DeliveredOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("delivered_on"); + + b.HasKey("Id") + .HasName("pk_xchange_delivery"); + + b.HasIndex("DeliveredOn") + .HasDatabaseName("ix_xchange_delivery_delivered_on"); + + b.ToTable("xchange_delivery", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeNotification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Exception") + .HasColumnType("text") + .HasColumnName("exception"); + + b.Property("FinishedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("finished_on"); + + b.Property("NotifierId") + .HasColumnType("integer") + .HasColumnName("notifier_id"); + + b.Property("NotifierName") + .HasColumnType("text") + .HasColumnName("notifier_name"); + + b.Property("Success") + .HasColumnType("boolean") + .HasColumnName("success"); + + b.Property("XchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("character varying(50)") + .HasColumnName("xchange_id"); + + b.HasKey("Id") + .HasName("pk_xchange_notification"); + + b.ToTable("xchange_notification", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("Hits") + .HasColumnType("integer[]") + .HasColumnName("hits"); + + b.Property>("Properties") + .HasColumnType("jsonb") + .HasColumnName("properties"); + + b.Property("PropertiesRaw") + .HasColumnType("text") + .HasColumnName("properties_raw"); + + b.HasKey("Id") + .HasName("pk_xchange_promoted_properties"); + + b.HasIndex("PropertiesRaw") + .HasDatabaseName("ix_xchange_promoted_properties_properties_raw"); + + b.ToTable("xchange_promoted_properties", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("AttemptNumber") + .HasColumnType("integer") + .HasColumnName("attempt_number"); + + b.Property("Exception") + .HasColumnType("text") + .HasColumnName("exception"); + + b.Property("FinishedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("finished_on"); + + b.Property("OutputBad") + .HasColumnType("boolean") + .HasColumnName("output_bad"); + + b.Property("OutputContentType") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("output_content_type"); + + b.Property("OutputHash") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("output_hash"); + + b.Property("OutputName") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("output_name"); + + b.Property("OutputSize") + .HasColumnType("integer") + .HasColumnName("output_size"); + + b.Property("ResponseBad") + .HasColumnType("boolean") + .HasColumnName("response_bad"); + + b.Property("ResponseContentType") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("response_content_type"); + + b.Property("ResponseHash") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("response_hash"); + + b.Property("ResponseName") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("response_name"); + + b.Property("ResponseSize") + .HasColumnType("integer") + .HasColumnName("response_size"); + + b.Property("ResponseXchangeId") + .HasColumnType("text") + .HasColumnName("response_xchange_id"); + + b.Property("RetryBlockedReason") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("retry_blocked_reason"); + + b.Property("RetryGroupId") + .HasColumnType("uuid") + .HasColumnName("retry_group_id"); + + b.Property("Success") + .HasColumnType("boolean") + .HasColumnName("success"); + + b.HasKey("Id") + .HasName("pk_xchange_result"); + + b.HasIndex("RetryGroupId") + .HasDatabaseName("ix_xchange_result_retry_group_id"); + + b.ToTable("xchange_result", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.RunFlagUpdater+RunningResult", b => + { + b.Property("IsRunning") + .HasColumnType("boolean") + .HasColumnName("is_running"); + + b.ToTable("running_result", "infolink"); + + b.ToView(null, (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.JobExecution", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Context") + .HasColumnType("text") + .HasColumnName("context"); + + b.Property("DurationMs") + .HasColumnType("bigint") + .HasColumnName("duration_ms"); + + b.Property("EndTimeUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("end_time_utc"); + + b.Property("Error") + .HasColumnType("text") + .HasColumnName("error"); + + b.Property("FireInstanceId") + .IsRequired() + .HasColumnType("text") + .HasColumnName("fire_instance_id"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property("JobTypeName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_type_name"); + + b.Property("Node") + .IsRequired() + .HasColumnType("text") + .HasColumnName("node"); + + b.Property("StartTimeUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("start_time_utc"); + + b.Property("Success") + .HasColumnType("boolean") + .HasColumnName("success"); + + b.HasKey("Id") + .HasName("pk_job_executions"); + + b.HasIndex("FireInstanceId") + .IsUnique() + .HasDatabaseName("idx_je_fire_instance_id"); + + b.HasIndex("StartTimeUtc") + .HasDatabaseName("idx_je_start_time"); + + b.HasIndex("Success") + .HasDatabaseName("idx_je_success"); + + b.HasIndex("JobGroup", "JobName", "StartTimeUtc") + .HasDatabaseName("idx_je_group_name_start"); + + b.ToTable("job_executions", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("BlobData") + .HasColumnType("bytea") + .HasColumnName("blob_data"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup") + .HasName("pk_qrtz_blob_triggers"); + + b.ToTable("qrtz_blob_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCalendar", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("CalendarName") + .HasColumnType("text") + .HasColumnName("calendar_name"); + + b.Property("Calendar") + .IsRequired() + .HasColumnType("bytea") + .HasColumnName("calendar"); + + b.HasKey("SchedulerName", "CalendarName") + .HasName("pk_qrtz_calendars"); + + b.ToTable("qrtz_calendars", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("CronExpression") + .IsRequired() + .HasColumnType("text") + .HasColumnName("cron_expression"); + + b.Property("TimeZoneId") + .HasColumnType("text") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup") + .HasName("pk_qrtz_cron_triggers"); + + b.ToTable("qrtz_cron_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzFiredTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("EntryId") + .HasColumnType("text") + .HasColumnName("entry_id"); + + b.Property("FiredTime") + .HasColumnType("bigint") + .HasColumnName("fired_time"); + + b.Property("InstanceName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("instance_name"); + + b.Property("IsNonConcurrent") + .HasColumnType("bool") + .HasColumnName("is_nonconcurrent"); + + b.Property("JobGroup") + .HasColumnType("text") + .HasColumnName("job_group"); + + b.Property("JobName") + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property("Priority") + .HasColumnType("integer") + .HasColumnName("priority"); + + b.Property("RequestsRecovery") + .HasColumnType("bool") + .HasColumnName("requests_recovery"); + + b.Property("ScheduledTime") + .HasColumnType("bigint") + .HasColumnName("sched_time"); + + b.Property("State") + .IsRequired() + .HasColumnType("text") + .HasColumnName("state"); + + b.Property("TriggerGroup") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("TriggerName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.HasKey("SchedulerName", "EntryId") + .HasName("pk_qrtz_fired_triggers"); + + b.HasIndex("InstanceName") + .HasDatabaseName("idx_qrtz_ft_trig_inst_name"); + + b.HasIndex("JobGroup") + .HasDatabaseName("idx_qrtz_ft_job_group"); + + b.HasIndex("JobName") + .HasDatabaseName("idx_qrtz_ft_job_name"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_qrtz_ft_job_req_recovery"); + + b.HasIndex("TriggerGroup") + .HasDatabaseName("idx_qrtz_ft_trig_group"); + + b.HasIndex("TriggerName") + .HasDatabaseName("idx_qrtz_ft_trig_name"); + + b.HasIndex("SchedulerName", "TriggerName", "TriggerGroup") + .HasDatabaseName("idx_qrtz_ft_trig_nm_gp"); + + b.ToTable("qrtz_fired_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("JobName") + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property("JobGroup") + .HasColumnType("text") + .HasColumnName("job_group"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("IsDurable") + .HasColumnType("bool") + .HasColumnName("is_durable"); + + b.Property("IsNonConcurrent") + .HasColumnType("bool") + .HasColumnName("is_nonconcurrent"); + + b.Property("IsUpdateData") + .HasColumnType("bool") + .HasColumnName("is_update_data"); + + b.Property("JobClassName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_class_name"); + + b.Property("JobData") + .HasColumnType("bytea") + .HasColumnName("job_data"); + + b.Property("RequestsRecovery") + .HasColumnType("bool") + .HasColumnName("requests_recovery"); + + b.HasKey("SchedulerName", "JobName", "JobGroup") + .HasName("pk_qrtz_job_details"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_j_req_recovery"); + + b.ToTable("qrtz_job_details", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzLock", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("LockName") + .HasColumnType("text") + .HasColumnName("lock_name"); + + b.HasKey("SchedulerName", "LockName") + .HasName("pk_qrtz_locks"); + + b.ToTable("qrtz_locks", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzPausedTriggerGroup", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.HasKey("SchedulerName", "TriggerGroup") + .HasName("pk_qrtz_paused_trigger_grps"); + + b.ToTable("qrtz_paused_trigger_grps", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSchedulerState", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("InstanceName") + .HasColumnType("text") + .HasColumnName("instance_name"); + + b.Property("CheckInInterval") + .HasColumnType("bigint") + .HasColumnName("checkin_interval"); + + b.Property("LastCheckInTime") + .HasColumnType("bigint") + .HasColumnName("last_checkin_time"); + + b.HasKey("SchedulerName", "InstanceName") + .HasName("pk_qrtz_scheduler_state"); + + b.ToTable("qrtz_scheduler_state", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("BooleanProperty1") + .HasColumnType("bool") + .HasColumnName("bool_prop_1"); + + b.Property("BooleanProperty2") + .HasColumnType("bool") + .HasColumnName("bool_prop_2"); + + b.Property("DecimalProperty1") + .HasColumnType("numeric") + .HasColumnName("dec_prop_1"); + + b.Property("DecimalProperty2") + .HasColumnType("numeric") + .HasColumnName("dec_prop_2"); + + b.Property("IntegerProperty1") + .HasColumnType("integer") + .HasColumnName("int_prop_1"); + + b.Property("IntegerProperty2") + .HasColumnType("integer") + .HasColumnName("int_prop_2"); + + b.Property("LongProperty1") + .HasColumnType("bigint") + .HasColumnName("long_prop_1"); + + b.Property("LongProperty2") + .HasColumnType("bigint") + .HasColumnName("long_prop_2"); + + b.Property("StringProperty1") + .HasColumnType("text") + .HasColumnName("str_prop_1"); + + b.Property("StringProperty2") + .HasColumnType("text") + .HasColumnName("str_prop_2"); + + b.Property("StringProperty3") + .HasColumnType("text") + .HasColumnName("str_prop_3"); + + b.Property("TimeZoneId") + .HasColumnType("text") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup") + .HasName("pk_qrtz_simprop_triggers"); + + b.ToTable("qrtz_simprop_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("RepeatCount") + .HasColumnType("bigint") + .HasColumnName("repeat_count"); + + b.Property("RepeatInterval") + .HasColumnType("bigint") + .HasColumnName("repeat_interval"); + + b.Property("TimesTriggered") + .HasColumnType("bigint") + .HasColumnName("times_triggered"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup") + .HasName("pk_qrtz_simple_triggers"); + + b.ToTable("qrtz_simple_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("CalendarName") + .HasColumnType("text") + .HasColumnName("calendar_name"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("EndTime") + .HasColumnType("bigint") + .HasColumnName("end_time"); + + b.Property("JobData") + .HasColumnType("bytea") + .HasColumnName("job_data"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property("MisfireInstruction") + .HasColumnType("integer") + .HasColumnName("misfire_instr"); + + b.Property("NextFireTime") + .HasColumnType("bigint") + .HasColumnName("next_fire_time"); + + b.Property("PreviousFireTime") + .HasColumnType("bigint") + .HasColumnName("prev_fire_time"); + + b.Property("Priority") + .HasColumnType("integer") + .HasColumnName("priority"); + + b.Property("StartTime") + .HasColumnType("bigint") + .HasColumnName("start_time"); + + b.Property("TriggerState") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_state"); + + b.Property("TriggerType") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_type"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup") + .HasName("pk_qrtz_triggers"); + + b.HasIndex("NextFireTime") + .HasDatabaseName("idx_t_next_fire_time"); + + b.HasIndex("TriggerState") + .HasDatabaseName("idx_t_state"); + + b.HasIndex("NextFireTime", "TriggerState") + .HasDatabaseName("idx_t_nft_st"); + + b.HasIndex("SchedulerName", "JobName", "JobGroup") + .HasDatabaseName("ix_qrtz_triggers_sched_name_job_name_job_group"); + + b.ToTable("qrtz_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_refresh_tokens_accounts_account_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DocumentTrail", b => + { + b.HasOne("SW.Bitween.Domain.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_document_trail_document_document_id"); + + b.Navigation("Document"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.ApiGateway", "ApiGateway") + .WithMany("Partners") + .HasForeignKey("ApiGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_api_gateway_partner_api_gateway_api_gateway_id"); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_api_gateway_partner_partner_partner_id"); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_api_gateway_partner_subscription_subscription_id"); + + b.Navigation("ApiGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_bus_gateway_document_document_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.BusGateway", "BusGateway") + .WithMany("Routes") + .HasForeignKey("BusGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_bus_gateway_route_bus_gateway_bus_gateway_id"); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_bus_gateway_route_partner_partner_id"); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_bus_gateway_route_subscription_subscription_id"); + + b.Navigation("BusGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.OwnsMany("SW.Bitween.Domain.ApiCredential", "ApiCredentials", b1 => + { + b1.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("Key") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("key"); + + b1.Property("Name") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("name"); + + b1.HasKey("PartnerId", "Id") + .HasName("pk_partner_api_credential"); + + b1.HasIndex("Key") + .IsUnique() + .HasDatabaseName("ix_partner_api_credential_key"); + + b1.ToTable("partner_api_credential", "infolink"); + + b1.WithOwner() + .HasForeignKey("PartnerId") + .HasConstraintName("fk_partner_api_credential_partner_partner_id"); + + b1.HasData( + new + { + PartnerId = 1, + Id = 1, + Key = "7facc758283844b49cc4ffd26a75b1de", + Name = "default" + }); + }); + + b.Navigation("ApiCredentials"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.HasOne("SW.Bitween.Domain.Subscription", null) + .WithMany() + .HasForeignKey("AggregationForId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_subscription_aggregation_for"); + + b.HasOne("SW.Bitween.Domain.SubscriptionCategory", "Category") + .WithMany() + .HasForeignKey("CategoryId") + .HasConstraintName("fk_subscription_subscription_category_category_id"); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_subscription_document_document_id"); + + b.HasOne("SW.Bitween.Domain.Partner", null) + .WithMany("Subscriptions") + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_subscription_partner_partner_id"); + + b.HasOne("SW.Bitween.Domain.Subscription", null) + .WithMany() + .HasForeignKey("ResponseSubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_subscription_response_subscriber"); + + b.HasOne("SW.Bitween.Domain.RetryPolicy", "RetryPolicy") + .WithMany() + .HasForeignKey("RetryPolicyId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_subscription_retry_policy_retry_policy_id"); + + b.HasOne("SW.Bitween.Domain.WorkGroup", "WorkGroup") + .WithMany() + .HasForeignKey("WorkGroupId") + .HasConstraintName("fk_subscription_work_group_work_group_id"); + + b.OwnsMany("SW.Bitween.Domain.Schedule", "Schedules", b1 => + { + b1.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("Backwards") + .HasColumnType("boolean") + .HasColumnName("backwards"); + + b1.Property("On") + .HasColumnType("bigint") + .HasColumnName("on"); + + b1.Property("Recurrence") + .HasColumnType("smallint") + .HasColumnName("recurrence"); + + b1.HasKey("SubscriptionId", "Id") + .HasName("pk_subscription_schedule"); + + b1.ToTable("subscription_schedule", "infolink"); + + b1.WithOwner() + .HasForeignKey("SubscriptionId") + .HasConstraintName("fk_subscription_schedule_subscription_subscription_id"); + }); + + b.Navigation("Category"); + + b.Navigation("RetryPolicy"); + + b.Navigation("Schedules"); + + b.Navigation("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionTrail", b => + { + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_subscription_trail_subscription_subscription_id"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_xchange_document_document_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeAggregation", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_xchange_aggregation_xchange_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeDelivery", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_xchange_delivery_xchange_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangePromotedProperties", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_xchange_promoted_properties_xchange_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeResult", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_xchange_result_xchange_id"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("BlobTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_qrtz_blob_triggers_qrtz_triggers_sched_name_trigger_name_tr"); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("CronTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_qrtz_cron_triggers_qrtz_triggers_sched_name_trigger_name_tr"); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimplePropertyTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_qrtz_simprop_triggers_qrtz_triggers_sched_name_trigger_name"); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimpleTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_qrtz_simple_triggers_qrtz_triggers_sched_name_trigger_name_"); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzJobDetail", "JobDetail") + .WithMany("Triggers") + .HasForeignKey("SchedulerName", "JobName", "JobGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_qrtz_triggers_qrtz_job_details_sched_name_job_name_job_group"); + + b.Navigation("JobDetail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Navigation("Partners"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Navigation("Routes"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Navigation("Subscriptions"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Navigation("Triggers"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Navigation("BlobTriggers"); + + b.Navigation("CronTriggers"); + + b.Navigation("SimplePropertyTriggers"); + + b.Navigation("SimpleTriggers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/SW.Bitween.PgSql/Migrations/20260817103433_RetryBudgetAlerts.cs b/SW.Bitween.PgSql/Migrations/20260817103433_RetryBudgetAlerts.cs new file mode 100644 index 00000000..ed6b8724 --- /dev/null +++ b/SW.Bitween.PgSql/Migrations/20260817103433_RetryBudgetAlerts.cs @@ -0,0 +1,137 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace SW.Bitween.PgSql.Migrations +{ + /// + public partial class RetryBudgetAlerts : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "attempt_number", + schema: "infolink", + table: "xchange_result", + type: "integer", + nullable: true); + + migrationBuilder.AddColumn( + name: "retry_group_id", + schema: "infolink", + table: "xchange_result", + type: "uuid", + nullable: true); + + migrationBuilder.AlterColumn( + name: "notifier_id", + schema: "infolink", + table: "xchange_notification", + type: "integer", + nullable: true, + oldClrType: typeof(int), + oldType: "integer"); + + migrationBuilder.AddColumn( + name: "alert_handler_id", + schema: "infolink", + table: "retry_policy", + type: "character varying(200)", + maxLength: 200, + nullable: true); + + migrationBuilder.AddColumn( + name: "alert_handler_properties", + schema: "infolink", + table: "retry_policy", + type: "text", + nullable: true); + + migrationBuilder.AddColumn( + name: "exhausted_notified_on", + schema: "infolink", + table: "retry_group_usage", + type: "timestamp with time zone", + nullable: true); + + migrationBuilder.CreateTable( + name: "retry_alert_override", + schema: "infolink", + columns: table => new + { + subscription_id = table.Column(type: "integer", nullable: false), + group_id = table.Column(type: "uuid", nullable: false), + alert_mode = table.Column(type: "smallint", nullable: false), + alert_handler_id = table.Column(type: "character varying(200)", maxLength: 200, nullable: true), + alert_handler_properties = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("pk_retry_alert_override", x => new { x.subscription_id, x.group_id }); + }); + + migrationBuilder.CreateIndex( + name: "ix_xchange_result_retry_group_id", + schema: "infolink", + table: "xchange_result", + column: "retry_group_id"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "retry_alert_override", + schema: "infolink"); + + migrationBuilder.DropIndex( + name: "ix_xchange_result_retry_group_id", + schema: "infolink", + table: "xchange_result"); + + migrationBuilder.DropColumn( + name: "attempt_number", + schema: "infolink", + table: "xchange_result"); + + migrationBuilder.DropColumn( + name: "retry_group_id", + schema: "infolink", + table: "xchange_result"); + + migrationBuilder.DropColumn( + name: "alert_handler_id", + schema: "infolink", + table: "retry_policy"); + + migrationBuilder.DropColumn( + name: "alert_handler_properties", + schema: "infolink", + table: "retry_policy"); + + migrationBuilder.DropColumn( + name: "exhausted_notified_on", + schema: "infolink", + table: "retry_group_usage"); + + // These rows are the alert's own delivery log, and they are the reason the column was + // made nullable. Rolling the feature back leaves nowhere to put them, and the column + // cannot go back to NOT NULL while they are here, so they go with the feature. + migrationBuilder.Sql( + "DELETE FROM infolink.xchange_notification WHERE notifier_id IS NULL;"); + + migrationBuilder.AlterColumn( + name: "notifier_id", + schema: "infolink", + table: "xchange_notification", + type: "integer", + nullable: false, + defaultValue: 0, + oldClrType: typeof(int), + oldType: "integer", + oldNullable: true); + } + } +} diff --git a/SW.Bitween.PgSql/Migrations/20260819100439_ManualRetryFlag.Designer.cs b/SW.Bitween.PgSql/Migrations/20260819100439_ManualRetryFlag.Designer.cs new file mode 100644 index 00000000..12ed1d41 --- /dev/null +++ b/SW.Bitween.PgSql/Migrations/20260819100439_ManualRetryFlag.Designer.cs @@ -0,0 +1,2246 @@ +// +using System; +using System.Collections.Generic; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using SW.Bitween.Model; +using SW.Bitween.PgSql; + +#nullable disable + +namespace SW.Bitween.PgSql.Migrations +{ + [DbContext(typeof(BitweenDbContext))] + [Migration("20260819100439_ManualRetryFlag")] + partial class ManualRetryFlag + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("infolink") + .HasAnnotation("ProductVersion", "8.0.23") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Deleted") + .HasColumnType("boolean") + .HasColumnName("deleted"); + + b.Property("Disabled") + .HasColumnType("boolean") + .HasColumnName("disabled"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("display_name"); + + b.Property("Email") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("character varying(200)") + .HasColumnName("email"); + + b.Property("EmailProvider") + .HasColumnType("smallint") + .HasColumnName("email_provider"); + + b.Property("FailedLoginCount") + .HasColumnType("integer") + .HasColumnName("failed_login_count"); + + b.Property("LockoutEnd") + .HasColumnType("timestamp with time zone") + .HasColumnName("lockout_end"); + + b.Property("LoginMethods") + .HasColumnType("smallint") + .HasColumnName("login_methods"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Password") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("character varying(500)") + .HasColumnName("password"); + + b.Property("Phone") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("character varying(20)") + .HasColumnName("phone"); + + b.Property("Role") + .HasColumnType("integer") + .HasColumnName("role"); + + b.HasKey("Id") + .HasName("pk_accounts"); + + b.HasIndex("Email") + .IsUnique() + .HasDatabaseName("ix_accounts_email"); + + b.ToTable("Accounts", "infolink"); + + b.HasData( + new + { + Id = 9999, + CreatedOn = new DateTime(2021, 12, 31, 22, 0, 0, 0, DateTimeKind.Utc), + Deleted = false, + Disabled = false, + DisplayName = "Admin", + Email = "admin@Bitween.systems", + EmailProvider = (byte)0, + FailedLoginCount = 0, + LoginMethods = (byte)2, + Password = "$SWHASH$V1$10000$VQCi48eitH4Ml5juvBMOFZrMdQwBbhuIQVXe6RR7qJdDF2bJ", + Role = 0 + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("AccountId") + .HasColumnType("integer") + .HasColumnName("account_id"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("LoginMethod") + .HasColumnType("smallint") + .HasColumnName("login_method"); + + b.HasKey("Id") + .HasName("pk_refresh_tokens"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_refresh_tokens_account_id"); + + b.ToTable("RefreshTokens", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DelayedRetry", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("On") + .HasColumnType("timestamp with time zone") + .HasColumnName("on"); + + b.HasKey("Id") + .HasName("pk_delayed_retry"); + + b.HasIndex("On") + .HasDatabaseName("ix_delayed_retry_on"); + + b.ToTable("delayed_retry", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Document", b => + { + b.Property("Id") + .HasColumnType("integer") + .HasColumnName("id"); + + b.Property("BusEnabled") + .HasColumnType("boolean") + .HasColumnName("bus_enabled"); + + b.Property("BusMessageTypeName") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("bus_message_type_name"); + + b.Property("DisregardsUnfilteredMessages") + .HasColumnType("boolean") + .HasColumnName("disregards_unfiltered_messages"); + + b.Property("DocumentFormat") + .HasColumnType("integer") + .HasColumnName("document_format"); + + b.Property("DuplicateInterval") + .HasColumnType("integer") + .HasColumnName("duplicate_interval"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("PromotedProperties") + .HasColumnType("jsonb") + .HasColumnName("promoted_properties"); + + b.HasKey("Id") + .HasName("pk_document"); + + b.HasIndex("BusMessageTypeName") + .IsUnique() + .HasDatabaseName("ix_document_bus_message_type_name"); + + b.HasIndex("Name") + .IsUnique() + .HasDatabaseName("ix_document_name"); + + b.ToTable("document", "infolink"); + + b.HasData( + new + { + Id = 10001, + BusEnabled = false, + DocumentFormat = 0, + DuplicateInterval = 0, + Name = "Aggregation Document", + PromotedProperties = "{}" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DocumentTrail", b => + { + b.Property("Id") + .HasColumnType("text") + .HasColumnName("id"); + + b.Property("Code") + .HasColumnType("integer") + .HasColumnName("code"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("DocumentId") + .HasColumnType("integer") + .HasColumnName("document_id"); + + b.Property("StateAfter") + .HasColumnType("text") + .HasColumnName("state_after"); + + b.Property("StateBefore") + .HasColumnType("text") + .HasColumnName("state_before"); + + b.HasKey("Id") + .HasName("pk_document_trail"); + + b.HasIndex("CreatedOn") + .HasDatabaseName("ix_document_trail_created_on"); + + b.HasIndex("DocumentId") + .HasDatabaseName("ix_document_trail_document_id"); + + b.ToTable("document_trail", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.Property("UrlName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("url_name"); + + b.HasKey("Id") + .HasName("pk_api_gateway"); + + b.HasIndex("UrlName") + .IsUnique() + .HasDatabaseName("ix_api_gateway_url_name"); + + b.ToTable("api_gateway", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.Property("ApiGatewayId") + .HasColumnType("integer") + .HasColumnName("api_gateway_id"); + + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.HasKey("ApiGatewayId", "PartnerId", "SubscriptionId") + .HasName("pk_api_gateway_partner"); + + b.HasIndex("PartnerId") + .HasDatabaseName("ix_api_gateway_partner_partner_id"); + + b.HasIndex("SubscriptionId") + .HasDatabaseName("ix_api_gateway_partner_subscription_id"); + + b.ToTable("api_gateway_partner", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("DocumentId") + .HasColumnType("integer") + .HasColumnName("document_id"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("pk_bus_gateway"); + + b.HasIndex("DocumentId") + .HasDatabaseName("ix_bus_gateway_document_id"); + + b.ToTable("bus_gateway", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BusGatewayId") + .HasColumnType("integer") + .HasColumnName("bus_gateway_id"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("MatchExpression") + .HasColumnType("text") + .HasColumnName("match_expression"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.HasKey("Id") + .HasName("pk_bus_gateway_route"); + + b.HasIndex("BusGatewayId") + .HasDatabaseName("ix_bus_gateway_route_bus_gateway_id"); + + b.HasIndex("PartnerId") + .HasDatabaseName("ix_bus_gateway_route_partner_id"); + + b.HasIndex("SubscriptionId") + .HasDatabaseName("ix_bus_gateway_route_subscription_id"); + + b.ToTable("bus_gateway_route", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.GlobalAdapterValuesSet", b => + { + b.Property("Id") + .HasColumnType("text") + .HasColumnName("id"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property>("Values") + .HasColumnType("jsonb") + .HasColumnName("values"); + + b.HasKey("Id") + .HasName("pk_global_adapter_values_set"); + + b.ToTable("global_adapter_values_set", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Notifier", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("character varying(200)") + .HasColumnName("handler_id"); + + b.Property("HandlerProperties") + .HasColumnType("text") + .HasColumnName("handler_properties"); + + b.Property("Inactive") + .HasColumnType("boolean") + .HasColumnName("inactive"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("RunOnBadResult") + .HasColumnType("boolean") + .HasColumnName("run_on_bad_result"); + + b.Property("RunOnFailedResult") + .HasColumnType("boolean") + .HasColumnName("run_on_failed_result"); + + b.Property("RunOnSubscriptions") + .HasColumnType("integer[]") + .HasColumnName("run_on_subscriptions"); + + b.Property("RunOnSuccessfulResult") + .HasColumnType("boolean") + .HasColumnName("run_on_successful_result"); + + b.HasKey("Id") + .HasName("pk_notifier"); + + b.ToTable("notifier", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OnHoldXchange", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BadData") + .HasColumnType("boolean") + .HasColumnName("bad_data"); + + b.Property("Data") + .HasColumnType("text") + .HasColumnName("data"); + + b.Property("FileName") + .HasColumnType("text") + .HasColumnName("file_name"); + + b.Property("References") + .HasColumnType("text[]") + .HasColumnName("references"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.HasKey("Id") + .HasName("pk_on_hold_xchange"); + + b.HasIndex("SubscriptionId") + .HasDatabaseName("ix_on_hold_xchange_subscription_id"); + + b.ToTable("on_hold_xchange", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property>("AdapterProperties") + .HasColumnType("jsonb") + .HasColumnName("adapter_properties"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("pk_partner"); + + b.ToTable("partner", "infolink"); + + b.HasData( + new + { + Id = 1, + Name = "SYSTEM" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryAlertOverride", b => + { + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.Property("GroupId") + .HasColumnType("uuid") + .HasColumnName("group_id"); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("alert_handler_id"); + + b.Property("AlertHandlerProperties") + .HasColumnType("text") + .HasColumnName("alert_handler_properties"); + + b.Property("AlertMode") + .HasColumnType("smallint") + .HasColumnName("alert_mode"); + + b.HasKey("SubscriptionId", "GroupId") + .HasName("pk_retry_alert_override"); + + b.ToTable("retry_alert_override", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryGroupUsage", b => + { + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.Property("GroupId") + .HasColumnType("uuid") + .HasColumnName("group_id"); + + b.Property("AttemptsUsed") + .HasColumnType("integer") + .HasColumnName("attempts_used"); + + b.Property("ExhaustedNotifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("exhausted_notified_on"); + + b.Property("LastAttemptOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_attempt_on"); + + b.HasKey("SubscriptionId", "GroupId") + .HasName("pk_retry_group_usage"); + + b.ToTable("retry_group_usage", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryPolicy", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("alert_handler_id"); + + b.Property("AlertHandlerProperties") + .HasColumnType("text") + .HasColumnName("alert_handler_properties"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Groups") + .HasColumnType("text") + .HasColumnName("groups"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("pk_retry_policy"); + + b.ToTable("retry_policy", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AggregateOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("aggregate_on"); + + b.Property("AggregationForId") + .HasColumnType("integer") + .HasColumnName("aggregation_for_id"); + + b.Property("AggregationTarget") + .HasColumnType("smallint") + .HasColumnName("aggregation_target"); + + b.Property("CategoryId") + .HasColumnType("integer") + .HasColumnName("category_id"); + + b.Property("ConsecutiveFailures") + .HasColumnType("integer") + .HasColumnName("consecutive_failures"); + + b.Property("CustomRetryPolicy") + .HasColumnType("text") + .HasColumnName("custom_retry_policy"); + + b.Property>("DocumentFilter") + .HasColumnType("jsonb") + .HasColumnName("document_filter"); + + b.Property("DocumentId") + .HasColumnType("integer") + .HasColumnName("document_id"); + + b.Property("HandlerId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("handler_id"); + + b.Property>("HandlerProperties") + .HasColumnType("jsonb") + .HasColumnName("handler_properties"); + + b.Property("Inactive") + .HasColumnType("boolean") + .HasColumnName("inactive"); + + b.Property("IsRunning") + .HasColumnType("boolean") + .HasColumnName("is_running"); + + b.Property("LastException") + .HasColumnType("text") + .HasColumnName("last_exception"); + + b.Property("MapperId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("mapper_id"); + + b.Property>("MapperProperties") + .HasColumnType("jsonb") + .HasColumnName("mapper_properties"); + + b.Property("MatchExpression") + .HasColumnType("text") + .HasColumnName("match_expression"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.Property("PausedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("paused_on"); + + b.Property("ReceiveOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("receive_on"); + + b.Property("ReceiverId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("receiver_id"); + + b.Property>("ReceiverProperties") + .HasColumnType("jsonb") + .HasColumnName("receiver_properties"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("response_message_type_name"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("integer") + .HasColumnName("response_subscription_id"); + + b.Property("RetryPolicyId") + .HasColumnType("integer") + .HasColumnName("retry_policy_id"); + + b.Property("Temporary") + .HasColumnType("boolean") + .HasColumnName("temporary"); + + b.Property("Type") + .HasColumnType("smallint") + .HasColumnName("type"); + + b.Property("ValidatorId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("validator_id"); + + b.Property>("ValidatorProperties") + .HasColumnType("jsonb") + .HasColumnName("validator_properties"); + + b.Property("WorkGroupId") + .HasColumnType("integer") + .HasColumnName("work_group_id"); + + b.HasKey("Id") + .HasName("pk_subscription"); + + b.HasIndex("AggregationForId") + .HasDatabaseName("ix_subscription_aggregation_for_id"); + + b.HasIndex("CategoryId") + .HasDatabaseName("ix_subscription_category_id"); + + b.HasIndex("DocumentId") + .HasDatabaseName("ix_subscription_document_id"); + + b.HasIndex("PartnerId") + .HasDatabaseName("ix_subscription_partner_id"); + + b.HasIndex("ResponseSubscriptionId") + .HasDatabaseName("ix_subscription_response_subscription_id"); + + b.HasIndex("RetryPolicyId") + .HasDatabaseName("ix_subscription_retry_policy_id"); + + b.HasIndex("WorkGroupId") + .HasDatabaseName("ix_subscription_work_group_id"); + + b.ToTable("subscription", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Code") + .HasColumnType("text") + .HasColumnName("code"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.HasKey("Id") + .HasName("pk_subscription_category"); + + b.HasIndex("Code") + .IsUnique() + .HasDatabaseName("ix_subscription_category_code"); + + b.ToTable("subscription_category", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionTrail", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("Code") + .HasColumnType("integer") + .HasColumnName("code"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("StateAfter") + .HasColumnType("text") + .HasColumnName("state_after"); + + b.Property("StateBefore") + .HasColumnType("text") + .HasColumnName("state_before"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.HasKey("Id") + .HasName("pk_subscription_trail"); + + b.HasIndex("CreatedOn") + .HasDatabaseName("ix_subscription_trail_created_on"); + + b.HasIndex("SubscriptionId") + .HasDatabaseName("ix_subscription_trail_subscription_id"); + + b.ToTable("subscription_trail", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.WorkGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BusMessageName") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("character varying(100)") + .HasColumnName("bus_message_name"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("Options") + .HasColumnType("jsonb") + .HasColumnName("options"); + + b.HasKey("Id") + .HasName("pk_work_group"); + + b.ToTable("work_group", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("CorrelationId") + .HasColumnType("text") + .HasColumnName("correlation_id"); + + b.Property("DocumentId") + .HasColumnType("integer") + .HasColumnName("document_id"); + + b.Property("HandlerId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("handler_id"); + + b.Property>("HandlerProperties") + .HasColumnType("jsonb") + .HasColumnName("handler_properties"); + + b.Property("InputContentType") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("input_content_type"); + + b.Property("InputHash") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("input_hash"); + + b.Property("InputName") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("input_name"); + + b.Property("InputSize") + .HasColumnType("integer") + .HasColumnName("input_size"); + + b.Property("ManualRetry") + .HasColumnType("boolean") + .HasColumnName("manual_retry"); + + b.Property("MapperId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("mapper_id"); + + b.Property>("MapperProperties") + .HasColumnType("jsonb") + .HasColumnName("mapper_properties"); + + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.Property("References") + .HasColumnType("text[]") + .HasColumnName("references"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("response_message_type_name"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("integer") + .HasColumnName("response_subscription_id"); + + b.Property("RetryFor") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("retry_for"); + + b.Property("StartedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("started_on"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.HasKey("Id") + .HasName("pk_xchange"); + + b.HasIndex("DocumentId") + .HasDatabaseName("ix_xchange_document_id"); + + b.HasIndex("InputHash") + .HasDatabaseName("ix_xchange_input_hash"); + + b.HasIndex("RetryFor") + .HasDatabaseName("ix_xchange_retry_for"); + + b.HasIndex("StartedOn") + .HasDatabaseName("ix_xchange_started_on"); + + b.HasIndex("SubscriptionId") + .HasDatabaseName("ix_xchange_subscription_id"); + + b.ToTable("xchange", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("AggregatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("aggregated_on"); + + b.Property("AggregationXchangeId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("aggregation_xchange_id"); + + b.HasKey("Id") + .HasName("pk_xchange_aggregation"); + + b.HasIndex("AggregationXchangeId") + .HasDatabaseName("ix_xchange_aggregation_aggregation_xchange_id"); + + b.ToTable("xchange_aggregation", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("DeliveredOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("delivered_on"); + + b.HasKey("Id") + .HasName("pk_xchange_delivery"); + + b.HasIndex("DeliveredOn") + .HasDatabaseName("ix_xchange_delivery_delivered_on"); + + b.ToTable("xchange_delivery", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeNotification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Exception") + .HasColumnType("text") + .HasColumnName("exception"); + + b.Property("FinishedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("finished_on"); + + b.Property("NotifierId") + .HasColumnType("integer") + .HasColumnName("notifier_id"); + + b.Property("NotifierName") + .HasColumnType("text") + .HasColumnName("notifier_name"); + + b.Property("Success") + .HasColumnType("boolean") + .HasColumnName("success"); + + b.Property("XchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("character varying(50)") + .HasColumnName("xchange_id"); + + b.HasKey("Id") + .HasName("pk_xchange_notification"); + + b.ToTable("xchange_notification", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("Hits") + .HasColumnType("integer[]") + .HasColumnName("hits"); + + b.Property>("Properties") + .HasColumnType("jsonb") + .HasColumnName("properties"); + + b.Property("PropertiesRaw") + .HasColumnType("text") + .HasColumnName("properties_raw"); + + b.HasKey("Id") + .HasName("pk_xchange_promoted_properties"); + + b.HasIndex("PropertiesRaw") + .HasDatabaseName("ix_xchange_promoted_properties_properties_raw"); + + b.ToTable("xchange_promoted_properties", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("AttemptNumber") + .HasColumnType("integer") + .HasColumnName("attempt_number"); + + b.Property("Exception") + .HasColumnType("text") + .HasColumnName("exception"); + + b.Property("FinishedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("finished_on"); + + b.Property("OutputBad") + .HasColumnType("boolean") + .HasColumnName("output_bad"); + + b.Property("OutputContentType") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("output_content_type"); + + b.Property("OutputHash") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("output_hash"); + + b.Property("OutputName") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("output_name"); + + b.Property("OutputSize") + .HasColumnType("integer") + .HasColumnName("output_size"); + + b.Property("ResponseBad") + .HasColumnType("boolean") + .HasColumnName("response_bad"); + + b.Property("ResponseContentType") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("response_content_type"); + + b.Property("ResponseHash") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("response_hash"); + + b.Property("ResponseName") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("response_name"); + + b.Property("ResponseSize") + .HasColumnType("integer") + .HasColumnName("response_size"); + + b.Property("ResponseXchangeId") + .HasColumnType("text") + .HasColumnName("response_xchange_id"); + + b.Property("RetryBlockedReason") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("retry_blocked_reason"); + + b.Property("RetryGroupId") + .HasColumnType("uuid") + .HasColumnName("retry_group_id"); + + b.Property("Success") + .HasColumnType("boolean") + .HasColumnName("success"); + + b.HasKey("Id") + .HasName("pk_xchange_result"); + + b.HasIndex("RetryGroupId") + .HasDatabaseName("ix_xchange_result_retry_group_id"); + + b.ToTable("xchange_result", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.RunFlagUpdater+RunningResult", b => + { + b.Property("IsRunning") + .HasColumnType("boolean") + .HasColumnName("is_running"); + + b.ToTable("running_result", "infolink"); + + b.ToView(null, (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.JobExecution", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Context") + .HasColumnType("text") + .HasColumnName("context"); + + b.Property("DurationMs") + .HasColumnType("bigint") + .HasColumnName("duration_ms"); + + b.Property("EndTimeUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("end_time_utc"); + + b.Property("Error") + .HasColumnType("text") + .HasColumnName("error"); + + b.Property("FireInstanceId") + .IsRequired() + .HasColumnType("text") + .HasColumnName("fire_instance_id"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property("JobTypeName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_type_name"); + + b.Property("Node") + .IsRequired() + .HasColumnType("text") + .HasColumnName("node"); + + b.Property("StartTimeUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("start_time_utc"); + + b.Property("Success") + .HasColumnType("boolean") + .HasColumnName("success"); + + b.HasKey("Id") + .HasName("pk_job_executions"); + + b.HasIndex("FireInstanceId") + .IsUnique() + .HasDatabaseName("idx_je_fire_instance_id"); + + b.HasIndex("StartTimeUtc") + .HasDatabaseName("idx_je_start_time"); + + b.HasIndex("Success") + .HasDatabaseName("idx_je_success"); + + b.HasIndex("JobGroup", "JobName", "StartTimeUtc") + .HasDatabaseName("idx_je_group_name_start"); + + b.ToTable("job_executions", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("BlobData") + .HasColumnType("bytea") + .HasColumnName("blob_data"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup") + .HasName("pk_qrtz_blob_triggers"); + + b.ToTable("qrtz_blob_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCalendar", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("CalendarName") + .HasColumnType("text") + .HasColumnName("calendar_name"); + + b.Property("Calendar") + .IsRequired() + .HasColumnType("bytea") + .HasColumnName("calendar"); + + b.HasKey("SchedulerName", "CalendarName") + .HasName("pk_qrtz_calendars"); + + b.ToTable("qrtz_calendars", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("CronExpression") + .IsRequired() + .HasColumnType("text") + .HasColumnName("cron_expression"); + + b.Property("TimeZoneId") + .HasColumnType("text") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup") + .HasName("pk_qrtz_cron_triggers"); + + b.ToTable("qrtz_cron_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzFiredTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("EntryId") + .HasColumnType("text") + .HasColumnName("entry_id"); + + b.Property("FiredTime") + .HasColumnType("bigint") + .HasColumnName("fired_time"); + + b.Property("InstanceName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("instance_name"); + + b.Property("IsNonConcurrent") + .HasColumnType("bool") + .HasColumnName("is_nonconcurrent"); + + b.Property("JobGroup") + .HasColumnType("text") + .HasColumnName("job_group"); + + b.Property("JobName") + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property("Priority") + .HasColumnType("integer") + .HasColumnName("priority"); + + b.Property("RequestsRecovery") + .HasColumnType("bool") + .HasColumnName("requests_recovery"); + + b.Property("ScheduledTime") + .HasColumnType("bigint") + .HasColumnName("sched_time"); + + b.Property("State") + .IsRequired() + .HasColumnType("text") + .HasColumnName("state"); + + b.Property("TriggerGroup") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("TriggerName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.HasKey("SchedulerName", "EntryId") + .HasName("pk_qrtz_fired_triggers"); + + b.HasIndex("InstanceName") + .HasDatabaseName("idx_qrtz_ft_trig_inst_name"); + + b.HasIndex("JobGroup") + .HasDatabaseName("idx_qrtz_ft_job_group"); + + b.HasIndex("JobName") + .HasDatabaseName("idx_qrtz_ft_job_name"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_qrtz_ft_job_req_recovery"); + + b.HasIndex("TriggerGroup") + .HasDatabaseName("idx_qrtz_ft_trig_group"); + + b.HasIndex("TriggerName") + .HasDatabaseName("idx_qrtz_ft_trig_name"); + + b.HasIndex("SchedulerName", "TriggerName", "TriggerGroup") + .HasDatabaseName("idx_qrtz_ft_trig_nm_gp"); + + b.ToTable("qrtz_fired_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("JobName") + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property("JobGroup") + .HasColumnType("text") + .HasColumnName("job_group"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("IsDurable") + .HasColumnType("bool") + .HasColumnName("is_durable"); + + b.Property("IsNonConcurrent") + .HasColumnType("bool") + .HasColumnName("is_nonconcurrent"); + + b.Property("IsUpdateData") + .HasColumnType("bool") + .HasColumnName("is_update_data"); + + b.Property("JobClassName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_class_name"); + + b.Property("JobData") + .HasColumnType("bytea") + .HasColumnName("job_data"); + + b.Property("RequestsRecovery") + .HasColumnType("bool") + .HasColumnName("requests_recovery"); + + b.HasKey("SchedulerName", "JobName", "JobGroup") + .HasName("pk_qrtz_job_details"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_j_req_recovery"); + + b.ToTable("qrtz_job_details", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzLock", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("LockName") + .HasColumnType("text") + .HasColumnName("lock_name"); + + b.HasKey("SchedulerName", "LockName") + .HasName("pk_qrtz_locks"); + + b.ToTable("qrtz_locks", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzPausedTriggerGroup", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.HasKey("SchedulerName", "TriggerGroup") + .HasName("pk_qrtz_paused_trigger_grps"); + + b.ToTable("qrtz_paused_trigger_grps", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSchedulerState", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("InstanceName") + .HasColumnType("text") + .HasColumnName("instance_name"); + + b.Property("CheckInInterval") + .HasColumnType("bigint") + .HasColumnName("checkin_interval"); + + b.Property("LastCheckInTime") + .HasColumnType("bigint") + .HasColumnName("last_checkin_time"); + + b.HasKey("SchedulerName", "InstanceName") + .HasName("pk_qrtz_scheduler_state"); + + b.ToTable("qrtz_scheduler_state", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("BooleanProperty1") + .HasColumnType("bool") + .HasColumnName("bool_prop_1"); + + b.Property("BooleanProperty2") + .HasColumnType("bool") + .HasColumnName("bool_prop_2"); + + b.Property("DecimalProperty1") + .HasColumnType("numeric") + .HasColumnName("dec_prop_1"); + + b.Property("DecimalProperty2") + .HasColumnType("numeric") + .HasColumnName("dec_prop_2"); + + b.Property("IntegerProperty1") + .HasColumnType("integer") + .HasColumnName("int_prop_1"); + + b.Property("IntegerProperty2") + .HasColumnType("integer") + .HasColumnName("int_prop_2"); + + b.Property("LongProperty1") + .HasColumnType("bigint") + .HasColumnName("long_prop_1"); + + b.Property("LongProperty2") + .HasColumnType("bigint") + .HasColumnName("long_prop_2"); + + b.Property("StringProperty1") + .HasColumnType("text") + .HasColumnName("str_prop_1"); + + b.Property("StringProperty2") + .HasColumnType("text") + .HasColumnName("str_prop_2"); + + b.Property("StringProperty3") + .HasColumnType("text") + .HasColumnName("str_prop_3"); + + b.Property("TimeZoneId") + .HasColumnType("text") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup") + .HasName("pk_qrtz_simprop_triggers"); + + b.ToTable("qrtz_simprop_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("RepeatCount") + .HasColumnType("bigint") + .HasColumnName("repeat_count"); + + b.Property("RepeatInterval") + .HasColumnType("bigint") + .HasColumnName("repeat_interval"); + + b.Property("TimesTriggered") + .HasColumnType("bigint") + .HasColumnName("times_triggered"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup") + .HasName("pk_qrtz_simple_triggers"); + + b.ToTable("qrtz_simple_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("CalendarName") + .HasColumnType("text") + .HasColumnName("calendar_name"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("EndTime") + .HasColumnType("bigint") + .HasColumnName("end_time"); + + b.Property("JobData") + .HasColumnType("bytea") + .HasColumnName("job_data"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property("MisfireInstruction") + .HasColumnType("integer") + .HasColumnName("misfire_instr"); + + b.Property("NextFireTime") + .HasColumnType("bigint") + .HasColumnName("next_fire_time"); + + b.Property("PreviousFireTime") + .HasColumnType("bigint") + .HasColumnName("prev_fire_time"); + + b.Property("Priority") + .HasColumnType("integer") + .HasColumnName("priority"); + + b.Property("StartTime") + .HasColumnType("bigint") + .HasColumnName("start_time"); + + b.Property("TriggerState") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_state"); + + b.Property("TriggerType") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_type"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup") + .HasName("pk_qrtz_triggers"); + + b.HasIndex("NextFireTime") + .HasDatabaseName("idx_t_next_fire_time"); + + b.HasIndex("TriggerState") + .HasDatabaseName("idx_t_state"); + + b.HasIndex("NextFireTime", "TriggerState") + .HasDatabaseName("idx_t_nft_st"); + + b.HasIndex("SchedulerName", "JobName", "JobGroup") + .HasDatabaseName("ix_qrtz_triggers_sched_name_job_name_job_group"); + + b.ToTable("qrtz_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_refresh_tokens_accounts_account_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DocumentTrail", b => + { + b.HasOne("SW.Bitween.Domain.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_document_trail_document_document_id"); + + b.Navigation("Document"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.ApiGateway", "ApiGateway") + .WithMany("Partners") + .HasForeignKey("ApiGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_api_gateway_partner_api_gateway_api_gateway_id"); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_api_gateway_partner_partner_partner_id"); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_api_gateway_partner_subscription_subscription_id"); + + b.Navigation("ApiGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_bus_gateway_document_document_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.BusGateway", "BusGateway") + .WithMany("Routes") + .HasForeignKey("BusGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_bus_gateway_route_bus_gateway_bus_gateway_id"); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_bus_gateway_route_partner_partner_id"); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_bus_gateway_route_subscription_subscription_id"); + + b.Navigation("BusGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.OwnsMany("SW.Bitween.Domain.ApiCredential", "ApiCredentials", b1 => + { + b1.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("Key") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("key"); + + b1.Property("Name") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("name"); + + b1.HasKey("PartnerId", "Id") + .HasName("pk_partner_api_credential"); + + b1.HasIndex("Key") + .IsUnique() + .HasDatabaseName("ix_partner_api_credential_key"); + + b1.ToTable("partner_api_credential", "infolink"); + + b1.WithOwner() + .HasForeignKey("PartnerId") + .HasConstraintName("fk_partner_api_credential_partner_partner_id"); + + b1.HasData( + new + { + PartnerId = 1, + Id = 1, + Key = "7facc758283844b49cc4ffd26a75b1de", + Name = "default" + }); + }); + + b.Navigation("ApiCredentials"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.HasOne("SW.Bitween.Domain.Subscription", null) + .WithMany() + .HasForeignKey("AggregationForId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_subscription_aggregation_for"); + + b.HasOne("SW.Bitween.Domain.SubscriptionCategory", "Category") + .WithMany() + .HasForeignKey("CategoryId") + .HasConstraintName("fk_subscription_subscription_category_category_id"); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_subscription_document_document_id"); + + b.HasOne("SW.Bitween.Domain.Partner", null) + .WithMany("Subscriptions") + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_subscription_partner_partner_id"); + + b.HasOne("SW.Bitween.Domain.Subscription", null) + .WithMany() + .HasForeignKey("ResponseSubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_subscription_response_subscriber"); + + b.HasOne("SW.Bitween.Domain.RetryPolicy", "RetryPolicy") + .WithMany() + .HasForeignKey("RetryPolicyId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_subscription_retry_policy_retry_policy_id"); + + b.HasOne("SW.Bitween.Domain.WorkGroup", "WorkGroup") + .WithMany() + .HasForeignKey("WorkGroupId") + .HasConstraintName("fk_subscription_work_group_work_group_id"); + + b.OwnsMany("SW.Bitween.Domain.Schedule", "Schedules", b1 => + { + b1.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("Backwards") + .HasColumnType("boolean") + .HasColumnName("backwards"); + + b1.Property("On") + .HasColumnType("bigint") + .HasColumnName("on"); + + b1.Property("Recurrence") + .HasColumnType("smallint") + .HasColumnName("recurrence"); + + b1.HasKey("SubscriptionId", "Id") + .HasName("pk_subscription_schedule"); + + b1.ToTable("subscription_schedule", "infolink"); + + b1.WithOwner() + .HasForeignKey("SubscriptionId") + .HasConstraintName("fk_subscription_schedule_subscription_subscription_id"); + }); + + b.Navigation("Category"); + + b.Navigation("RetryPolicy"); + + b.Navigation("Schedules"); + + b.Navigation("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionTrail", b => + { + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_subscription_trail_subscription_subscription_id"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_xchange_document_document_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeAggregation", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_xchange_aggregation_xchange_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeDelivery", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_xchange_delivery_xchange_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangePromotedProperties", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_xchange_promoted_properties_xchange_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeResult", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_xchange_result_xchange_id"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("BlobTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_qrtz_blob_triggers_qrtz_triggers_sched_name_trigger_name_tr"); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("CronTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_qrtz_cron_triggers_qrtz_triggers_sched_name_trigger_name_tr"); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimplePropertyTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_qrtz_simprop_triggers_qrtz_triggers_sched_name_trigger_name"); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimpleTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_qrtz_simple_triggers_qrtz_triggers_sched_name_trigger_name_"); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzJobDetail", "JobDetail") + .WithMany("Triggers") + .HasForeignKey("SchedulerName", "JobName", "JobGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_qrtz_triggers_qrtz_job_details_sched_name_job_name_job_group"); + + b.Navigation("JobDetail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Navigation("Partners"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Navigation("Routes"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Navigation("Subscriptions"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Navigation("Triggers"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Navigation("BlobTriggers"); + + b.Navigation("CronTriggers"); + + b.Navigation("SimplePropertyTriggers"); + + b.Navigation("SimpleTriggers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/SW.Bitween.PgSql/Migrations/20260819100439_ManualRetryFlag.cs b/SW.Bitween.PgSql/Migrations/20260819100439_ManualRetryFlag.cs new file mode 100644 index 00000000..31efce51 --- /dev/null +++ b/SW.Bitween.PgSql/Migrations/20260819100439_ManualRetryFlag.cs @@ -0,0 +1,31 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace SW.Bitween.PgSql.Migrations +{ + /// + public partial class ManualRetryFlag : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "manual_retry", + schema: "infolink", + table: "xchange", + type: "boolean", + nullable: false, + defaultValue: false); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "manual_retry", + schema: "infolink", + table: "xchange"); + } + } +} diff --git a/SW.Bitween.PgSql/Migrations/20260823104201_GatewayInactiveFlag.Designer.cs b/SW.Bitween.PgSql/Migrations/20260823104201_GatewayInactiveFlag.Designer.cs new file mode 100644 index 00000000..dccb27e0 --- /dev/null +++ b/SW.Bitween.PgSql/Migrations/20260823104201_GatewayInactiveFlag.Designer.cs @@ -0,0 +1,2413 @@ +// +using System; +using System.Collections.Generic; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using SW.Bitween.Model; +using SW.Bitween.PgSql; + +#nullable disable + +namespace SW.Bitween.PgSql.Migrations +{ + [DbContext(typeof(BitweenDbContext))] + [Migration("20260823104201_GatewayInactiveFlag")] + partial class GatewayInactiveFlag + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("infolink") + .HasAnnotation("ProductVersion", "8.0.23") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Deleted") + .HasColumnType("boolean") + .HasColumnName("deleted"); + + b.Property("Disabled") + .HasColumnType("boolean") + .HasColumnName("disabled"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("display_name"); + + b.Property("Email") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("character varying(200)") + .HasColumnName("email"); + + b.Property("EmailProvider") + .HasColumnType("smallint") + .HasColumnName("email_provider"); + + b.Property("FailedLoginCount") + .HasColumnType("integer") + .HasColumnName("failed_login_count"); + + b.Property("LockoutEnd") + .HasColumnType("timestamp with time zone") + .HasColumnName("lockout_end"); + + b.Property("LoginMethods") + .HasColumnType("smallint") + .HasColumnName("login_methods"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Password") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("character varying(500)") + .HasColumnName("password"); + + b.Property("Role") + .HasColumnType("integer") + .HasColumnName("role"); + + b.HasKey("Id") + .HasName("pk_accounts"); + + b.HasIndex("Email") + .IsUnique() + .HasDatabaseName("ix_accounts_email"); + + b.ToTable("Accounts", "infolink"); + + b.HasData( + new + { + Id = 9999, + CreatedOn = new DateTime(2021, 12, 31, 22, 0, 0, 0, DateTimeKind.Utc), + Deleted = false, + Disabled = false, + DisplayName = "Admin", + Email = "admin@Bitween.systems", + EmailProvider = (byte)0, + FailedLoginCount = 0, + LoginMethods = (byte)2, + Password = "$SWHASH$V1$10000$VQCi48eitH4Ml5juvBMOFZrMdQwBbhuIQVXe6RR7qJdDF2bJ", + Role = 0 + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.AccountRoleLink", b => + { + b.Property("AccountId") + .HasColumnType("integer") + .HasColumnName("account_id"); + + b.Property("RoleId") + .HasColumnType("integer") + .HasColumnName("role_id"); + + b.HasKey("AccountId", "RoleId") + .HasName("pk_account_roles"); + + b.HasIndex("RoleId") + .HasDatabaseName("ix_account_roles_role_id"); + + b.ToTable("AccountRoles", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("AccountId") + .HasColumnType("integer") + .HasColumnName("account_id"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("LoginMethod") + .HasColumnType("smallint") + .HasColumnName("login_method"); + + b.HasKey("Id") + .HasName("pk_refresh_tokens"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_refresh_tokens_account_id"); + + b.ToTable("RefreshTokens", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("description"); + + b.Property("IsSystem") + .HasColumnType("boolean") + .HasColumnName("is_system"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("Permissions") + .HasColumnType("text") + .HasColumnName("permissions"); + + b.HasKey("Id") + .HasName("pk_roles"); + + b.HasIndex("Name") + .IsUnique() + .HasDatabaseName("ix_roles_name"); + + b.ToTable("Roles", "infolink"); + + b.HasData( + new + { + Id = 1, + CreatedOn = new DateTime(2021, 12, 31, 22, 0, 0, 0, DateTimeKind.Utc), + Description = "Full access to everything, including members, roles and settings.", + IsSystem = true, + Name = "Administrator", + Permissions = "[]" + }, + new + { + Id = 2, + CreatedOn = new DateTime(2021, 12, 31, 22, 0, 0, 0, DateTimeKind.Utc), + Description = "Runs and configures integrations. Can't manage members, roles or settings.", + IsSystem = true, + Name = "Member", + Permissions = "[]" + }, + new + { + Id = 3, + CreatedOn = new DateTime(2021, 12, 31, 22, 0, 0, 0, DateTimeKind.Utc), + Description = "Read-only access to integrations, exchanges and configuration.", + IsSystem = true, + Name = "Viewer", + Permissions = "[]" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DelayedRetry", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("On") + .HasColumnType("timestamp with time zone") + .HasColumnName("on"); + + b.HasKey("Id") + .HasName("pk_delayed_retry"); + + b.HasIndex("On") + .HasDatabaseName("ix_delayed_retry_on"); + + b.ToTable("delayed_retry", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Document", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BusEnabled") + .HasColumnType("boolean") + .HasColumnName("bus_enabled"); + + b.Property("BusMessageTypeName") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("bus_message_type_name"); + + b.Property("Code") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("code"); + + b.Property("DisregardsUnfilteredMessages") + .HasColumnType("boolean") + .HasColumnName("disregards_unfiltered_messages"); + + b.Property("DocumentFormat") + .HasColumnType("integer") + .HasColumnName("document_format"); + + b.Property("DuplicateInterval") + .HasColumnType("integer") + .HasColumnName("duplicate_interval"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("PromotedProperties") + .HasColumnType("jsonb") + .HasColumnName("promoted_properties"); + + b.HasKey("Id") + .HasName("pk_document"); + + b.HasIndex("BusMessageTypeName") + .IsUnique() + .HasDatabaseName("ix_document_bus_message_type_name"); + + b.HasIndex("Code") + .IsUnique() + .HasDatabaseName("ix_document_code"); + + b.HasIndex("Name") + .IsUnique() + .HasDatabaseName("ix_document_name"); + + b.ToTable("document", "infolink"); + + b.HasData( + new + { + Id = 10001, + BusEnabled = false, + DocumentFormat = 0, + DuplicateInterval = 0, + Name = "Aggregation Document", + PromotedProperties = "{}" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DocumentTrail", b => + { + b.Property("Id") + .HasColumnType("text") + .HasColumnName("id"); + + b.Property("Code") + .HasColumnType("integer") + .HasColumnName("code"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("DocumentId") + .HasColumnType("integer") + .HasColumnName("document_id"); + + b.Property("StateAfter") + .HasColumnType("text") + .HasColumnName("state_after"); + + b.Property("StateBefore") + .HasColumnType("text") + .HasColumnName("state_before"); + + b.HasKey("Id") + .HasName("pk_document_trail"); + + b.HasIndex("CreatedOn") + .HasDatabaseName("ix_document_trail_created_on"); + + b.HasIndex("DocumentId") + .HasDatabaseName("ix_document_trail_document_id"); + + b.ToTable("document_trail", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Inactive") + .HasColumnType("boolean") + .HasColumnName("inactive"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.Property("UrlName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("url_name"); + + b.HasKey("Id") + .HasName("pk_api_gateway"); + + b.HasIndex("UrlName") + .IsUnique() + .HasDatabaseName("ix_api_gateway_url_name"); + + b.ToTable("api_gateway", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.Property("ApiGatewayId") + .HasColumnType("integer") + .HasColumnName("api_gateway_id"); + + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.HasKey("ApiGatewayId", "PartnerId", "SubscriptionId") + .HasName("pk_api_gateway_partner"); + + b.HasIndex("PartnerId") + .HasDatabaseName("ix_api_gateway_partner_partner_id"); + + b.HasIndex("SubscriptionId") + .HasDatabaseName("ix_api_gateway_partner_subscription_id"); + + b.ToTable("api_gateway_partner", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("DocumentId") + .HasColumnType("integer") + .HasColumnName("document_id"); + + b.Property("Inactive") + .HasColumnType("boolean") + .HasColumnName("inactive"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("pk_bus_gateway"); + + b.HasIndex("DocumentId") + .HasDatabaseName("ix_bus_gateway_document_id"); + + b.ToTable("bus_gateway", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BusGatewayId") + .HasColumnType("integer") + .HasColumnName("bus_gateway_id"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("MatchExpression") + .HasColumnType("text") + .HasColumnName("match_expression"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.HasKey("Id") + .HasName("pk_bus_gateway_route"); + + b.HasIndex("BusGatewayId") + .HasDatabaseName("ix_bus_gateway_route_bus_gateway_id"); + + b.HasIndex("PartnerId") + .HasDatabaseName("ix_bus_gateway_route_partner_id"); + + b.HasIndex("SubscriptionId") + .HasDatabaseName("ix_bus_gateway_route_subscription_id"); + + b.ToTable("bus_gateway_route", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.GlobalAdapterValuesSet", b => + { + b.Property("Id") + .HasColumnType("text") + .HasColumnName("id"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property>("Values") + .HasColumnType("jsonb") + .HasColumnName("values"); + + b.HasKey("Id") + .HasName("pk_global_adapter_values_set"); + + b.ToTable("global_adapter_values_set", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Notifier", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("character varying(200)") + .HasColumnName("handler_id"); + + b.Property("HandlerProperties") + .HasColumnType("text") + .HasColumnName("handler_properties"); + + b.Property("Inactive") + .HasColumnType("boolean") + .HasColumnName("inactive"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("RunOnBadResult") + .HasColumnType("boolean") + .HasColumnName("run_on_bad_result"); + + b.Property("RunOnFailedResult") + .HasColumnType("boolean") + .HasColumnName("run_on_failed_result"); + + b.Property("RunOnSubscriptions") + .HasColumnType("integer[]") + .HasColumnName("run_on_subscriptions"); + + b.Property("RunOnSuccessfulResult") + .HasColumnType("boolean") + .HasColumnName("run_on_successful_result"); + + b.HasKey("Id") + .HasName("pk_notifier"); + + b.ToTable("notifier", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OnHoldXchange", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BadData") + .HasColumnType("boolean") + .HasColumnName("bad_data"); + + b.Property("Data") + .HasColumnType("text") + .HasColumnName("data"); + + b.Property("FileName") + .HasColumnType("text") + .HasColumnName("file_name"); + + b.Property("References") + .HasColumnType("text[]") + .HasColumnName("references"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.HasKey("Id") + .HasName("pk_on_hold_xchange"); + + b.HasIndex("SubscriptionId") + .HasDatabaseName("ix_on_hold_xchange_subscription_id"); + + b.ToTable("on_hold_xchange", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property>("AdapterProperties") + .HasColumnType("jsonb") + .HasColumnName("adapter_properties"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("pk_partner"); + + b.ToTable("partner", "infolink"); + + b.HasData( + new + { + Id = 1, + Name = "SYSTEM" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryAlertOverride", b => + { + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.Property("GroupId") + .HasColumnType("uuid") + .HasColumnName("group_id"); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("alert_handler_id"); + + b.Property("AlertHandlerProperties") + .HasColumnType("text") + .HasColumnName("alert_handler_properties"); + + b.Property("AlertMode") + .HasColumnType("smallint") + .HasColumnName("alert_mode"); + + b.HasKey("SubscriptionId", "GroupId") + .HasName("pk_retry_alert_override"); + + b.ToTable("retry_alert_override", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryGroupUsage", b => + { + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.Property("GroupId") + .HasColumnType("uuid") + .HasColumnName("group_id"); + + b.Property("AttemptsUsed") + .HasColumnType("integer") + .HasColumnName("attempts_used"); + + b.Property("ExhaustedNotifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("exhausted_notified_on"); + + b.Property("LastAttemptOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_attempt_on"); + + b.HasKey("SubscriptionId", "GroupId") + .HasName("pk_retry_group_usage"); + + b.ToTable("retry_group_usage", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryPolicy", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("alert_handler_id"); + + b.Property("AlertHandlerProperties") + .HasColumnType("text") + .HasColumnName("alert_handler_properties"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Groups") + .HasColumnType("text") + .HasColumnName("groups"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("pk_retry_policy"); + + b.ToTable("retry_policy", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Setting", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("character varying(200)") + .HasColumnName("id"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Value") + .HasColumnType("text") + .HasColumnName("value"); + + b.HasKey("Id") + .HasName("pk_settings"); + + b.ToTable("Settings", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AggregateOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("aggregate_on"); + + b.Property("AggregationForId") + .HasColumnType("integer") + .HasColumnName("aggregation_for_id"); + + b.Property("AggregationTarget") + .HasColumnType("smallint") + .HasColumnName("aggregation_target"); + + b.Property("CategoryId") + .HasColumnType("integer") + .HasColumnName("category_id"); + + b.Property("ConsecutiveFailures") + .HasColumnType("integer") + .HasColumnName("consecutive_failures"); + + b.Property("CustomRetryPolicy") + .HasColumnType("text") + .HasColumnName("custom_retry_policy"); + + b.Property>("DocumentFilter") + .HasColumnType("jsonb") + .HasColumnName("document_filter"); + + b.Property("DocumentId") + .HasColumnType("integer") + .HasColumnName("document_id"); + + b.Property("HandlerId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("handler_id"); + + b.Property>("HandlerProperties") + .HasColumnType("jsonb") + .HasColumnName("handler_properties"); + + b.Property("Inactive") + .HasColumnType("boolean") + .HasColumnName("inactive"); + + b.Property("IsRunning") + .HasColumnType("boolean") + .HasColumnName("is_running"); + + b.Property("LastException") + .HasColumnType("text") + .HasColumnName("last_exception"); + + b.Property("MapperId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("mapper_id"); + + b.Property>("MapperProperties") + .HasColumnType("jsonb") + .HasColumnName("mapper_properties"); + + b.Property("MatchExpression") + .HasColumnType("text") + .HasColumnName("match_expression"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.Property("PausedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("paused_on"); + + b.Property("ReceiveOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("receive_on"); + + b.Property("ReceiverId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("receiver_id"); + + b.Property>("ReceiverProperties") + .HasColumnType("jsonb") + .HasColumnName("receiver_properties"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("response_message_type_name"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("integer") + .HasColumnName("response_subscription_id"); + + b.Property("RetryPolicyId") + .HasColumnType("integer") + .HasColumnName("retry_policy_id"); + + b.Property("Temporary") + .HasColumnType("boolean") + .HasColumnName("temporary"); + + b.Property("Type") + .HasColumnType("smallint") + .HasColumnName("type"); + + b.Property("ValidatorId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("validator_id"); + + b.Property>("ValidatorProperties") + .HasColumnType("jsonb") + .HasColumnName("validator_properties"); + + b.Property("WorkGroupId") + .HasColumnType("integer") + .HasColumnName("work_group_id"); + + b.HasKey("Id") + .HasName("pk_subscription"); + + b.HasIndex("AggregationForId") + .HasDatabaseName("ix_subscription_aggregation_for_id"); + + b.HasIndex("CategoryId") + .HasDatabaseName("ix_subscription_category_id"); + + b.HasIndex("DocumentId") + .HasDatabaseName("ix_subscription_document_id"); + + b.HasIndex("PartnerId") + .HasDatabaseName("ix_subscription_partner_id"); + + b.HasIndex("ResponseSubscriptionId") + .HasDatabaseName("ix_subscription_response_subscription_id"); + + b.HasIndex("RetryPolicyId") + .HasDatabaseName("ix_subscription_retry_policy_id"); + + b.HasIndex("WorkGroupId") + .HasDatabaseName("ix_subscription_work_group_id"); + + b.ToTable("subscription", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Code") + .HasColumnType("text") + .HasColumnName("code"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.HasKey("Id") + .HasName("pk_subscription_category"); + + b.HasIndex("Code") + .IsUnique() + .HasDatabaseName("ix_subscription_category_code"); + + b.ToTable("subscription_category", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionTrail", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("Code") + .HasColumnType("integer") + .HasColumnName("code"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("StateAfter") + .HasColumnType("text") + .HasColumnName("state_after"); + + b.Property("StateBefore") + .HasColumnType("text") + .HasColumnName("state_before"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.HasKey("Id") + .HasName("pk_subscription_trail"); + + b.HasIndex("CreatedOn") + .HasDatabaseName("ix_subscription_trail_created_on"); + + b.HasIndex("SubscriptionId") + .HasDatabaseName("ix_subscription_trail_subscription_id"); + + b.ToTable("subscription_trail", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.WorkGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BusMessageName") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("character varying(100)") + .HasColumnName("bus_message_name"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("Options") + .HasColumnType("jsonb") + .HasColumnName("options"); + + b.HasKey("Id") + .HasName("pk_work_group"); + + b.ToTable("work_group", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("CorrelationId") + .HasColumnType("text") + .HasColumnName("correlation_id"); + + b.Property("DocumentId") + .HasColumnType("integer") + .HasColumnName("document_id"); + + b.Property("HandlerId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("handler_id"); + + b.Property>("HandlerProperties") + .HasColumnType("jsonb") + .HasColumnName("handler_properties"); + + b.Property("InputContentType") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("input_content_type"); + + b.Property("InputHash") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("input_hash"); + + b.Property("InputName") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("input_name"); + + b.Property("InputSize") + .HasColumnType("integer") + .HasColumnName("input_size"); + + b.Property("ManualRetry") + .HasColumnType("boolean") + .HasColumnName("manual_retry"); + + b.Property("MapperId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("mapper_id"); + + b.Property>("MapperProperties") + .HasColumnType("jsonb") + .HasColumnName("mapper_properties"); + + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.Property("References") + .HasColumnType("text[]") + .HasColumnName("references"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("response_message_type_name"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("integer") + .HasColumnName("response_subscription_id"); + + b.Property("RetryFor") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("retry_for"); + + b.Property("StartedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("started_on"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.HasKey("Id") + .HasName("pk_xchange"); + + b.HasIndex("DocumentId") + .HasDatabaseName("ix_xchange_document_id"); + + b.HasIndex("InputHash") + .HasDatabaseName("ix_xchange_input_hash"); + + b.HasIndex("RetryFor") + .HasDatabaseName("ix_xchange_retry_for"); + + b.HasIndex("StartedOn") + .HasDatabaseName("ix_xchange_started_on"); + + b.HasIndex("SubscriptionId") + .HasDatabaseName("ix_xchange_subscription_id"); + + b.ToTable("xchange", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("AggregatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("aggregated_on"); + + b.Property("AggregationXchangeId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("aggregation_xchange_id"); + + b.HasKey("Id") + .HasName("pk_xchange_aggregation"); + + b.HasIndex("AggregationXchangeId") + .HasDatabaseName("ix_xchange_aggregation_aggregation_xchange_id"); + + b.ToTable("xchange_aggregation", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("DeliveredOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("delivered_on"); + + b.HasKey("Id") + .HasName("pk_xchange_delivery"); + + b.HasIndex("DeliveredOn") + .HasDatabaseName("ix_xchange_delivery_delivered_on"); + + b.ToTable("xchange_delivery", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeNotification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Exception") + .HasColumnType("text") + .HasColumnName("exception"); + + b.Property("FinishedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("finished_on"); + + b.Property("NotifierId") + .HasColumnType("integer") + .HasColumnName("notifier_id"); + + b.Property("NotifierName") + .HasColumnType("text") + .HasColumnName("notifier_name"); + + b.Property("Success") + .HasColumnType("boolean") + .HasColumnName("success"); + + b.Property("XchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("character varying(50)") + .HasColumnName("xchange_id"); + + b.HasKey("Id") + .HasName("pk_xchange_notification"); + + b.ToTable("xchange_notification", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("Hits") + .HasColumnType("integer[]") + .HasColumnName("hits"); + + b.Property>("Properties") + .HasColumnType("jsonb") + .HasColumnName("properties"); + + b.Property("PropertiesRaw") + .HasColumnType("text") + .HasColumnName("properties_raw"); + + b.HasKey("Id") + .HasName("pk_xchange_promoted_properties"); + + b.HasIndex("PropertiesRaw") + .HasDatabaseName("ix_xchange_promoted_properties_properties_raw"); + + b.ToTable("xchange_promoted_properties", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("AttemptNumber") + .HasColumnType("integer") + .HasColumnName("attempt_number"); + + b.Property("Exception") + .HasColumnType("text") + .HasColumnName("exception"); + + b.Property("FinishedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("finished_on"); + + b.Property("OutputBad") + .HasColumnType("boolean") + .HasColumnName("output_bad"); + + b.Property("OutputContentType") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("output_content_type"); + + b.Property("OutputHash") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("output_hash"); + + b.Property("OutputName") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("output_name"); + + b.Property("OutputSize") + .HasColumnType("integer") + .HasColumnName("output_size"); + + b.Property("ResponseBad") + .HasColumnType("boolean") + .HasColumnName("response_bad"); + + b.Property("ResponseContentType") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("response_content_type"); + + b.Property("ResponseHash") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("response_hash"); + + b.Property("ResponseName") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("response_name"); + + b.Property("ResponseSize") + .HasColumnType("integer") + .HasColumnName("response_size"); + + b.Property("ResponseXchangeId") + .HasColumnType("text") + .HasColumnName("response_xchange_id"); + + b.Property("RetryBlockedReason") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("retry_blocked_reason"); + + b.Property("RetryGroupId") + .HasColumnType("uuid") + .HasColumnName("retry_group_id"); + + b.Property("Success") + .HasColumnType("boolean") + .HasColumnName("success"); + + b.HasKey("Id") + .HasName("pk_xchange_result"); + + b.HasIndex("RetryGroupId") + .HasDatabaseName("ix_xchange_result_retry_group_id"); + + b.ToTable("xchange_result", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.RunFlagUpdater+RunningResult", b => + { + b.Property("IsRunning") + .HasColumnType("boolean") + .HasColumnName("is_running"); + + b.ToTable("running_result", "infolink"); + + b.ToView(null, (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.JobExecution", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Context") + .HasColumnType("text") + .HasColumnName("context"); + + b.Property("DurationMs") + .HasColumnType("bigint") + .HasColumnName("duration_ms"); + + b.Property("EndTimeUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("end_time_utc"); + + b.Property("Error") + .HasColumnType("text") + .HasColumnName("error"); + + b.Property("FireInstanceId") + .IsRequired() + .HasColumnType("text") + .HasColumnName("fire_instance_id"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property("JobTypeName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_type_name"); + + b.Property("Node") + .IsRequired() + .HasColumnType("text") + .HasColumnName("node"); + + b.Property("StartTimeUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("start_time_utc"); + + b.Property("Success") + .HasColumnType("boolean") + .HasColumnName("success"); + + b.HasKey("Id") + .HasName("pk_job_executions"); + + b.HasIndex("FireInstanceId") + .IsUnique() + .HasDatabaseName("idx_je_fire_instance_id"); + + b.HasIndex("StartTimeUtc") + .HasDatabaseName("idx_je_start_time"); + + b.HasIndex("Success") + .HasDatabaseName("idx_je_success"); + + b.HasIndex("JobGroup", "JobName", "StartTimeUtc") + .HasDatabaseName("idx_je_group_name_start"); + + b.ToTable("job_executions", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("BlobData") + .HasColumnType("bytea") + .HasColumnName("blob_data"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup") + .HasName("pk_qrtz_blob_triggers"); + + b.ToTable("qrtz_blob_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCalendar", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("CalendarName") + .HasColumnType("text") + .HasColumnName("calendar_name"); + + b.Property("Calendar") + .IsRequired() + .HasColumnType("bytea") + .HasColumnName("calendar"); + + b.HasKey("SchedulerName", "CalendarName") + .HasName("pk_qrtz_calendars"); + + b.ToTable("qrtz_calendars", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("CronExpression") + .IsRequired() + .HasColumnType("text") + .HasColumnName("cron_expression"); + + b.Property("TimeZoneId") + .HasColumnType("text") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup") + .HasName("pk_qrtz_cron_triggers"); + + b.ToTable("qrtz_cron_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzFiredTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("EntryId") + .HasColumnType("text") + .HasColumnName("entry_id"); + + b.Property("FiredTime") + .HasColumnType("bigint") + .HasColumnName("fired_time"); + + b.Property("InstanceName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("instance_name"); + + b.Property("IsNonConcurrent") + .HasColumnType("bool") + .HasColumnName("is_nonconcurrent"); + + b.Property("JobGroup") + .HasColumnType("text") + .HasColumnName("job_group"); + + b.Property("JobName") + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property("Priority") + .HasColumnType("integer") + .HasColumnName("priority"); + + b.Property("RequestsRecovery") + .HasColumnType("bool") + .HasColumnName("requests_recovery"); + + b.Property("ScheduledTime") + .HasColumnType("bigint") + .HasColumnName("sched_time"); + + b.Property("State") + .IsRequired() + .HasColumnType("text") + .HasColumnName("state"); + + b.Property("TriggerGroup") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("TriggerName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.HasKey("SchedulerName", "EntryId") + .HasName("pk_qrtz_fired_triggers"); + + b.HasIndex("InstanceName") + .HasDatabaseName("idx_qrtz_ft_trig_inst_name"); + + b.HasIndex("JobGroup") + .HasDatabaseName("idx_qrtz_ft_job_group"); + + b.HasIndex("JobName") + .HasDatabaseName("idx_qrtz_ft_job_name"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_qrtz_ft_job_req_recovery"); + + b.HasIndex("TriggerGroup") + .HasDatabaseName("idx_qrtz_ft_trig_group"); + + b.HasIndex("TriggerName") + .HasDatabaseName("idx_qrtz_ft_trig_name"); + + b.HasIndex("SchedulerName", "TriggerName", "TriggerGroup") + .HasDatabaseName("idx_qrtz_ft_trig_nm_gp"); + + b.ToTable("qrtz_fired_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("JobName") + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property("JobGroup") + .HasColumnType("text") + .HasColumnName("job_group"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("IsDurable") + .HasColumnType("bool") + .HasColumnName("is_durable"); + + b.Property("IsNonConcurrent") + .HasColumnType("bool") + .HasColumnName("is_nonconcurrent"); + + b.Property("IsUpdateData") + .HasColumnType("bool") + .HasColumnName("is_update_data"); + + b.Property("JobClassName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_class_name"); + + b.Property("JobData") + .HasColumnType("bytea") + .HasColumnName("job_data"); + + b.Property("RequestsRecovery") + .HasColumnType("bool") + .HasColumnName("requests_recovery"); + + b.HasKey("SchedulerName", "JobName", "JobGroup") + .HasName("pk_qrtz_job_details"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_j_req_recovery"); + + b.ToTable("qrtz_job_details", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzLock", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("LockName") + .HasColumnType("text") + .HasColumnName("lock_name"); + + b.HasKey("SchedulerName", "LockName") + .HasName("pk_qrtz_locks"); + + b.ToTable("qrtz_locks", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzPausedTriggerGroup", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.HasKey("SchedulerName", "TriggerGroup") + .HasName("pk_qrtz_paused_trigger_grps"); + + b.ToTable("qrtz_paused_trigger_grps", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSchedulerState", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("InstanceName") + .HasColumnType("text") + .HasColumnName("instance_name"); + + b.Property("CheckInInterval") + .HasColumnType("bigint") + .HasColumnName("checkin_interval"); + + b.Property("LastCheckInTime") + .HasColumnType("bigint") + .HasColumnName("last_checkin_time"); + + b.HasKey("SchedulerName", "InstanceName") + .HasName("pk_qrtz_scheduler_state"); + + b.ToTable("qrtz_scheduler_state", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("BooleanProperty1") + .HasColumnType("bool") + .HasColumnName("bool_prop_1"); + + b.Property("BooleanProperty2") + .HasColumnType("bool") + .HasColumnName("bool_prop_2"); + + b.Property("DecimalProperty1") + .HasColumnType("numeric") + .HasColumnName("dec_prop_1"); + + b.Property("DecimalProperty2") + .HasColumnType("numeric") + .HasColumnName("dec_prop_2"); + + b.Property("IntegerProperty1") + .HasColumnType("integer") + .HasColumnName("int_prop_1"); + + b.Property("IntegerProperty2") + .HasColumnType("integer") + .HasColumnName("int_prop_2"); + + b.Property("LongProperty1") + .HasColumnType("bigint") + .HasColumnName("long_prop_1"); + + b.Property("LongProperty2") + .HasColumnType("bigint") + .HasColumnName("long_prop_2"); + + b.Property("StringProperty1") + .HasColumnType("text") + .HasColumnName("str_prop_1"); + + b.Property("StringProperty2") + .HasColumnType("text") + .HasColumnName("str_prop_2"); + + b.Property("StringProperty3") + .HasColumnType("text") + .HasColumnName("str_prop_3"); + + b.Property("TimeZoneId") + .HasColumnType("text") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup") + .HasName("pk_qrtz_simprop_triggers"); + + b.ToTable("qrtz_simprop_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("RepeatCount") + .HasColumnType("bigint") + .HasColumnName("repeat_count"); + + b.Property("RepeatInterval") + .HasColumnType("bigint") + .HasColumnName("repeat_interval"); + + b.Property("TimesTriggered") + .HasColumnType("bigint") + .HasColumnName("times_triggered"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup") + .HasName("pk_qrtz_simple_triggers"); + + b.ToTable("qrtz_simple_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("CalendarName") + .HasColumnType("text") + .HasColumnName("calendar_name"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("EndTime") + .HasColumnType("bigint") + .HasColumnName("end_time"); + + b.Property("JobData") + .HasColumnType("bytea") + .HasColumnName("job_data"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property("MisfireInstruction") + .HasColumnType("integer") + .HasColumnName("misfire_instr"); + + b.Property("NextFireTime") + .HasColumnType("bigint") + .HasColumnName("next_fire_time"); + + b.Property("PreviousFireTime") + .HasColumnType("bigint") + .HasColumnName("prev_fire_time"); + + b.Property("Priority") + .HasColumnType("integer") + .HasColumnName("priority"); + + b.Property("StartTime") + .HasColumnType("bigint") + .HasColumnName("start_time"); + + b.Property("TriggerState") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_state"); + + b.Property("TriggerType") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_type"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup") + .HasName("pk_qrtz_triggers"); + + b.HasIndex("NextFireTime") + .HasDatabaseName("idx_t_next_fire_time"); + + b.HasIndex("TriggerState") + .HasDatabaseName("idx_t_state"); + + b.HasIndex("NextFireTime", "TriggerState") + .HasDatabaseName("idx_t_nft_st"); + + b.HasIndex("SchedulerName", "JobName", "JobGroup") + .HasDatabaseName("ix_qrtz_triggers_sched_name_job_name_job_group"); + + b.ToTable("qrtz_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.AccountRoleLink", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_account_roles_accounts_account_id"); + + b.HasOne("SW.Bitween.Domain.Accounts.Role", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_account_roles_roles_role_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_refresh_tokens_accounts_account_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DocumentTrail", b => + { + b.HasOne("SW.Bitween.Domain.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_document_trail_document_document_id"); + + b.Navigation("Document"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.ApiGateway", "ApiGateway") + .WithMany("Partners") + .HasForeignKey("ApiGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_api_gateway_partner_api_gateway_api_gateway_id"); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_api_gateway_partner_partner_partner_id"); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_api_gateway_partner_subscription_subscription_id"); + + b.Navigation("ApiGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_bus_gateway_document_document_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.BusGateway", "BusGateway") + .WithMany("Routes") + .HasForeignKey("BusGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_bus_gateway_route_bus_gateway_bus_gateway_id"); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_bus_gateway_route_partner_partner_id"); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_bus_gateway_route_subscription_subscription_id"); + + b.Navigation("BusGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.OwnsMany("SW.Bitween.Domain.ApiCredential", "ApiCredentials", b1 => + { + b1.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("Key") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("key"); + + b1.Property("Name") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("name"); + + b1.HasKey("PartnerId", "Id") + .HasName("pk_partner_api_credential"); + + b1.HasIndex("Key") + .IsUnique() + .HasDatabaseName("ix_partner_api_credential_key"); + + b1.ToTable("partner_api_credential", "infolink"); + + b1.WithOwner() + .HasForeignKey("PartnerId") + .HasConstraintName("fk_partner_api_credential_partner_partner_id"); + + b1.HasData( + new + { + PartnerId = 1, + Id = 1, + Key = "7facc758283844b49cc4ffd26a75b1de", + Name = "default" + }); + }); + + b.Navigation("ApiCredentials"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.HasOne("SW.Bitween.Domain.Subscription", null) + .WithMany() + .HasForeignKey("AggregationForId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_subscription_aggregation_for"); + + b.HasOne("SW.Bitween.Domain.SubscriptionCategory", "Category") + .WithMany() + .HasForeignKey("CategoryId") + .HasConstraintName("fk_subscription_subscription_category_category_id"); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_subscription_document_document_id"); + + b.HasOne("SW.Bitween.Domain.Partner", null) + .WithMany("Subscriptions") + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_subscription_partner_partner_id"); + + b.HasOne("SW.Bitween.Domain.Subscription", null) + .WithMany() + .HasForeignKey("ResponseSubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_subscription_response_subscriber"); + + b.HasOne("SW.Bitween.Domain.RetryPolicy", "RetryPolicy") + .WithMany() + .HasForeignKey("RetryPolicyId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_subscription_retry_policy_retry_policy_id"); + + b.HasOne("SW.Bitween.Domain.WorkGroup", "WorkGroup") + .WithMany() + .HasForeignKey("WorkGroupId") + .HasConstraintName("fk_subscription_work_group_work_group_id"); + + b.OwnsMany("SW.Bitween.Domain.Schedule", "Schedules", b1 => + { + b1.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("Backwards") + .HasColumnType("boolean") + .HasColumnName("backwards"); + + b1.Property("On") + .HasColumnType("bigint") + .HasColumnName("on"); + + b1.Property("Recurrence") + .HasColumnType("smallint") + .HasColumnName("recurrence"); + + b1.HasKey("SubscriptionId", "Id") + .HasName("pk_subscription_schedule"); + + b1.ToTable("subscription_schedule", "infolink"); + + b1.WithOwner() + .HasForeignKey("SubscriptionId") + .HasConstraintName("fk_subscription_schedule_subscription_subscription_id"); + }); + + b.Navigation("Category"); + + b.Navigation("RetryPolicy"); + + b.Navigation("Schedules"); + + b.Navigation("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionTrail", b => + { + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_subscription_trail_subscription_subscription_id"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_xchange_document_document_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeAggregation", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_xchange_aggregation_xchange_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeDelivery", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_xchange_delivery_xchange_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangePromotedProperties", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_xchange_promoted_properties_xchange_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeResult", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_xchange_result_xchange_id"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("BlobTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_qrtz_blob_triggers_qrtz_triggers_sched_name_trigger_name_tr"); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("CronTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_qrtz_cron_triggers_qrtz_triggers_sched_name_trigger_name_tr"); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimplePropertyTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_qrtz_simprop_triggers_qrtz_triggers_sched_name_trigger_name"); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimpleTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_qrtz_simple_triggers_qrtz_triggers_sched_name_trigger_name_"); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzJobDetail", "JobDetail") + .WithMany("Triggers") + .HasForeignKey("SchedulerName", "JobName", "JobGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_qrtz_triggers_qrtz_job_details_sched_name_job_name_job_group"); + + b.Navigation("JobDetail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Navigation("Partners"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Navigation("Routes"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Navigation("Subscriptions"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Navigation("Triggers"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Navigation("BlobTriggers"); + + b.Navigation("CronTriggers"); + + b.Navigation("SimplePropertyTriggers"); + + b.Navigation("SimpleTriggers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/SW.Bitween.PgSql/Migrations/20260823104201_GatewayInactiveFlag.cs b/SW.Bitween.PgSql/Migrations/20260823104201_GatewayInactiveFlag.cs new file mode 100644 index 00000000..459832fe --- /dev/null +++ b/SW.Bitween.PgSql/Migrations/20260823104201_GatewayInactiveFlag.cs @@ -0,0 +1,44 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace SW.Bitween.PgSql.Migrations +{ + /// + public partial class GatewayInactiveFlag : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "inactive", + schema: "infolink", + table: "bus_gateway", + type: "boolean", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "inactive", + schema: "infolink", + table: "api_gateway", + type: "boolean", + nullable: false, + defaultValue: false); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "inactive", + schema: "infolink", + table: "bus_gateway"); + + migrationBuilder.DropColumn( + name: "inactive", + schema: "infolink", + table: "api_gateway"); + } + } +} diff --git a/SW.Bitween.PgSql/Migrations/20260824093537_AddReceiveAttempts.Designer.cs b/SW.Bitween.PgSql/Migrations/20260824093537_AddReceiveAttempts.Designer.cs new file mode 100644 index 00000000..c1722c67 --- /dev/null +++ b/SW.Bitween.PgSql/Migrations/20260824093537_AddReceiveAttempts.Designer.cs @@ -0,0 +1,2455 @@ +// +using System; +using System.Collections.Generic; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using SW.Bitween.Model; +using SW.Bitween.PgSql; + +#nullable disable + +namespace SW.Bitween.PgSql.Migrations +{ + [DbContext(typeof(BitweenDbContext))] + [Migration("20260824093537_AddReceiveAttempts")] + partial class AddReceiveAttempts + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("infolink") + .HasAnnotation("ProductVersion", "8.0.23") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Deleted") + .HasColumnType("boolean") + .HasColumnName("deleted"); + + b.Property("Disabled") + .HasColumnType("boolean") + .HasColumnName("disabled"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("display_name"); + + b.Property("Email") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("character varying(200)") + .HasColumnName("email"); + + b.Property("EmailProvider") + .HasColumnType("smallint") + .HasColumnName("email_provider"); + + b.Property("FailedLoginCount") + .HasColumnType("integer") + .HasColumnName("failed_login_count"); + + b.Property("LockoutEnd") + .HasColumnType("timestamp with time zone") + .HasColumnName("lockout_end"); + + b.Property("LoginMethods") + .HasColumnType("smallint") + .HasColumnName("login_methods"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Password") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("character varying(500)") + .HasColumnName("password"); + + b.Property("Role") + .HasColumnType("integer") + .HasColumnName("role"); + + b.HasKey("Id") + .HasName("pk_accounts"); + + b.HasIndex("Email") + .IsUnique() + .HasDatabaseName("ix_accounts_email"); + + b.ToTable("Accounts", "infolink"); + + b.HasData( + new + { + Id = 9999, + CreatedOn = new DateTime(2021, 12, 31, 22, 0, 0, 0, DateTimeKind.Utc), + Deleted = false, + Disabled = false, + DisplayName = "Admin", + Email = "admin@Bitween.systems", + EmailProvider = (byte)0, + FailedLoginCount = 0, + LoginMethods = (byte)2, + Password = "$SWHASH$V1$10000$VQCi48eitH4Ml5juvBMOFZrMdQwBbhuIQVXe6RR7qJdDF2bJ", + Role = 0 + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.AccountRoleLink", b => + { + b.Property("AccountId") + .HasColumnType("integer") + .HasColumnName("account_id"); + + b.Property("RoleId") + .HasColumnType("integer") + .HasColumnName("role_id"); + + b.HasKey("AccountId", "RoleId") + .HasName("pk_account_roles"); + + b.HasIndex("RoleId") + .HasDatabaseName("ix_account_roles_role_id"); + + b.ToTable("AccountRoles", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("AccountId") + .HasColumnType("integer") + .HasColumnName("account_id"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("LoginMethod") + .HasColumnType("smallint") + .HasColumnName("login_method"); + + b.HasKey("Id") + .HasName("pk_refresh_tokens"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_refresh_tokens_account_id"); + + b.ToTable("RefreshTokens", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("description"); + + b.Property("IsSystem") + .HasColumnType("boolean") + .HasColumnName("is_system"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("Permissions") + .HasColumnType("text") + .HasColumnName("permissions"); + + b.HasKey("Id") + .HasName("pk_roles"); + + b.HasIndex("Name") + .IsUnique() + .HasDatabaseName("ix_roles_name"); + + b.ToTable("Roles", "infolink"); + + b.HasData( + new + { + Id = 1, + CreatedOn = new DateTime(2021, 12, 31, 22, 0, 0, 0, DateTimeKind.Utc), + Description = "Full access to everything, including members, roles and settings.", + IsSystem = true, + Name = "Administrator", + Permissions = "[]" + }, + new + { + Id = 2, + CreatedOn = new DateTime(2021, 12, 31, 22, 0, 0, 0, DateTimeKind.Utc), + Description = "Runs and configures integrations. Can't manage members, roles or settings.", + IsSystem = true, + Name = "Member", + Permissions = "[]" + }, + new + { + Id = 3, + CreatedOn = new DateTime(2021, 12, 31, 22, 0, 0, 0, DateTimeKind.Utc), + Description = "Read-only access to integrations, exchanges and configuration.", + IsSystem = true, + Name = "Viewer", + Permissions = "[]" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DelayedRetry", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("On") + .HasColumnType("timestamp with time zone") + .HasColumnName("on"); + + b.HasKey("Id") + .HasName("pk_delayed_retry"); + + b.HasIndex("On") + .HasDatabaseName("ix_delayed_retry_on"); + + b.ToTable("delayed_retry", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Document", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BusEnabled") + .HasColumnType("boolean") + .HasColumnName("bus_enabled"); + + b.Property("BusMessageTypeName") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("bus_message_type_name"); + + b.Property("Code") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("code"); + + b.Property("DisregardsUnfilteredMessages") + .HasColumnType("boolean") + .HasColumnName("disregards_unfiltered_messages"); + + b.Property("DocumentFormat") + .HasColumnType("integer") + .HasColumnName("document_format"); + + b.Property("DuplicateInterval") + .HasColumnType("integer") + .HasColumnName("duplicate_interval"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("PromotedProperties") + .HasColumnType("jsonb") + .HasColumnName("promoted_properties"); + + b.HasKey("Id") + .HasName("pk_document"); + + b.HasIndex("BusMessageTypeName") + .IsUnique() + .HasDatabaseName("ix_document_bus_message_type_name"); + + b.HasIndex("Code") + .IsUnique() + .HasDatabaseName("ix_document_code"); + + b.HasIndex("Name") + .IsUnique() + .HasDatabaseName("ix_document_name"); + + b.ToTable("document", "infolink"); + + b.HasData( + new + { + Id = 10001, + BusEnabled = false, + DocumentFormat = 0, + DuplicateInterval = 0, + Name = "Aggregation Document", + PromotedProperties = "{}" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DocumentTrail", b => + { + b.Property("Id") + .HasColumnType("text") + .HasColumnName("id"); + + b.Property("Code") + .HasColumnType("integer") + .HasColumnName("code"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("DocumentId") + .HasColumnType("integer") + .HasColumnName("document_id"); + + b.Property("StateAfter") + .HasColumnType("text") + .HasColumnName("state_after"); + + b.Property("StateBefore") + .HasColumnType("text") + .HasColumnName("state_before"); + + b.HasKey("Id") + .HasName("pk_document_trail"); + + b.HasIndex("CreatedOn") + .HasDatabaseName("ix_document_trail_created_on"); + + b.HasIndex("DocumentId") + .HasDatabaseName("ix_document_trail_document_id"); + + b.ToTable("document_trail", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Inactive") + .HasColumnType("boolean") + .HasColumnName("inactive"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.Property("UrlName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("url_name"); + + b.HasKey("Id") + .HasName("pk_api_gateway"); + + b.HasIndex("UrlName") + .IsUnique() + .HasDatabaseName("ix_api_gateway_url_name"); + + b.ToTable("api_gateway", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.Property("ApiGatewayId") + .HasColumnType("integer") + .HasColumnName("api_gateway_id"); + + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.HasKey("ApiGatewayId", "PartnerId", "SubscriptionId") + .HasName("pk_api_gateway_partner"); + + b.HasIndex("PartnerId") + .HasDatabaseName("ix_api_gateway_partner_partner_id"); + + b.HasIndex("SubscriptionId") + .HasDatabaseName("ix_api_gateway_partner_subscription_id"); + + b.ToTable("api_gateway_partner", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("DocumentId") + .HasColumnType("integer") + .HasColumnName("document_id"); + + b.Property("Inactive") + .HasColumnType("boolean") + .HasColumnName("inactive"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("pk_bus_gateway"); + + b.HasIndex("DocumentId") + .HasDatabaseName("ix_bus_gateway_document_id"); + + b.ToTable("bus_gateway", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BusGatewayId") + .HasColumnType("integer") + .HasColumnName("bus_gateway_id"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("MatchExpression") + .HasColumnType("text") + .HasColumnName("match_expression"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.HasKey("Id") + .HasName("pk_bus_gateway_route"); + + b.HasIndex("BusGatewayId") + .HasDatabaseName("ix_bus_gateway_route_bus_gateway_id"); + + b.HasIndex("PartnerId") + .HasDatabaseName("ix_bus_gateway_route_partner_id"); + + b.HasIndex("SubscriptionId") + .HasDatabaseName("ix_bus_gateway_route_subscription_id"); + + b.ToTable("bus_gateway_route", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.GlobalAdapterValuesSet", b => + { + b.Property("Id") + .HasColumnType("text") + .HasColumnName("id"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property>("Values") + .HasColumnType("jsonb") + .HasColumnName("values"); + + b.HasKey("Id") + .HasName("pk_global_adapter_values_set"); + + b.ToTable("global_adapter_values_set", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Notifier", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("character varying(200)") + .HasColumnName("handler_id"); + + b.Property("HandlerProperties") + .HasColumnType("text") + .HasColumnName("handler_properties"); + + b.Property("Inactive") + .HasColumnType("boolean") + .HasColumnName("inactive"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("RunOnBadResult") + .HasColumnType("boolean") + .HasColumnName("run_on_bad_result"); + + b.Property("RunOnFailedResult") + .HasColumnType("boolean") + .HasColumnName("run_on_failed_result"); + + b.Property("RunOnSubscriptions") + .HasColumnType("integer[]") + .HasColumnName("run_on_subscriptions"); + + b.Property("RunOnSuccessfulResult") + .HasColumnType("boolean") + .HasColumnName("run_on_successful_result"); + + b.HasKey("Id") + .HasName("pk_notifier"); + + b.ToTable("notifier", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OnHoldXchange", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BadData") + .HasColumnType("boolean") + .HasColumnName("bad_data"); + + b.Property("Data") + .HasColumnType("text") + .HasColumnName("data"); + + b.Property("FileName") + .HasColumnType("text") + .HasColumnName("file_name"); + + b.Property("References") + .HasColumnType("text[]") + .HasColumnName("references"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.HasKey("Id") + .HasName("pk_on_hold_xchange"); + + b.HasIndex("SubscriptionId") + .HasDatabaseName("ix_on_hold_xchange_subscription_id"); + + b.ToTable("on_hold_xchange", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property>("AdapterProperties") + .HasColumnType("jsonb") + .HasColumnName("adapter_properties"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("pk_partner"); + + b.ToTable("partner", "infolink"); + + b.HasData( + new + { + Id = 1, + Name = "SYSTEM" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.ReceiveAttempt", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ErrorMessage") + .HasColumnType("text") + .HasColumnName("error_message"); + + b.Property("ExchangeIds") + .HasColumnType("text[]") + .HasColumnName("exchange_ids"); + + b.Property("FinishedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("finished_on"); + + b.Property("Outcome") + .HasColumnType("integer") + .HasColumnName("outcome"); + + b.Property("StartedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("started_on"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.HasKey("Id") + .HasName("pk_receive_attempt"); + + b.HasIndex("SubscriptionId", "StartedOn") + .HasDatabaseName("ix_receive_attempt_subscription_id_started_on"); + + b.ToTable("receive_attempt", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryAlertOverride", b => + { + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.Property("GroupId") + .HasColumnType("uuid") + .HasColumnName("group_id"); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("alert_handler_id"); + + b.Property("AlertHandlerProperties") + .HasColumnType("text") + .HasColumnName("alert_handler_properties"); + + b.Property("AlertMode") + .HasColumnType("smallint") + .HasColumnName("alert_mode"); + + b.HasKey("SubscriptionId", "GroupId") + .HasName("pk_retry_alert_override"); + + b.ToTable("retry_alert_override", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryGroupUsage", b => + { + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.Property("GroupId") + .HasColumnType("uuid") + .HasColumnName("group_id"); + + b.Property("AttemptsUsed") + .HasColumnType("integer") + .HasColumnName("attempts_used"); + + b.Property("ExhaustedNotifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("exhausted_notified_on"); + + b.Property("LastAttemptOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_attempt_on"); + + b.HasKey("SubscriptionId", "GroupId") + .HasName("pk_retry_group_usage"); + + b.ToTable("retry_group_usage", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryPolicy", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("alert_handler_id"); + + b.Property("AlertHandlerProperties") + .HasColumnType("text") + .HasColumnName("alert_handler_properties"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Groups") + .HasColumnType("text") + .HasColumnName("groups"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("pk_retry_policy"); + + b.ToTable("retry_policy", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Setting", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("character varying(200)") + .HasColumnName("id"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Value") + .HasColumnType("text") + .HasColumnName("value"); + + b.HasKey("Id") + .HasName("pk_settings"); + + b.ToTable("Settings", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AggregateOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("aggregate_on"); + + b.Property("AggregationForId") + .HasColumnType("integer") + .HasColumnName("aggregation_for_id"); + + b.Property("AggregationTarget") + .HasColumnType("smallint") + .HasColumnName("aggregation_target"); + + b.Property("CategoryId") + .HasColumnType("integer") + .HasColumnName("category_id"); + + b.Property("ConsecutiveFailures") + .HasColumnType("integer") + .HasColumnName("consecutive_failures"); + + b.Property("CustomRetryPolicy") + .HasColumnType("text") + .HasColumnName("custom_retry_policy"); + + b.Property>("DocumentFilter") + .HasColumnType("jsonb") + .HasColumnName("document_filter"); + + b.Property("DocumentId") + .HasColumnType("integer") + .HasColumnName("document_id"); + + b.Property("HandlerId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("handler_id"); + + b.Property>("HandlerProperties") + .HasColumnType("jsonb") + .HasColumnName("handler_properties"); + + b.Property("Inactive") + .HasColumnType("boolean") + .HasColumnName("inactive"); + + b.Property("IsRunning") + .HasColumnType("boolean") + .HasColumnName("is_running"); + + b.Property("LastException") + .HasColumnType("text") + .HasColumnName("last_exception"); + + b.Property("MapperId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("mapper_id"); + + b.Property>("MapperProperties") + .HasColumnType("jsonb") + .HasColumnName("mapper_properties"); + + b.Property("MatchExpression") + .HasColumnType("text") + .HasColumnName("match_expression"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.Property("PausedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("paused_on"); + + b.Property("ReceiveOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("receive_on"); + + b.Property("ReceiverId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("receiver_id"); + + b.Property>("ReceiverProperties") + .HasColumnType("jsonb") + .HasColumnName("receiver_properties"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("response_message_type_name"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("integer") + .HasColumnName("response_subscription_id"); + + b.Property("RetryPolicyId") + .HasColumnType("integer") + .HasColumnName("retry_policy_id"); + + b.Property("Temporary") + .HasColumnType("boolean") + .HasColumnName("temporary"); + + b.Property("Type") + .HasColumnType("smallint") + .HasColumnName("type"); + + b.Property("ValidatorId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("validator_id"); + + b.Property>("ValidatorProperties") + .HasColumnType("jsonb") + .HasColumnName("validator_properties"); + + b.Property("WorkGroupId") + .HasColumnType("integer") + .HasColumnName("work_group_id"); + + b.HasKey("Id") + .HasName("pk_subscription"); + + b.HasIndex("AggregationForId") + .HasDatabaseName("ix_subscription_aggregation_for_id"); + + b.HasIndex("CategoryId") + .HasDatabaseName("ix_subscription_category_id"); + + b.HasIndex("DocumentId") + .HasDatabaseName("ix_subscription_document_id"); + + b.HasIndex("PartnerId") + .HasDatabaseName("ix_subscription_partner_id"); + + b.HasIndex("ResponseSubscriptionId") + .HasDatabaseName("ix_subscription_response_subscription_id"); + + b.HasIndex("RetryPolicyId") + .HasDatabaseName("ix_subscription_retry_policy_id"); + + b.HasIndex("WorkGroupId") + .HasDatabaseName("ix_subscription_work_group_id"); + + b.ToTable("subscription", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Code") + .HasColumnType("text") + .HasColumnName("code"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.HasKey("Id") + .HasName("pk_subscription_category"); + + b.HasIndex("Code") + .IsUnique() + .HasDatabaseName("ix_subscription_category_code"); + + b.ToTable("subscription_category", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionTrail", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("Code") + .HasColumnType("integer") + .HasColumnName("code"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("StateAfter") + .HasColumnType("text") + .HasColumnName("state_after"); + + b.Property("StateBefore") + .HasColumnType("text") + .HasColumnName("state_before"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.HasKey("Id") + .HasName("pk_subscription_trail"); + + b.HasIndex("CreatedOn") + .HasDatabaseName("ix_subscription_trail_created_on"); + + b.HasIndex("SubscriptionId") + .HasDatabaseName("ix_subscription_trail_subscription_id"); + + b.ToTable("subscription_trail", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.WorkGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BusMessageName") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("character varying(100)") + .HasColumnName("bus_message_name"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("Options") + .HasColumnType("jsonb") + .HasColumnName("options"); + + b.HasKey("Id") + .HasName("pk_work_group"); + + b.ToTable("work_group", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("CorrelationId") + .HasColumnType("text") + .HasColumnName("correlation_id"); + + b.Property("DocumentId") + .HasColumnType("integer") + .HasColumnName("document_id"); + + b.Property("HandlerId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("handler_id"); + + b.Property>("HandlerProperties") + .HasColumnType("jsonb") + .HasColumnName("handler_properties"); + + b.Property("InputContentType") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("input_content_type"); + + b.Property("InputHash") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("input_hash"); + + b.Property("InputName") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("input_name"); + + b.Property("InputSize") + .HasColumnType("integer") + .HasColumnName("input_size"); + + b.Property("ManualRetry") + .HasColumnType("boolean") + .HasColumnName("manual_retry"); + + b.Property("MapperId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("mapper_id"); + + b.Property>("MapperProperties") + .HasColumnType("jsonb") + .HasColumnName("mapper_properties"); + + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.Property("References") + .HasColumnType("text[]") + .HasColumnName("references"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("response_message_type_name"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("integer") + .HasColumnName("response_subscription_id"); + + b.Property("RetryFor") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("retry_for"); + + b.Property("StartedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("started_on"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.HasKey("Id") + .HasName("pk_xchange"); + + b.HasIndex("DocumentId") + .HasDatabaseName("ix_xchange_document_id"); + + b.HasIndex("InputHash") + .HasDatabaseName("ix_xchange_input_hash"); + + b.HasIndex("RetryFor") + .HasDatabaseName("ix_xchange_retry_for"); + + b.HasIndex("StartedOn") + .HasDatabaseName("ix_xchange_started_on"); + + b.HasIndex("SubscriptionId") + .HasDatabaseName("ix_xchange_subscription_id"); + + b.ToTable("xchange", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("AggregatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("aggregated_on"); + + b.Property("AggregationXchangeId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("aggregation_xchange_id"); + + b.HasKey("Id") + .HasName("pk_xchange_aggregation"); + + b.HasIndex("AggregationXchangeId") + .HasDatabaseName("ix_xchange_aggregation_aggregation_xchange_id"); + + b.ToTable("xchange_aggregation", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("DeliveredOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("delivered_on"); + + b.HasKey("Id") + .HasName("pk_xchange_delivery"); + + b.HasIndex("DeliveredOn") + .HasDatabaseName("ix_xchange_delivery_delivered_on"); + + b.ToTable("xchange_delivery", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeNotification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Exception") + .HasColumnType("text") + .HasColumnName("exception"); + + b.Property("FinishedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("finished_on"); + + b.Property("NotifierId") + .HasColumnType("integer") + .HasColumnName("notifier_id"); + + b.Property("NotifierName") + .HasColumnType("text") + .HasColumnName("notifier_name"); + + b.Property("Success") + .HasColumnType("boolean") + .HasColumnName("success"); + + b.Property("XchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("character varying(50)") + .HasColumnName("xchange_id"); + + b.HasKey("Id") + .HasName("pk_xchange_notification"); + + b.ToTable("xchange_notification", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("Hits") + .HasColumnType("integer[]") + .HasColumnName("hits"); + + b.Property>("Properties") + .HasColumnType("jsonb") + .HasColumnName("properties"); + + b.Property("PropertiesRaw") + .HasColumnType("text") + .HasColumnName("properties_raw"); + + b.HasKey("Id") + .HasName("pk_xchange_promoted_properties"); + + b.HasIndex("PropertiesRaw") + .HasDatabaseName("ix_xchange_promoted_properties_properties_raw"); + + b.ToTable("xchange_promoted_properties", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("AttemptNumber") + .HasColumnType("integer") + .HasColumnName("attempt_number"); + + b.Property("Exception") + .HasColumnType("text") + .HasColumnName("exception"); + + b.Property("FinishedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("finished_on"); + + b.Property("OutputBad") + .HasColumnType("boolean") + .HasColumnName("output_bad"); + + b.Property("OutputContentType") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("output_content_type"); + + b.Property("OutputHash") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("output_hash"); + + b.Property("OutputName") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("output_name"); + + b.Property("OutputSize") + .HasColumnType("integer") + .HasColumnName("output_size"); + + b.Property("ResponseBad") + .HasColumnType("boolean") + .HasColumnName("response_bad"); + + b.Property("ResponseContentType") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("response_content_type"); + + b.Property("ResponseHash") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("response_hash"); + + b.Property("ResponseName") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("response_name"); + + b.Property("ResponseSize") + .HasColumnType("integer") + .HasColumnName("response_size"); + + b.Property("ResponseXchangeId") + .HasColumnType("text") + .HasColumnName("response_xchange_id"); + + b.Property("RetryBlockedReason") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("retry_blocked_reason"); + + b.Property("RetryGroupId") + .HasColumnType("uuid") + .HasColumnName("retry_group_id"); + + b.Property("Success") + .HasColumnType("boolean") + .HasColumnName("success"); + + b.HasKey("Id") + .HasName("pk_xchange_result"); + + b.HasIndex("RetryGroupId") + .HasDatabaseName("ix_xchange_result_retry_group_id"); + + b.ToTable("xchange_result", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.RunFlagUpdater+RunningResult", b => + { + b.Property("IsRunning") + .HasColumnType("boolean") + .HasColumnName("is_running"); + + b.ToTable("running_result", "infolink"); + + b.ToView(null, (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.JobExecution", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Context") + .HasColumnType("text") + .HasColumnName("context"); + + b.Property("DurationMs") + .HasColumnType("bigint") + .HasColumnName("duration_ms"); + + b.Property("EndTimeUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("end_time_utc"); + + b.Property("Error") + .HasColumnType("text") + .HasColumnName("error"); + + b.Property("FireInstanceId") + .IsRequired() + .HasColumnType("text") + .HasColumnName("fire_instance_id"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property("JobTypeName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_type_name"); + + b.Property("Node") + .IsRequired() + .HasColumnType("text") + .HasColumnName("node"); + + b.Property("StartTimeUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("start_time_utc"); + + b.Property("Success") + .HasColumnType("boolean") + .HasColumnName("success"); + + b.HasKey("Id") + .HasName("pk_job_executions"); + + b.HasIndex("FireInstanceId") + .IsUnique() + .HasDatabaseName("idx_je_fire_instance_id"); + + b.HasIndex("StartTimeUtc") + .HasDatabaseName("idx_je_start_time"); + + b.HasIndex("Success") + .HasDatabaseName("idx_je_success"); + + b.HasIndex("JobGroup", "JobName", "StartTimeUtc") + .HasDatabaseName("idx_je_group_name_start"); + + b.ToTable("job_executions", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("BlobData") + .HasColumnType("bytea") + .HasColumnName("blob_data"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup") + .HasName("pk_qrtz_blob_triggers"); + + b.ToTable("qrtz_blob_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCalendar", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("CalendarName") + .HasColumnType("text") + .HasColumnName("calendar_name"); + + b.Property("Calendar") + .IsRequired() + .HasColumnType("bytea") + .HasColumnName("calendar"); + + b.HasKey("SchedulerName", "CalendarName") + .HasName("pk_qrtz_calendars"); + + b.ToTable("qrtz_calendars", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("CronExpression") + .IsRequired() + .HasColumnType("text") + .HasColumnName("cron_expression"); + + b.Property("TimeZoneId") + .HasColumnType("text") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup") + .HasName("pk_qrtz_cron_triggers"); + + b.ToTable("qrtz_cron_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzFiredTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("EntryId") + .HasColumnType("text") + .HasColumnName("entry_id"); + + b.Property("FiredTime") + .HasColumnType("bigint") + .HasColumnName("fired_time"); + + b.Property("InstanceName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("instance_name"); + + b.Property("IsNonConcurrent") + .HasColumnType("bool") + .HasColumnName("is_nonconcurrent"); + + b.Property("JobGroup") + .HasColumnType("text") + .HasColumnName("job_group"); + + b.Property("JobName") + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property("Priority") + .HasColumnType("integer") + .HasColumnName("priority"); + + b.Property("RequestsRecovery") + .HasColumnType("bool") + .HasColumnName("requests_recovery"); + + b.Property("ScheduledTime") + .HasColumnType("bigint") + .HasColumnName("sched_time"); + + b.Property("State") + .IsRequired() + .HasColumnType("text") + .HasColumnName("state"); + + b.Property("TriggerGroup") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("TriggerName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.HasKey("SchedulerName", "EntryId") + .HasName("pk_qrtz_fired_triggers"); + + b.HasIndex("InstanceName") + .HasDatabaseName("idx_qrtz_ft_trig_inst_name"); + + b.HasIndex("JobGroup") + .HasDatabaseName("idx_qrtz_ft_job_group"); + + b.HasIndex("JobName") + .HasDatabaseName("idx_qrtz_ft_job_name"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_qrtz_ft_job_req_recovery"); + + b.HasIndex("TriggerGroup") + .HasDatabaseName("idx_qrtz_ft_trig_group"); + + b.HasIndex("TriggerName") + .HasDatabaseName("idx_qrtz_ft_trig_name"); + + b.HasIndex("SchedulerName", "TriggerName", "TriggerGroup") + .HasDatabaseName("idx_qrtz_ft_trig_nm_gp"); + + b.ToTable("qrtz_fired_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("JobName") + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property("JobGroup") + .HasColumnType("text") + .HasColumnName("job_group"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("IsDurable") + .HasColumnType("bool") + .HasColumnName("is_durable"); + + b.Property("IsNonConcurrent") + .HasColumnType("bool") + .HasColumnName("is_nonconcurrent"); + + b.Property("IsUpdateData") + .HasColumnType("bool") + .HasColumnName("is_update_data"); + + b.Property("JobClassName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_class_name"); + + b.Property("JobData") + .HasColumnType("bytea") + .HasColumnName("job_data"); + + b.Property("RequestsRecovery") + .HasColumnType("bool") + .HasColumnName("requests_recovery"); + + b.HasKey("SchedulerName", "JobName", "JobGroup") + .HasName("pk_qrtz_job_details"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_j_req_recovery"); + + b.ToTable("qrtz_job_details", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzLock", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("LockName") + .HasColumnType("text") + .HasColumnName("lock_name"); + + b.HasKey("SchedulerName", "LockName") + .HasName("pk_qrtz_locks"); + + b.ToTable("qrtz_locks", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzPausedTriggerGroup", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.HasKey("SchedulerName", "TriggerGroup") + .HasName("pk_qrtz_paused_trigger_grps"); + + b.ToTable("qrtz_paused_trigger_grps", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSchedulerState", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("InstanceName") + .HasColumnType("text") + .HasColumnName("instance_name"); + + b.Property("CheckInInterval") + .HasColumnType("bigint") + .HasColumnName("checkin_interval"); + + b.Property("LastCheckInTime") + .HasColumnType("bigint") + .HasColumnName("last_checkin_time"); + + b.HasKey("SchedulerName", "InstanceName") + .HasName("pk_qrtz_scheduler_state"); + + b.ToTable("qrtz_scheduler_state", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("BooleanProperty1") + .HasColumnType("bool") + .HasColumnName("bool_prop_1"); + + b.Property("BooleanProperty2") + .HasColumnType("bool") + .HasColumnName("bool_prop_2"); + + b.Property("DecimalProperty1") + .HasColumnType("numeric") + .HasColumnName("dec_prop_1"); + + b.Property("DecimalProperty2") + .HasColumnType("numeric") + .HasColumnName("dec_prop_2"); + + b.Property("IntegerProperty1") + .HasColumnType("integer") + .HasColumnName("int_prop_1"); + + b.Property("IntegerProperty2") + .HasColumnType("integer") + .HasColumnName("int_prop_2"); + + b.Property("LongProperty1") + .HasColumnType("bigint") + .HasColumnName("long_prop_1"); + + b.Property("LongProperty2") + .HasColumnType("bigint") + .HasColumnName("long_prop_2"); + + b.Property("StringProperty1") + .HasColumnType("text") + .HasColumnName("str_prop_1"); + + b.Property("StringProperty2") + .HasColumnType("text") + .HasColumnName("str_prop_2"); + + b.Property("StringProperty3") + .HasColumnType("text") + .HasColumnName("str_prop_3"); + + b.Property("TimeZoneId") + .HasColumnType("text") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup") + .HasName("pk_qrtz_simprop_triggers"); + + b.ToTable("qrtz_simprop_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("RepeatCount") + .HasColumnType("bigint") + .HasColumnName("repeat_count"); + + b.Property("RepeatInterval") + .HasColumnType("bigint") + .HasColumnName("repeat_interval"); + + b.Property("TimesTriggered") + .HasColumnType("bigint") + .HasColumnName("times_triggered"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup") + .HasName("pk_qrtz_simple_triggers"); + + b.ToTable("qrtz_simple_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("CalendarName") + .HasColumnType("text") + .HasColumnName("calendar_name"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("EndTime") + .HasColumnType("bigint") + .HasColumnName("end_time"); + + b.Property("JobData") + .HasColumnType("bytea") + .HasColumnName("job_data"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property("MisfireInstruction") + .HasColumnType("integer") + .HasColumnName("misfire_instr"); + + b.Property("NextFireTime") + .HasColumnType("bigint") + .HasColumnName("next_fire_time"); + + b.Property("PreviousFireTime") + .HasColumnType("bigint") + .HasColumnName("prev_fire_time"); + + b.Property("Priority") + .HasColumnType("integer") + .HasColumnName("priority"); + + b.Property("StartTime") + .HasColumnType("bigint") + .HasColumnName("start_time"); + + b.Property("TriggerState") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_state"); + + b.Property("TriggerType") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_type"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup") + .HasName("pk_qrtz_triggers"); + + b.HasIndex("NextFireTime") + .HasDatabaseName("idx_t_next_fire_time"); + + b.HasIndex("TriggerState") + .HasDatabaseName("idx_t_state"); + + b.HasIndex("NextFireTime", "TriggerState") + .HasDatabaseName("idx_t_nft_st"); + + b.HasIndex("SchedulerName", "JobName", "JobGroup") + .HasDatabaseName("ix_qrtz_triggers_sched_name_job_name_job_group"); + + b.ToTable("qrtz_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.AccountRoleLink", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_account_roles_accounts_account_id"); + + b.HasOne("SW.Bitween.Domain.Accounts.Role", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_account_roles_roles_role_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_refresh_tokens_accounts_account_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DocumentTrail", b => + { + b.HasOne("SW.Bitween.Domain.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_document_trail_document_document_id"); + + b.Navigation("Document"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.ApiGateway", "ApiGateway") + .WithMany("Partners") + .HasForeignKey("ApiGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_api_gateway_partner_api_gateway_api_gateway_id"); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_api_gateway_partner_partner_partner_id"); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_api_gateway_partner_subscription_subscription_id"); + + b.Navigation("ApiGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_bus_gateway_document_document_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.BusGateway", "BusGateway") + .WithMany("Routes") + .HasForeignKey("BusGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_bus_gateway_route_bus_gateway_bus_gateway_id"); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_bus_gateway_route_partner_partner_id"); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_bus_gateway_route_subscription_subscription_id"); + + b.Navigation("BusGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.OwnsMany("SW.Bitween.Domain.ApiCredential", "ApiCredentials", b1 => + { + b1.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("Key") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("key"); + + b1.Property("Name") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("name"); + + b1.HasKey("PartnerId", "Id") + .HasName("pk_partner_api_credential"); + + b1.HasIndex("Key") + .IsUnique() + .HasDatabaseName("ix_partner_api_credential_key"); + + b1.ToTable("partner_api_credential", "infolink"); + + b1.WithOwner() + .HasForeignKey("PartnerId") + .HasConstraintName("fk_partner_api_credential_partner_partner_id"); + + b1.HasData( + new + { + PartnerId = 1, + Id = 1, + Key = "7facc758283844b49cc4ffd26a75b1de", + Name = "default" + }); + }); + + b.Navigation("ApiCredentials"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.HasOne("SW.Bitween.Domain.Subscription", null) + .WithMany() + .HasForeignKey("AggregationForId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_subscription_aggregation_for"); + + b.HasOne("SW.Bitween.Domain.SubscriptionCategory", "Category") + .WithMany() + .HasForeignKey("CategoryId") + .HasConstraintName("fk_subscription_subscription_category_category_id"); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_subscription_document_document_id"); + + b.HasOne("SW.Bitween.Domain.Partner", null) + .WithMany("Subscriptions") + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_subscription_partner_partner_id"); + + b.HasOne("SW.Bitween.Domain.Subscription", null) + .WithMany() + .HasForeignKey("ResponseSubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_subscription_response_subscriber"); + + b.HasOne("SW.Bitween.Domain.RetryPolicy", "RetryPolicy") + .WithMany() + .HasForeignKey("RetryPolicyId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_subscription_retry_policy_retry_policy_id"); + + b.HasOne("SW.Bitween.Domain.WorkGroup", "WorkGroup") + .WithMany() + .HasForeignKey("WorkGroupId") + .HasConstraintName("fk_subscription_work_group_work_group_id"); + + b.OwnsMany("SW.Bitween.Domain.Schedule", "Schedules", b1 => + { + b1.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("Backwards") + .HasColumnType("boolean") + .HasColumnName("backwards"); + + b1.Property("On") + .HasColumnType("bigint") + .HasColumnName("on"); + + b1.Property("Recurrence") + .HasColumnType("smallint") + .HasColumnName("recurrence"); + + b1.HasKey("SubscriptionId", "Id") + .HasName("pk_subscription_schedule"); + + b1.ToTable("subscription_schedule", "infolink"); + + b1.WithOwner() + .HasForeignKey("SubscriptionId") + .HasConstraintName("fk_subscription_schedule_subscription_subscription_id"); + }); + + b.Navigation("Category"); + + b.Navigation("RetryPolicy"); + + b.Navigation("Schedules"); + + b.Navigation("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionTrail", b => + { + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_subscription_trail_subscription_subscription_id"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_xchange_document_document_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeAggregation", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_xchange_aggregation_xchange_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeDelivery", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_xchange_delivery_xchange_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangePromotedProperties", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_xchange_promoted_properties_xchange_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeResult", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_xchange_result_xchange_id"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("BlobTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_qrtz_blob_triggers_qrtz_triggers_sched_name_trigger_name_tr"); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("CronTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_qrtz_cron_triggers_qrtz_triggers_sched_name_trigger_name_tr"); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimplePropertyTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_qrtz_simprop_triggers_qrtz_triggers_sched_name_trigger_name"); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimpleTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_qrtz_simple_triggers_qrtz_triggers_sched_name_trigger_name_"); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzJobDetail", "JobDetail") + .WithMany("Triggers") + .HasForeignKey("SchedulerName", "JobName", "JobGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_qrtz_triggers_qrtz_job_details_sched_name_job_name_job_group"); + + b.Navigation("JobDetail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Navigation("Partners"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Navigation("Routes"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Navigation("Subscriptions"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Navigation("Triggers"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Navigation("BlobTriggers"); + + b.Navigation("CronTriggers"); + + b.Navigation("SimplePropertyTriggers"); + + b.Navigation("SimpleTriggers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/SW.Bitween.PgSql/Migrations/20260824093537_AddReceiveAttempts.cs b/SW.Bitween.PgSql/Migrations/20260824093537_AddReceiveAttempts.cs new file mode 100644 index 00000000..cee14373 --- /dev/null +++ b/SW.Bitween.PgSql/Migrations/20260824093537_AddReceiveAttempts.cs @@ -0,0 +1,49 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace SW.Bitween.PgSql.Migrations +{ + /// + public partial class AddReceiveAttempts : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "receive_attempt", + schema: "infolink", + columns: table => new + { + id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + subscription_id = table.Column(type: "integer", nullable: false), + started_on = table.Column(type: "timestamp with time zone", nullable: false), + finished_on = table.Column(type: "timestamp with time zone", nullable: false), + outcome = table.Column(type: "integer", nullable: false), + error_message = table.Column(type: "text", nullable: true), + exchange_ids = table.Column(type: "text[]", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("pk_receive_attempt", x => x.id); + }); + + migrationBuilder.CreateIndex( + name: "ix_receive_attempt_subscription_id_started_on", + schema: "infolink", + table: "receive_attempt", + columns: new[] { "subscription_id", "started_on" }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "receive_attempt", + schema: "infolink"); + } + } +} diff --git a/SW.Bitween.PgSql/Migrations/BitweenDbContextModelSnapshot.cs b/SW.Bitween.PgSql/Migrations/BitweenDbContextModelSnapshot.cs index 70683aaf..1a37f6f0 100644 --- a/SW.Bitween.PgSql/Migrations/BitweenDbContextModelSnapshot.cs +++ b/SW.Bitween.PgSql/Migrations/BitweenDbContextModelSnapshot.cs @@ -66,6 +66,14 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("smallint") .HasColumnName("email_provider"); + b.Property("FailedLoginCount") + .HasColumnType("integer") + .HasColumnName("failed_login_count"); + + b.Property("LockoutEnd") + .HasColumnType("timestamp with time zone") + .HasColumnName("lockout_end"); + b.Property("LoginMethods") .HasColumnType("smallint") .HasColumnName("login_methods"); @@ -107,6 +115,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) DisplayName = "Admin", Email = "admin@Bitween.systems", EmailProvider = (byte)0, + FailedLoginCount = 0, LoginMethods = (byte)2, Password = "$SWHASH$V1$10000$VQCi48eitH4Ml5juvBMOFZrMdQwBbhuIQVXe6RR7qJdDF2bJ", Role = 0 @@ -251,10 +260,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("character varying(50)") .HasColumnName("id"); - b.Property>("GroupAttemptCounts") - .HasColumnType("jsonb") - .HasColumnName("group_attempt_counts"); - b.Property("On") .HasColumnType("timestamp with time zone") .HasColumnName("on"); @@ -401,6 +406,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("timestamp with time zone") .HasColumnName("created_on"); + b.Property("Inactive") + .HasColumnType("boolean") + .HasColumnName("inactive"); + b.Property("ModifiedBy") .HasColumnType("text") .HasColumnName("modified_by"); @@ -494,6 +503,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("integer") .HasColumnName("document_id"); + b.Property("Inactive") + .HasColumnType("boolean") + .HasColumnName("inactive"); + b.Property("ModifiedBy") .HasColumnType("text") .HasColumnName("modified_by"); @@ -714,6 +727,105 @@ protected override void BuildModel(ModelBuilder modelBuilder) }); }); + modelBuilder.Entity("SW.Bitween.Domain.ReceiveAttempt", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ErrorMessage") + .HasColumnType("text") + .HasColumnName("error_message"); + + b.Property("ExchangeIds") + .HasColumnType("text[]") + .HasColumnName("exchange_ids"); + + b.Property("FinishedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("finished_on"); + + b.Property("Outcome") + .HasColumnType("integer") + .HasColumnName("outcome"); + + b.Property("StartedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("started_on"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.HasKey("Id") + .HasName("pk_receive_attempt"); + + b.HasIndex("SubscriptionId", "StartedOn") + .HasDatabaseName("ix_receive_attempt_subscription_id_started_on"); + + b.ToTable("receive_attempt", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryAlertOverride", b => + { + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.Property("GroupId") + .HasColumnType("uuid") + .HasColumnName("group_id"); + + b.Property("AlertHandlerId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("alert_handler_id"); + + b.Property("AlertHandlerProperties") + .HasColumnType("text") + .HasColumnName("alert_handler_properties"); + + b.Property("AlertMode") + .HasColumnType("smallint") + .HasColumnName("alert_mode"); + + b.HasKey("SubscriptionId", "GroupId") + .HasName("pk_retry_alert_override"); + + b.ToTable("retry_alert_override", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryGroupUsage", b => + { + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.Property("GroupId") + .HasColumnType("uuid") + .HasColumnName("group_id"); + + b.Property("AttemptsUsed") + .HasColumnType("integer") + .HasColumnName("attempts_used"); + + b.Property("ExhaustedNotifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("exhausted_notified_on"); + + b.Property("LastAttemptOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_attempt_on"); + + b.HasKey("SubscriptionId", "GroupId") + .HasName("pk_retry_group_usage"); + + b.ToTable("retry_group_usage", "infolink"); + }); + modelBuilder.Entity("SW.Bitween.Domain.RetryPolicy", b => { b.Property("Id") @@ -723,6 +835,15 @@ protected override void BuildModel(ModelBuilder modelBuilder) NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + b.Property("AlertHandlerId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("alert_handler_id"); + + b.Property("AlertHandlerProperties") + .HasColumnType("text") + .HasColumnName("alert_handler_properties"); + b.Property("CreatedBy") .HasColumnType("text") .HasColumnName("created_by"); @@ -1083,10 +1204,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("integer") .HasColumnName("document_id"); - b.Property>("GroupAttemptCounts") - .HasColumnType("jsonb") - .HasColumnName("group_attempt_counts"); - b.Property("HandlerId") .HasMaxLength(200) .HasColumnType("character varying(200)") @@ -1116,6 +1233,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("integer") .HasColumnName("input_size"); + b.Property("ManualRetry") + .HasColumnType("boolean") + .HasColumnName("manual_retry"); + b.Property("MapperId") .HasMaxLength(200) .HasColumnType("character varying(200)") @@ -1239,7 +1360,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("timestamp with time zone") .HasColumnName("finished_on"); - b.Property("NotifierId") + b.Property("NotifierId") .HasColumnType("integer") .HasColumnName("notifier_id"); @@ -1298,6 +1419,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("character varying(50)") .HasColumnName("id"); + b.Property("AttemptNumber") + .HasColumnType("integer") + .HasColumnName("attempt_number"); + b.Property("Exception") .HasColumnType("text") .HasColumnName("exception"); @@ -1356,6 +1481,15 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("text") .HasColumnName("response_xchange_id"); + b.Property("RetryBlockedReason") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("retry_blocked_reason"); + + b.Property("RetryGroupId") + .HasColumnType("uuid") + .HasColumnName("retry_group_id"); + b.Property("Success") .HasColumnType("boolean") .HasColumnName("success"); @@ -1363,6 +1497,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasKey("Id") .HasName("pk_xchange_result"); + b.HasIndex("RetryGroupId") + .HasDatabaseName("ix_xchange_result_retry_group_id"); + b.ToTable("xchange_result", "infolink"); }); diff --git a/SW.Bitween.Sdk/Model/Account.cs b/SW.Bitween.Sdk/Model/Account.cs index b98990bb..b1921407 100644 --- a/SW.Bitween.Sdk/Model/Account.cs +++ b/SW.Bitween.Sdk/Model/Account.cs @@ -61,6 +61,9 @@ public class AccountModel public bool Disabled { get; set; } public DateTime CreatedOn { get; set; } public List Roles { get; set; } = []; + + // Non-null and in the future => the account is currently locked out. + public DateTime? LockoutEnd { get; set; } } /// The signed-in account, plus everything the UI needs to decide what to show. @@ -69,6 +72,10 @@ public class ProfileModel : AccountModel public List Permissions { get; set; } = []; } +public class UnlockAccountModel +{ +} + public class ChangePasswordModel { public string NewPassword { get; set; } diff --git a/SW.Bitween.Sdk/Model/ApiGateway.cs b/SW.Bitween.Sdk/Model/ApiGateway.cs index dfb8a562..2bd43160 100644 --- a/SW.Bitween.Sdk/Model/ApiGateway.cs +++ b/SW.Bitween.Sdk/Model/ApiGateway.cs @@ -7,6 +7,9 @@ public class ApiGatewayCreate : IName { public string Name { get; set; } public string UrlName { get; set; } + + /// Off but kept, with its partner attachments. Calls to it are refused. + public bool Inactive { get; set; } } public class ApiGatewayRow : ApiGatewayUpdate @@ -31,7 +34,22 @@ public class ApiGatewayPartnerDto public class ApiGatewayPartnerCreate { public int PartnerId { get; set; } - public int SubscriptionId { get; set; } + + /// An integration that already exists. Exactly one of this and + /// is given. + public int? SubscriptionId { get; set; } + + /// Define the integration here instead of creating it first. It is created as a + /// GatewayApiCall in the same transaction as the attachment. + public InlineIntegrationCreate NewIntegration { get; set; } + } + + public class SearchApiGatewayAttachmentsModel + { + public int ApiGatewayId { get; set; } + public string Search { get; set; } + public int? Offset { get; set; } + public int? Limit { get; set; } } } diff --git a/SW.Bitween.Sdk/Model/AutoRetry/IRetryGroupBudget.cs b/SW.Bitween.Sdk/Model/AutoRetry/IRetryGroupBudget.cs new file mode 100644 index 00000000..06a9a22a --- /dev/null +++ b/SW.Bitween.Sdk/Model/AutoRetry/IRetryGroupBudget.cs @@ -0,0 +1,67 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace SW.Bitween.Model; + +/// +/// The outcome of asking a 's shared total budget for one attempt. +/// +/// +/// true when a slot was claimed and a retry may be scheduled. +/// +/// +/// true only for the single caller that first found the budget spent, so an +/// exhaustion alert is raised once rather than on every failure that follows. +/// Always false when is true. +/// +public readonly record struct RetryBudgetClaim(bool Granted, bool JustExhausted) +{ + /// A slot was claimed. + public static RetryBudgetClaim Allowed => new(true, false); + + /// No slot available, and someone else has already taken responsibility for alerting. + public static RetryBudgetClaim Denied => new(false, false); + + /// No slot available, and this caller owns the alert for it. + public static RetryBudgetClaim DeniedAndJustExhausted => new(false, true); +} + +/// +/// Tracks how much of a 's +/// has already been spent. +/// +/// +/// The total is a ceiling shared by every message that hits the group, so the count cannot +/// live on the message being evaluated — it needs a store that outlives a single xchange. +/// The implementation decides the scope: the production one keys by integration + group and +/// persists, while counts only for one dry-run. +/// +public interface IRetryGroupBudget +{ + /// Claims one attempt from the group's total budget. + Task TryConsume(Guid groupId, int maxAttemptsTotal); +} + +/// +/// In-memory for the policy dry-run endpoint, where nothing +/// should be persisted and the budget spans only the simulated run. +/// +public class InMemoryRetryGroupBudget : IRetryGroupBudget +{ + private readonly Dictionary _used = new(); + + /// + /// + /// Never reports : simulating a policy must not + /// send anyone an alert. + /// + public Task TryConsume(Guid groupId, int maxAttemptsTotal) + { + var used = _used.GetValueOrDefault(groupId, 0); + if (used >= maxAttemptsTotal) return Task.FromResult(RetryBudgetClaim.Denied); + + _used[groupId] = used + 1; + return Task.FromResult(RetryBudgetClaim.Allowed); + } +} diff --git a/SW.Bitween.Sdk/Model/AutoRetry/Matcher.cs b/SW.Bitween.Sdk/Model/AutoRetry/Matcher.cs index 4b6f6146..92eaa763 100644 --- a/SW.Bitween.Sdk/Model/AutoRetry/Matcher.cs +++ b/SW.Bitween.Sdk/Model/AutoRetry/Matcher.cs @@ -34,7 +34,7 @@ public enum JsonPathOp /// /// /// For groups the content is the exception stack-trace text. -/// For groups the content is the raw JSON response string. +/// For groups the content is the raw response body. /// Matcher implementations are serialised polymorphically via System.Text.Json. /// [JsonPolymorphic(TypeDiscriminatorPropertyName = "type")] @@ -44,8 +44,13 @@ public enum JsonPathOp [JsonDerivedType(typeof(JsonPathMatcher), typeDiscriminator: "jsonPath")] public abstract class Matcher { - /// The result type this matcher operates on. - public abstract XchangeResultType ResultType { get; } + /// + /// Returns true when this matcher can be evaluated against + /// content. The evaluator skips incompatible matchers, + /// so a group whose matchers all return false here can never fire for that + /// result type. + /// + public abstract bool Supports(XchangeResultType resultType); /// /// Returns true when satisfies this matcher's condition. @@ -54,16 +59,17 @@ public abstract class Matcher public abstract bool IsMatch(string content); } -// ── Error matchers ──────────────────────────────────────────────────────────── +// ── Text matchers (Error and BadResult) ─────────────────────────────────────── /// -/// Matches when the exception text contains a literal substring. -/// Applies to content. +/// Matches when the failure text contains a literal substring — the exception text for +/// , the response body for . /// public class ContainsMatcher : Matcher { /// - public override XchangeResultType ResultType => XchangeResultType.Error; + public override bool Supports(XchangeResultType resultType) => + resultType is XchangeResultType.Error or XchangeResultType.BadResult; /// The substring to search for. public required string Value { get; init; } @@ -78,13 +84,14 @@ public override bool IsMatch(string content) => } /// -/// Matches when the exception text satisfies a regular expression. -/// Applies to content. +/// Matches when the failure text satisfies a regular expression — the exception text for +/// , the response body for . /// public class RegexMatcher : Matcher { /// - public override XchangeResultType ResultType => XchangeResultType.Error; + public override bool Supports(XchangeResultType resultType) => + resultType is XchangeResultType.Error or XchangeResultType.BadResult; /// .NET-compatible regular expression pattern. public required string Pattern { get; init; } @@ -106,6 +113,8 @@ public class RegexMatcher : Matcher public override bool IsMatch(string content) => Compiled.IsMatch(content); } +// ── Error-only matcher ──────────────────────────────────────────────────────── + /// /// Matches when the exception text mentions a specific .NET exception type name, /// scanning the entire stack-trace including inner exceptions. @@ -118,7 +127,8 @@ public class RegexMatcher : Matcher public class ExceptionTypeMatcher : Matcher { /// - public override XchangeResultType ResultType => XchangeResultType.Error; + public override bool Supports(XchangeResultType resultType) => + resultType == XchangeResultType.Error; /// /// Fully-qualified or short exception type name, e.g. "System.TimeoutException" @@ -163,7 +173,8 @@ public override bool IsMatch(string content) public class JsonPathMatcher : Matcher { /// - public override XchangeResultType ResultType => XchangeResultType.BadResult; + public override bool Supports(XchangeResultType resultType) => + resultType == XchangeResultType.BadResult; /// JSONPath expression, e.g. "$.error.code" or "$.lines[0].status". public required string Path { get; init; } diff --git a/SW.Bitween.Sdk/Model/AutoRetry/RetryAlertMode.cs b/SW.Bitween.Sdk/Model/AutoRetry/RetryAlertMode.cs new file mode 100644 index 00000000..bc4d2113 --- /dev/null +++ b/SW.Bitween.Sdk/Model/AutoRetry/RetryAlertMode.cs @@ -0,0 +1,39 @@ +namespace SW.Bitween.Model; + +/// +/// Whether a level of the alert hierarchy defines its own destination for +/// "retry budget exhausted" alerts, or defers to the level above it. +/// +/// +/// The hierarchy is resolved per failing subscription and group, most specific first: +/// the subscription+group override, then the group, then the policy. An overriding level +/// replaces the level above rather than merging into it, so the handler and +/// every property it needs must be present on whichever level wins. +/// +public enum RetryAlertMode +{ + /// Defer to the level above. The default, so existing policies keep behaving as before. + Inherit, + + /// Send through this level's own handler, ignoring anything configured above it. + Send, + + /// Send nothing, and stop the walk — an alert configured above is deliberately suppressed here. + Silent, +} + +/// +/// Which level of the hierarchy decided where an alert goes. Surfaced in the management UI so a +/// destination that looks wrong can be traced to the level that set it. +/// +public enum RetryAlertLevel +{ + /// An override for this one subscription and group. + SubscriptionGroup, + + /// The group's own setting, applying to every subscription using the policy. + Group, + + /// The policy default. + Policy, +} diff --git a/SW.Bitween.Sdk/Model/AutoRetry/RetryGroup.cs b/SW.Bitween.Sdk/Model/AutoRetry/RetryGroup.cs index 65c057bc..6811e3c4 100644 --- a/SW.Bitween.Sdk/Model/AutoRetry/RetryGroup.cs +++ b/SW.Bitween.Sdk/Model/AutoRetry/RetryGroup.cs @@ -59,6 +59,22 @@ public class RetryGroup /// Optional free-text notes visible in the management UI. public string? Notes { get; init; } + + /// + /// Whether this group defines its own destination for budget-exhausted alerts, suppresses the + /// policy's, or defers to it. Defaults to so groups saved + /// before alerts existed keep using the policy's setting. + /// + public RetryAlertMode AlertMode { get; init; } = RetryAlertMode.Inherit; + + /// + /// Adapter that delivers this group's alert. Required when is + /// , ignored otherwise. + /// + public string? AlertHandlerId { get; init; } + + /// That adapter's own settings — api key, recipients, subject. + public Dictionary? AlertHandlerProperties { get; init; } } /// @@ -73,8 +89,11 @@ public class RetryBudget public int MaxAttemptsPerError { get; init; } /// - /// Hard ceiling on the total number of group-level retries across all messages in the - /// current processing window. Prevents a burst of failures from hammering the downstream. + /// Hard ceiling on the total number of group-level retries across all messages, counted per + /// subscription so one shared policy does not let a single noisy subscription spend everyone's + /// allowance. Prevents a burst of failures from hammering the downstream. It is not a rate over a + /// rolling window: the count only falls once it has been reached — the subscription's next success + /// then lifts it — or when somebody resets it by hand. /// public int MaxAttemptsTotal { get; init; } diff --git a/SW.Bitween.Sdk/Model/AutoRetry/RetryPolicyEvaluator.cs b/SW.Bitween.Sdk/Model/AutoRetry/RetryPolicyEvaluator.cs index 0a62b0a4..97ba93d4 100644 --- a/SW.Bitween.Sdk/Model/AutoRetry/RetryPolicyEvaluator.cs +++ b/SW.Bitween.Sdk/Model/AutoRetry/RetryPolicyEvaluator.cs @@ -1,6 +1,6 @@ using System; -using System.Collections.Generic; using System.Linq; +using System.Threading.Tasks; namespace SW.Bitween.Model; @@ -10,47 +10,18 @@ namespace SW.Bitween.Model; /// /// /// -/// Stateful per processing window. The evaluator accumulates -/// per-group attempt totals in _groupAttemptCounts. For a single in-process -/// window (e.g. a running RetryJob batch) one instance handles all messages. +/// Two independent caps. is +/// per message and is derived from the caller's attemptIndexForThisMessage, while +/// is shared by every message hitting the group and +/// is owned by the injected . The evaluator itself keeps no +/// counters, so a fresh instance per failure enforces both caps correctly. /// /// -/// Cross-invocation persistence. When a retry is scheduled the -/// current counts are saved to and stored on -/// the DelayedRetry entity (and on the new Xchange so that a later -/// failure of the retry itself can pick up where the budgets left off). On the next -/// evaluation call before . -/// -/// -/// Not thread-safe. Each goroutine/task should use its own instance. +/// Not thread-safe. Each task should use its own instance. /// /// -public class RetryPolicyEvaluator(IRetryPolicy policy) +public class RetryPolicyEvaluator(IRetryPolicy policy, IRetryGroupBudget groupBudget) { - private readonly Dictionary _groupAttemptCounts = new(); - - /// - /// Restores previously persisted group-level attempt counts, allowing budgets - /// to continue from where they left off across separate process invocations. - /// - /// - /// The dictionary returned by from a prior evaluation. - /// String keys are parsed back to — invalid entries are silently ignored. - /// - public void RestoreGroupAttemptCounts(Dictionary counts) - { - foreach (var kv in counts) - if (Guid.TryParse(kv.Key, out var guid)) - _groupAttemptCounts[guid] = kv.Value; - } - - /// - /// Returns the current group-level attempt counts as a string-keyed dictionary - /// suitable for JSON serialisation and storage on DelayedRetry / Xchange. - /// - public Dictionary GetGroupAttemptCounts() => - _groupAttemptCounts.ToDictionary(kv => kv.Key.ToString(), kv => kv.Value); - /// /// Evaluates the policy and returns a retry decision for the failed xchange. /// @@ -69,7 +40,7 @@ public Dictionary GetGroupAttemptCounts() => /// /// A indicating whether to retry and, if so, how long to wait. /// - public RetryDecision Evaluate( + public async Task Evaluate( XchangeResultType resultType, string content, int attemptIndexForThisMessage) @@ -83,23 +54,32 @@ public RetryDecision Evaluate( return RetryDecision.Block("No matching group (default block)"); if (group.Action == RetryAction.Block) - return RetryDecision.Block($"Group '{group.Name}' explicitly blocks this error"); + return RetryDecision.Block($"Group '{group.Name}' explicitly blocks this error", group); - var budget = group.Budget!; + // A group that allows retries without saying how many is refused rather than trusted: the + // dereference used to throw here, and because the caller logs and swallows that, retries just + // stopped happening with no reason recorded anywhere. Validation keeps new policies out of this + // state; this is for the ones already saved in it. + if (group.Budget is null) + return RetryDecision.Block( + $"Group '{group.Name}' allows retries but has no budget, so none can be scheduled", group); + + var budget = group.Budget; if (attemptIndexForThisMessage >= budget.MaxAttemptsPerError) return RetryDecision.Block( - $"Per-message cap reached ({budget.MaxAttemptsPerError}) in group '{group.Name}'"); + $"Per-message cap reached ({budget.MaxAttemptsPerError}) in group '{group.Name}'", group); - var totalUsed = _groupAttemptCounts.GetValueOrDefault(group.Id, 0); - if (totalUsed >= budget.MaxAttemptsTotal) + // Claimed last so a message already stopped by its own per-message cap doesn't + // eat a slot out of the shared total. + var claim = await groupBudget.TryConsume(group.Id, budget.MaxAttemptsTotal); + if (!claim.Granted) return RetryDecision.Block( - $"Group total cap reached ({budget.MaxAttemptsTotal}) for group '{group.Name}'"); - - _groupAttemptCounts[group.Id] = totalUsed + 1; + $"Group total cap reached ({budget.MaxAttemptsTotal}) for group '{group.Name}'", group, + claim.JustExhausted); var delay = budget.DelayStrategy.GetDelay(attemptIndexForThisMessage); - return RetryDecision.Allow(delay, group.Name); + return RetryDecision.Allow(delay, group); } private RetryGroup? FindMatchingGroup(XchangeResultType resultType, string content) @@ -112,7 +92,7 @@ public RetryDecision Evaluate( if (group.Matchers.Count == 0) return group; - var compatibleMatchers = group.Matchers.Where(m => m.ResultType == resultType); + var compatibleMatchers = group.Matchers.Where(m => m.Supports(resultType)); if (compatibleMatchers.Any(m => m.IsMatch(content))) return group; } @@ -134,22 +114,44 @@ public class RetryDecision /// Human-readable explanation of the decision, useful for audit/debug logs. public string Reason { get; private init; } = ""; - /// Name of the that matched, or null when blocked. - public string? MatchedGroupName { get; private init; } + /// + /// The group that matched this failure, or null when none did. Set on blocked + /// decisions too — internal callers (attempt tracking, exhaustion alerts) need to know which + /// group refused a failure, not only which group allowed one. + /// + public RetryGroup? MatchedGroup { get; private init; } + + /// + /// Name of the that allowed a retry, or null when blocked — + /// including when a group matched but refused. A refusal by a matched group and a failure no + /// group was ever configured to catch should look the same to a caller that only cares whether + /// something is retrying, which is what is for instead. + /// + public string? MatchedGroupName => ShouldRetry ? MatchedGroup?.Name : null; + + /// + /// true when this decision was the one that found the group's shared total spent for + /// the first time, meaning the caller owes an exhaustion alert. Never true twice for + /// the same subscription and group until the budget is reset. + /// + public bool BudgetJustExhausted { get; private init; } /// Creates an Allow decision — a retry will be scheduled after . - public static RetryDecision Allow(TimeSpan delay, string groupName) => new() + public static RetryDecision Allow(TimeSpan delay, RetryGroup group) => new() { ShouldRetry = true, Delay = delay, - MatchedGroupName = groupName, - Reason = $"Allowed by group '{groupName}'" + MatchedGroup = group, + Reason = $"Allowed by group '{group.Name}'" }; /// Creates a Block decision — no retry will be scheduled. - public static RetryDecision Block(string reason) => new() + public static RetryDecision Block(string reason, RetryGroup? group = null, + bool budgetJustExhausted = false) => new() { ShouldRetry = false, - Reason = reason + Reason = reason, + MatchedGroup = group, + BudgetJustExhausted = budgetJustExhausted }; } diff --git a/SW.Bitween.Sdk/Model/BusGateway.cs b/SW.Bitween.Sdk/Model/BusGateway.cs index 9240b24a..9e103156 100644 --- a/SW.Bitween.Sdk/Model/BusGateway.cs +++ b/SW.Bitween.Sdk/Model/BusGateway.cs @@ -7,6 +7,9 @@ public class BusGatewayCreate : IName { public string Name { get; set; } public int DocumentId { get; set; } + + /// Off but kept, with its routes. Messages stop reaching them. + public bool Inactive { get; set; } } public class BusGatewayUpdate : BusGatewayCreate @@ -33,7 +36,14 @@ public class BusGatewayRouteDto public class BusGatewayRouteCreate { - public int SubscriptionId { get; set; } + /// An integration that already exists. Exactly one of this and + /// is given. + public int? SubscriptionId { get; set; } + + /// Define the integration here instead of creating it first. It is created + /// carrying the gateway's own information type, in the same transaction as the route. + public InlineIntegrationCreate NewIntegration { get; set; } + public int? PartnerId { get; set; } public IPropertyMatchSpecification MatchExpression { get; set; } } @@ -43,6 +53,13 @@ public class BusGatewayRouteUpdate : BusGatewayRouteCreate public int RouteId { get; set; } } + /// Shared by the two gateway kinds: which integration a link points at. + public static class GatewayLinkTarget + { + public const string BothGiven = "INTEGRATION_AMBIGUOUS"; + public const string NeitherGiven = "INTEGRATION_REQUIRED"; + } + public class RemoveRouteRequest { public int RouteId { get; set; } diff --git a/SW.Bitween.Sdk/Model/Document.cs b/SW.Bitween.Sdk/Model/Document.cs index fc54bffe..40b0eba9 100644 --- a/SW.Bitween.Sdk/Model/Document.cs +++ b/SW.Bitween.Sdk/Model/Document.cs @@ -19,6 +19,13 @@ public class DocumentCreate : IName public string Name { get; set; } public bool BusEnabled { get; set; } public string BusMessageTypeName { get; set; } + public int DuplicateInterval { get; set; } + + public bool DisregardsUnfilteredMessages { get; set; } + + /// Carried on create too, so a new type arrives complete rather than + /// needing a second save before it can be filtered on. + public ICollection PromotedProperties { get; set; } } public class SearchDocumentTrailModel @@ -36,11 +43,6 @@ public class DocumentTrailModel : TrailBaseModel public class DocumentUpdate : DocumentCreate { public int Id { get; set; } - public int DuplicateInterval { get; set; } - - public bool DisregardsUnfilteredMessages { get; set; } - - public ICollection PromotedProperties { get; set; } } public class DocumentRow : DocumentUpdate diff --git a/SW.Bitween.Sdk/Model/Notifier.cs b/SW.Bitween.Sdk/Model/Notifier.cs index dc388f59..c238a207 100644 --- a/SW.Bitween.Sdk/Model/Notifier.cs +++ b/SW.Bitween.Sdk/Model/Notifier.cs @@ -34,6 +34,8 @@ public class NotifierSearch public bool? RunOnFailedResult { get; set; } public string HandlerId { get; set; } public bool? Inactive { get; set; } + /// So the list page can show a watched-integration count without a per-row detail fetch. + public int[] RunOnSubscriptions { get; set; } } diff --git a/SW.Bitween.Sdk/Model/Permissions.cs b/SW.Bitween.Sdk/Model/Permissions.cs index d5e3cc32..083d1bc2 100644 --- a/SW.Bitween.Sdk/Model/Permissions.cs +++ b/SW.Bitween.Sdk/Model/Permissions.cs @@ -60,12 +60,12 @@ public static class GlobalValues public const string Delete = "global-values.delete"; } - /// No delete: notifiers have no delete endpoint, so there is nothing to guard. public static class Notifiers { public const string View = "notifiers.view"; public const string Create = "notifiers.create"; public const string Edit = "notifiers.edit"; + public const string Delete = "notifiers.delete"; } public static class ApiGateways @@ -208,7 +208,8 @@ private static PermissionAreaModel Area(string id, string label, string group, s Area("notifiers", "Notifiers", "Integrations", "Alerts sent when exchanges fail or succeed.", (View, "Browse notifiers and their delivery history."), (Create, "Create notifiers."), - (Edit, "Change notifiers.")), + (Edit, "Change notifiers."), + (Delete, "Remove notifiers.")), Area("api-gateways", "API gateways", "Integrations", "HTTP entry points partners call into.", (View, "Browse API gateways and attached partners."), diff --git a/SW.Bitween.Sdk/Model/RetryBudgetExhaustedNotification.cs b/SW.Bitween.Sdk/Model/RetryBudgetExhaustedNotification.cs new file mode 100644 index 00000000..7e68f8f7 --- /dev/null +++ b/SW.Bitween.Sdk/Model/RetryBudgetExhaustedNotification.cs @@ -0,0 +1,39 @@ +using System; + +namespace SW.Bitween.Model; + +/// +/// The JSON handed to an alert handler when a retry group's shared budget runs out for one +/// subscription, meaning failures matching that group have stopped being retried. +/// +/// +/// Sent once per subscription and group, and not again until the budget is reset — unlike +/// , which is sent per exchange. +/// +public class RetryBudgetExhaustedNotification +{ + /// The failure that found the budget empty. + public string XchangeId { get; set; } + + public int SubscriptionId { get; set; } + public string SubscriptionName { get; set; } + public string DocumentName { get; set; } + public string CorrelationId { get; set; } + + /// Null when the subscription uses an inline policy rather than a named one. + public string PolicyName { get; set; } + + /// The group whose budget is spent — the condition that has stopped being retried. + public string GroupName { get; set; } + + /// The ceiling that was reached. + public int MaxAttemptsTotal { get; set; } + + /// The policy's own words for why this failure was refused. + public string BlockedReason { get; set; } + + /// The failure text of the exchange that hit the empty budget. + public string Exception { get; set; } + + public DateTime OccurredOn { get; set; } +} diff --git a/SW.Bitween.Sdk/Model/RetryPolicyModel.cs b/SW.Bitween.Sdk/Model/RetryPolicyModel.cs index 5d617542..20983ecf 100644 --- a/SW.Bitween.Sdk/Model/RetryPolicyModel.cs +++ b/SW.Bitween.Sdk/Model/RetryPolicyModel.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; namespace SW.Bitween.Model; @@ -6,6 +7,15 @@ public class RetryPolicyCreate { public required string Name { get; set; } public List Groups { get; set; } = []; + + /// + /// Default destination for budget-exhausted alerts, inherited by every group that does not + /// override it. Null means no alert unless a group or a subscription+group override sets one. + /// + public string? AlertHandlerId { get; set; } + + /// That adapter's own settings — api key, recipients, subject. + public Dictionary? AlertHandlerProperties { get; set; } } public class RetryPolicyUpdate : RetryPolicyCreate { } @@ -17,6 +27,185 @@ public class RetryPolicyRow public int GroupCount { get; set; } } +/// +/// The whole state of one subscription-and-group pair under a policy: how much of the group's +/// that subscription has spent, and where the pair's +/// budget-exhausted alert goes. +/// +/// +/// +/// Both halves are keyed by the same (SubscriptionId, GroupId) pair, which is why they +/// travel together rather than in two reports the reader has to join by eye: the question asked +/// when a budget runs out is "did anyone get told?", and that needs both. +/// +/// +/// A row exists for every pair, including subscriptions that have never failed — an alert override +/// has to be configurable before the first failure, not after. Those rows carry the group's ceiling +/// with nothing spent against it, and a null . +/// +/// +public class RetryGroupUsageRow +{ + public int SubscriptionId { get; set; } + public string SubscriptionName { get; set; } + public Guid GroupId { get; set; } + public string GroupName { get; set; } + + public int AttemptsUsed { get; set; } + public int MaxAttemptsTotal { get; set; } + + /// True when the budget is spent and this subscription will get no further retries. + public bool Exhausted { get; set; } + + /// + /// Null when this pair has never failed, which is also how a caller knows there is no counter + /// to reset for it. + /// + public DateTime? LastAttemptOn { get; set; } + + /// + /// When the exhaustion alert was raised, or null if the budget still has room — or ran out + /// before alerts existed. + /// + public DateTime? ExhaustedNotifiedOn { get; set; } + + /// + /// Whether the raised alert actually reached its handler. + /// + /// + /// Separate from because they are different facts: + /// claiming the alert is what stops it firing twice, and it is claimed before the send is + /// attempted. A send through a customer-configured adapter can fail — a wrong password, a + /// refused TLS handshake — and the counter records none of that. Reporting only the claim + /// tells the reader someone was told when nobody was, which is the one thing this page + /// must never do. + /// + /// Null when no alert has been claimed, or when one was claimed with no delivery attempt + /// recorded against it. "Not known" and "did not arrive" are not the same answer. + /// + public bool? AlertDelivered { get; set; } + + /// Why delivery failed, when it did. + public string? AlertError { get; set; } + + /// This pair's own override mode. Inherit when no override row exists. + public RetryAlertMode AlertMode { get; set; } + + /// The override's handler, when it defines one. Not the resolved handler. + public string? OverrideHandlerId { get; set; } + + /// That override's own settings. + public Dictionary? OverrideHandlerProperties { get; set; } + + /// Where the alert actually goes, or null when nothing sends for this pair. + public string? ResolvedHandlerId { get; set; } + + /// + /// The winning level's own settings. Carried so that overriding an inherited alert can start + /// from what it currently sends: an override replaces rather than merges, so a handler copied + /// without its properties would save an override that fails at send time. + /// + public Dictionary? ResolvedHandlerProperties { get; set; } + + /// Which level supplied , or null when nothing sends. + public RetryAlertLevel? ResolvedFrom { get; set; } + + /// + /// Which level deliberately switched this pair's alert off, when one did. Resolution returns + /// nothing in that case exactly as it does when no level ever configured an alert, and the two + /// need telling apart: one is a decision, the other is an oversight. + /// + public RetryAlertLevel? SilencedAt { get; set; } +} + +/// Asks for one subscription-and-group pair's most recent failures. +public class RetryGroupAttemptsRequest +{ + public int SubscriptionId { get; set; } + public Guid GroupId { get; set; } +} + +/// +/// The failures one group caught for one subscription: what the spent budget on a +/// was actually spent on. +/// +/// +/// Failures are kept for good, while the budget counter is reset, so counts +/// every failure this group has ever caught for this subscription and not the counter's value. +/// Failures recorded before a group was stamped onto them are not counted at all. +/// +public class RetryGroupAttempts +{ + /// How many failures exist, of which carries the latest few. + public int Total { get; set; } + + public List Attempts { get; set; } = []; +} + +public class RetryGroupAttemptRow +{ + /// The failed exchange, so the full input, output and error can be opened. + public string XchangeId { get; set; } + + /// + /// How deep the retry chain was, 0 being the original delivery. Null for failures recorded + /// before the number was stored. + /// + public int? AttemptNumber { get; set; } + + public DateTime FailedOn { get; set; } + + public string Exception { get; set; } + + /// + /// True while another attempt is still scheduled for this failure. The one thing here that is + /// not history: it stops being true the moment the retry runs. + /// + public bool RetryPending { get; set; } + + /// Why no further attempt was scheduled, when the policy refused one. + public string RetryBlockedReason { get; set; } +} + +/// +/// Creates, changes or clears the alert override for one subscription and group. Sending +/// removes the override rather than storing a row that does +/// nothing. +/// +public class RetryAlertOverrideSave +{ + public int SubscriptionId { get; set; } + public Guid GroupId { get; set; } + public RetryAlertMode AlertMode { get; set; } + public string? AlertHandlerId { get; set; } + public Dictionary? AlertHandlerProperties { get; set; } +} + +/// Empty request body — the subject is identified by the route key. +public class RetryPolicyUsageRequest +{ +} + +/// +/// Clears one subscription's spent budget, for one group or for all of them. Reaches a subscription +/// whose policy is an inline CustomRetryPolicy, which has no id for the policy-scoped reset to +/// address. +/// +public class SubscriptionRetryResetUsage +{ + public Guid? GroupId { get; set; } +} + +/// +/// Clears spent budget so a group starts retrying again. Omit both fields to reset every +/// subscription and group of the policy. +/// +public class RetryPolicyResetUsage +{ + public int? SubscriptionId { get; set; } + public Guid? GroupId { get; set; } +} + /// /// Simulates evaluating a (possibly unsaved/draft) set of retry groups against a single /// failure, across as many consecutive attempts as requested, so the management UI can diff --git a/SW.Bitween.Sdk/Model/Subscription.cs b/SW.Bitween.Sdk/Model/Subscription.cs index 614facf7..5211a967 100644 --- a/SW.Bitween.Sdk/Model/Subscription.cs +++ b/SW.Bitween.Sdk/Model/Subscription.cs @@ -84,6 +84,45 @@ public class SearchSubscriptionLastRunsModel { } + /// + /// One execution of a Receiving subscription's own receive step, recorded directly by + /// ReceivingJob — independent of the scheduler's own run history, which only knows + /// whether Execute() threw (it never does; the receive step's own failures are caught + /// and reported here instead, alongside the successes and no-op checks history never covers). + /// + public enum ReceiveOutcome + { + Failed = 0, + NoNewData = 1, + Received = 2, + } + + public class ReceiveAttemptExchangeRef + { + public string Id { get; set; } + public bool? Status { get; set; } + public bool? ResponseBad { get; set; } + public IDictionary PromotedProperties { get; set; } + } + + public class ReceiveAttemptModel + { + public int Id { get; set; } + public DateTime StartedOn { get; set; } + public DateTime FinishedOn { get; set; } + public ReceiveOutcome Outcome { get; set; } + public string ErrorMessage { get; set; } + public ICollection Exchanges { get; set; } + } + + public class SearchReceiveAttemptsModel + { + public int SubscriptionId { get; set; } + public ReceiveOutcome? Outcome { get; set; } + public int? Offset { get; set; } + public int? Limit { get; set; } + } + /// /// Whether a scheduled subscription will actually fire — read from the scheduler /// itself, not from what Bitween thinks it configured. The two can disagree, and @@ -163,6 +202,20 @@ public abstract class SubscriptionConfiguration : SubscriptionCreateUpdateBase /// and is optional — a caller /// that sends only those still gets the empty, inactive subscription it always did. /// + /// + /// An integration defined while it is being wired up, so the integration and the thing + /// that points at it land in one transaction instead of two calls that can half-succeed. + /// + /// Deriving from is the point: the whole pipeline + /// is applied by the same code an ordinary create uses. The type is always the gateway's. + /// DocumentId is ignored for a bus gateway, which is bound to one information type and + /// imposes it; an API gateway is not bound to one, so there it is required. + /// + /// + public class InlineIntegrationCreate : SubscriptionConfiguration + { + } + public class SubscriptionCreate : SubscriptionConfiguration { public SubscriptionType Type { get; set; } diff --git a/SW.Bitween.Sdk/Model/Workgroups.cs b/SW.Bitween.Sdk/Model/Workgroups.cs index 5a37696a..1cc88d44 100644 --- a/SW.Bitween.Sdk/Model/Workgroups.cs +++ b/SW.Bitween.Sdk/Model/Workgroups.cs @@ -41,6 +41,7 @@ public class SearchWorkGroupModel { public int? Limit { get; set; } public int? Offset { get; set; } + public string Name { get; set; } } public class UpdateWorkGroupModel : CreateWorkGroupModel diff --git a/SW.Bitween.Sdk/Model/Xchange.cs b/SW.Bitween.Sdk/Model/Xchange.cs index fa321a61..e29dbbc8 100644 --- a/SW.Bitween.Sdk/Model/Xchange.cs +++ b/SW.Bitween.Sdk/Model/Xchange.cs @@ -97,5 +97,8 @@ public class XchangeRow public string CorrelationId { get; set; } public int? PartnerId { get; set; } public DateTime? ScheduledRetryOn { get; set; } + + /// Why the retry policy declined to schedule another attempt, when it declined. + public string RetryBlockedReason { get; set; } } } \ No newline at end of file diff --git a/SW.Bitween.UnitTests/NativeSmtpHandlerTests.cs b/SW.Bitween.UnitTests/NativeSmtpHandlerTests.cs new file mode 100644 index 00000000..ec0528b9 --- /dev/null +++ b/SW.Bitween.UnitTests/NativeSmtpHandlerTests.cs @@ -0,0 +1,178 @@ +using System.Collections.Generic; +using System.Net.Security; +using System.Security.Cryptography.X509Certificates; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Newtonsoft.Json; +using SW.Bitween.Model; +using SW.Bitween.NativeAdapters; +using SW.Bitween.NativeAdapters.SmtpHandler; + +namespace SW.Bitween.UnitTests; + +[TestClass] +public class NativeSmtpHandlerTests +{ + // ─── Subject and body templating ──────────────────────────────────────────── + + [TestMethod] + public void Fill_SubstitutesPayloadFields() + { + var payload = JsonConvert.SerializeObject(new {GroupName = "timeouts", MaxAttemptsTotal = 8}); + + var result = NativeSmtpHandler.Fill("{{ GroupName }} used all {{ MaxAttemptsTotal }} retries", payload); + + Assert.AreEqual("timeouts used all 8 retries", result); + } + + [TestMethod] + public void Fill_RendersTheRealAlertPayload() + { + // The shape RetryAlertService actually sends, serialised the same way (PascalCase). + var payload = JsonConvert.SerializeObject(new RetryBudgetExhaustedNotification + { + SubscriptionName = "QA - ShipaDelivery - CreateOrder", + GroupName = "FRT charges cannot be found", + MaxAttemptsTotal = 8 + }); + + var result = NativeSmtpHandler.Fill( + "Retries stopped for {{ SubscriptionName }}: {{ GroupName }} ({{ MaxAttemptsTotal }})", payload); + + Assert.AreEqual( + "Retries stopped for QA - ShipaDelivery - CreateOrder: FRT charges cannot be found (8)", result); + } + + [TestMethod] + public void Fill_LeavesTemplateAloneForNonJsonPayload() + { + // Normal for a pipeline handler shipping a flat file — there are no fields to substitute. + Assert.AreEqual("Nightly export", NativeSmtpHandler.Fill("Nightly export", "id,name\n1,alpha")); + } + + [TestMethod] + public void Fill_LeavesTemplateAloneForEmptyPayload() + { + Assert.AreEqual("Nightly export", NativeSmtpHandler.Fill("Nightly export", "")); + } + + [TestMethod] + public void Fill_MissingFieldRendersEmptyRatherThanThePlaceholder() + { + var result = NativeSmtpHandler.Fill("Group: {{ GroupName }}", "{\"Other\":1}"); + + Assert.AreEqual("Group: ", result); + } + + // ─── Startup values ───────────────────────────────────────────────────────── + + [TestMethod] + public void StartupValues_ParseNumbersAndFlags() + { + var input = new Dictionary + { + ["Host"] = "smtp.example.com", + ["Port"] = "465", + ["UseTls"] = "true", + ["IsHtml"] = "false", + ["From"] = "alerts@example.com", + ["To"] = "ops@example.com" + }.ConvertTo(); + + Assert.AreEqual("smtp.example.com", input.Host); + Assert.AreEqual(465, input.Port); + Assert.IsTrue(input.UseTls); + Assert.IsFalse(input.IsHtml); + } + + [TestMethod] + public void StartupValues_KeepDefaultsWhenOmitted() + { + var input = new Dictionary + { + ["Host"] = "smtp.example.com", + ["From"] = "alerts@example.com", + ["To"] = "ops@example.com" + }.ConvertTo(); + + // The common provider setup should need no port or TLS choice at all. + Assert.AreEqual(587, input.Port); + Assert.IsTrue(input.UseTls); + Assert.IsTrue(input.IsHtml); + Assert.IsNull(input.Password); + } + + // ─── Server certificate acceptance ────────────────────────────────────────── + + [TestMethod] + public void Certificate_WithNothingWrong_IsAccepted() + { + Assert.IsTrue(NativeSmtpHandler.IsCertificateAcceptable( + SslPolicyErrors.None, new[] { X509ChainStatusFlags.NoError })); + } + + [TestMethod] + public void Certificate_WhoseRevocationCouldNotBeChecked_IsAccepted() + { + // The whole point of the soft-fail: the CA's OCSP or CRL server was unreachable, which says + // nothing bad about the certificate itself. + Assert.IsTrue(NativeSmtpHandler.IsCertificateAcceptable( + SslPolicyErrors.RemoteCertificateChainErrors, + new[] { X509ChainStatusFlags.RevocationStatusUnknown })); + + Assert.IsTrue(NativeSmtpHandler.IsCertificateAcceptable( + SslPolicyErrors.RemoteCertificateChainErrors, + new[] { X509ChainStatusFlags.OfflineRevocation })); + + Assert.IsTrue(NativeSmtpHandler.IsCertificateAcceptable( + SslPolicyErrors.RemoteCertificateChainErrors, + new[] { X509ChainStatusFlags.RevocationStatusUnknown | X509ChainStatusFlags.OfflineRevocation })); + } + + [TestMethod] + public void Certificate_ThatWasRevoked_IsRefused() + { + Assert.IsFalse(NativeSmtpHandler.IsCertificateAcceptable( + SslPolicyErrors.RemoteCertificateChainErrors, + new[] { X509ChainStatusFlags.Revoked })); + } + + [TestMethod] + public void Certificate_RevokedAlongsideAnUncheckableStatus_IsRefused() + { + // One chain entry can carry several flags at once, so the tolerated ones have to be masked + // out rather than compared — otherwise a revoked certificate rides in on the same entry. + Assert.IsFalse(NativeSmtpHandler.IsCertificateAcceptable( + SslPolicyErrors.RemoteCertificateChainErrors, + new[] { X509ChainStatusFlags.Revoked | X509ChainStatusFlags.RevocationStatusUnknown })); + + Assert.IsFalse(NativeSmtpHandler.IsCertificateAcceptable( + SslPolicyErrors.RemoteCertificateChainErrors, + new[] { X509ChainStatusFlags.RevocationStatusUnknown, X509ChainStatusFlags.Revoked })); + } + + [TestMethod] + public void Certificate_WithAnyOtherDefect_IsRefused() + { + Assert.IsFalse(NativeSmtpHandler.IsCertificateAcceptable( + SslPolicyErrors.RemoteCertificateChainErrors, + new[] { X509ChainStatusFlags.UntrustedRoot })); + + Assert.IsFalse(NativeSmtpHandler.IsCertificateAcceptable( + SslPolicyErrors.RemoteCertificateChainErrors, + new[] { X509ChainStatusFlags.NotTimeValid })); + + // A wrong hostname or no certificate at all is not a chain question, so the chain flags must + // not be allowed to excuse it. + Assert.IsFalse(NativeSmtpHandler.IsCertificateAcceptable( + SslPolicyErrors.RemoteCertificateNameMismatch, + new[] { X509ChainStatusFlags.NoError })); + + Assert.IsFalse(NativeSmtpHandler.IsCertificateAcceptable( + SslPolicyErrors.RemoteCertificateNotAvailable, + new[] { X509ChainStatusFlags.NoError })); + + Assert.IsFalse(NativeSmtpHandler.IsCertificateAcceptable( + SslPolicyErrors.RemoteCertificateChainErrors | SslPolicyErrors.RemoteCertificateNameMismatch, + new[] { X509ChainStatusFlags.RevocationStatusUnknown })); + } +} diff --git a/SW.Bitween.UnitTests/RetryAlertResolverTests.cs b/SW.Bitween.UnitTests/RetryAlertResolverTests.cs new file mode 100644 index 00000000..c2a2a29d --- /dev/null +++ b/SW.Bitween.UnitTests/RetryAlertResolverTests.cs @@ -0,0 +1,162 @@ +using System.Collections.Generic; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using SW.Bitween.Domain; +using SW.Bitween.Model; + +namespace SW.Bitween.UnitTests; + +[TestClass] +public class RetryAlertResolverTests +{ + // ─── Helpers ──────────────────────────────────────────────────────────────── + + private static RetryGroup Group(RetryAlertMode mode = RetryAlertMode.Inherit, string handler = null) => + new() + { + Name = "timeouts", + AppliesTo = [XchangeResultType.Error], + AlertMode = mode, + AlertHandlerId = handler, + AlertHandlerProperties = handler == null ? null : new Dictionary { ["to"] = "group@x" } + }; + + private static RetryPolicy Policy(string handler = null) => new() + { + Name = "policy", + AlertHandlerId = handler, + AlertHandlerProperties = handler == null ? null : new Dictionary { ["to"] = "policy@x" } + }; + + private static RetryAlertOverride Override(RetryAlertMode mode, string handler = null) => new() + { + SubscriptionId = 1, + AlertMode = mode, + AlertHandlerId = handler, + AlertHandlerProperties = handler == null ? null : new Dictionary { ["to"] = "sub@x" } + }; + + // ─── Nothing configured ───────────────────────────────────────────────────── + + [TestMethod] + public void NoLevelConfigured_ResolvesToNothing() + { + Assert.IsNull(RetryAlertResolver.Resolve(null, Group(), Policy())); + } + + // ─── Policy level ─────────────────────────────────────────────────────────── + + [TestMethod] + public void PolicyOnly_ResolvesToPolicy() + { + var target = RetryAlertResolver.Resolve(null, Group(), Policy("native.smtp")); + + Assert.IsNotNull(target); + Assert.AreEqual("native.smtp", target.HandlerId); + Assert.AreEqual(RetryAlertLevel.Policy, target.Level); + Assert.AreEqual("policy@x", target.HandlerProperties["to"]); + } + + // ─── Group level ──────────────────────────────────────────────────────────── + + [TestMethod] + public void GroupSend_ReplacesPolicyEntirely() + { + var target = RetryAlertResolver.Resolve(null, + Group(RetryAlertMode.Send, "native.teams"), Policy("native.smtp")); + + Assert.AreEqual("native.teams", target.HandlerId); + Assert.AreEqual(RetryAlertLevel.Group, target.Level); + // Replace, not merge: nothing of the policy's own properties survives. + Assert.AreEqual("group@x", target.HandlerProperties["to"]); + } + + [TestMethod] + public void GroupSilent_SuppressesPolicyAlert() + { + Assert.IsNull(RetryAlertResolver.Resolve(null, + Group(RetryAlertMode.Silent), Policy("native.smtp"))); + } + + [TestMethod] + public void GroupInherit_FallsThroughToPolicy() + { + var target = RetryAlertResolver.Resolve(null, + Group(RetryAlertMode.Inherit), Policy("native.smtp")); + + Assert.AreEqual(RetryAlertLevel.Policy, target.Level); + } + + // ─── Subscription + group level ───────────────────────────────────────────── + + [TestMethod] + public void SubscriptionOverrideSend_WinsOverGroupAndPolicy() + { + var target = RetryAlertResolver.Resolve( + Override(RetryAlertMode.Send, "native.webhook"), + Group(RetryAlertMode.Send, "native.teams"), + Policy("native.smtp")); + + Assert.AreEqual("native.webhook", target.HandlerId); + Assert.AreEqual(RetryAlertLevel.SubscriptionGroup, target.Level); + Assert.AreEqual("sub@x", target.HandlerProperties["to"]); + } + + [TestMethod] + public void SubscriptionOverrideSilent_SuppressesEverythingAbove() + { + Assert.IsNull(RetryAlertResolver.Resolve( + Override(RetryAlertMode.Silent), + Group(RetryAlertMode.Send, "native.teams"), + Policy("native.smtp"))); + } + + [TestMethod] + public void SubscriptionOverrideInherit_FallsThroughToGroup() + { + var target = RetryAlertResolver.Resolve( + Override(RetryAlertMode.Inherit), + Group(RetryAlertMode.Send, "native.teams"), + Policy("native.smtp")); + + Assert.AreEqual(RetryAlertLevel.Group, target.Level); + } + + // ─── Edge cases ───────────────────────────────────────────────────────────── + + [TestMethod] + public void InlineCustomPolicy_HasNoPolicyLevel_ButGroupStillSends() + { + // A subscription with a CustomRetryPolicy has no policy row at all. + var target = RetryAlertResolver.Resolve(null, Group(RetryAlertMode.Send, "native.teams"), null); + + Assert.AreEqual(RetryAlertLevel.Group, target.Level); + } + + [TestMethod] + public void InlineCustomPolicy_WithInheritingGroup_ResolvesToNothing() + { + Assert.IsNull(RetryAlertResolver.Resolve(null, Group(), null)); + } + + [TestMethod] + public void MissingGroup_StillFallsBackToPolicy() + { + // The group was removed from the policy between the failure and the send. + var target = RetryAlertResolver.Resolve(null, null, Policy("native.smtp")); + + Assert.AreEqual(RetryAlertLevel.Policy, target.Level); + } + + [TestMethod] + public void SendWithNoHandler_FallsThroughRatherThanSilencing() + { + // Validation rejects this on save, so it only exists on rows written before that guard. + // Falling through is more useful than silently sending nothing. + var target = RetryAlertResolver.Resolve( + Override(RetryAlertMode.Send), + Group(RetryAlertMode.Send), + Policy("native.smtp")); + + Assert.AreEqual(RetryAlertLevel.Policy, target.Level); + } +} diff --git a/SW.Bitween.UnitTests/RetryPolicyEvaluatorTests.cs b/SW.Bitween.UnitTests/RetryPolicyEvaluatorTests.cs index 14214390..23fa42c5 100644 --- a/SW.Bitween.UnitTests/RetryPolicyEvaluatorTests.cs +++ b/SW.Bitween.UnitTests/RetryPolicyEvaluatorTests.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Threading.Tasks; using Microsoft.VisualStudio.TestTools.UnitTesting; using SW.Bitween.Model; @@ -57,11 +58,41 @@ private static RetryGroup BadResultGroup( } }; + // Each call gets its own budget, so tests that don't care about the shared total + // stay isolated from each other. + private static RetryPolicyEvaluator Evaluator(IRetryPolicy policy) => + new RetryPolicyEvaluator(policy, new InMemoryRetryGroupBudget()); + private sealed class TestPolicy(RetryGroup[] groups) : IRetryPolicy { public List Groups { get; } = new List(groups); } + // ─── Allow with no budget ─────────────────────────────────────────────────── + + [TestMethod] + public async Task AllowWithoutBudget_IsRefusedWithAReason() + { + // Only reachable for a policy saved before validation rejected this shape. It used to throw, + // and the caller logs and swallows the throw, so retries stopped happening and nothing said why. + var group = new RetryGroup + { + Name = "No budget", + Priority = 10, + Enabled = true, + AppliesTo = [XchangeResultType.Error], + Action = RetryAction.Allow, + Matchers = [new ContainsMatcher { Value = "timeout" }], + Budget = null + }; + + var decision = await Evaluator(PolicyWith(group)).Evaluate(XchangeResultType.Error, "timeout", 0); + + Assert.IsFalse(decision.ShouldRetry); + StringAssert.Contains(decision.Reason, "no budget"); + Assert.AreEqual("No budget", decision.MatchedGroup?.Name); + } + // ─── ContainsMatcher ──────────────────────────────────────────────────────── [TestMethod] @@ -218,36 +249,75 @@ public void JsonPathMatcher_ArrayIndexer_Match() // ─── Evaluator: basic routing ──────────────────────────────────────────────── [TestMethod] - public void Evaluator_MatchingGroup_AllowsRetry() + public async Task Evaluator_MatchingGroup_AllowsRetry() { var policy = PolicyWith(ErrorGroup("transient", new ContainsMatcher { Value = "timeout" })); - var ev = new RetryPolicyEvaluator(policy); - var decision = ev.Evaluate(XchangeResultType.Error, "Connection timeout", 0); + var ev = Evaluator(policy); + var decision = await ev.Evaluate(XchangeResultType.Error, "Connection timeout", 0); Assert.IsTrue(decision.ShouldRetry); Assert.AreEqual("transient", decision.MatchedGroupName); } [TestMethod] - public void Evaluator_NoMatchingGroup_Blocks() + public async Task Evaluator_NoMatchingGroup_Blocks() { var policy = PolicyWith(ErrorGroup("transient", new ContainsMatcher { Value = "timeout" })); - var ev = new RetryPolicyEvaluator(policy); - var decision = ev.Evaluate(XchangeResultType.Error, "Disk full", 0); + var ev = Evaluator(policy); + var decision = await ev.Evaluate(XchangeResultType.Error, "Disk full", 0); Assert.IsFalse(decision.ShouldRetry); } [TestMethod] - public void Evaluator_WrongResultType_GroupSkipped() + public async Task Evaluator_WrongResultType_GroupSkipped() { var policy = PolicyWith(BadResultGroup("bad", new JsonPathMatcher { Path = "$.retryable", Op = JsonPathOp.Exists })); - var ev = new RetryPolicyEvaluator(policy); + var ev = Evaluator(policy); // Group is for BadResult only — must be skipped for Error - var decision = ev.Evaluate(XchangeResultType.Error, "some exception", 0); + var decision = await ev.Evaluate(XchangeResultType.Error, "some exception", 0); Assert.IsFalse(decision.ShouldRetry); } [TestMethod] - public void Evaluator_EmptyMatchers_MatchesEveryApplicableFailure() + public async Task Evaluator_ContainsMatcher_MatchesBadResultBody() + { + var policy = PolicyWith(BadResultGroup("bad", new ContainsMatcher { Value = "INSUFFICIENT_STOCK" })); + var ev = Evaluator(policy); + var decision = await ev.Evaluate(XchangeResultType.BadResult, "{\"code\":\"INSUFFICIENT_STOCK\"}", 0); + Assert.IsTrue(decision.ShouldRetry); + Assert.AreEqual("bad", decision.MatchedGroupName); + } + + [TestMethod] + public async Task Evaluator_ContainsMatcher_MatchesNonJsonBadResultBody() + { + // A 4xx body need not be JSON — text matchers are the only way to reach these. + var policy = PolicyWith(BadResultGroup("bad", new ContainsMatcher { Value = "rate limit" })); + var ev = Evaluator(policy); + var decision = await ev.Evaluate(XchangeResultType.BadResult, "Rate limit exceeded", 0); + Assert.IsTrue(decision.ShouldRetry); + } + + [TestMethod] + public async Task Evaluator_RegexMatcher_MatchesBadResultBody() + { + var policy = PolicyWith(BadResultGroup("bad", new RegexMatcher { Pattern = @"""status"":\s*""FAILED""" })); + var ev = Evaluator(policy); + var decision = await ev.Evaluate(XchangeResultType.BadResult, "{\"status\": \"FAILED\"}", 0); + Assert.IsTrue(decision.ShouldRetry); + } + + [TestMethod] + public async Task Evaluator_ExceptionTypeMatcher_SkippedForBadResult() + { + // Exception type names are meaningless against a response body — stays Error-only. + var policy = PolicyWith(BadResultGroup("bad", new ExceptionTypeMatcher { Value = "System.TimeoutException" })); + var ev = Evaluator(policy); + var decision = await ev.Evaluate(XchangeResultType.BadResult, "System.TimeoutException in body", 0); + Assert.IsFalse(decision.ShouldRetry); + } + + [TestMethod] + public async Task Evaluator_EmptyMatchers_MatchesEveryApplicableFailure() { var group = new RetryGroup { @@ -265,8 +335,8 @@ public void Evaluator_EmptyMatchers_MatchesEveryApplicableFailure() } }; var policy = PolicyWith(group); - var ev = new RetryPolicyEvaluator(policy); - var decision = ev.Evaluate(XchangeResultType.Error, "anything at all", 0); + var ev = Evaluator(policy); + var decision = await ev.Evaluate(XchangeResultType.Error, "anything at all", 0); Assert.IsTrue(decision.ShouldRetry); Assert.AreEqual("catch-all", decision.MatchedGroupName); } @@ -274,54 +344,54 @@ public void Evaluator_EmptyMatchers_MatchesEveryApplicableFailure() // ─── Evaluator: priority ordering ─────────────────────────────────────────── [TestMethod] - public void Evaluator_LowerPriorityEvaluatedFirst() + public async Task Evaluator_LowerPriorityEvaluatedFirst() { var g1 = ErrorGroup("low-num", new ContainsMatcher { Value = "error" }, priority: 1); var g2 = ErrorGroup("high-num", new ContainsMatcher { Value = "error" }, priority: 20); var policy = PolicyWith(g2, g1); // intentionally reversed in array - var ev = new RetryPolicyEvaluator(policy); - var decision = ev.Evaluate(XchangeResultType.Error, "error occurred", 0); + var ev = Evaluator(policy); + var decision = await ev.Evaluate(XchangeResultType.Error, "error occurred", 0); Assert.AreEqual("low-num", decision.MatchedGroupName); } [TestMethod] - public void Evaluator_OnlyMatchingGroupFires() + public async Task Evaluator_OnlyMatchingGroupFires() { var g1 = ErrorGroup("timeouts", new ContainsMatcher { Value = "timeout" }, priority: 1); var g2 = ErrorGroup("disk", new ContainsMatcher { Value = "disk" }, priority: 20); var policy = PolicyWith(g1, g2); - var ev = new RetryPolicyEvaluator(policy); - var decision = ev.Evaluate(XchangeResultType.Error, "disk full", 0); + var ev = Evaluator(policy); + var decision = await ev.Evaluate(XchangeResultType.Error, "disk full", 0); Assert.AreEqual("disk", decision.MatchedGroupName); } // ─── Evaluator: budget — MaxAttemptsPerError ───────────────────────────────── [TestMethod] - public void Evaluator_MaxAttemptsPerError_BlocksAfterCap() + public async Task Evaluator_MaxAttemptsPerError_BlocksAfterCap() { var policy = PolicyWith(ErrorGroup("transient", new ContainsMatcher { Value = "err" }, maxPerError: 2)); - var ev = new RetryPolicyEvaluator(policy); + var ev = Evaluator(policy); - Assert.IsTrue(ev.Evaluate(XchangeResultType.Error, "err", 0).ShouldRetry); - Assert.IsTrue(ev.Evaluate(XchangeResultType.Error, "err", 1).ShouldRetry); - Assert.IsFalse(ev.Evaluate(XchangeResultType.Error, "err", 2).ShouldRetry); // cap = 2 + Assert.IsTrue((await ev.Evaluate(XchangeResultType.Error, "err", 0)).ShouldRetry); + Assert.IsTrue((await ev.Evaluate(XchangeResultType.Error, "err", 1)).ShouldRetry); + Assert.IsFalse((await ev.Evaluate(XchangeResultType.Error, "err", 2)).ShouldRetry); // cap = 2 } // ─── Evaluator: budget — MaxAttemptsTotal ──────────────────────────────────── [TestMethod] - public void Evaluator_MaxAttemptsTotal_BlocksAfterGroupCap() + public async Task Evaluator_MaxAttemptsTotal_BlocksAfterGroupCap() { var policy = PolicyWith(ErrorGroup("transient", new ContainsMatcher { Value = "err" }, maxPerError: 100, maxTotal: 3)); - var ev = new RetryPolicyEvaluator(policy); + var ev = Evaluator(policy); // Three different "messages" (attempt index 0 each time) exhaust the group total - Assert.IsTrue(ev.Evaluate(XchangeResultType.Error, "err", 0).ShouldRetry); - Assert.IsTrue(ev.Evaluate(XchangeResultType.Error, "err", 0).ShouldRetry); - Assert.IsTrue(ev.Evaluate(XchangeResultType.Error, "err", 0).ShouldRetry); - Assert.IsFalse(ev.Evaluate(XchangeResultType.Error, "err", 0).ShouldRetry); // exceeded total=3 + Assert.IsTrue((await ev.Evaluate(XchangeResultType.Error, "err", 0)).ShouldRetry); + Assert.IsTrue((await ev.Evaluate(XchangeResultType.Error, "err", 0)).ShouldRetry); + Assert.IsTrue((await ev.Evaluate(XchangeResultType.Error, "err", 0)).ShouldRetry); + Assert.IsFalse((await ev.Evaluate(XchangeResultType.Error, "err", 0)).ShouldRetry); // exceeded total=3 } // ─── Evaluator: delay strategies ───────────────────────────────────────────── @@ -355,67 +425,74 @@ public void ExponentialDelay_DoublesAndCaps() } [TestMethod] - public void Evaluator_DelayFromStrategy_ReturnsCorrectValue() + public async Task Evaluator_DelayFromStrategy_ReturnsCorrectValue() { var policy = PolicyWith(ErrorGroup("transient", new ContainsMatcher { Value = "err" }, delay: new FixedDelayStrategy { DelayMs = 3000 })); - var ev = new RetryPolicyEvaluator(policy); - var decision = ev.Evaluate(XchangeResultType.Error, "err", 0); + var ev = Evaluator(policy); + var decision = await ev.Evaluate(XchangeResultType.Error, "err", 0); Assert.AreEqual(TimeSpan.FromMilliseconds(3000), decision.Delay); } - // ─── Evaluator: GroupAttemptCounts persistence ─────────────────────────────── + // ─── Evaluator: the total cap is shared, not per message ───────────────────── [TestMethod] - public void GroupAttemptCounts_RestoredAcrossEvaluators_ContinuesBudget() + public async Task Evaluator_MaxAttemptsTotal_SharedAcrossSeparateMessages() { + // The bug this covers: one evaluator per failed xchange, each starting from zero, so + // four failing messages × 3 per-message attempts produced 12 retries under a total of 10. var policy = PolicyWith(ErrorGroup("transient", new ContainsMatcher { Value = "err" }, - maxPerError: 100, maxTotal: 2)); - - var ev1 = new RetryPolicyEvaluator(policy); - Assert.IsTrue(ev1.Evaluate(XchangeResultType.Error, "err", 0).ShouldRetry); // group count = 1 - var counts = ev1.GetGroupAttemptCounts(); + maxPerError: 3, maxTotal: 4)); + var budget = new InMemoryRetryGroupBudget(); - // Simulate the next evaluator instance restoring saved state - var ev2 = new RetryPolicyEvaluator(policy); - ev2.RestoreGroupAttemptCounts(counts); // group count restored to 1 - Assert.IsTrue(ev2.Evaluate(XchangeResultType.Error, "err", 0).ShouldRetry); // group count = 2 - var counts2 = ev2.GetGroupAttemptCounts(); + var allowed = 0; + for (var message = 0; message < 4; message++) + for (var attempt = 0; attempt < 3; attempt++) + { + // A fresh evaluator per failure, exactly as XchangeService builds one. + var ev = new RetryPolicyEvaluator(policy, budget); + if ((await ev.Evaluate(XchangeResultType.Error, "err", attempt)).ShouldRetry) allowed++; + } - var ev3 = new RetryPolicyEvaluator(policy); - ev3.RestoreGroupAttemptCounts(counts2); // group count restored to 2 - Assert.IsFalse(ev3.Evaluate(XchangeResultType.Error, "err", 0).ShouldRetry); // exceeded total=2 + Assert.AreEqual(4, allowed); } [TestMethod] - public void GroupAttemptCounts_WithoutRestore_BudgetResetsToZero() + public async Task Evaluator_MaxAttemptsTotal_SeparateBudgetsDoNotShare() { + // Two integrations pointed at the same policy template get independent totals. var policy = PolicyWith(ErrorGroup("transient", new ContainsMatcher { Value = "err" }, maxPerError: 100, maxTotal: 1)); - var ev1 = new RetryPolicyEvaluator(policy); - Assert.IsTrue(ev1.Evaluate(XchangeResultType.Error, "err", 0).ShouldRetry); // exhausted - - // Fresh evaluator without restore — budget starts from 0 again - var ev2 = new RetryPolicyEvaluator(policy); - Assert.IsTrue(ev2.Evaluate(XchangeResultType.Error, "err", 0).ShouldRetry); + Assert.IsTrue((await Evaluator(policy).Evaluate(XchangeResultType.Error, "err", 0)).ShouldRetry); + Assert.IsTrue((await Evaluator(policy).Evaluate(XchangeResultType.Error, "err", 0)).ShouldRetry); } [TestMethod] - public void GetGroupAttemptCounts_ReturnsNonEmptyAfterMatch() + public async Task Evaluator_PerMessageCapBlocked_DoesNotSpendSharedTotal() { - var policy = PolicyWith(ErrorGroup("transient", new ContainsMatcher { Value = "err" })); - var ev = new RetryPolicyEvaluator(policy); - ev.Evaluate(XchangeResultType.Error, "err", 0); - var counts = ev.GetGroupAttemptCounts(); - Assert.IsTrue(counts.Count > 0); + var policy = PolicyWith(ErrorGroup("transient", new ContainsMatcher { Value = "err" }, + maxPerError: 1, maxTotal: 2)); + var budget = new InMemoryRetryGroupBudget(); + + // Attempt index 1 is past the per-message cap, so it must not claim a slot. + Assert.IsFalse((await new RetryPolicyEvaluator(policy, budget) + .Evaluate(XchangeResultType.Error, "err", 1)).ShouldRetry); + + // Both slots of the total are therefore still there. + Assert.IsTrue((await new RetryPolicyEvaluator(policy, budget) + .Evaluate(XchangeResultType.Error, "err", 0)).ShouldRetry); + Assert.IsTrue((await new RetryPolicyEvaluator(policy, budget) + .Evaluate(XchangeResultType.Error, "err", 0)).ShouldRetry); + Assert.IsFalse((await new RetryPolicyEvaluator(policy, budget) + .Evaluate(XchangeResultType.Error, "err", 0)).ShouldRetry); } // ─── Evaluator: Block action ───────────────────────────────────────────────── [TestMethod] - public void Evaluator_BlockAction_NeverRetries() + public async Task Evaluator_BlockAction_NeverRetries() { var blockGroup = new RetryGroup { @@ -428,8 +505,8 @@ public void Evaluator_BlockAction_NeverRetries() Budget = null }; var policy = PolicyWith(blockGroup); - var ev = new RetryPolicyEvaluator(policy); - var decision = ev.Evaluate(XchangeResultType.Error, "fatal: cannot recover", 0); + var ev = Evaluator(policy); + var decision = await ev.Evaluate(XchangeResultType.Error, "fatal: cannot recover", 0); Assert.IsFalse(decision.ShouldRetry); } } diff --git a/SW.Bitween.Web/ClientApp/src/api/client.ts b/SW.Bitween.Web/ClientApp/src/api/client.ts index 113f0cd6..c861fbae 100644 --- a/SW.Bitween.Web/ClientApp/src/api/client.ts +++ b/SW.Bitween.Web/ClientApp/src/api/client.ts @@ -1,7 +1,9 @@ +import type { AddBusRouteInput, AttachPartnerInput } from "./http/gateways"; import type { AdapterInfo, AdapterKind, ApiGateway, + ApiGatewayAttachment, ApiGatewayDetail, ApiGatewayRow, BusGateway, @@ -14,7 +16,6 @@ import type { GlobalValuesSetRow, InformationType, InformationTypeDetail, - InformationTypeFormat, InformationTypeRow, Integration, IntegrationDetail, @@ -33,10 +34,15 @@ import type { PermissionArea, PermissionKey, QueueHealthSnapshot, + ReceiveAttemptRow, + ReceiveOutcome, RetryGroup, + RetryAlertConfig, + RetryAttempts, RetryPolicy, RetryPolicyDetail, RetryPolicyListRow, + RetryUsageRow, RetryResultType, RetryTestAttempt, Role, @@ -86,6 +92,8 @@ export interface ApiClient { /** Stands in for a self-service reset, which would need mail Bitween doesn't have. */ setUserPassword(id: string, password: string): Promise; updateUserRoles(id: string, roleIds: string[]): Promise; + /** Clears a failed-sign-in lockout early, rather than waiting it out. */ + unlockUser(id: string): Promise; setUserDisabled(id: string, disabled: boolean): Promise; deleteUser(id: string): Promise; @@ -103,6 +111,7 @@ export interface ApiClient { // — partners — listPartners(): Promise; + searchPartners(query: { search: string; offset: number; limit: number }): Promise>; getPartner(id: number): Promise; /** Light fetch used by the mapper editor's test-partner selector. */ getPartnerAdapterProperties(id: number): Promise>; @@ -118,14 +127,16 @@ export interface ApiClient { // — information types — listInformationTypes(): Promise; + searchInformationTypes(query: { + search: string; + offset: number; + limit: number; + }): Promise>; getInformationType(id: number): Promise; - createInformationType(input: { - name: string; - code: string; - format: InformationTypeFormat; - busEnabled?: boolean; - busMessageTypeName?: string; - }): Promise; + /** Same payload as update: a new type arrives complete, promoted properties included. */ + createInformationType( + input: Omit, + ): Promise; updateInformationType( id: number, changes: Omit, @@ -151,12 +162,23 @@ export interface ApiClient { // — integrations — listIntegrationRows(): Promise; + searchIntegrationRows(query: { + search: string; + type: IntegrationType | null; + informationTypeId?: number | null; + partnerId?: number | null; + inactive?: boolean | null; + offset: number; + limit: number; + }): Promise>; getIntegration(id: number): Promise; - /** Only Receiving / GatewayApiCall / BusGateway — always from a create page. */ + /** One call, one transaction: the integration exists as asked for, or not at all. */ createIntegration(input: { type: IntegrationType; name: string; informationTypeId: number; + /** Required by the types that carry their own partner — Internal and ApiCall. */ + partnerId?: number | null; receiverId?: string | null; receiverProperties?: Record; validatorId?: string | null; @@ -201,6 +223,10 @@ export interface ApiClient { receiveNow(id: number): Promise; /** Run history for one scheduled integration, newest first. Empty for unscheduled types. */ listIntegrationRuns(id: number, limit?: number): Promise; + searchReceiveAttempts( + subscriptionId: number, + query: { outcome: ReceiveOutcome | null; offset: number; limit: number }, + ): Promise>; /** Newest run of every scheduled integration — one request for a whole list. */ listLastRuns(): Promise; /** Will these schedules actually fire? Asks the scheduler, not the integration record. */ @@ -209,6 +235,7 @@ export interface ApiClient { // — work groups — listWorkGroups(): Promise; + searchWorkGroups(query: { search: string; offset: number; limit: number }): Promise>; getWorkGroup(id: number): Promise; createWorkGroup(input: { name: string; @@ -224,24 +251,38 @@ export interface ApiClient { // — API gateways — listApiGateways(): Promise; + searchApiGateways(query: { search: string; offset: number; limit: number }): Promise>; getApiGateway(id: number): Promise; + searchGatewayAttachments( + apiGatewayId: number, + query: { search: string; offset: number; limit: number }, + ): Promise>; createApiGateway(input: { name: string; urlName: string }): Promise; - updateApiGateway(id: number, changes: { name: string; urlName: string }): Promise; + updateApiGateway( + id: number, + changes: { name: string; urlName: string; inactive: boolean }, + ): Promise; deleteApiGateway(id: number): Promise; - attachGatewayPartner(id: number, input: { partnerId: number; integrationId: number }): Promise; + /** The integration is either an existing id or defined inline; the endpoint commits both as one. */ + attachGatewayPartner(id: number, input: AttachPartnerInput): Promise; updateGatewayAttachment(id: number, input: { partnerId: number; integrationId: number }): Promise; removeGatewayAttachment(id: number, partnerId: number): Promise; // — bus gateways — listBusGateways(): Promise; + searchBusGateways(query: { + search: string; + informationTypeId?: number | null; + inactive?: boolean | null; + offset: number; + limit: number; + }): Promise>; getBusGateway(id: number): Promise; createBusGateway(input: { name: string; informationTypeId: number }): Promise; - updateBusGateway(id: number, changes: { name: string }): Promise; + updateBusGateway(id: number, changes: { name: string; inactive: boolean }): Promise; deleteBusGateway(id: number): Promise; - addBusRoute( - id: number, - input: { integrationId: number; partnerId: number | null; matchExpression: MatchGroup | null }, - ): Promise; + /** The integration is either an existing id or defined inline; the endpoint commits both as one. */ + addBusRoute(id: number, input: AddBusRouteInput): Promise; updateBusRoute( id: number, routeId: number, @@ -251,9 +292,22 @@ export interface ApiClient { // — retry policies — listRetryPolicies(): Promise; + searchRetryPolicies(query: { + search: string; + offset: number; + limit: number; + }): Promise>; getRetryPolicy(id: number): Promise; createRetryPolicy(input: { name: string }): Promise; - updateRetryPolicy(id: number, changes: { name: string; groups: RetryGroup[] }): Promise; + updateRetryPolicy( + id: number, + changes: { + name: string; + groups: RetryGroup[]; + alertHandlerId: string | null; + alertHandlerProperties: Record; + }, + ): Promise; deleteRetryPolicy(id: number): Promise; /** Dry-runs draft groups against a simulated failure over N attempts. */ testRetryPolicy(input: { @@ -263,6 +317,26 @@ export interface ApiClient { attempts: number; }): Promise; + /** Spent budget and alert routing for every integration-and-group pair under this policy. */ + getRetryUsage(policyId: number): Promise; + /** + * The same report for one integration, which is the only way to reach one whose policy is an + * inline `CustomRetryPolicy` — those carry no policy id for the policy-scoped report to address, + * yet still spend budget and can sit stopped with no counter anyone can see. + */ + getIntegrationRetryUsage(integrationId: number): Promise; + /** The failures one group caught for one integration — what its spent budget went on. */ + getRetryAttempts(policyId: number, pair: { integrationId: number; groupId: string }): Promise; + /** Hands a spent budget back so the group retries again. Omit a field to reset across it. */ + resetRetryUsage(policyId: number, pair?: { integrationId?: number; groupId?: string }): Promise; + /** Reset by integration, for the inline-policy case the policy-scoped reset cannot reach. */ + resetIntegrationRetryUsage(integrationId: number, groupId?: string): Promise; + /** Sets, changes or clears where one pair's alert goes — the most specific level. */ + saveRetryAlertOverride( + policyId: number, + input: { integrationId: number; groupId: string } & RetryAlertConfig, + ): Promise; + // — settings — listSettings(): Promise; /** `value: null` resets the setting back to its default. */ @@ -271,10 +345,11 @@ export interface ApiClient { // — notifiers — // No backend delete/test-send endpoint exists yet (BACKEND_WIRING_PLAN.md G8) — hidden in the UI. // Channel choices come from listAdapters("handler") — same catalog as any other handler slot. - listNotifiers(): Promise; + searchNotifiers(query: { search: string; offset: number; limit: number }): Promise>; getNotifier(id: number): Promise; createNotifier(input: { name: string }): Promise; updateNotifier(id: number, changes: Omit): Promise; + deleteNotifier(id: number): Promise; // — exchanges — searchExchanges(query: ExchangeQuery): Promise>; diff --git a/SW.Bitween.Web/ClientApp/src/api/http/documents.ts b/SW.Bitween.Web/ClientApp/src/api/http/documents.ts index cceb2053..9e1652ee 100644 --- a/SW.Bitween.Web/ClientApp/src/api/http/documents.ts +++ b/SW.Bitween.Web/ClientApp/src/api/http/documents.ts @@ -5,11 +5,13 @@ import type { InformationTypeFormat, InformationTypeRow, IntegrationType, + Paged, TrailEntry, } from "../types"; import { exchangeMethods } from "./exchanges"; import { gatewayMethods } from "./gateways"; import { get, getEnrichment, post, request } from "./request"; +import { buildListQuery, SEARCHY_RULE } from "./searchQuery"; interface SearchyResponse { result: T[]; @@ -122,6 +124,18 @@ async function fetchDetail(id: number): Promise { }; } +/** One wire shape for both create and update, so they cannot drift apart. */ +const documentBody = (t: Omit) => ({ + code: t.code?.trim() || undefined, + name: t.name, + documentFormat: t.format, + busEnabled: t.busEnabled, + busMessageTypeName: t.busEnabled ? t.busMessageTypeName : undefined, + duplicateInterval: t.duplicateIntervalMinutes, + disregardsUnfilteredMessages: t.disregardsUnfilteredMessages, + promotedProperties: t.promotedProperties.map((p) => ({ key: p.key, value: p.path })), +}); + export const documentMethods = { async listInformationTypes(): Promise { const [res, subs] = await Promise.all([ @@ -134,22 +148,35 @@ export const documentMethods = { return (res.result ?? []).map((d) => ({ ...toInformationType(d), usedByCount: countByDocument.get(d.id) ?? 0 })); }, + async searchInformationTypes(query: { + search: string; + offset: number; + limit: number; + }): Promise> { + const qs = buildListQuery({ + filters: [["Name", SEARCHY_RULE.contains, query.search.trim()]], + offset: query.offset, + limit: query.limit, + }); + const [res, subs] = await Promise.all([ + get>(`/documents?${qs}`), + getEnrichment>("/subscriptions", { result: [], totalCount: 0 }), + ]); + const countByDocument = new Map(); + for (const s of subs.result ?? []) + countByDocument.set(s.documentId, (countByDocument.get(s.documentId) ?? 0) + 1); + return { + total: res.totalCount, + result: (res.result ?? []).map((d) => ({ ...toInformationType(d), usedByCount: countByDocument.get(d.id) ?? 0 })), + }; + }, + getInformationType: fetchDetail, - async createInformationType(input: { - name: string; - code?: string; - format: InformationTypeFormat; - busEnabled?: boolean; - busMessageTypeName?: string; - }): Promise { - const id = await post("/documents", { - code: input.code?.trim() || undefined, - name: input.name, - documentFormat: input.format, - busEnabled: input.busEnabled ?? false, - busMessageTypeName: input.busEnabled ? input.busMessageTypeName : undefined, - }); + async createInformationType( + input: Omit, + ): Promise { + const id = await post("/documents", documentBody(input)); return fetchDetail(id); }, @@ -157,17 +184,7 @@ export const documentMethods = { id: number, changes: Omit, ): Promise { - await post(`/documents/${id}`, { - id, - code: changes.code?.trim() || undefined, - name: changes.name, - documentFormat: changes.format, - busEnabled: changes.busEnabled, - busMessageTypeName: changes.busEnabled ? changes.busMessageTypeName : undefined, - duplicateInterval: changes.duplicateIntervalMinutes, - disregardsUnfilteredMessages: changes.disregardsUnfilteredMessages, - promotedProperties: changes.promotedProperties.map((p) => ({ key: p.key, value: p.path })), - }); + await post(`/documents/${id}`, { id, ...documentBody(changes) }); return fetchDetail(id); }, diff --git a/SW.Bitween.Web/ClientApp/src/api/http/exchanges.ts b/SW.Bitween.Web/ClientApp/src/api/http/exchanges.ts index 0ff6f882..643236f1 100644 --- a/SW.Bitween.Web/ClientApp/src/api/http/exchanges.ts +++ b/SW.Bitween.Web/ClientApp/src/api/http/exchanges.ts @@ -60,7 +60,7 @@ interface RawDelayedRetryRow { * *response* (the handler delivered but the receiver answered with an error) * is distinct from an outright failure. */ -const deriveStatus = (raw: Pick): ExchangeStatus => +export const deriveStatus = (raw: Pick): ExchangeStatus => raw.status === null ? "processing" : !raw.status ? "failed" : raw.responseBad ? "badResponse" : "success"; const STATUS_FILTER: Record = { @@ -134,7 +134,13 @@ function buildExchangeQuery(query: ExchangeQuery): string { params.append("filter", `Id:4:text|${ids.join("|")}`); } if (query.correlationId?.trim()) params.append("filter", `CorrelationId:1:${query.correlationId.trim()}`); - if (query.property?.trim()) params.append("filter", `PromotedPropertiesRaw:4:${query.property.trim()}`); + // PromotedPropertiesRaw is stored as "key:value,key:value", so prefixing the key turns + // the same substring match into a scoped one — no schema or endpoint change needed. + // Typing "merchant:Acme" into the value box has therefore always worked; the picker + // just makes it something you can find. + const propertyValue = query.property?.trim() ?? ""; + const propertyTerm = query.propertyKey ? `${query.propertyKey}:${propertyValue}` : propertyValue; + if (propertyTerm) params.append("filter", `PromotedPropertiesRaw:4:${propertyTerm}`); if (query.from) params.append("filter", `StartedOn:6:${query.from}`); if (query.to) params.append("filter", `StartedOn:8:${query.to}`); params.set("page", String(Math.floor(query.offset / query.limit))); diff --git a/SW.Bitween.Web/ClientApp/src/api/http/gateways.ts b/SW.Bitween.Web/ClientApp/src/api/http/gateways.ts index 0c5d25b7..ef76bcdd 100644 --- a/SW.Bitween.Web/ClientApp/src/api/http/gateways.ts +++ b/SW.Bitween.Web/ClientApp/src/api/http/gateways.ts @@ -8,10 +8,14 @@ import type { BusGatewayDetail, BusGatewayRoute, BusGatewayRow, + InlineIntegrationDraft, MatchGroup, + Paged, } from "../types"; import { toMatchGroup, toRawMatchExpression, type RawMatchSpec } from "./matchExpression"; +import { inlineIntegrationBody } from "./subscriptionBody"; import { get, post, request } from "./request"; +import { buildListQuery, SEARCHY_RULE } from "./searchQuery"; // ——— backend shapes (camelCase over the wire) ——— interface SearchyResponse { @@ -29,6 +33,7 @@ interface RawApiGateway { name: string; urlName: string; partnersCount: number | null; + inactive: boolean | null; // Search's list projection includes this too (backend change made alongside // this batch) — but keep it optional since Create's bare POST response has none. partners: RawApiGatewayPartner[] | null; @@ -47,6 +52,7 @@ interface RawBusGateway { documentId: number; documentName: string | null; routesCount: number | null; + inactive: boolean | null; routes: RawBusGatewayRoute[] | null; } @@ -61,6 +67,7 @@ const toApiGatewayRow = (raw: RawApiGateway): ApiGatewayRow => ({ id: raw.id, name: raw.name, urlName: raw.urlName, + inactive: raw.inactive ?? false, createdOn: "", partnerCount: raw.partnersCount ?? raw.partners?.length ?? 0, attachments: (raw.partners ?? []).map(toApiGatewayAttachment), @@ -70,6 +77,7 @@ const toApiGatewayDetail = (raw: RawApiGateway): ApiGatewayDetail => ({ id: raw.id, name: raw.name, urlName: raw.urlName, + inactive: raw.inactive ?? false, createdOn: "", attachments: (raw.partners ?? []).map(toApiGatewayAttachment), }); @@ -87,6 +95,7 @@ const toBusGatewayRow = (raw: RawBusGateway): BusGatewayRow => ({ id: raw.id, name: raw.name, informationTypeId: raw.documentId, + inactive: raw.inactive ?? false, createdOn: "", informationTypeCode: raw.documentName ?? "UNKNOWN", routeCount: raw.routesCount ?? raw.routes?.length ?? 0, @@ -97,12 +106,28 @@ const toBusGatewayDetail = (raw: RawBusGateway): BusGatewayDetail => ({ id: raw.id, name: raw.name, informationTypeId: raw.documentId, + inactive: raw.inactive ?? false, createdOn: "", informationTypeCode: raw.documentName ?? "UNKNOWN", informationTypeName: raw.documentName ?? "Unknown", routes: (raw.routes ?? []).map(toBusGatewayRoute), }); +/** The attachment always points at an integration that already exists — a new one is + * created on its own page first, not inline here (unlike a bus gateway route, which + * still creates one in the same transaction — see `AddBusRouteInput`). */ +export type AttachPartnerInput = { partnerId: number; integrationId: number }; + +/** + * A route points at an integration that already exists, or defines one. Exactly one, + * which the endpoint enforces — the union makes that unrepresentable rather than + * merely wrong. + */ +export type AddBusRouteInput = { + partnerId: number | null; + matchExpression: MatchGroup | null; +} & ({ integrationId: number } | { newIntegration: InlineIntegrationDraft }); + export const gatewayMethods = { // ——— API gateways ——— @@ -111,26 +136,65 @@ export const gatewayMethods = { return (res.result ?? []).map(toApiGatewayRow); }, + async searchApiGateways(query: { search: string; offset: number; limit: number }): Promise> { + const qs = buildListQuery({ + filters: [["Name", SEARCHY_RULE.contains, query.search.trim()]], + offset: query.offset, + limit: query.limit, + }); + const res = await get>(`/apigateways?${qs}`); + return { total: res.totalCount, result: (res.result ?? []).map(toApiGatewayRow) }; + }, + async getApiGateway(id: number): Promise { return toApiGatewayDetail(await get(`/apigateways/${id}`)); }, + /** Paged, searched view of one gateway's attachments, for the gateway page's own + * table — `getApiGateway` keeps returning the full list, still needed by the + * attach-partner picker's exclude list. */ + async searchGatewayAttachments( + apiGatewayId: number, + query: { search: string; offset: number; limit: number }, + ): Promise> { + const params = new URLSearchParams({ + apiGatewayId: String(apiGatewayId), + offset: String(query.offset), + limit: String(query.limit), + }); + if (query.search.trim()) params.set("search", query.search.trim()); + const res = await get>(`/apigateways/attachments?${params.toString()}`); + return { total: res.totalCount, result: (res.result ?? []).map(toApiGatewayAttachment) }; + }, + async createApiGateway({ name, urlName }: { name: string; urlName: string }): Promise { - const id = await post("/apigateways", { name, urlName }); - return { id, name, urlName, createdOn: "" }; + const id = await post("/apigateways", { name, urlName, inactive: false }); + return { id, name, urlName, inactive: false, createdOn: "" }; }, - async updateApiGateway(id: number, changes: { name: string; urlName: string }): Promise { - await post(`/apigateways/${id}`, { name: changes.name, urlName: changes.urlName }); - return { id, name: changes.name, urlName: changes.urlName, createdOn: "" }; + async updateApiGateway( + id: number, + changes: { name: string; urlName: string; inactive: boolean }, + ): Promise { + // Update replaces the record, so every field it accepts has to be sent back — + // omitting `inactive` would quietly reactivate a paused gateway on a rename. + await post(`/apigateways/${id}`, { + name: changes.name, + urlName: changes.urlName, + inactive: changes.inactive, + }); + return { id, ...changes, createdOn: "" }; }, async deleteApiGateway(id: number): Promise { await request(`/apigateways/${id}`, { method: "DELETE" }); }, - async attachGatewayPartner(id: number, input: { partnerId: number; integrationId: number }): Promise { - await post(`/apigateways/${id}/addpartner`, { partnerId: input.partnerId, subscriptionId: input.integrationId }); + async attachGatewayPartner(id: number, input: AttachPartnerInput): Promise { + await post(`/apigateways/${id}/addpartner`, { + partnerId: input.partnerId, + subscriptionId: input.integrationId, + }); }, async updateGatewayAttachment(id: number, input: { partnerId: number; integrationId: number }): Promise { @@ -153,6 +217,26 @@ export const gatewayMethods = { return (res.result ?? []).map(toBusGatewayRow); }, + async searchBusGateways(query: { + search: string; + informationTypeId?: number | null; + inactive?: boolean | null; + offset: number; + limit: number; + }): Promise> { + const qs = buildListQuery({ + filters: [ + ["Name", SEARCHY_RULE.contains, query.search.trim()], + ["DocumentId", SEARCHY_RULE.equalsTo, query.informationTypeId ?? ""], + ["Inactive", SEARCHY_RULE.equalsTo, query.inactive == null ? "" : String(query.inactive)], + ], + offset: query.offset, + limit: query.limit, + }); + const res = await get>(`/busgateways?${qs}`); + return { total: res.totalCount, result: (res.result ?? []).map(toBusGatewayRow) }; + }, + async getBusGateway(id: number): Promise { return toBusGatewayDetail(await get(`/busgateways/${id}`)); }, @@ -164,29 +248,47 @@ export const gatewayMethods = { name: string; informationTypeId: number; }): Promise { - const id = await post("/busgateways", { name, documentId: informationTypeId }); - return { id, name, informationTypeId, createdOn: "" }; + const id = await post("/busgateways", { + name, + documentId: informationTypeId, + inactive: false, + }); + return { id, name, informationTypeId, inactive: false, createdOn: "" }; }, - async updateBusGateway(id: number, changes: { name: string }): Promise { + async updateBusGateway( + id: number, + changes: { name: string; inactive: boolean }, + ): Promise { // The bound information type is fixed at creation — Update.cs silently // ignores documentId — but the request DTO still requires a value, so // fetch the current one to round-trip it rather than sending a bogus 0. const current = await get(`/busgateways/${id}`); - await post(`/busgateways/${id}`, { name: changes.name, documentId: current.documentId }); - return { id, name: changes.name, informationTypeId: current.documentId, createdOn: "" }; + await post(`/busgateways/${id}`, { + name: changes.name, + documentId: current.documentId, + inactive: changes.inactive, + }); + return { + id, + name: changes.name, + informationTypeId: current.documentId, + inactive: changes.inactive, + createdOn: "", + }; }, async deleteBusGateway(id: number): Promise { await request(`/busgateways/${id}`, { method: "DELETE" }); }, - async addBusRoute( - id: number, - input: { integrationId: number; partnerId: number | null; matchExpression: MatchGroup | null }, - ): Promise { + async addBusRoute(id: number, input: AddBusRouteInput): Promise { await post(`/busgateways/${id}/addroute`, { - subscriptionId: input.integrationId, + // Exactly one of the two, which is what the endpoint enforces. An integration + // defined here is created in the same transaction as the route. + ...("newIntegration" in input + ? { newIntegration: inlineIntegrationBody(input.newIntegration) } + : { subscriptionId: input.integrationId }), partnerId: input.partnerId, matchExpression: toRawMatchExpression(input.matchExpression), }); diff --git a/SW.Bitween.Web/ClientApp/src/api/http/integrations.ts b/SW.Bitween.Web/ClientApp/src/api/http/integrations.ts index 10cdcb32..910ce616 100644 --- a/SW.Bitween.Web/ClientApp/src/api/http/integrations.ts +++ b/SW.Bitween.Web/ClientApp/src/api/http/integrations.ts @@ -8,34 +8,58 @@ import { type IntegrationRow, type IntegrationRun, type IntegrationType, + type InformationTypeRow, + type Paged, + type PartnerRow, + type ReceiveAttemptRow, + type ReceiveOutcome, type Schedule, type ScheduleHealth, } from "../types"; import { schedulesSummary } from "../../lib/schedules"; import { documentMethods } from "./documents"; -import { exchangeMethods } from "./exchanges"; +import { deriveStatus, exchangeMethods } from "./exchanges"; import { gatewayMethods } from "./gateways"; import { partnerMethods } from "./partners"; import { scanReferenceTokens } from "./references"; import { get, post, request } from "./request"; +import { buildListQuery, SEARCHY_RULE, SEARCHY_SORT } from "./searchQuery"; import { toMatchGroup, toRawMatchExpression, type RawMatchSpec } from "./matchExpression"; +import { + toKvArray, + toRawSchedules, + type RawKeyAndValue, + type RawSchedule, +} from "./subscriptionBody"; // ——— backend shapes (camelCase over the wire) ——— interface SearchyResponse { result: T[]; totalCount: number; } -interface RawKeyAndValue { - key: string; - value: string; + +interface RawReceiveAttemptExchange { + id: string; + status: boolean | null; + responseBad: boolean | null; + promotedProperties: Record | null; } -interface RawSchedule { - recurrence: Schedule["recurrence"]; - days: number; - hours: number; - minutes: number; - backwards: boolean; +interface RawReceiveAttempt { + id: number; + startedOn: string; + finishedOn: string; + // Enums may arrive as the numeric value or the name, depending on the endpoint. + outcome: number | string; + errorMessage: string | null; + exchanges: RawReceiveAttemptExchange[]; } +const RECEIVE_OUTCOME_BY_NUM: Record = { + 0: "Failed", + 1: "NoNewData", + 2: "Received", +}; +const toReceiveOutcome = (o: number | string): ReceiveOutcome => + typeof o === "number" ? (RECEIVE_OUTCOME_BY_NUM[o] ?? "Failed") : (o as ReceiveOutcome); interface RawSubscription { id?: number; @@ -99,8 +123,6 @@ const toIntegrationType = (t: number | string): IntegrationType => // stored data that never had the key — leaving the Save bar up after an undo. const toRecord = (kvs: RawKeyAndValue[] | null): Record => Object.fromEntries((kvs ?? []).filter((kv) => kv.value !== "").map((kv) => [kv.key, kv.value])); -const toKvArray = (record: Record): RawKeyAndValue[] => - Object.entries(record).map(([key, value]) => ({ key, value })); const toSchedules = (raw: RawSchedule[] | null): Schedule[] => (raw ?? []).map((s) => ({ @@ -110,14 +132,6 @@ const toSchedules = (raw: RawSchedule[] | null): Schedule[] => minutes: s.minutes, backwards: s.backwards, })); -const toRawSchedules = (schedules: Schedule[]): RawSchedule[] => - schedules.map((s) => ({ - recurrence: s.recurrence, - days: s.days, - hours: s.hours, - minutes: s.minutes, - backwards: s.backwards, - })); function toIntegration(raw: RawSubscription, idOverride?: number): Integration { return { @@ -235,6 +249,41 @@ async function applyChanges(id: number, current: RawSubscription, changes: Updat }); } +function toIntegrationRow( + raw: RawSubscription, + infoTypeById: Map, + partnerById: Map, +): IntegrationRow { + const type = toIntegrationType(raw.type); + const infoType = infoTypeById.get(raw.documentId); + const partner = raw.partnerId !== null ? partnerById.get(raw.partnerId) : undefined; + const schedules = toSchedules(raw.schedules); + return { + id: raw.id!, + name: raw.name, + type, + informationTypeId: raw.documentId, + informationTypeCode: infoType?.code ?? infoType?.name ?? "", + // Gateway-derived partners (GatewayApiCall/BusGateway) land in Batch 3. + partners: partner ? [{ id: partner.id, name: partner.name }] : [], + enabled: !raw.inactive, + paused: raw.pausedOn !== null, + isRunning: raw.isRunning ?? false, + consecutiveFailures: raw.consecutiveFailures ?? 0, + lastException: raw.lastException ?? null, + // Search.cs can't select Schedules in this joined query without + // breaking SQL translation (Postgres date_part type mismatch), so + // schedules is always empty here — showing "No schedule" would be + // actively wrong for a job that has one. Leave it unset instead. + scheduleSummary: + schedules.length > 0 && (type === "Receiving" || type === "Aggregation") + ? schedulesSummary(schedules) + : undefined, + nextReceiveOn: raw.receiveOn ?? null, + createdOn: "", + }; +} + export const integrationMethods = { async listIntegrations(): Promise { const rows = await fetchAllRaw(); @@ -246,6 +295,7 @@ export const integrationMethods = { informationTypeId: raw.documentId, workGroupId: raw.workGroupId ?? null, retryPolicyId: raw.retryPolicyId ?? null, + handlerId: raw.handlerId ?? null, responseMessageTypeName: raw.responseMessageTypeName ?? null, responseIntegrationId: raw.responseSubscriptionId ?? null, // No backend endpoint indexes reference tokens, but the search rows carry @@ -269,37 +319,41 @@ export const integrationMethods = { ]); const infoTypeById = new Map(infoTypes.map((t) => [t.id, t])); const partnerById = new Map(partners.map((p) => [p.id, p])); + return rows.map((raw) => toIntegrationRow(raw, infoTypeById, partnerById)); + }, - return rows.map((raw) => { - const type = toIntegrationType(raw.type); - const infoType = infoTypeById.get(raw.documentId); - const partner = raw.partnerId !== null ? partnerById.get(raw.partnerId) : undefined; - const schedules = toSchedules(raw.schedules); - return { - id: raw.id!, - name: raw.name, - type, - informationTypeId: raw.documentId, - informationTypeCode: infoType?.code ?? infoType?.name ?? "", - // Gateway-derived partners (GatewayApiCall/BusGateway) land in Batch 3. - partners: partner ? [{ id: partner.id, name: partner.name }] : [], - enabled: !raw.inactive, - paused: raw.pausedOn !== null, - isRunning: raw.isRunning ?? false, - consecutiveFailures: raw.consecutiveFailures ?? 0, - lastException: raw.lastException ?? null, - // Search.cs can't select Schedules in this joined query without - // breaking SQL translation (Postgres date_part type mismatch), so - // schedules is always empty here — showing "No schedule" would be - // actively wrong for a job that has one. Leave it unset instead. - scheduleSummary: - schedules.length > 0 && (type === "Receiving" || type === "Aggregation") - ? schedulesSummary(schedules) - : undefined, - nextReceiveOn: raw.receiveOn ?? null, - createdOn: "", - }; + async searchIntegrationRows(query: { + search: string; + type: IntegrationType | null; + informationTypeId?: number | null; + partnerId?: number | null; + inactive?: boolean | null; + offset: number; + limit: number; + }): Promise> { + const qs = buildListQuery({ + filters: [ + ["Name", SEARCHY_RULE.contains, query.search.trim()], + ["Type", SEARCHY_RULE.equalsTo, query.type ?? ""], + ["DocumentId", SEARCHY_RULE.equalsTo, query.informationTypeId ?? ""], + ["PartnerId", SEARCHY_RULE.equalsTo, query.partnerId ?? ""], + ["Inactive", SEARCHY_RULE.equalsTo, query.inactive == null ? "" : String(query.inactive)], + ], + sort: ["Name", SEARCHY_SORT.asc], + offset: query.offset, + limit: query.limit, }); + const [res, infoTypes, partners] = await Promise.all([ + get>(`/subscriptions?${qs}`), + documentMethods.listInformationTypes(), + partnerMethods.listPartners(), + ]); + const infoTypeById = new Map(infoTypes.map((t) => [t.id, t])); + const partnerById = new Map(partners.map((p) => [p.id, p])); + return { + total: res.totalCount, + result: (res.result ?? []).map((raw) => toIntegrationRow(raw, infoTypeById, partnerById)), + }; }, async getIntegration(id: number): Promise { @@ -349,6 +403,8 @@ export const integrationMethods = { type: IntegrationType; name: string; informationTypeId: number; + /** Required by the types that carry their own partner — Internal and ApiCall. */ + partnerId?: number | null; receiverId?: string | null; receiverProperties?: Record; validatorId?: string | null; @@ -370,7 +426,7 @@ export const integrationMethods = { name: input.name, documentId: input.informationTypeId, type: input.type, - partnerId: null, + partnerId: input.partnerId ?? null, aggregationForId: null, receiverId: input.receiverId ?? null, receiverProperties: toKvArray(input.receiverProperties ?? {}), @@ -418,6 +474,34 @@ export const integrationMethods = { return get(`/subscriptions/runs?subscriptionId=${id}&limit=${limit}`); }, + async searchReceiveAttempts( + subscriptionId: number, + query: { outcome: ReceiveOutcome | null; offset: number; limit: number }, + ): Promise> { + const params = new URLSearchParams({ + subscriptionId: String(subscriptionId), + offset: String(query.offset), + limit: String(query.limit), + }); + if (query.outcome) params.set("outcome", query.outcome); + const res = await get>(`/subscriptions/receiveattempts?${params.toString()}`); + return { + total: res.totalCount, + result: (res.result ?? []).map((a) => ({ + id: a.id, + startedOn: a.startedOn, + finishedOn: a.finishedOn, + outcome: toReceiveOutcome(a.outcome), + errorMessage: a.errorMessage, + exchanges: a.exchanges.map((x) => ({ + id: x.id, + status: deriveStatus(x), + promotedProperties: x.promotedProperties, + })), + })), + }; + }, + async listLastRuns(): Promise { const rows = await get<(Omit & { subscriptionId: number })[]>( diff --git a/SW.Bitween.Web/ClientApp/src/api/http/notifiers.ts b/SW.Bitween.Web/ClientApp/src/api/http/notifiers.ts index d26f242d..b55a54fb 100644 --- a/SW.Bitween.Web/ClientApp/src/api/http/notifiers.ts +++ b/SW.Bitween.Web/ClientApp/src/api/http/notifiers.ts @@ -1,6 +1,7 @@ import type { ApiClient } from "../client"; -import { ApiRequestError, type NotificationEntry, type Notifier, type NotifierDetail } from "../types"; -import { get, post } from "./request"; +import { ApiRequestError, type NotificationEntry, type Notifier, type NotifierDetail, type Paged } from "../types"; +import { get, post, request } from "./request"; +import { buildListQuery, SEARCHY_RULE } from "./searchQuery"; interface SearchyResponse { result: T[]; @@ -21,6 +22,17 @@ interface RawNotifier { runOnFailedResult: boolean; runOnSubscriptions: { id: number; name: string | null }[] | null; } +/** Shape of a search-endpoint row — lighter than `RawNotifier`, no adapter properties. */ +interface RawNotifierRow { + id: number; + name: string; + inactive: boolean | null; + handlerId: string | null; + runOnSuccessfulResult: boolean | null; + runOnBadResult: boolean | null; + runOnFailedResult: boolean | null; + runOnSubscriptions: number[] | null; +} interface RawNotification { xchangeId: string; success: boolean; @@ -68,9 +80,28 @@ async function fetchDetail(id: number): Promise { } export const notifierMethods = { - async listNotifiers(): Promise { - const res = await get>("/notifiers"); - return Promise.all((res.result ?? []).map((r) => fetchDetail(r.id))); + async searchNotifiers(query: { search: string; offset: number; limit: number }): Promise> { + const qs = buildListQuery({ + filters: [["Name", SEARCHY_RULE.contains, query.search.trim()]], + offset: query.offset, + limit: query.limit, + }); + const res = await get>(`/notifiers?${qs}`); + return { + total: res.totalCount, + result: (res.result ?? []).map((r) => ({ + id: r.id, + name: r.name, + enabled: !r.inactive, + onFailed: !!r.runOnFailedResult, + onBadResult: !!r.runOnBadResult, + onSuccess: !!r.runOnSuccessfulResult, + channelId: r.handlerId ?? "", + channelProperties: {}, + integrationIds: r.runOnSubscriptions ?? [], + createdOn: "", + })), + }; }, getNotifier: fetchDetail, @@ -104,4 +135,8 @@ export const notifierMethods = { }); return { id, createdOn: "", ...changes }; }, + + async deleteNotifier(id: number): Promise { + await request(`/notifiers/${id}`, { method: "DELETE" }); + }, } satisfies Partial; diff --git a/SW.Bitween.Web/ClientApp/src/api/http/partners.ts b/SW.Bitween.Web/ClientApp/src/api/http/partners.ts index a949ebaa..567da971 100644 --- a/SW.Bitween.Web/ClientApp/src/api/http/partners.ts +++ b/SW.Bitween.Web/ClientApp/src/api/http/partners.ts @@ -1,9 +1,10 @@ import type { ApiClient } from "../client"; -import { ApiRequestError, type Partner, type PartnerDetail, type PartnerRow } from "../types"; +import { ApiRequestError, type Paged, type Partner, type PartnerDetail, type PartnerRow } from "../types"; import { exchangeMethods } from "./exchanges"; import { gatewayMethods } from "./gateways"; import { matchSummary } from "../../lib/match"; import { get, post, request } from "./request"; +import { buildListQuery, SEARCHY_RULE } from "./searchQuery"; // The built-in SYSTEM partner (Partner.SystemId) can't be renamed or deleted. const SYSTEM_PARTNER_ID = 1; @@ -77,6 +78,28 @@ export const partnerMethods = { })); }, + async searchPartners(query: { search: string; offset: number; limit: number }): Promise> { + const qs = buildListQuery({ + filters: [["Name", SEARCHY_RULE.contains, query.search.trim()]], + offset: query.offset, + limit: query.limit, + }); + const res = await get>(`/partners?${qs}`); + return { + total: res.totalCount, + result: (res.result ?? []).map((p) => ({ + id: p.id, + name: p.name, + adapterProperties: {}, + propertyKeys: p.propertyKeys ?? [], + isSystem: p.id === SYSTEM_PARTNER_ID, + createdOn: "", + credentialCount: p.keys, + usedByCount: p.subscriptionsCount, + })), + }; + }, + /** Light single-field fetch for the mapper editor's test-partner selector — avoids getPartner's gateway/exchange lookups. */ async getPartnerAdapterProperties(id: number): Promise> { const d = await requireDetail(id); diff --git a/SW.Bitween.Web/ClientApp/src/api/http/retryPolicies.ts b/SW.Bitween.Web/ClientApp/src/api/http/retryPolicies.ts index 06b9c41a..ade1a22e 100644 --- a/SW.Bitween.Web/ClientApp/src/api/http/retryPolicies.ts +++ b/SW.Bitween.Web/ClientApp/src/api/http/retryPolicies.ts @@ -2,6 +2,9 @@ import type { ApiClient } from "../client"; import { ApiRequestError, type IntegrationType, + type RetryAlertConfig, + type RetryAlertLevel, + type RetryAttempts, type RetryDelay, type RetryGroup, type RetryPolicy, @@ -9,8 +12,11 @@ import { type RetryPolicyListRow, type RetryResultType, type RetryTestAttempt, + type RetryUsageRow, + type Paged, } from "../types"; import { get, getEnrichment, post, request } from "./request"; +import { buildListQuery, SEARCHY_RULE } from "./searchQuery"; interface SearchyResponse { result: T[]; @@ -24,6 +30,8 @@ interface RawRetryPolicyRow { interface RawRetryPolicy { name: string; groups: RawRetryGroup[] | null; + alertHandlerId: string | null; + alertHandlerProperties: Record | null; } interface RawSubscriptionRef { id: number; @@ -70,8 +78,9 @@ interface RawRetryBudget { maxAttemptsTotal: number; delayStrategy: RawDelayStrategy; } -interface RawRetryGroup extends Omit { +interface RawRetryGroup extends Omit { budget?: RawRetryBudget | null; + alertHandlerProperties?: Record | null; } interface RawTestAttempt { attemptNumber: number; @@ -126,6 +135,12 @@ const toGroup = (g: RawRetryGroup): RetryGroup => ({ delay: toDelay(g.budget.delayStrategy), } : undefined, + // Named rather than left to the spread: a policy saved from an older client has no + // alert fields at all, and an undefined mode would compare unequal to "Inherit" and + // leave the Save bar up on a page nobody had edited. + alertMode: g.alertMode ?? "Inherit", + alertHandlerId: g.alertHandlerId ?? null, + alertHandlerProperties: g.alertHandlerProperties ?? {}, }); const toRawGroup = (g: RetryGroup): RawRetryGroup => ({ @@ -150,10 +165,67 @@ async function fetchDetail(id: number): Promise { name: r.name, groups: (r.groups ?? []).map(toGroup), createdOn: "", + alertHandlerId: r.alertHandlerId ?? null, + alertHandlerProperties: r.alertHandlerProperties ?? {}, integrations: subs.map((s) => ({ id: s.id, name: s.name, type: toIntegrationType(s.type) })), }; } +interface RawUsageRow { + subscriptionId: number; + subscriptionName: string; + groupId: string; + groupName: string; + attemptsUsed: number; + maxAttemptsTotal: number; + exhausted: boolean; + lastAttemptOn: string | null; + exhaustedNotifiedOn: string | null; + alertDelivered: boolean | null; + alertError: string | null; + alertMode: RetryAlertConfig["alertMode"]; + overrideHandlerId: string | null; + overrideHandlerProperties: Record | null; + resolvedHandlerId: string | null; + resolvedHandlerProperties: Record | null; + resolvedFrom: RetryAlertLevel | null; + silencedAt: RetryAlertLevel | null; +} + +const toUsageRow = (r: RawUsageRow): RetryUsageRow => ({ + integrationId: r.subscriptionId, + integrationName: r.subscriptionName, + groupId: r.groupId, + groupName: r.groupName, + used: r.attemptsUsed, + total: r.maxAttemptsTotal, + exhausted: r.exhausted, + lastAttemptOn: r.lastAttemptOn, + resolvedHandlerId: r.resolvedHandlerId, + resolvedHandlerProperties: r.resolvedHandlerProperties ?? {}, + resolvedFrom: r.resolvedFrom, + silencedAt: r.silencedAt, + override: { + alertMode: r.alertMode ?? "Inherit", + alertHandlerId: r.overrideHandlerId, + alertHandlerProperties: r.overrideHandlerProperties ?? {}, + }, + // Only an alert that was actually raised has an outcome. Delivery is reported apart from + // the claim because the two can disagree, and the disagreement is the whole point. + alert: r.exhaustedNotifiedOn + ? { claimedOn: r.exhaustedNotifiedOn, delivered: r.alertDelivered, error: r.alertError } + : null, +}); + +interface RawAttempt { + xchangeId: string; + attemptNumber: number | null; + failedOn: string; + exception: string; + retryPending: boolean; + retryBlockedReason: string | null; +} + export const retryPolicyMethods = { async listRetryPolicies(): Promise { const [res, subs] = await Promise.all([ @@ -174,16 +246,118 @@ export const retryPolicyMethods = { })); }, + async searchRetryPolicies(query: { + search: string; + offset: number; + limit: number; + }): Promise> { + const qs = buildListQuery({ + filters: [["Name", SEARCHY_RULE.contains, query.search.trim()]], + offset: query.offset, + limit: query.limit, + }); + const [res, subs] = await Promise.all([ + get>(`/retrypolicies?${qs}`), + getEnrichment>("/subscriptions", { result: [], totalCount: 0 }), + ]); + const countByRetryPolicy = new Map(); + for (const s of subs.result ?? []) { + if (s.retryPolicyId == null) continue; + countByRetryPolicy.set(s.retryPolicyId, (countByRetryPolicy.get(s.retryPolicyId) ?? 0) + 1); + } + return { + total: res.totalCount, + result: (res.result ?? []).map((p) => ({ + id: p.id, + name: p.name, + groupCount: p.groupCount, + createdOn: "", + usedByCount: countByRetryPolicy.get(p.id) ?? 0, + })), + }; + }, + getRetryPolicy: fetchDetail, async createRetryPolicy({ name }: { name: string }): Promise { const id = await post("/retrypolicies", { name, groups: [] }); - return { id, name, groups: [], createdOn: "" }; + return { id, name, groups: [], createdOn: "", alertHandlerId: null, alertHandlerProperties: {} }; + }, + + async updateRetryPolicy( + id: number, + changes: { + name: string; + groups: RetryGroup[]; + alertHandlerId: string | null; + alertHandlerProperties: Record; + }, + ): Promise { + // Update replaces the whole policy, so every field it accepts has to be sent back. Omitting + // the alert cleared it on the server on every save — the settings were still on screen, and + // gone from the database. + await post(`/retrypolicies/${id}`, { + name: changes.name, + groups: changes.groups.map(toRawGroup), + alertHandlerId: changes.alertHandlerId, + alertHandlerProperties: changes.alertHandlerProperties, + }); + return { id, ...changes, createdOn: "" }; + }, + + async getRetryUsage(policyId: number): Promise { + const rows = await post(`/retrypolicies/${policyId}/usage`, {}); + return (rows ?? []).map(toUsageRow); + }, + + async getIntegrationRetryUsage(integrationId: number): Promise { + const rows = await post(`/subscriptions/${integrationId}/retryusage`, {}); + return (rows ?? []).map(toUsageRow); }, - async updateRetryPolicy(id: number, changes: { name: string; groups: RetryGroup[] }): Promise { - await post(`/retrypolicies/${id}`, { name: changes.name, groups: changes.groups.map(toRawGroup) }); - return { id, name: changes.name, groups: changes.groups, createdOn: "" }; + async getRetryAttempts( + policyId: number, + pair: { integrationId: number; groupId: string }, + ): Promise { + const res = await post<{ total: number; attempts: RawAttempt[] }>( + `/retrypolicies/${policyId}/attempts`, + { subscriptionId: pair.integrationId, groupId: pair.groupId }, + ); + return { + total: res?.total ?? 0, + attempts: (res?.attempts ?? []).map((a) => ({ + exchangeId: a.xchangeId, + attemptNumber: a.attemptNumber, + failedOn: a.failedOn, + error: a.exception, + retryPending: a.retryPending, + blockedReason: a.retryBlockedReason, + })), + }; + }, + + async resetRetryUsage(policyId: number, pair?: { integrationId?: number; groupId?: string }): Promise { + await post(`/retrypolicies/${policyId}/resetusage`, { + subscriptionId: pair?.integrationId ?? null, + groupId: pair?.groupId ?? null, + }); + }, + + async resetIntegrationRetryUsage(integrationId: number, groupId?: string): Promise { + await post(`/subscriptions/${integrationId}/resetretryusage`, { groupId: groupId ?? null }); + }, + + async saveRetryAlertOverride( + policyId: number, + input: { integrationId: number; groupId: string } & RetryAlertConfig, + ): Promise { + await post(`/retrypolicies/${policyId}/savealertoverride`, { + subscriptionId: input.integrationId, + groupId: input.groupId, + alertMode: input.alertMode, + alertHandlerId: input.alertHandlerId, + alertHandlerProperties: input.alertHandlerProperties, + }); }, async deleteRetryPolicy(id: number): Promise { diff --git a/SW.Bitween.Web/ClientApp/src/api/http/searchQuery.ts b/SW.Bitween.Web/ClientApp/src/api/http/searchQuery.ts new file mode 100644 index 00000000..ffeba36a --- /dev/null +++ b/SW.Bitween.Web/ClientApp/src/api/http/searchQuery.ts @@ -0,0 +1,38 @@ +/** + * Builds a Searchy query string — the `filter=Field:Rule:Value` + `page`/`size` + * convention every list endpoint in this API already speaks (see `buildExchangeQuery` + * for the hand-written original this generalizes). + * + * `page` and `size` are always sent together, never one without the other: at least + * one backend handler (`Subscriptions/Search.cs`, filtering by name) diverts a text + * filter through an in-memory `Skip(size * page).Take(size)` with no "size == 0 means + * unbounded" guard, so an omitted size silently returns zero rows despite a correct + * total count. Always supplying both sidesteps that regardless of which handler runs. + */ +export function buildListQuery(opts: { + /** [Field, Rule, Value] triples — Rule 1 = EqualsTo, 4 = Contains. Skipped when Value is "". */ + filters?: [string, number, string | number][]; + /** [Field, Order] — Order 1 = ascending, 2 = descending. */ + sort?: [string, number]; + offset: number; + limit: number; +}): string { + const params = new URLSearchParams(); + for (const [field, rule, value] of opts.filters ?? []) { + if (value !== "" && value !== undefined && value !== null) params.append("filter", `${field}:${rule}:${value}`); + } + if (opts.sort) params.append("sort", `${opts.sort[0]}:${opts.sort[1]}`); + params.set("page", String(Math.floor(opts.offset / opts.limit))); + params.set("size", String(opts.limit)); + return params.toString(); +} + +export const SEARCHY_RULE = { + equalsTo: 1, + contains: 4, +} as const; + +export const SEARCHY_SORT = { + asc: 1, + desc: 2, +} as const; diff --git a/SW.Bitween.Web/ClientApp/src/api/http/session.ts b/SW.Bitween.Web/ClientApp/src/api/http/session.ts index 87e8df01..88076776 100644 --- a/SW.Bitween.Web/ClientApp/src/api/http/session.ts +++ b/SW.Bitween.Web/ClientApp/src/api/http/session.ts @@ -27,6 +27,8 @@ const buildSession = (profile: Profile): Session => { email: profile.email, roleIds: (profile.roles ?? []).map((r) => String(r.id)), status: profile.disabled ? "disabled" : "active", + // Always null here: you cannot be signed in and locked out at the same time. + lockedUntil: null, microsoftLinked: false, createdOn: profile.createdOn, }; diff --git a/SW.Bitween.Web/ClientApp/src/api/http/subscriptionBody.ts b/SW.Bitween.Web/ClientApp/src/api/http/subscriptionBody.ts new file mode 100644 index 00000000..a8feb501 --- /dev/null +++ b/SW.Bitween.Web/ClientApp/src/api/http/subscriptionBody.ts @@ -0,0 +1,61 @@ +import type { InlineIntegrationDraft, Schedule } from "../types"; +import { toRawMatchExpression } from "./matchExpression"; + +/** + * The subscription wire shape, in the one place both the integration endpoints and + * the gateway endpoints can reach. + * + * It lives here rather than in `integrations.ts` because `gateways.ts` needs it too, + * and `integrations.ts` already imports `gateways.ts` — putting it there would make + * that cycle mutual. + */ + +export interface RawKeyAndValue { + key: string; + value: string; +} + +export interface RawSchedule { + recurrence: Schedule["recurrence"]; + days: number; + hours: number; + minutes: number; + backwards: boolean; +} + +export const toKvArray = (record: Record): RawKeyAndValue[] => + Object.entries(record).map(([key, value]) => ({ key, value })); + +export const toRawSchedules = (schedules: Schedule[]): RawSchedule[] => + schedules.map((s) => ({ + recurrence: s.recurrence, + days: s.days, + hours: s.hours, + minutes: s.minutes, + backwards: s.backwards, + })); + +/** + * An integration defined on a gateway's canvas, in the shape the gateway endpoints + * take. No `documentId`: a bus gateway imposes its own, and the API-gateway caller + * adds the one its picker chose. + */ +export const inlineIntegrationBody = (d: InlineIntegrationDraft) => ({ + name: d.name.trim(), + inactive: !d.enabled, + workGroupId: d.workGroupId, + retryPolicyId: d.retryPolicyId, + customRetryPolicy: null, + receiverId: d.receiverId, + receiverProperties: toKvArray(d.receiverProperties), + validatorId: d.validatorId, + validatorProperties: toKvArray(d.validatorProperties), + mapperId: d.mapperId, + mapperProperties: toKvArray(d.mapperProperties), + handlerId: d.handlerId, + handlerProperties: toKvArray(d.handlerProperties), + matchExpression: toRawMatchExpression(d.matchExpression), + schedules: toRawSchedules(d.schedules), + responseSubscriptionId: d.responseIntegrationId, + responseMessageTypeName: d.responseMessageTypeName, +}); diff --git a/SW.Bitween.Web/ClientApp/src/api/http/team.ts b/SW.Bitween.Web/ClientApp/src/api/http/team.ts index 45b4b5e9..c00f919b 100644 --- a/SW.Bitween.Web/ClientApp/src/api/http/team.ts +++ b/SW.Bitween.Web/ClientApp/src/api/http/team.ts @@ -24,6 +24,8 @@ interface RawAccount { email: string; role: string; disabled: boolean; + /** Set by the backend's failed-sign-in lockout; null or in the past means not locked. */ + lockoutEnd: string | null; createdOn: string; roles: RawRoleSummary[] | null; } @@ -44,6 +46,8 @@ const toUser = (r: RawAccount): User => ({ email: r.email, roleIds: (r.roles ?? []).map((role) => String(role.id)), status: r.disabled ? "disabled" : "active", + // Past lockouts are not state anyone can act on, so they read as no lockout at all. + lockedUntil: r.lockoutEnd && new Date(r.lockoutEnd) > new Date() ? r.lockoutEnd : null, // Not tracked by the backend: no login-method projection, no last-seen column. microsoftLinked: false, createdOn: r.createdOn, @@ -113,6 +117,12 @@ export const teamMethods = { return fetchUser(id); }, + /** Clears a failed-sign-in lockout early, rather than waiting it out. */ + async unlockUser(id: string): Promise { + await post(`/accounts/${id}/unlock`, {}); + return fetchUser(id); + }, + async setUserDisabled(id: string, disabled: boolean): Promise { await post(`/accounts/${id}/setDisabled`, { disabled }); return fetchUser(id); diff --git a/SW.Bitween.Web/ClientApp/src/api/http/workGroups.ts b/SW.Bitween.Web/ClientApp/src/api/http/workGroups.ts index 76749ae4..51b9a303 100644 --- a/SW.Bitween.Web/ClientApp/src/api/http/workGroups.ts +++ b/SW.Bitween.Web/ClientApp/src/api/http/workGroups.ts @@ -5,6 +5,7 @@ import { type WorkGroup, type WorkGroupDetail, type WorkGroupRow, + type Paged, } from "../types"; import { get, getEnrichment, post } from "./request"; @@ -70,11 +71,28 @@ const toWorkGroup = (w: RawWorkGroup): WorkGroup => ({ createdOn: "", }); +// The backend's own default kicks in only when the caller omits the param +// entirely (`request.Limit ??= 20`), so passing an explicit, generously large +// one is how "everything" is asked for — there is no separate "unbounded" +// value the way the shared Searchy endpoints have with size=0. +const EVERYTHING = 1_000_000; + async function fetchRows(): Promise { - const res = await get>("/workgroups"); + const res = await get>(`/workgroups?offset=0&limit=${EVERYTHING}`); return res.result ?? []; } +async function fetchPagedRows(query: { + search: string; + offset: number; + limit: number; +}): Promise<{ rows: RawWorkGroup[]; total: number }> { + const params = new URLSearchParams({ offset: String(query.offset), limit: String(query.limit) }); + if (query.search.trim()) params.set("name", query.search.trim()); + const res = await get>(`/workgroups?${params.toString()}`); + return { rows: res.result ?? [], total: res.totalCount }; +} + export const workGroupMethods = { async listWorkGroups(): Promise { const [rows, subs] = await Promise.all([ @@ -93,6 +111,30 @@ export const workGroupMethods = { })); }, + async searchWorkGroups(query: { + search: string; + offset: number; + limit: number; + }): Promise> { + const [{ rows, total }, subs] = await Promise.all([ + fetchPagedRows(query), + getEnrichment>("/subscriptions", { result: [], totalCount: 0 }), + ]); + const countByWorkGroup = new Map(); + for (const s of subs.result ?? []) { + if (s.workGroupId == null) continue; + countByWorkGroup.set(s.workGroupId, (countByWorkGroup.get(s.workGroupId) ?? 0) + 1); + } + return { + total, + result: rows.map((w) => ({ + ...toWorkGroup(w), + usedByCount: countByWorkGroup.get(w.id) ?? 0, + consumerCount: w.processorNodeCount ?? 0, + })), + }; + }, + async getWorkGroup(id: number): Promise { const [rows, subs] = await Promise.all([fetchRows(), fetchSubscriptionsByWorkGroup(id)]); const w = rows.find((x) => x.id === id); diff --git a/SW.Bitween.Web/ClientApp/src/api/types.ts b/SW.Bitween.Web/ClientApp/src/api/types.ts index dbc2dee0..4e35d18c 100644 --- a/SW.Bitween.Web/ClientApp/src/api/types.ts +++ b/SW.Bitween.Web/ClientApp/src/api/types.ts @@ -38,6 +38,12 @@ export interface User { email: string; roleIds: string[]; status: UserStatus; + /** + * Set while the account is locked out after repeated failed sign-ins. Orthogonal to + * `status`: a lockout is automatic and expires on its own, where disabling is an + * admin decision that does not. Null once it has passed. + */ + lockedUntil: string | null; /** Whether a Microsoft account is linked for SSO. */ microsoftLinked: boolean; createdOn: string; @@ -137,6 +143,12 @@ export interface IntegrationInfo { informationTypeId: number; workGroupId: number | null; retryPolicyId: number | null; + /** + * Its delivery step. Null means nothing is delivered — and since a response is + * whatever the delivery hands back, null here means the two response fields below + * can never be reached, however they are set. + */ + handlerId: string | null; /** The bus message its delivery response is published as, if any. */ responseMessageTypeName: string | null; /** The integration its delivery response is handed straight to, if any. */ @@ -245,6 +257,26 @@ export type RetryDelay = | { type: "linear"; initialSeconds: number; incrementSeconds: number } | { type: "exponential"; initialSeconds: number; multiplier: number; maxSeconds: number }; +/** + * Whether a level of the alert hierarchy names its own destination or defers upward. + * + * Resolved most-specific-first per integration and group: the pair's own override, then + * the group, then the policy. A level that sends **replaces** the one above rather than + * merging with it, so whichever level wins has to carry the handler and every property + * it needs. + */ +export type RetryAlertMode = "Inherit" | "Send" | "Silent"; + +/** Which level of that hierarchy decided, so a wrong destination can be traced to its source. */ +export type RetryAlertLevel = "SubscriptionGroup" | "Group" | "Policy"; + +/** A destination for budget-exhausted alerts, as configured at one level. */ +export interface RetryAlertConfig { + alertMode: RetryAlertMode; + alertHandlerId: string | null; + alertHandlerProperties: Record; +} + export interface RetryGroup { id: string; name: string; @@ -257,6 +289,10 @@ export interface RetryGroup { action: "Allow" | "Block"; budget?: { maxAttemptsPerError: number; maxAttemptsTotal: number; delay: RetryDelay }; notes?: string; + /** Where this group's budget-exhausted alert goes, for every integration using the policy. */ + alertMode: RetryAlertMode; + alertHandlerId: string | null; + alertHandlerProperties: Record; } export interface RetryPolicy { @@ -264,6 +300,9 @@ export interface RetryPolicy { name: string; groups: RetryGroup[]; createdOn: string; + /** The policy-wide alert destination, inherited by every group that doesn't name its own. */ + alertHandlerId: string | null; + alertHandlerProperties: Record; } export interface RetryPolicyListRow { id: number; @@ -277,6 +316,74 @@ export interface RetryPolicyDetail extends RetryPolicy { integrations: IntegrationSetupRef[]; } +/** + * What became of a budget-exhausted alert. + * + * `claimedOn` is when the alert was raised — all the counter itself records. Whether it then + * reached anyone is a separate fact that can fail, so the two are reported apart: a page showing + * only the claim tells the reader someone was notified when nobody was. + */ +export interface RetryAlertOutcome { + claimedOn: string; + /** Null when the alert was claimed but no delivery attempt was ever recorded. */ + delivered: boolean | null; + /** Why delivery failed, when it did. */ + error: string | null; +} + +/** + * The whole state of one integration-and-group pair: how much of the group's budget that + * integration has spent, and where the pair's budget-exhausted alert would go. + * + * Budgets are counted per pair — a shared policy gives every integration its own separate total + * — so there is no such thing as "this policy's usage". Any single figure on a policy or a group + * would be an aggregate matching nothing anyone can act on, which is why the pair is also what + * resetting and overriding both address. + */ +export interface RetryUsageRow { + integrationId: number; + integrationName: string; + groupId: string; + groupName: string; + used: number; + total: number; + /** Spent out: this integration gets no further automatic retries from this group. */ + exhausted: boolean; + /** Null when the pair has never failed — also how you know there is no counter to reset. */ + lastAttemptOn: string | null; + /** Where the alert actually goes, or null when nothing sends for this pair. */ + resolvedHandlerId: string | null; + resolvedHandlerProperties: Record; + resolvedFrom: RetryAlertLevel | null; + /** Which level deliberately switched the alert off — a decision, as against an oversight. */ + silencedAt: RetryAlertLevel | null; + /** This pair's own override; `Inherit` when it has none. */ + override: RetryAlertConfig; + alert: RetryAlertOutcome | null; +} + +/** One failure a group caught — what a usage row spent its budget on. */ +export interface RetryAttempt { + exchangeId: string; + /** How deep the retry chain was, 0 being the original delivery. Null for older failures. */ + attemptNumber: number | null; + failedOn: string; + error: string; + /** True while another attempt is still scheduled: the one thing here that is not history. */ + retryPending: boolean; + /** Why no further attempt was scheduled, when the policy refused one. */ + blockedReason: string | null; +} + +export interface RetryAttempts { + /** + * Every failure this group has caught for this integration. Failures outlive the counter, + * which is reset, so this is not the counter's value. + */ + total: number; + attempts: RetryAttempt[]; +} + export interface RetryTestAttempt { attempt: number; shouldRetry: boolean; @@ -321,6 +428,30 @@ export interface NotifierDetail extends Notifier { * Backend Subscription.Type. Aggregation exists in data but is deferred in * this UI; Internal and ApiCall are legacy — shown and editable, never created. */ +/** + * The editable fields of an integration being defined inline, while whatever points + * at it is being made. Mirrors the studio's own draft — deliberately, so the canvas + * can hand its draft straight to the client. + */ +export interface InlineIntegrationDraft { + name: string; + enabled: boolean; + workGroupId: number | null; + retryPolicyId: number | null; + receiverId: string | null; + receiverProperties: Record; + validatorId: string | null; + validatorProperties: Record; + mapperId: string | null; + mapperProperties: Record; + handlerId: string | null; + handlerProperties: Record; + matchExpression: MatchGroup | null; + schedules: Schedule[]; + responseIntegrationId: number | null; + responseMessageTypeName: string | null; +} + export type IntegrationType = | "Receiving" | "GatewayApiCall" @@ -459,6 +590,26 @@ export interface IntegrationLastRun extends IntegrationRun { recentSucceeded: number; } +/** One poll of a Receiving integration's own receive step — independent of the scheduler's + * run history, which only knows whether the method threw (it never does; failures here are + * caught and reported this way instead). */ +export type ReceiveOutcome = "Failed" | "NoNewData" | "Received"; + +export interface ReceiveAttemptExchange { + id: string; + status: ExchangeStatus; + promotedProperties: Record | null; +} + +export interface ReceiveAttemptRow { + id: number; + startedOn: string; + finishedOn: string; + outcome: ReceiveOutcome; + errorMessage: string | null; + exchanges: ReceiveAttemptExchange[]; +} + /** * Whether a scheduled integration will actually fire, straight from the scheduler. * Everything here can disagree with what the integration's own record says, and @@ -525,6 +676,8 @@ export interface ApiGateway { id: number; name: string; urlName: string; + /** Off but kept, with its attachments. Partners calling it get a 503. */ + inactive: boolean; createdOn: string; } export interface ApiGatewayRow extends ApiGateway { @@ -550,6 +703,8 @@ export interface BusGateway { id: number; name: string; informationTypeId: number; + /** Off but kept, with its routes. The message stops being offered to them. */ + inactive: boolean; createdOn: string; } export interface BusGatewayRow extends BusGateway { @@ -664,6 +819,11 @@ export interface ExchangeQuery { correlationId?: string; /** Substring match against promoted property keys and values. */ property?: string; + /** + * Narrows `property` to one promoted key. Set on its own it asks "has this key at + * all", which is worth being able to ask. + */ + propertyKey?: string; from?: string; to?: string; offset: number; diff --git a/SW.Bitween.Web/ClientApp/src/components/config/AdapterConfig.tsx b/SW.Bitween.Web/ClientApp/src/components/config/AdapterConfig.tsx index 920fb102..0156c04a 100644 --- a/SW.Bitween.Web/ClientApp/src/components/config/AdapterConfig.tsx +++ b/SW.Bitween.Web/ClientApp/src/components/config/AdapterConfig.tsx @@ -420,7 +420,8 @@ export function AdapterConfig({ /** What "no adapter" means here, e.g. "None — payload passes through unchanged". */ noneLabel?: string; /** When the native JSON mapper is selected, where its visual editor lives. */ - mapperEditorHref?: string; + /** Null while the integration is still a draft — there is no page to open yet. */ + mapperEditorHref?: string | null; }) { const catalog = useAdapterCatalog(kind); const adapter = catalog.data?.find((a) => a.id === adapterId); @@ -503,16 +504,22 @@ export function AdapterConfig({ {adapter && adapter.props.length > 0 && ( -
+ /* + Two columns are decided by how wide *this* box is, not how wide the window is. + A `sm:` breakpoint reads the viewport, so a 360px side panel on a 2000px screen + still got two columns, and every email address wrapped onto three lines. The + container query asks the box instead, so a narrow one simply stacks. + */ +
{requiredProps.length > 0 && ( -
{requiredProps.map(renderProp)}
+
{requiredProps.map(renderProp)}
)} {optionalProps.length > 0 && (

Optional · {optionalProps.length} field{optionalProps.length === 1 ? "" : "s"}

-
{optionalProps.map(renderProp)}
+
{optionalProps.map(renderProp)}
)}
diff --git a/SW.Bitween.Web/ClientApp/src/components/config/InformationTypeDialog.tsx b/SW.Bitween.Web/ClientApp/src/components/config/InformationTypeDialog.tsx index 000de5bb..f1aaac6a 100644 --- a/SW.Bitween.Web/ClientApp/src/components/config/InformationTypeDialog.tsx +++ b/SW.Bitween.Web/ClientApp/src/components/config/InformationTypeDialog.tsx @@ -17,11 +17,11 @@ import { /** * An information type, created or edited without leaving whatever you were doing. * - * Same shape as `PartnerDialog`, and for the same reason: creating does not close - * it. Promoted properties can only be attached to a type that exists, so instead - * of asking for them in a create call that cannot carry them — or sending you to - * the type's page — it saves, hands the id back so the picker updates behind it, - * and reopens as an editor with that section live. + * Creating is one save: `POST /documents` carries the whole definition, promoted + * properties included, so the dialog asks for everything once and closes. (It used + * to stay open as an editor, because create could not carry them — unlike + * `PartnerDialog`, which still must, since a key can only be issued to a partner + * that already exists.) */ export function InformationTypeDialog({ typeId, @@ -39,13 +39,11 @@ export function InformationTypeDialog({ }) { const queryClient = useQueryClient(); const canEdit = useSessionCan("documents.edit"); - const [id, setId] = useState(typeId); - const [justCreated, setJustCreated] = useState(false); const existing = useQuery({ - queryKey: ["information-type", id], - queryFn: () => api.getInformationType(id!), - enabled: id !== null, + queryKey: ["information-type", typeId], + queryFn: () => api.getInformationType(typeId!), + enabled: typeId !== null, }); const seed = { ...EMPTY_INFORMATION_TYPE, busEnabled: busRequired }; @@ -63,39 +61,33 @@ export function InformationTypeDialog({ const save = useMutation({ mutationFn: async () => { const changes = informationTypeChanges(draft!); - if (id !== null) { - await api.updateInformationType(id, { - name: changes.name, - code: changes.code, - format: changes.format, - busEnabled: changes.busEnabled, - busMessageTypeName: changes.busMessageTypeName, - duplicateIntervalMinutes: changes.duplicateIntervalMinutes, - disregardsUnfilteredMessages: changes.disregardsUnfilteredMessages, - promotedProperties: changes.promotedProperties, - }); - return id; - } - // Create carries the definition but not promoted properties, which is why - // the dialog stays open afterwards rather than asking for them here. - const created = await api.createInformationType({ + const body = { name: changes.name, - code: changes.code ?? "", + code: changes.code, format: changes.format, busEnabled: changes.busEnabled, - ...(changes.busMessageTypeName ? { busMessageTypeName: changes.busMessageTypeName } : {}), - }); - return created.id; + busMessageTypeName: changes.busMessageTypeName, + duplicateIntervalMinutes: changes.duplicateIntervalMinutes, + disregardsUnfilteredMessages: changes.disregardsUnfilteredMessages, + promotedProperties: changes.promotedProperties, + }; + if (typeId !== null) { + await api.updateInformationType(typeId, body); + return typeId; + } + return (await api.createInformationType(body)).id; }, onSuccess: async (savedId) => { - const creating = id === null; - setId(savedId); void queryClient.invalidateQueries({ queryKey: ["information-types"] }); await queryClient.invalidateQueries({ queryKey: ["information-type", savedId] }); onSaved?.({ id: savedId, busMessageTypeName: informationTypeChanges(draft!).busMessageTypeName ?? "" }); + if (typeId === null) { + onClose(); + return; + } + // Re-seed from the server, so what the dialog compares against is what was stored. setDraft(null); setSaved(null); - if (creating) setJustCreated(true); }, }); @@ -103,22 +95,15 @@ export function InformationTypeDialog({ const missing = draft ? informationTypeMissing(draft) : []; return ( - + {!draft ? ( ) : (
- {justCreated && ( -

- Created and selected. Promote the properties you want to filter on below, or close. -

- )} - @@ -132,14 +117,14 @@ export function InformationTypeDialog({ {missing.at(-1)}.

)} - +
diff --git a/SW.Bitween.Web/ClientApp/src/components/config/InformationTypeFields.tsx b/SW.Bitween.Web/ClientApp/src/components/config/InformationTypeFields.tsx index f6da5e98..3b042f96 100644 --- a/SW.Bitween.Web/ClientApp/src/components/config/InformationTypeFields.tsx +++ b/SW.Bitween.Web/ClientApp/src/components/config/InformationTypeFields.tsx @@ -2,6 +2,7 @@ import type { InformationType, InformationTypeDetail, InformationTypeFormat } fr import { Checkbox, Field, Select, TextInput } from "../ui/forms"; import { KeyValueEditor, type KvRow } from "../ui/KeyValueEditor"; import { Panel } from "../ui/Panel"; +import { BUS_MESSAGE_NAME_PLACEHOLDER, busMessageNameProblem } from "../../lib/busMessageName"; /** * Everything about an information type that can be edited, as one component. @@ -68,7 +69,10 @@ export function informationTypeMissing(draft: InformationTypeDraft): string[] { return [ draft.name.trim().length < 2 && "a name", draft.busEnabled && !draft.busMessageTypeName.trim() && "a bus message name", - draft.busEnabled && /\s/.test(draft.busMessageTypeName.trim()) && "a bus message name without spaces", + // Same rule the field shows under itself, so the gate and the message cannot disagree. + draft.busEnabled && + busMessageNameProblem(draft.busMessageTypeName.trim()) !== null && + "a bus message name without spaces", ].filter((m): m is string => typeof m === "string"); } @@ -76,8 +80,6 @@ export function InformationTypeFields({ draft, onChange, canEdit, - /** null while it doesn't exist yet — promoted properties arrive with the first save. */ - typeId, /** The flow that opened this needs the type on the bus, so the choice is made for it. */ busRequired = false, idPrefix = "it", @@ -85,7 +87,6 @@ export function InformationTypeFields({ draft: InformationTypeDraft; onChange: (draft: InformationTypeDraft) => void; canEdit: boolean; - typeId: number | null; busRequired?: boolean; idPrefix?: string; }) { @@ -162,16 +163,24 @@ export function InformationTypeFields({ set("busMessageTypeName", e.target.value.replace(/\s+/g, ""))} + // Kept as typed. It used to strip whitespace on the way in, so a + // pasted "my message" turned into "mymessage" with nothing said — + // the same rule the response field states out loud. + onChange={(e) => set("busMessageTypeName", e.target.value)} className="font-mono" - placeholder="purchase-order" + placeholder={BUS_MESSAGE_NAME_PLACEHOLDER} /> + {busMessageNameProblem(draft.busMessageTypeName) && ( +

+ {busMessageNameProblem(draft.busMessageTypeName)} +

+ )}
)} @@ -192,23 +201,16 @@ export function InformationTypeFields({ draft.format === "Json" ? "JSON path" : "XML path" } — routes and filters match on them.`} > - {typeId === null ? ( -

- Added once the type exists. Create it and this section opens right here — you won't be - sent anywhere. -

- ) : ( - set("promotedProperties", promotedProperties)} - keyLabel="Friendly name" - valueLabel={draft.format === "Xml" ? "XML path" : "JSON path"} - keyPlaceholder="OrderNumber" - valuePlaceholder={draft.format === "Xml" ? "//Order/Number" : "$.order.id"} - editable={canEdit} - emptyText="No promoted properties — routes can only match on the whole payload." - /> - )} + set("promotedProperties", promotedProperties)} + keyLabel="Friendly name" + valueLabel={draft.format === "Xml" ? "XML path" : "JSON path"} + keyPlaceholder="OrderNumber" + valuePlaceholder={draft.format === "Xml" ? "//Order/Number" : "$.order.id"} + editable={canEdit} + emptyText="No promoted properties — routes can only match on the whole payload." + /> ); diff --git a/SW.Bitween.Web/ClientApp/src/components/config/IntegrationDialog.tsx b/SW.Bitween.Web/ClientApp/src/components/config/IntegrationDialog.tsx index 84bdcec8..b10a4879 100644 --- a/SW.Bitween.Web/ClientApp/src/components/config/IntegrationDialog.tsx +++ b/SW.Bitween.Web/ClientApp/src/components/config/IntegrationDialog.tsx @@ -63,6 +63,7 @@ export function IntegrationDialog({ onSuccess: (created) => { void queryClient.invalidateQueries({ queryKey: ["integrations"] }); void queryClient.invalidateQueries({ queryKey: ["integration-rows"] }); + void queryClient.invalidateQueries({ queryKey: ["integration-rows-search"] }); onCreated(created.id); onClose(); }, diff --git a/SW.Bitween.Web/ClientApp/src/components/config/WorkGroupDialog.tsx b/SW.Bitween.Web/ClientApp/src/components/config/WorkGroupDialog.tsx index 5dfa3eb2..3d49b1ae 100644 --- a/SW.Bitween.Web/ClientApp/src/components/config/WorkGroupDialog.tsx +++ b/SW.Bitween.Web/ClientApp/src/components/config/WorkGroupDialog.tsx @@ -151,6 +151,7 @@ export function WorkGroupDialog({ }, onSuccess: (id) => { void queryClient.invalidateQueries({ queryKey: ["work-groups"] }); + void queryClient.invalidateQueries({ queryKey: ["work-groups-search"] }); void queryClient.invalidateQueries({ queryKey: ["work-group", id] }); onSaved?.(id); onClose(); diff --git a/SW.Bitween.Web/ClientApp/src/components/config/pickers.tsx b/SW.Bitween.Web/ClientApp/src/components/config/pickers.tsx index 7bbd1b19..9f23d49c 100644 --- a/SW.Bitween.Web/ClientApp/src/components/config/pickers.tsx +++ b/SW.Bitween.Web/ClientApp/src/components/config/pickers.tsx @@ -132,6 +132,11 @@ export function IntegrationPicker({ value, onChange, id, + /** + * Given, "New integration" defines one where the caller stands instead of opening a + * dialog — the caller renders its fields and saves it with whatever points at it. + */ + onDefineHere, }: { type: "GatewayApiCall" | "BusGateway"; /** Bus routes only run integrations carrying the gateway's own information type. */ @@ -139,6 +144,7 @@ export function IntegrationPicker({ value: number | null; onChange: (id: number) => void; id?: string; + onDefineHere?: () => void; }) { const integrations = useIntegrationsCache(); const infoTypes = useQuery({ queryKey: ["information-types"], queryFn: () => api.listInformationTypes() }); @@ -169,7 +175,17 @@ export function IntegrationPicker({ setCreating(true) }] : []} + actions={ + canCreate + ? [ + { + label: "New integration", + icon: true, + onAct: () => (onDefineHere ? onDefineHere() : setCreating(true)), + }, + ] + : [] + } /> {creating && ( = { Aggregation: "Aggregation", }; -export const isLegacyType = (type: IntegrationType) => type === "Internal" || type === "ApiCall"; +export const isLegacyType = (type: IntegrationType) => + type === "Internal" || type === "ApiCall"; export function TypeBadge({ type }: { type: IntegrationType }) { return ( @@ -45,11 +47,28 @@ export function TypeBadge({ type }: { type: IntegrationType }) { } /** Enabled/paused pair — an integration can be both enabled and paused. */ -export function IntegrationStatusBadges({ enabled, paused }: { enabled: boolean; paused: boolean }) { +export function IntegrationStatusBadges({ + enabled, + paused, +}: { + enabled: boolean; + paused: boolean; +}) { return ( - {enabled ? Active : Disabled} - {paused && Paused} + {enabled ? ( + Active + ) : ( + Disabled + )} + {paused && ( + + Paused + + )} ); } @@ -78,37 +97,63 @@ export function scheduleFault( return { label: "Not scheduled", tone: "danger", - title: "The scheduler has no trigger for this schedule — it will never fire.", + title: + "The scheduler has no trigger for this schedule — it will never fire.", }; case "Error": return { label: "Trigger error", tone: "danger", - title: "The scheduler put this trigger in an error state; it will not fire again until fixed.", + title: + "The scheduler put this trigger in an error state; it will not fire again until fixed.", }; case "Paused": return { label: "Trigger paused", tone: "warn", - title: "Paused inside the scheduler — this is not the integration's own pause.", + title: + "Paused inside the scheduler — this is not the integration's own pause.", }; case "Blocked": return { label: "Blocked", tone: "warn", - title: "A previous run is still going and this job doesn't allow overlap, so fires are being held.", + title: + "A previous run is still going and this job doesn't allow overlap, so fires are being held.", }; case "Complete": return { label: "Schedule ended", tone: "warn", - title: "The schedule has run to completion and has no future fire times.", + title: + "The schedule has run to completion and has no future fire times.", }; default: return null; } } +/** + * What a lane's severity is actually reporting — the three sources this fans out + * to (Work groups, Queue health, the live stats strip) used to each spell out the + * same three words with no explanation of what put a lane there. + * + * Matches `AlertEvaluator` in SW.Bus: critical is no running consumer or a + * backlog past its critical threshold; warning is a backlog past its warning + * threshold, a queue depth past the backpressure threshold, or messages arriving + * with essentially nothing being acknowledged. + */ +export function queueHealthTitle(severity: QueueSeverity): string { + switch (severity) { + case "critical": + return "No consumer is running for this lane, or a backlog has passed its critical threshold."; + case "warning": + return "A backlog, queue depth or the incoming-vs-acknowledged rate has passed its warning threshold."; + default: + return "No active alerts for this lane."; + } +} + export function HealthBadge({ isRunning, consecutiveFailures, @@ -118,19 +163,39 @@ export function HealthBadge({ }) { if (consecutiveFailures > 0) return ( - + {consecutiveFailures} failure{consecutiveFailures === 1 ? "" : "s"} ); - if (isRunning) return Running; - return Idle; + if (isRunning) return Running; + return Idle; } -export function ExchangeStatusBadge({ status }: { status: ExchangeRef["status"] }) { - if (status === "success") return Success; - if (status === "failed") return Failed; - if (status === "badResponse") return Bad response; - return Processing; +export function ExchangeStatusBadge({ + status, +}: { + status: ExchangeRef["status"]; +}) { + if (status === "success") return Success; + if (status === "failed") + return ( + + Failed + + ); + if (status === "badResponse") + return ( + + Bad response + + ); + return Processing; } /** @@ -142,12 +207,27 @@ export function ExchangeStatusBadge({ status }: { status: ExchangeRef["status"] export function PromotedProps({ properties, max = 3, + fallbackId, }: { properties: Record | null; max?: number; + /** + * Shown when the information type promotes nothing, or promotes nothing this + * payload carried. A bare em dash left the row with no identity at all — the id + * is a poor name but it is the only one left, and it makes the row addressable. + * Truncated because the drawer carries it in full, with a copy button. + */ + fallbackId?: string; }) { const entries = Object.entries(properties ?? {}); - if (entries.length === 0) return ; + if (entries.length === 0) + return fallbackId ? ( + + {fallbackId.slice(0, 8)}… + + ) : ( + + ); const shown = entries.slice(0, max); const rest = entries.length - shown.length; return ( @@ -156,7 +236,10 @@ export function PromotedProps({ title={entries.map(([k, v]) => `${k}=${v}`).join("\n")} > {shown.map(([k, v]) => ( - + {k}= {v} @@ -204,14 +287,17 @@ export function ExchangesList({ return ( 0 ? `${x.id}\n\n${properties.map(([k, v]) => `${k}=${v}`).join("\n")}` : x.id} + title={ + properties.length > 0 + ? `${x.id}\n\n${properties.map(([k, v]) => `${k}=${v}`).join("\n")}` + : x.id + } className="block hover:opacity-70" > - {properties.length > 0 ? ( - - ) : ( - {x.id.slice(0, 8)}… - )} + ); }, @@ -222,7 +308,9 @@ export function ExchangesList({ { header: "Type", cell: (x: ExchangeRef) => ( - {x.informationTypeCode} + + {x.informationTypeCode} + ), }, ]), @@ -232,20 +320,34 @@ export function ExchangesList({ { header: "Partner", cell: (x: ExchangeRef) => ( - {x.partnerName ?? "—"} + + {x.partnerName ?? "—"} + ), }, ]), - { header: "Status", cell: (x: ExchangeRef) => }, + { + header: "Status", + cell: (x: ExchangeRef) => , + }, { header: "When", align: "right" as const, className: "whitespace-nowrap", - cell: (x: ExchangeRef) => {timeAgo(x.on)}, + cell: (x: ExchangeRef) => ( + {timeAgo(x.on)} + ), }, ]; - return x.id} empty="No exchanges yet." columns={columns} />; + return ( + x.id} + empty="No exchanges yet." + columns={columns} + /> + ); } /** Integrations referencing this entity, each linking to its page. */ @@ -268,7 +370,11 @@ export function SetupList({ items }: { items: IntegrationSetupRef[] }) { ), }, - { header: "Type", align: "right", cell: (s) => }, + { + header: "Type", + align: "right", + cell: (s) => , + }, ]} /> ); @@ -300,9 +406,17 @@ export function usePartnerIntegrations(): Map { const canSeeApi = useSessionCan("api-gateways.view"); const canSeeBus = useSessionCan("bus-gateways.view"); const apiGateways = - useQuery({ queryKey: ["api-gateways"], queryFn: () => api.listApiGateways(), enabled: canSeeApi }).data ?? []; + useQuery({ + queryKey: ["api-gateways"], + queryFn: () => api.listApiGateways(), + enabled: canSeeApi, + }).data ?? []; const busGateways = - useQuery({ queryKey: ["bus-gateways"], queryFn: () => api.listBusGateways(), enabled: canSeeBus }).data ?? []; + useQuery({ + queryKey: ["bus-gateways"], + queryFn: () => api.listBusGateways(), + enabled: canSeeBus, + }).data ?? []; return useMemo(() => { const byId = new Map(integrations.map((s) => [s.id, s])); @@ -317,9 +431,12 @@ export function usePartnerIntegrations(): Map { out.set(partnerId, list); } }; - for (const s of integrations) for (const pid of s.partnerIds) add(pid, s.id); - for (const g of apiGateways) for (const a of g.attachments) add(a.partnerId, a.integrationId); - for (const g of busGateways) for (const r of g.routes) add(r.partnerId, r.integrationId); + for (const s of integrations) + for (const pid of s.partnerIds) add(pid, s.id); + for (const g of apiGateways) + for (const a of g.attachments) add(a.partnerId, a.integrationId); + for (const g of busGateways) + for (const r of g.routes) add(r.partnerId, r.integrationId); return out; }, [integrations, apiGateways, busGateways]); } @@ -332,17 +449,32 @@ export function usePartnerIntegrations(): Map { * the modern types never have — without this, every gateway-fed integration * shows a dash where its partner should be. */ -export function useGatewayPartners(): Map { +export function useGatewayPartners(): Map< + number, + { id: number; name: string }[] +> { const canSeeApi = useSessionCan("api-gateways.view"); const canSeeBus = useSessionCan("bus-gateways.view"); const apiGateways = - useQuery({ queryKey: ["api-gateways"], queryFn: () => api.listApiGateways(), enabled: canSeeApi }).data ?? []; + useQuery({ + queryKey: ["api-gateways"], + queryFn: () => api.listApiGateways(), + enabled: canSeeApi, + }).data ?? []; const busGateways = - useQuery({ queryKey: ["bus-gateways"], queryFn: () => api.listBusGateways(), enabled: canSeeBus }).data ?? []; + useQuery({ + queryKey: ["bus-gateways"], + queryFn: () => api.listBusGateways(), + enabled: canSeeBus, + }).data ?? []; return useMemo(() => { const out = new Map(); - const add = (integrationId: number, partnerId: number | null, partnerName: string | null) => { + const add = ( + integrationId: number, + partnerId: number | null, + partnerName: string | null, + ) => { if (partnerId === null || partnerName === null) return; const list = out.get(integrationId) ?? []; if (!list.some((p) => p.id === partnerId)) { @@ -350,15 +482,23 @@ export function useGatewayPartners(): Map { - const rows = useQuery({ queryKey: ["integration-rows"], queryFn: () => api.listIntegrationRows() }).data ?? []; + const rows = + useQuery({ + queryKey: ["integration-rows"], + queryFn: () => api.listIntegrationRows(), + }).data ?? []; return useMemo(() => new Map(rows.map((r) => [r.id, r])), [rows]); } @@ -366,7 +506,11 @@ export function useIntegrationRowsById(): Map { export function useWorkGroupNames(): Map { const canSee = useSessionCan("workgroups.view"); const groups = - useQuery({ queryKey: ["work-groups"], queryFn: () => api.listWorkGroups(), enabled: canSee }).data ?? []; + useQuery({ + queryKey: ["work-groups"], + queryFn: () => api.listWorkGroups(), + enabled: canSee, + }).data ?? []; return useMemo(() => new Map(groups.map((g) => [g.id, g.name])), [groups]); } @@ -374,8 +518,15 @@ export function useWorkGroupNames(): Map { export function useRetryPolicyNames(): Map { const canSee = useSessionCan("retry-policies.view"); const policies = - useQuery({ queryKey: ["retry-policies"], queryFn: () => api.listRetryPolicies(), enabled: canSee }).data ?? []; - return useMemo(() => new Map(policies.map((p) => [p.id, p.name])), [policies]); + useQuery({ + queryKey: ["retry-policies"], + queryFn: () => api.listRetryPolicies(), + enabled: canSee, + }).data ?? []; + return useMemo( + () => new Map(policies.map((p) => [p.id, p.name])), + [policies], + ); } /** @@ -383,15 +534,38 @@ export function useRetryPolicyNames(): Map { * reporting — it is only as healthy as the pipelines behind it, and "3 partners * attached" says nothing about whether any of them currently works. */ -export function WiredHealthBadge({ rows, empty }: { rows: IntegrationRow[]; empty: string }) { - if (rows.length === 0) return {empty}; +export function WiredHealthBadge({ + rows, + empty, +}: { + rows: IntegrationRow[]; + empty: string; +}) { + if (rows.length === 0) return {empty}; const failing = rows.filter((r) => r.consecutiveFailures > 0).length; - if (failing > 0) return {failing} failing; + if (failing > 0) + return ( + + {failing} failing + + ); const paused = rows.filter((r) => r.paused).length; - if (paused > 0) return {paused} paused; + if (paused > 0) + return ( + + {paused} paused + + ); const disabled = rows.filter((r) => !r.enabled).length; - if (disabled > 0) return {disabled} disabled; - return Healthy; + if (disabled > 0) + return ( + {disabled} disabled + ); + return ( + + Healthy + + ); } /** @@ -413,7 +587,10 @@ export function useWiredIntegrationColumns( ): Column[] { const rowsById = useIntegrationRowsById(); const setups = useIntegrationsCache().data ?? []; - const setupById = useMemo(() => new Map(setups.map((s) => [s.id, s])), [setups]); + const setupById = useMemo( + () => new Map(setups.map((s) => [s.id, s])), + [setups], + ); const workGroupNames = useWorkGroupNames(); const retryPolicyNames = useRetryPolicyNames(); const canSeeInfoTypes = useSessionCan("documents.view"); @@ -434,7 +611,9 @@ export function useWiredIntegrationColumns( {r.informationTypeCode} ) : ( - {r.informationTypeCode} + + {r.informationTypeCode} + ); }, }); @@ -448,10 +627,14 @@ export function useWiredIntegrationColumns( // lane, it's `WorkGroup.None` — a real shared queue (`0Ungrouped`) that // every ungrouped integration competes in. Matches the wording the // integration page's work-group picker already uses. - if (id === null) return Ungrouped; + if (id === null) + return Ungrouped; const name = workGroupNames.get(id); return name ? ( - + {name} ) : ( @@ -463,10 +646,14 @@ export function useWiredIntegrationColumns( header: "Retry policy", cell: (row) => { const id = setupById.get(integrationIdOf(row))?.retryPolicyId ?? null; - if (id === null) return None; + if (id === null) + return None; const name = retryPolicyNames.get(id); return name ? ( - + {name} ) : ( @@ -482,7 +669,10 @@ export function useWiredIntegrationColumns( return ( - + ); }, @@ -495,7 +685,10 @@ export function useWiredIntegrationColumns( cell: (row) => { const message = rowsById.get(integrationIdOf(row))?.lastException; return message ? ( - + {message} ) : ( @@ -529,66 +722,74 @@ export interface CellLink { export function LinkListCell({ items, label, - max = 2, }: { items: CellLink[]; /** Plural noun for the popover heading, e.g. "integrations". */ label: string; - max?: number; }) { if (items.length === 0) return ; - const shown = items.slice(0, max); - const rest = items.length - shown.length; + + // One of something is just that thing. A chip reading "1" would cost the name and + // buy a popover with a single row in it. + if (items.length === 1) { + const only = items[0]; + return ( + e.stopPropagation()} + title={only.name} + className="block truncate text-[13px] text-ink-700 hover:text-crimson-700 hover:underline" + > + {only.name} + + ); + } + return ( - s.name).join(", ")}> - {shown.map((s, i) => ( - - {i > 0 && , } - e.stopPropagation()} - className="text-ink-700 hover:text-crimson-700 hover:underline" - > - {s.name} - + s.name).join(", ")} + > + {items.length} {label} - ))} - - {rest > 0 && ( - +{rest} more} - > -

- {items.length} {label} -

-
    - {items.map((s) => ( -
  • - - {s.name} - {s.note} - -
  • - ))} -
- - )} + } + > +

+ {items.length} {label} +

+
    + {items.map((s) => ( +
  • + + + {s.name} + + {s.note} + +
  • + ))} +
+
); } /** `LinkListCell` for the commonest case: the integrations using something. */ -export function UsedByCell({ items, max = 2 }: { items: IntegrationInfo[]; max?: number }) { +export function UsedByCell({ items }: { items: IntegrationInfo[] }) { return ( ({ key: s.id, name: s.name, @@ -607,7 +808,12 @@ export function TrailTable({ entries }: { entries: TrailEntry[] }) { rowKey={(e) => e.i} empty="Nothing recorded yet." columns={[ - { header: "Action", cell: (e) => {e.action} }, + { + header: "Action", + cell: (e) => ( + {e.action} + ), + }, { header: "By", truncate: true, @@ -627,7 +833,9 @@ export function TrailTable({ entries }: { entries: TrailEntry[] }) { header: "When", align: "right", className: "whitespace-nowrap", - cell: (e) => {formatDate(e.on)}, + cell: (e) => ( + {formatDate(e.on)} + ), }, ]} /> diff --git a/SW.Bitween.Web/ClientApp/src/components/ui/BackLink.tsx b/SW.Bitween.Web/ClientApp/src/components/ui/BackLink.tsx new file mode 100644 index 00000000..dfc08bdb --- /dev/null +++ b/SW.Bitween.Web/ClientApp/src/components/ui/BackLink.tsx @@ -0,0 +1,57 @@ +import { Link, useLocation, useNavigate } from "react-router"; +import { ArrowLeft } from "lucide-react"; + +/** + * The way back out of a detail page. + * + * These were all fixed `Link`s to a list page, which is only right when the list is + * where you came from. `subscriptions/:id` is reached from Scheduled jobs, Exchanges, + * an API gateway's page, the retry usage panel and three places on the dashboard — and + * from every one of them "← Integrations" landed you somewhere you had never been. + * + * So it steps back when there is somewhere to step back to, and falls back to `to` when + * there isn't: a pasted link, a new tab, a refresh. The label follows the behaviour + * rather than the other way round — naming a destination it wasn't going to is exactly + * how the old one misled people. + * + * Signing in can't be what's behind you: `Login` navigates with `replace`, so that entry + * is already gone. + */ +export function BackLink({ + to, + label, + className = "mb-4", +}: { + to: string; + label: string; + /** Layout only — one caller sits in a toolbar rather than above a page title. */ + className?: string; +}) { + const navigate = useNavigate(); + // Subscribed to purely so a navigation re-renders this and the index below is re-read. + useLocation(); + + // The history index, which React Router keeps in `history.state`: it counts pushes and + // is untouched by replaces. `location.key` looked like the same signal and isn't — a + // replace mints a fresh key, so a page that syncs a query param on mount (the bus + // gateway selecting its first route) looked like it had somewhere to go back to on a + // cold load. Reading it during render is fine because `location` above re-renders us + // on every navigation. + const historyIndex = (window.history.state as { idx?: number } | null)?.idx ?? 0; + const cameFromInsideTheApp = historyIndex > 0; + + const classes = `${className} inline-flex items-center gap-1 text-[13px] font-medium text-ink-500 hover:text-ink-800`; + + if (cameFromInsideTheApp) + return ( + + ); + + return ( + + {label} + + ); +} diff --git a/SW.Bitween.Web/ClientApp/src/components/ui/Pagination.tsx b/SW.Bitween.Web/ClientApp/src/components/ui/Pagination.tsx new file mode 100644 index 00000000..a9c743d1 --- /dev/null +++ b/SW.Bitween.Web/ClientApp/src/components/ui/Pagination.tsx @@ -0,0 +1,36 @@ +import { Button } from "./basics"; + +/** + * The "Showing X–Y of Z" + Previous/Next footer, lifted out of the Exchanges + * page so every other table gets the same server-paged behavior without + * hand-rolling it again. Fixed page size, no jump-to-page — matches the one + * table that already did this. + */ +export function Pagination({ + offset, + limit, + total, + onOffsetChange, +}: { + offset: number; + limit: number; + total: number; + onOffsetChange: (offset: number) => void; +}) { + if (total === 0) return null; + return ( +
+ + Showing {offset + 1}–{Math.min(offset + limit, total)} of {total} + + + + + +
+ ); +} diff --git a/SW.Bitween.Web/ClientApp/src/components/ui/SearchSelect.tsx b/SW.Bitween.Web/ClientApp/src/components/ui/SearchSelect.tsx index 4e44968c..9e2b595f 100644 --- a/SW.Bitween.Web/ClientApp/src/components/ui/SearchSelect.tsx +++ b/SW.Bitween.Web/ClientApp/src/components/ui/SearchSelect.tsx @@ -29,6 +29,7 @@ export function SearchSelect({ size = "md", id, "aria-label": ariaLabel, + freeText, }: { /** "" = nothing selected. */ value: string; @@ -41,6 +42,16 @@ export function SearchSelect({ size?: "sm" | "md"; id?: string; "aria-label"?: string; + /** + * Accept a typed value the options don't carry, offered as the last row. + * For fields where the known list is a convenience rather than the rule — + * without it, typing a name nothing matches dead-ends on "Nothing matches", + * and the way out is a link elsewhere on the page that reads as unrelated. + * + * Return a string instead of an option to refuse this particular value and + * say why, rather than offering a row that only fails on save. + */ + freeText?: (query: string) => SearchSelectOption | string; }) { const [query, setQuery] = useState(""); @@ -59,6 +70,12 @@ export function SearchSelect({ const selected = all.find((o) => o.value === value); + // The typed value as its own choice: an option to accept it, a string saying why it + // can't be, or null when there is nothing to offer. + const typed = query.trim(); + const offered = + freeText && typed !== "" && !all.some((o) => o.label === typed) ? freeText(typed) : null; + return ( ))} - {filtered.length === 0 && ( + {offered !== null && typeof offered !== "string" && ( + + {offered.render ?? ( + {offered.label} + )} + + )} + {typeof offered === "string" && ( +
+ {offered} +
+ )} + {filtered.length === 0 && offered === null && (
Nothing matches “{query}”.
)} diff --git a/SW.Bitween.Web/ClientApp/src/components/ui/Table.tsx b/SW.Bitween.Web/ClientApp/src/components/ui/Table.tsx index 0550c9f5..08b34a3d 100644 --- a/SW.Bitween.Web/ClientApp/src/components/ui/Table.tsx +++ b/SW.Bitween.Web/ClientApp/src/components/ui/Table.tsx @@ -102,29 +102,40 @@ export function MiniTable({ rows, rowKey, empty, + onRowClick, + fitWidth = false, }: { columns: Column[]; rows: T[]; rowKey: (row: T) => string | number; /** Shown instead of the table when there are no rows. */ empty: ReactNode; + /** Makes the whole row the way in, as on the page-level table. */ + onRowClick?: (row: T) => void; + /** + * Honour `truncate` and stay inside the panel instead of growing past it. + * + * Off by default because most callers are 2–4 column lists in the ~360px + * sidebar, where collapsing a column to ellipsis its text gives a row of + * "3c8…" — there, growing and scrolling sideways is the better trade. A + * wide table in the main column is the opposite case: it has the room, and + * what it pushes out of reach is the action buttons on the right, which + * nobody thinks to scroll a table sideways to find. + */ + fitWidth?: boolean; }) { if (rows.length === 0) return

{empty}

; + const cell = (c: Column) => + `px-1 ${fitWidth ? widthClass(c) : "whitespace-nowrap"} ${c.align === "right" ? "text-right" : ""} ${c.className ?? ""}`; + return ( - // Unlike the page-level table, this one does NOT honour `truncate`. These - // panels sit in a ~360px sidebar column, where letting a column collapse - // so its text can ellipsis leaves a row of "3c8…" and headers spilling - // over each other. Full values, natural widths, scroll sideways instead.
- +
{columns.map((c, i) => ( - ))} @@ -132,12 +143,15 @@ export function MiniTable({ {rows.map((row) => ( - + onRowClick(row) : undefined} + className={`border-b border-ink-50 last:border-b-0 ${ + onRowClick ? "cursor-pointer hover:bg-ink-50/60" : "" + }`} + > {columns.map((c, i) => ( - ))} diff --git a/SW.Bitween.Web/ClientApp/src/lib/busMessageName.ts b/SW.Bitween.Web/ClientApp/src/lib/busMessageName.ts new file mode 100644 index 00000000..3e7bfe6c --- /dev/null +++ b/SW.Bitween.Web/ClientApp/src/lib/busMessageName.ts @@ -0,0 +1,22 @@ +/** + * The one rule for a bus message name, and the one way of saying it. + * + * Two fields name the same thing — an information type's `busMessageTypeName` and an + * integration's `responseMessageTypeName` — and they had drifted: one silently deleted + * spaces as you typed, the other refused them and said why, and only one mentioned the + * rule at all. Whatever the rule becomes, both fields read it from here. + */ + +/** Why a name is refused, or null when it is fine. */ +export const busMessageNameProblem = (name: string): string | null => + /\s/.test(name) + ? "A bus message name cannot contain spaces — it becomes the routing key." + : null; + +/** + * Matches the convention every existing type follows. Deliberately not `purchase-order`: + * that suggested kebab-case while the real names are all `ShipmentLabelIssued`, and + * capitals are free — publisher and consumer both lower-case the routing key, so + * `MyMessage` and `mymessage` are the same message on the wire. + */ +export const BUS_MESSAGE_NAME_PLACEHOLDER = "PurchaseOrderReceived"; diff --git a/SW.Bitween.Web/ClientApp/src/lib/identifiers.ts b/SW.Bitween.Web/ClientApp/src/lib/identifiers.ts index 736ea424..d1f2b3e5 100644 --- a/SW.Bitween.Web/ClientApp/src/lib/identifiers.ts +++ b/SW.Bitween.Web/ClientApp/src/lib/identifiers.ts @@ -16,3 +16,23 @@ export const suggestSlug = (name: string) => .replace(/[^a-z0-9]+/g, "-") .replace(/^-+|-+$/g, "") .slice(0, 50); + +/** + * What a person is typing into a URL-name box, kept usable as a path segment. + * + * Spaces become hyphens as you type rather than being rejected on save: the box + * looks like a name field, so people type "returns intake", and the gateway that + * saves is one whose endpoint 404s with nothing on screen saying why. + * + * A trailing separator survives, or "orders-" could never become "orders-inbound". + * `finishUrlName` takes it off at save time, which is when it has to be gone. + */ +export const toUrlName = (typed: string) => + typed + .toLowerCase() + .replace(/[^a-z0-9_-]+/g, "-") + .replace(/^[-_]+/, "") + .slice(0, 50); + +/** `toUrlName` plus the trailing separator that only mattered mid-typing. */ +export const finishUrlName = (typed: string) => toUrlName(typed).replace(/[-_]+$/, ""); diff --git a/SW.Bitween.Web/ClientApp/src/nav.ts b/SW.Bitween.Web/ClientApp/src/nav.ts index 4f8406bd..7012f172 100644 --- a/SW.Bitween.Web/ClientApp/src/nav.ts +++ b/SW.Bitween.Web/ClientApp/src/nav.ts @@ -58,13 +58,14 @@ export const NAV_GROUPS: NavGroup[] = [ // pipelines it runs through, then who it's with. A gateway is not an integration. label: "Integrations", items: [ - // Gated on the bus alone: bus messages are what carry work *between* - // gateways, so without that permission there is no flow left to map. - { label: "Flow map", path: "/flow", icon: Network, permissions: ["bus-gateways.view"] }, { label: "API gateways", path: "/api-gateways", icon: Webhook, permissions: ["api-gateways.view"] }, { label: "Bus gateways", path: "/bus-gateways", icon: Cable, permissions: ["bus-gateways.view"] }, { label: "Scheduled jobs", path: "/scheduled-jobs", icon: CalendarClock, permissions: ["subscriptions.view"] }, - { label: "Integrations", path: "/subscriptions", icon: Workflow, permissions: ["subscriptions.view"] }, + // After the three ways work enters, because it is the picture of how they join up + // rather than a fourth kind of them. Gated on the bus alone: bus messages are what + // carry work *between* gateways, so without that permission there is no flow to map. + { label: "Flow map", path: "/flow", icon: Network, permissions: ["bus-gateways.view"] }, + { label: "All integrations", path: "/subscriptions", icon: Workflow, permissions: ["subscriptions.view"] }, { label: "Partners", path: "/partners", icon: Handshake, permissions: ["partners.view"] }, ], }, @@ -73,9 +74,11 @@ export const NAV_GROUPS: NavGroup[] = [ items: [ { label: "Information types", path: "/information-types", icon: FileText, permissions: ["documents.view"] }, { label: "Global values", path: "/global-values", icon: SlidersHorizontal, permissions: ["global-values.view"] }, - { label: "Notifiers", path: "/notifiers", icon: BellRing, permissions: ["notifiers.view"] }, { label: "Work groups", path: "/work-groups", icon: Layers, permissions: ["workgroups.view"] }, { label: "Retry policies", path: "/retry-policies", icon: RotateCcw, permissions: ["retry-policies.view"] }, + // Directly under retry policies: both are about what happens when something goes + // wrong, and a budget-exhausted alert is delivered by a notifier. + { label: "Notifiers", path: "/notifiers", icon: BellRing, permissions: ["notifiers.view"] }, ], }, { diff --git a/SW.Bitween.Web/ClientApp/src/pages/api-gateways/ApiGatewayNewPage.tsx b/SW.Bitween.Web/ClientApp/src/pages/api-gateways/ApiGatewayNewPage.tsx index ca8cdb5c..0efcfbae 100644 --- a/SW.Bitween.Web/ClientApp/src/pages/api-gateways/ApiGatewayNewPage.tsx +++ b/SW.Bitween.Web/ClientApp/src/pages/api-gateways/ApiGatewayNewPage.tsx @@ -1,11 +1,11 @@ import { useState, type FormEvent } from "react"; -import { Link, useNavigate } from "react-router"; +import { useNavigate } from "react-router"; import { useMutation, useQueryClient } from "@tanstack/react-query"; -import { ArrowLeft } from "lucide-react"; import { api } from "../../api"; import { Button, FormError } from "../../components/ui/basics"; import { Field, TextInput } from "../../components/ui/forms"; -import { suggestSlug } from "../../lib/identifiers"; +import { finishUrlName, suggestSlug, toUrlName } from "../../lib/identifiers"; +import { BackLink } from "../../components/ui/BackLink"; export function ApiGatewayNewPage() { const navigate = useNavigate(); @@ -15,7 +15,7 @@ export function ApiGatewayNewPage() { const [urlTouched, setUrlTouched] = useState(false); const create = useMutation({ - mutationFn: () => api.createApiGateway({ name, urlName }), + mutationFn: () => api.createApiGateway({ name, urlName: finishUrlName(urlName) }), onSuccess: (gateway) => { void queryClient.invalidateQueries({ queryKey: ["api-gateways"] }); const base = `/api-gateways/${gateway.id}`; @@ -30,12 +30,7 @@ export function ApiGatewayNewPage() { return (
- - {"Integrations"} - +

New API gateway

@@ -68,7 +63,7 @@ export function ApiGatewayNewPage() { className="font-mono" onChange={(e) => { setUrlTouched(true); - setUrlName(e.target.value.toLowerCase()); + setUrlName(toUrlName(e.target.value)); }} placeholder="orders" /> diff --git a/SW.Bitween.Web/ClientApp/src/pages/api-gateways/ApiGatewayPage.tsx b/SW.Bitween.Web/ClientApp/src/pages/api-gateways/ApiGatewayPage.tsx index 81c13e8b..5b4dc2cd 100644 --- a/SW.Bitween.Web/ClientApp/src/pages/api-gateways/ApiGatewayPage.tsx +++ b/SW.Bitween.Web/ClientApp/src/pages/api-gateways/ApiGatewayPage.tsx @@ -1,22 +1,28 @@ import { useEffect, useMemo, useState } from "react"; -import { Link, useNavigate, useParams } from "react-router"; -import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; -import { ArrowLeft, Pencil, Plus, Trash2 } from "lucide-react"; +import { Link, useNavigate, useParams, useSearchParams } from "react-router"; +import { keepPreviousData, useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { Pause, Pencil, Play, Plus, Search, Trash2 } from "lucide-react"; import { api, type ApiGatewayAttachment } from "../../api"; import { Can, useSessionCan } from "../../auth/guards"; -import { Button, EmptyState, LoadingBlock } from "../../components/ui/basics"; +import { finishUrlName, toUrlName } from "../../lib/identifiers"; +import { Badge, Button, EmptyState, LoadingBlock } from "../../components/ui/basics"; import { Field, TextInput } from "../../components/ui/forms"; import { ConfirmDialog } from "../../components/ui/overlays"; import { CopyField } from "../../components/ui/CopyField"; import { EditableTitle, Panel, UnsavedBar } from "../../components/ui/Panel"; import { MiniTable } from "../../components/ui/Table"; +import { Pagination } from "../../components/ui/Pagination"; import { useWiredIntegrationColumns } from "../../components/config/shared"; +import { BackLink } from "../../components/ui/BackLink"; + +const ATTACHMENTS_PAGE_SIZE = 10; export function ApiGatewayPage() { const { id = "" } = useParams(); const gatewayId = Number(id); const navigate = useNavigate(); const queryClient = useQueryClient(); + const [searchParams, setSearchParams] = useSearchParams(); const canEdit = useSessionCan("api-gateways.edit"); const wiredColumns = useWiredIntegrationColumns((a) => a.integrationId); @@ -26,10 +32,36 @@ export function ApiGatewayPage() { retry: false, }); + const attachmentsQuery = searchParams.get("aq") ?? ""; + const attachmentsOffset = searchParams.get("aoffset") ? Number(searchParams.get("aoffset")) : 0; + const attachments = useQuery({ + queryKey: ["api-gateway-attachments-search", gatewayId, attachmentsQuery, attachmentsOffset], + queryFn: () => + api.searchGatewayAttachments(gatewayId, { + search: attachmentsQuery, + offset: attachmentsOffset, + limit: ATTACHMENTS_PAGE_SIZE, + }), + placeholderData: keepPreviousData, + }); + + const setAttachmentsParam = (key: "aq" | "aoffset", value: string | null, resetOffset = key === "aq") => + setSearchParams( + (prev) => { + const next = new URLSearchParams(prev); + if (value) next.set(key, value); + else next.delete(key); + if (resetOffset) next.delete("aoffset"); + return next; + }, + { replace: key === "aq" }, + ); + const [name, setName] = useState(""); const [urlName, setUrlName] = useState(""); const [removing, setRemoving] = useState<{ partnerId: number; partnerName: string } | null>(null); const [deleting, setDeleting] = useState(false); + const [confirmingActive, setConfirmingActive] = useState(false); const [loaded, setLoaded] = useState(false); useEffect(() => { @@ -46,7 +78,14 @@ export function ApiGatewayPage() { ); const save = useMutation({ - mutationFn: () => api.updateApiGateway(gatewayId, { name, urlName }), + mutationFn: () => + api.updateApiGateway(gatewayId, { + name, + urlName: finishUrlName(urlName), + // Round-tripped, never edited here — Update replaces the record, so leaving it + // out would reactivate a deactivated gateway on an unrelated rename. + inactive: gateway.data?.inactive ?? false, + }), onSuccess: async () => { // Await the detail refetch before re-syncing the draft (avoids stale-data race). await queryClient.invalidateQueries({ queryKey: ["api-gateway", gatewayId] }); @@ -69,24 +108,35 @@ export function ApiGatewayPage() { return (

- - API gateways - +
-

+

+ {g.inactive && Deactivated}

- - - +
+ {canEdit && ( + + )} + + + +
{/* Endpoint above rather than beside: the attachments table below carries a @@ -100,7 +150,7 @@ export function ApiGatewayPage() { value={urlName} disabled={!canEdit} className="font-mono" - onChange={(e) => setUrlName(e.target.value.toLowerCase())} + onChange={(e) => setUrlName(toUrlName(e.target.value))} /> @@ -110,7 +160,7 @@ export function ApiGatewayPage() { - - - - ), - }, - ]} - /> +
+ + setAttachmentsParam("aq", e.target.value || null)} + placeholder="Search attached partners" + aria-label="Search attached partners" + className="h-9 w-full rounded-lg border border-ink-200 bg-white pr-3 pl-9 text-sm placeholder:text-ink-400 focus:border-crimson-400 focus:ring-2 focus:ring-crimson-100 focus:outline-none" + /> +
+ {attachments.isPending ? ( + + ) : ( + a.partnerId} + empty={ + attachmentsQuery + ? "No attached partners match." + : "No partners attached — the gateway answers 401 to everyone. Attach a partner to bring it to life." + } + columns={[ + { + header: "Partner", + cell: (a) => ( + + {a.partnerName} + + ), + }, + { + header: "Runs", + cell: (a) => ( + + {a.integrationName} + + ), + }, + ...wiredColumns, + { + header: "", + align: "right", + cell: (a) => ( + + + + + + + ), + }, + ]} + /> + )} +
+ setAttachmentsParam("aoffset", String(o), false)} + /> +
@@ -198,12 +275,35 @@ export function ApiGatewayPage() { onConfirm={async () => { await api.removeGatewayAttachment(gatewayId, removing.partnerId); void queryClient.invalidateQueries({ queryKey: ["api-gateway", gatewayId] }); + void queryClient.invalidateQueries({ queryKey: ["api-gateway-attachments-search"] }); void queryClient.invalidateQueries({ queryKey: ["integrations"] }); }} onClose={() => setRemoving(null)} /> )} + {confirmingActive && ( + { + await api.updateApiGateway(gatewayId, { + name: g.name, + urlName: g.urlName, + inactive: !g.inactive, + }); + await queryClient.invalidateQueries({ queryKey: ["api-gateway", gatewayId] }); + void queryClient.invalidateQueries({ queryKey: ["api-gateways"] }); + }} + onClose={() => setConfirmingActive(false)} + /> + )} + {deleting && ( api.listApiGateways() }); + const gateways = useQuery({ + queryKey: ["api-gateways-search", q, offset], + queryFn: () => api.searchApiGateways({ search: q, offset, limit: PAGE_SIZE }), + placeholderData: keepPreviousData, + }); const integrationsById = useIntegrationRowsById(); - const setQ = (value: string) => + const setParam = (key: string, value: string | null, resetOffset = true) => setSearchParams( (prev) => { const next = new URLSearchParams(prev); - if (value) next.set("q", value); - else next.delete("q"); + if (value) next.set(key, value); + else next.delete(key); + if (resetOffset) next.delete("offset"); return next; }, { replace: true }, ); - const filtered = useMemo(() => { - const needle = q.trim().toLowerCase(); - return (gateways.data ?? []).filter( - (g) => !needle || g.name.toLowerCase().includes(needle) || g.urlName.toLowerCase().includes(needle), - ); - }, [gateways.data, q]); + const rows = gateways.data?.result ?? []; + const total = gateways.data?.total ?? 0; return (
@@ -63,7 +67,7 @@ export function ApiGatewaysPage() { setQ(e.target.value)} + onChange={(e) => setParam("q", e.target.value || null)} placeholder="Search gateways" aria-label="Search API gateways" className="h-9 w-full rounded-lg border border-ink-200 bg-white pr-3 pl-9 text-sm placeholder:text-ink-400 focus:border-crimson-400 focus:ring-2 focus:ring-crimson-100 focus:outline-none" @@ -72,21 +76,39 @@ export function ApiGatewaysPage() { {gateways.isPending ? ( - ) : filtered.length === 0 ? ( + ) : rows.length === 0 ? ( } title={q ? "No gateways match" : "No API gateways yet"}> {q ? "Try a different search." : "Create a gateway to give partners a URL to push documents to."} ) : (
+ {c.header}
+ {c.cell(row)}
g.id} minWidth="min-w-200" onRowClick={(g) => navigate(`/api-gateways/${g.id}`)} + footer={ + setParam("offset", String(o), false)} + /> + } columns={[ { header: "Gateway", truncate: true, - cell: (g) => {g.name}, + cell: (g) => ( + + + {g.name} + + {/* Beside the name, not in the Health column: health reports on what the + gateway feeds, and a deactivated one feeds nothing, so it would read + as healthy. */} + {g.inactive && Off} + + ), }, { header: "URL", diff --git a/SW.Bitween.Web/ClientApp/src/pages/api-gateways/AttachPartnerPage.tsx b/SW.Bitween.Web/ClientApp/src/pages/api-gateways/AttachPartnerPage.tsx index 0ebf34a7..9c882cb7 100644 --- a/SW.Bitween.Web/ClientApp/src/pages/api-gateways/AttachPartnerPage.tsx +++ b/SW.Bitween.Web/ClientApp/src/pages/api-gateways/AttachPartnerPage.tsx @@ -1,11 +1,11 @@ -import { useState } from "react"; -import { Link, useNavigate, useParams } from "react-router"; +import { useEffect, useState } from "react"; +import { Link, useNavigate, useParams, useSearchParams } from "react-router"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; -import { ArrowLeft } from "lucide-react"; import { api } from "../../api"; import { Button, EmptyState, FormError, LoadingBlock } from "../../components/ui/basics"; import { Field } from "../../components/ui/forms"; import { IntegrationPicker, PartnerPicker } from "../../components/config/pickers"; +import { BackLink } from "../../components/ui/BackLink"; /** Local draft state with the patch-and-clear shape the form bodies already use. */ function useDraft(initial: T) { @@ -15,7 +15,6 @@ function useDraft(initial: T) { return [draft, update, clear] as const; } - interface Draft { partnerId: number | null; integrationId: number | null; @@ -25,12 +24,17 @@ interface Draft { * Routed create page for one gateway attachment — who calls, and what runs when * they do. Deliberately shaped like `EditAttachmentPage`, its edit twin: two * questions on one form, not a guided flow. + * + * "New integration" leaves this page rather than opening in place — see + * `NewGatewayIntegrationPage` — so `?picked=`/`?partnerId=` restore the choices + * this page had on the way out. */ export function AttachPartnerPage() { const { id = "" } = useParams(); const gatewayId = Number(id); const navigate = useNavigate(); const queryClient = useQueryClient(); + const [searchParams, setSearchParams] = useSearchParams(); const gateway = useQuery({ queryKey: ["api-gateway", gatewayId], @@ -39,10 +43,16 @@ export function AttachPartnerPage() { }); const [draft, update, clear] = useDraft({ - partnerId: null, - integrationId: null, + partnerId: searchParams.get("partnerId") ? Number(searchParams.get("partnerId")) : null, + integrationId: searchParams.get("picked") ? Number(searchParams.get("picked")) : null, }); + // Consumed once, on the way back from creating an integration — cleared so a + // refresh of this page doesn't keep re-seeding the same values. + useEffect(() => { + if (searchParams.has("picked") || searchParams.has("partnerId")) setSearchParams({}, { replace: true }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); const create = useMutation({ mutationFn: () => @@ -68,22 +78,24 @@ export function AttachPartnerPage() { ); const g = gateway.data; - const valid = draft.partnerId !== null && draft.integrationId !== null; + + // The same rule the server enforces, said before the button is pressed rather than + // after: an integration cannot be attached half-made. + const missing = [ + draft.partnerId === null && "a partner", + draft.integrationId === null && "an integration", + ].filter((m): m is string => typeof m === "string"); return (
- - {g.name} - +

Attach a partner to {g.name}

- Anything you don't have yet — the partner, the integration — is created right here. + Who calls in, and what runs when they do. No partner yet, create one right here; no + integration yet, its own page opens next and brings you back.

@@ -106,16 +118,30 @@ export function AttachPartnerPage() { type="GatewayApiCall" value={draft.integrationId} onChange={(integrationId) => update({ integrationId })} + onDefineHere={() => + navigate( + `/api-gateways/${gatewayId}/attach/new-integration${ + draft.partnerId ? `?partnerId=${draft.partnerId}` : "" + }`, + ) + } /> {create.error?.message} -
+
+ {missing.length > 0 && ( +

+ Still needs {missing.slice(0, -1).join(", ")} + {missing.length > 1 ? " and " : ""} + {missing.at(-1)}. +

+ )} +
+ )} + +
+ {missing.length > 0 && ( +

+ Still needs {missing.slice(0, -1).join(", ")} + {missing.length > 1 ? " and " : ""} + {missing.at(-1)}. +

+ )} + + +
+ {create.error?.message} +
+ ); +} diff --git a/SW.Bitween.Web/ClientApp/src/pages/auth/Login.tsx b/SW.Bitween.Web/ClientApp/src/pages/auth/Login.tsx index 1ae6acee..d36258cb 100644 --- a/SW.Bitween.Web/ClientApp/src/pages/auth/Login.tsx +++ b/SW.Bitween.Web/ClientApp/src/pages/auth/Login.tsx @@ -73,7 +73,7 @@ export function LoginPage() { setEmail(e.target.value)} @@ -83,7 +83,7 @@ export function LoginPage() { setPassword(e.target.value)} diff --git a/SW.Bitween.Web/ClientApp/src/pages/bus-gateways/BusGatewayNewPage.tsx b/SW.Bitween.Web/ClientApp/src/pages/bus-gateways/BusGatewayNewPage.tsx index 4db09edb..c18dcc25 100644 --- a/SW.Bitween.Web/ClientApp/src/pages/bus-gateways/BusGatewayNewPage.tsx +++ b/SW.Bitween.Web/ClientApp/src/pages/bus-gateways/BusGatewayNewPage.tsx @@ -1,11 +1,11 @@ import { type FormEvent, useState } from "react"; -import { Link, useNavigate } from "react-router"; +import { useNavigate } from "react-router"; import { useMutation, useQueryClient } from "@tanstack/react-query"; -import { ArrowLeft } from "lucide-react"; import { api, type InformationTypeRow } from "../../api"; import { Button, FormError } from "../../components/ui/basics"; import { Field, TextInput } from "../../components/ui/forms"; import { InfoTypePicker } from "../../components/config/pickers"; +import { BackLink } from "../../components/ui/BackLink"; /** Local draft state with the patch-and-clear shape the form body uses. */ function useDraft(initial: T) { @@ -48,12 +48,7 @@ export function BusGatewayNewPage() { return (
- - {"Integrations"} - +

New bus gateway

diff --git a/SW.Bitween.Web/ClientApp/src/pages/bus-gateways/BusGatewayPage.tsx b/SW.Bitween.Web/ClientApp/src/pages/bus-gateways/BusGatewayPage.tsx index 587c4d0e..ee6d5948 100644 --- a/SW.Bitween.Web/ClientApp/src/pages/bus-gateways/BusGatewayPage.tsx +++ b/SW.Bitween.Web/ClientApp/src/pages/bus-gateways/BusGatewayPage.tsx @@ -1,16 +1,17 @@ import { useEffect, useMemo, useState } from "react"; import { Link, useNavigate, useParams, useSearchParams } from "react-router"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; -import { ArrowLeft, PanelLeftClose, PanelLeftOpen, Trash2 } from "lucide-react"; +import { Pause, PanelLeftClose, PanelLeftOpen, Play, Trash2 } from "lucide-react"; import { api, type BusGatewayDetail, type IntegrationDetail } from "../../api"; import { Can, useSessionCan } from "../../auth/guards"; -import { Button, EmptyState, FormError, LoadingBlock } from "../../components/ui/basics"; +import { Badge, Button, EmptyState, FormError, LoadingBlock } from "../../components/ui/basics"; import { ConfirmDialog, dialogsOpen } from "../../components/ui/overlays"; import { CodeBadge, EditableTitle } from "../../components/ui/Panel"; import { SearchSelect } from "../../components/ui/SearchSelect"; import { useAdapterCatalog } from "../../components/config/AdapterConfig"; import { useIntegrationRowsById, useIntegrationsCache } from "../../components/config/shared"; -import { draftOf } from "../integrations/studio/model"; +import { EMPTY_INTEGRATION, NEW_INTEGRATION_ID, draftOf } from "../integrations/studio/model"; +import { adapterIncomplete } from "../integrations/studio/faces"; import { Canvas, type Hop } from "./studio/Canvas"; import { DeliveryBody, @@ -21,8 +22,8 @@ import { TransformationBody, } from "./studio/Inspector"; import { PartnerDialog } from "../../components/config/PartnerDialog"; -import { NewIntegrationDialog } from "./studio/QuickCreate"; import { RouteList, type Selection } from "./studio/RouteList"; +import { BackLink } from "../../components/ui/BackLink"; import { BUS_NODES, NEW_ROUTE, @@ -70,7 +71,6 @@ export function BusGatewayPage() { const queryClient = useQueryClient(); const canEdit = useSessionCan("bus-gateways.edit"); const canEditIntegration = useSessionCan("subscriptions.edit"); - const canCreateIntegration = useSessionCan("subscriptions.create"); const [params, setParams] = useSearchParams(); const gateway = useQuery({ @@ -101,11 +101,11 @@ export function BusGatewayPage() { const [edit, setEdit] = useState(null); const [collapsedInspector, setCollapsedInspector] = useState(false); const [listOpen, setListOpen] = useState(() => localStorage.getItem(LIST_KEY) !== "0"); - const [creating, setCreating] = useState(null); /** undefined = closed, null = creating, number = editing that partner's values. */ const [partnerDialog, setPartnerDialog] = useState(undefined); const [removingRoute, setRemovingRoute] = useState(null); const [deletingGateway, setDeletingGateway] = useState(false); + const [confirmingActive, setConfirmingActive] = useState(false); /** A move the user asked for that would drop unsaved edits. */ const [guarded, setGuarded] = useState void }>(null); @@ -162,7 +162,8 @@ export function BusGatewayPage() { const q0 = useQuery({ queryKey: ["integration", id0], queryFn: () => api.getIntegration(id0!), - enabled: id0 !== null, + // The integration being defined here has no server side to fetch yet. + enabled: id0 !== null && id0 !== NEW_INTEGRATION_ID, }); const d0 = useHopDraft(edit, id0, q0.data); const id1 = d0?.responseIntegrationId ?? null; @@ -198,6 +199,16 @@ export function BusGatewayPage() { return; } if (edit?.integrationId === active.id) return; + if (active.id === NEW_INTEGRATION_ID) { + // Blank, and `saved` blank too: every field the user fills counts as a change, + // so the save bar names them the same way it does for an existing integration. + setEdit({ + integrationId: NEW_INTEGRATION_ID, + draft: structuredClone(EMPTY_INTEGRATION), + saved: structuredClone(EMPTY_INTEGRATION), + }); + return; + } if (!activeData || activeData.id !== active.id) return; const seeded = draftOf(activeData); setEdit({ integrationId: active.id, draft: seeded, saved: structuredClone(seeded) }); @@ -279,22 +290,39 @@ export function BusGatewayPage() { const save = useMutation({ mutationFn: async () => { - if (nameDirty && name !== null) await api.updateBusGateway(gatewayId, { name }); - // The integration first: if the route write then fails, what was saved is - // the part that stands on its own. - if (edit && intIsDirty) await api.updateIntegration(edit.integrationId, edit.draft); + // `inactive` is round-tripped, not edited here: Update replaces the record, so + // omitting it would reactivate a deactivated gateway on a rename. + if (nameDirty && name !== null) + await api.updateBusGateway(gatewayId, { name, inactive: g.inactive }); + // An integration being defined here is not written on its own: it goes with the + // route, in the one call the endpoint commits as a single transaction, so a + // failure can't leave an integration nothing points at. + const definingIntegration = routeEdit?.draft.integrationId === NEW_INTEGRATION_ID; + const integrationDraft = edit?.draft; + + // Otherwise the integration first: if the route write then fails, what was saved + // is the part that stands on its own. + if (edit && intIsDirty && !definingIntegration) + await api.updateIntegration(edit.integrationId, integrationDraft!); + if (routeEdit && (isNewRoute || routeIsDirty)) { - const input = { - integrationId: routeEdit.draft.integrationId!, - partnerId: routeEdit.draft.partner === "none" ? null : routeEdit.draft.partner, - matchExpression: routeEdit.draft.matchExpression, - }; + const partnerId = routeEdit.draft.partner === "none" ? null : routeEdit.draft.partner; if (isNewRoute) { const before = new Set((g?.routes ?? []).map((r) => r.id)); - await api.addBusRoute(gatewayId, input); + await api.addBusRoute(gatewayId, { + ...(definingIntegration + ? { newIntegration: integrationDraft! } + : { integrationId: routeEdit.draft.integrationId! }), + partnerId, + matchExpression: routeEdit.draft.matchExpression, + }); return before; } - await api.updateBusRoute(gatewayId, routeEdit.routeId as number, input); + await api.updateBusRoute(gatewayId, routeEdit.routeId as number, { + integrationId: routeEdit.draft.integrationId!, + partnerId, + matchExpression: routeEdit.draft.matchExpression, + }); } return null; }, @@ -305,10 +333,12 @@ export function BusGatewayPage() { queryKey: ["bus-gateway", gatewayId], queryFn: () => api.getBusGateway(gatewayId), }); - if (edit) await queryClient.invalidateQueries({ queryKey: ["integration", edit.integrationId] }); + if (edit && edit.integrationId !== NEW_INTEGRATION_ID) + await queryClient.invalidateQueries({ queryKey: ["integration", edit.integrationId] }); void queryClient.invalidateQueries({ queryKey: ["bus-gateways"] }); void queryClient.invalidateQueries({ queryKey: ["integrations"] }); void queryClient.invalidateQueries({ queryKey: ["integration-rows"] }); + void queryClient.invalidateQueries({ queryKey: ["integration-rows-search"] }); setRouteEdit(null); setEdit(null); setName(fresh.name); @@ -337,7 +367,10 @@ export function BusGatewayPage() { const hops: Hop[] = chain.map((h) => ({ integrationId: h.id, - name: h.draft?.name ?? h.data?.name ?? `#${h.id}`, + name: + h.draft?.name?.trim() || + h.data?.name || + (h.id === NEW_INTEGRATION_ID ? "New integration" : `#${h.id}`), draft: h.draft, saved: edit?.integrationId === h.id ? edit.saved : h.draft, row: rowsById.get(h.id), @@ -347,6 +380,22 @@ export function BusGatewayPage() { : null, })); + // What still blocks a save, said the way the modal used to say it at its Create + // button. The rule outlives the modal: an integration defined here cannot be saved + // half-made, and the server refuses it too. + const missing = [ + ...(routeEdit?.draft.integrationId === NEW_INTEGRATION_ID && edit + ? [ + edit.draft.name.trim().length < 2 && "a name", + !edit.draft.handlerId && "a delivery", + adapterIncomplete(catalogs.handlers, edit.draft.handlerId, edit.draft.handlerProperties) && + "its required delivery fields", + ] + : isNewRoute && routeEdit?.draft.integrationId === null + ? ["an integration"] + : []), + ].filter((m): m is string => typeof m === "string"); + const nodeIsDirty = node ? OWNER[node] === "route" ? routeIsDirty || isNewRoute @@ -367,7 +416,14 @@ export function BusGatewayPage() { disabled={!canEdit} onNewPartner={() => setPartnerDialog(null)} onEditPartner={(id) => setPartnerDialog(id)} - onNewIntegration={() => setCreating("route-integration")} + onNewIntegration={() => { + // No modal: the route draft points at the integration being defined, and the + // canvas draws it like any other. Straight to its own node, where the name is. + setRouteEdit((r) => + r ? { ...r, draft: { ...r.draft, integrationId: NEW_INTEGRATION_ID } } : r, + ); + setQuery({ node: "integration" }); + }} /> ) ); @@ -375,7 +431,7 @@ export function BusGatewayPage() { return (

{routeEdit?.draft.integrationId === null - ? "Pick the integration this route runs first — this step belongs to it." + ? "Pick the integration this route runs, or define one — this step belongs to it." : "Loading the integration…"}

); @@ -394,6 +450,7 @@ export function BusGatewayPage() { : null } lastException={activeData?.lastException ?? null} + autoFocusName={edit.integrationId === NEW_INTEGRATION_ID} /> ); case "transformation": @@ -402,7 +459,11 @@ export function BusGatewayPage() { draft={edit.draft} onChange={onChange} disabled={!canEditIntegration} - mapperEditorHref={`/subscriptions/${edit.integrationId}/mapper`} + mapperEditorHref={ + edit.integrationId === NEW_INTEGRATION_ID + ? null + : `/subscriptions/${edit.integrationId}/mapper` + } /> ); case "delivery": @@ -414,8 +475,6 @@ export function BusGatewayPage() { onChange={onChange} disabled={!canEditIntegration} candidates={(allIntegrations.data ?? []).filter((x) => x.id !== edit.integrationId)} - onNewIntegration={() => setCreating("response-integration")} - canCreate={canCreateIntegration} /> ); } @@ -425,13 +484,7 @@ export function BusGatewayPage() {
{/* ——— toolbar ——— */}
- - Bus gateways - + )} + {canEdit && ( + + )}
g.id} minWidth="min-w-200" onRowClick={(g) => navigate(`/bus-gateways/${g.id}`)} + footer={ + setParam("offset", String(o), false)} + /> + } columns={[ { header: "Gateway", truncate: true, - cell: (g) => {g.name}, + cell: (g) => ( + + + {g.name} + + {/* Beside the name, not in the Health column: health reports on what the + gateway feeds, and a deactivated one feeds nothing, so it would read + as healthy. */} + {g.inactive && Off} + + ), }, { header: "Listens for", diff --git a/SW.Bitween.Web/ClientApp/src/pages/bus-gateways/studio/Canvas.tsx b/SW.Bitween.Web/ClientApp/src/pages/bus-gateways/studio/Canvas.tsx index ea0b2871..835cfdfe 100644 --- a/SW.Bitween.Web/ClientApp/src/pages/bus-gateways/studio/Canvas.tsx +++ b/SW.Bitween.Web/ClientApp/src/pages/bus-gateways/studio/Canvas.tsx @@ -5,10 +5,12 @@ import type { IntegrationRow } from "../../../api"; import { PanZoomCanvas } from "../../../components/ui/PanZoomCanvas"; import { Connector, StageNode, type StageFace } from "../../integrations/studio/StageRail"; import { faceOf, type AdapterCatalogs } from "../../integrations/studio/faces"; +import { NEW_INTEGRATION_ID } from "../../integrations/studio/model"; import { BUS_NODES, HEALTH_DOT, HEALTH_LABEL, + HEALTH_TITLE, nodeDirty, routeHealth, type BusDestination, @@ -251,6 +253,7 @@ function ExpandedHop({ integrationNames: { id: number; name: string }[]; }) { const health = routeHealth(hop.row); + const isNew = hop.integrationId === NEW_INTEGRATION_ID; const headerDirty = hop.draft && hop.saved ? nodeDirty("integration", hop.draft, hop.saved) : false; if (!hop.draft) @@ -275,8 +278,8 @@ function ExpandedHop({ }`} > - - {hop.draft.name} + + {hop.draft.name.trim() || hop.name} {headerDirty && ( @@ -286,11 +289,14 @@ function ExpandedHop({ Disabled )} - + {/* Nothing has run yet, so there is no health to report. */} + {!isNew && ( + + )}
@@ -304,6 +310,7 @@ function ExpandedHop({ saved: hop.saved ?? undefined, catalogs, integrationNames, + unsaved: isNew, })} label={BUS_NODES[node].label} icon={BUS_NODES[node].icon} @@ -348,7 +355,7 @@ function CollapsedHop({ hop, label, onOpen }: { hop: Hop; label: string; onOpen:
diff --git a/SW.Bitween.Web/ClientApp/src/pages/bus-gateways/studio/Inspector.tsx b/SW.Bitween.Web/ClientApp/src/pages/bus-gateways/studio/Inspector.tsx index 1ae9ac10..2c2c44da 100644 --- a/SW.Bitween.Web/ClientApp/src/pages/bus-gateways/studio/Inspector.tsx +++ b/SW.Bitween.Web/ClientApp/src/pages/bus-gateways/studio/Inspector.tsx @@ -1,7 +1,7 @@ import { Link } from "react-router"; import { useQuery } from "@tanstack/react-query"; import { ChevronDown, ChevronUp, Plus, X } from "lucide-react"; -import { api } from "../../../api"; +import { api, type IntegrationType } from "../../../api"; import { useSessionCan } from "../../../auth/guards"; import { Badge } from "../../../components/ui/basics"; import { Field, TextInput } from "../../../components/ui/forms"; @@ -10,7 +10,12 @@ import { AdapterConfig } from "../../../components/config/AdapterConfig"; import { MatchExpressionEditor } from "../../../components/config/MatchExpressionEditor"; import { HealthBadge } from "../../../components/config/shared"; import { ResponseFields } from "../../integrations/studio/ResponseFields"; -import { BUS_NODES, type BusNodeId, type IntegrationDraft, type RouteDraft } from "./model"; +import { + BUS_NODES, + type BusNodeId, + type IntegrationDraft, + type RouteDraft, +} from "./model"; /** * The configuration surface: one node's form, docked under the canvas. @@ -208,12 +213,15 @@ export function IntegrationBody({ disabled, health, lastException, + /** Set while the integration is being defined here — the name is the first thing asked. */ + autoFocusName = false, }: { draft: IntegrationDraft; onChange: (patch: Partial) => void; disabled: boolean; health: { isRunning: boolean; consecutiveFailures: number } | null; lastException: string | null; + autoFocusName?: boolean; }) { const workGroups = useQuery({ queryKey: ["work-groups"], @@ -230,6 +238,8 @@ export function IntegrationBody({ id="bs-int-name" value={draft.name} disabled={disabled} + autoFocus={autoFocusName} + placeholder="e.g. Coral orders to SAP" onChange={(e) => onChange({ name: e.target.value })} /> @@ -290,7 +300,7 @@ export function TransformationBody({ draft: IntegrationDraft; onChange: (patch: Partial) => void; disabled: boolean; - mapperEditorHref?: string; + mapperEditorHref?: string | null; }) { return ( ) => void; disabled: boolean; - candidates: { id: number; name: string }[]; - onNewIntegration: () => void; - canCreate: boolean; + candidates: { id: number; name: string; type: IntegrationType }[]; }) { return (
@@ -352,13 +358,10 @@ export function ResponseBody({ candidates={candidates} idPrefix="bs-resp" /> - {draft.handlerId !== null && canCreate && !disabled && ( - - )} {draft.handlerId !== null && (

- Both can be set. A bus message is a fan-out — every route bound to that message's information - type picks it up, on this gateway and any other. + A bus message is a fan-out — every route bound to that message's information type picks + it up, on this gateway and any other.

)}
diff --git a/SW.Bitween.Web/ClientApp/src/pages/bus-gateways/studio/QuickCreate.tsx b/SW.Bitween.Web/ClientApp/src/pages/bus-gateways/studio/QuickCreate.tsx deleted file mode 100644 index 2276dfda..00000000 --- a/SW.Bitween.Web/ClientApp/src/pages/bus-gateways/studio/QuickCreate.tsx +++ /dev/null @@ -1,117 +0,0 @@ -import { useState } from "react"; -import { useMutation, useQueryClient } from "@tanstack/react-query"; -import { api } from "../../../api"; -import { Button, FormError } from "../../../components/ui/basics"; -import { Field, TextInput } from "../../../components/ui/forms"; -import { Dialog } from "../../../components/ui/overlays"; -import { AdapterConfig, useAdapterCatalog } from "../../../components/config/AdapterConfig"; -import { adapterIncomplete } from "../../integrations/studio/faces"; - -/* - * Creating from inside the studio. - * - * The route itself is never a dialog — it is a row and a set of nodes, so the - * diagram stays on screen while you answer its three questions. An integration is - * a *different* record and has to exist before the route can point at it, so it - * gets a dialog; what it creates is then configured on the canvas like anything - * else. Partners use the app-wide `PartnerDialog`. - */ - -/** - * A new integration for this gateway, asked down to the two things it can't run - * without: a name and somewhere to deliver. Transformation, response and the rest - * are nodes on the canvas the moment it exists — no reason to ask twice. - */ -export function NewIntegrationDialog({ - informationTypeId, - informationTypeCode, - onClose, - onCreated, -}: { - informationTypeId: number; - informationTypeCode: string; - onClose: () => void; - onCreated: (id: number) => void; -}) { - const queryClient = useQueryClient(); - const handlers = useAdapterCatalog("handler"); - const [name, setName] = useState(""); - const [handlerId, setHandlerId] = useState(null); - const [handlerProperties, setHandlerProperties] = useState>({}); - - const create = useMutation({ - mutationFn: () => - api.createIntegration({ - type: "BusGateway", - name: name.trim(), - informationTypeId, - handlerId, - handlerProperties, - // Safe to enable: a BusGateway integration only ever runs through a route, - // and this one has none until you save the route that is being built. - enabled: true, - }), - onSuccess: (created) => { - void queryClient.invalidateQueries({ queryKey: ["integrations"] }); - void queryClient.invalidateQueries({ queryKey: ["integration-rows"] }); - onCreated(created.id); - onClose(); - }, - }); - - const missing = [ - name.trim().length < 2 && "a name", - handlerId === null && "a delivery", - adapterIncomplete(handlers, handlerId, handlerProperties) && "its required delivery fields", - ].filter((m): m is string => typeof m === "string"); - - return ( - -
- - setName(e.target.value)} - /> - - - - { - setHandlerId(id); - setHandlerProperties(props); - }} - disabled={false} - required - /> - - - {create.error?.message} -
- {missing.length > 0 && ( -

- Still needs {missing.slice(0, -1).join(", ")} - {missing.length > 1 ? " and " : ""} - {missing.at(-1)}. -

- )} - - -
-
-
- ); -} diff --git a/SW.Bitween.Web/ClientApp/src/pages/bus-gateways/studio/RouteList.tsx b/SW.Bitween.Web/ClientApp/src/pages/bus-gateways/studio/RouteList.tsx index 60f4ba68..98991670 100644 --- a/SW.Bitween.Web/ClientApp/src/pages/bus-gateways/studio/RouteList.tsx +++ b/SW.Bitween.Web/ClientApp/src/pages/bus-gateways/studio/RouteList.tsx @@ -1,7 +1,7 @@ import { useMemo, useState } from "react"; import { Plus, Search } from "lucide-react"; import type { BusGatewayRoute, IntegrationRow } from "../../../api"; -import { HEALTH_DOT, HEALTH_LABEL, conditionText, routeHealth, type RouteDraft } from "./model"; +import { HEALTH_DOT, HEALTH_LABEL, HEALTH_TITLE, conditionText, routeHealth, type RouteDraft } from "./model"; export type Selection = number | "new" | null; @@ -154,7 +154,7 @@ function Row({ = { paused: "bg-warn-400", unknown: "bg-ink-200", }; + +/** What each dot means, for hovering — `HEALTH_LABEL` alone is a single word with no context. */ +export const HEALTH_TITLE: Record = { + ok: "Healthy — enabled, unpaused, and its last run succeeded.", + failing: "Enabled and unpaused, but its recent runs ended in errors.", + disabled: "Turned off — this route's integration won't run, even if the filter matches.", + paused: "Held without being disabled — matches still route here, but nothing runs until unpaused.", + unknown: "No integration data available for this route yet.", +}; diff --git a/SW.Bitween.Web/ClientApp/src/pages/exchanges/ExchangesPage.tsx b/SW.Bitween.Web/ClientApp/src/pages/exchanges/ExchangesPage.tsx index 2fd7453c..3e605741 100644 --- a/SW.Bitween.Web/ClientApp/src/pages/exchanges/ExchangesPage.tsx +++ b/SW.Bitween.Web/ClientApp/src/pages/exchanges/ExchangesPage.tsx @@ -25,7 +25,7 @@ const REFRESH_OPTIONS = [ ]; /** Everything except paging counts as "a filter" for the Clear affordance. */ -const FILTER_KEYS = ["status", "integrationId", "partnerId", "informationTypeId", "ids", "correlationId", "property", "from", "to"] as const; +const FILTER_KEYS = ["status", "integrationId", "partnerId", "informationTypeId", "ids", "correlationId", "propertyKey", "property", "from", "to"] as const; const readQuery = (sp: URLSearchParams): ExchangeQuery => ({ status: (sp.get("status") as ExchangeStatus | null) ?? undefined, @@ -34,6 +34,7 @@ const readQuery = (sp: URLSearchParams): ExchangeQuery => ({ informationTypeId: sp.get("informationTypeId") ? Number(sp.get("informationTypeId")) : undefined, ids: sp.get("ids") ?? undefined, correlationId: sp.get("correlationId") ?? undefined, + propertyKey: sp.get("propertyKey") ?? undefined, property: sp.get("property") ?? undefined, from: sp.get("from") ? new Date(sp.get("from")! + "T00:00:00").toISOString() : undefined, to: sp.get("to") ? new Date(sp.get("to")! + "T23:59:59").toISOString() : undefined, @@ -63,6 +64,25 @@ export function ExchangesPage() { const infoTypes = useQuery({ queryKey: ["information-types"], queryFn: () => api.listInformationTypes() }).data ?? []; + /** + * Every promoted key any information type declares, with the types that declare it. + * Read off the list already fetched for the information-type filter, so offering the + * keys costs nothing — and picking from real names beats remembering how one was spelled. + */ + const propertyKeyOptions = useMemo(() => { + const owners = new Map(); + for (const t of infoTypes) + for (const p of t.promotedProperties ?? []) { + const carriers = owners.get(p.key) ?? []; + const name = t.code ?? t.name; + if (!carriers.includes(name)) carriers.push(name); + owners.set(p.key, carriers); + } + return [...owners.entries()] + .sort(([a], [b]) => a.localeCompare(b)) + .map(([key, carriers]) => ({ value: key, label: key, hint: carriers.join(", ") })); + }, [infoTypes]); + /** Set (or drop) one URL param; changing any filter resets paging. */ const setParam = (key: string, value: string | null, resetOffset = true) => { const next = new URLSearchParams(searchParams); @@ -216,10 +236,20 @@ export function ExchangesPage() { } onKeyDown={(e) => e.key === "Enter" && setParam("correlationId", e.currentTarget.value || null)} /> +
+ setParam("propertyKey", v || null)} + /> +
e.target.value !== (query.property ?? "") && setParam("property", e.target.value || null)} @@ -324,7 +354,7 @@ export function ExchangesPage() {
- + diff --git a/SW.Bitween.Web/ClientApp/src/pages/team/RoleEditor.tsx b/SW.Bitween.Web/ClientApp/src/pages/team/RoleEditor.tsx index 68e2f0db..55801802 100644 --- a/SW.Bitween.Web/ClientApp/src/pages/team/RoleEditor.tsx +++ b/SW.Bitween.Web/ClientApp/src/pages/team/RoleEditor.tsx @@ -1,7 +1,7 @@ import { useEffect, useMemo, useState } from "react"; import { Link, useNavigate, useParams, useSearchParams } from "react-router"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; -import { ArrowLeft, Copy, EyeOff, ShieldCheck, Trash2 } from "lucide-react"; +import { Copy, EyeOff, ShieldCheck, Trash2 } from "lucide-react"; import { api, type ActionId, type PermissionKey } from "../../api"; import { ACTION_LABELS, @@ -16,6 +16,7 @@ import { visibleGroups } from "../../nav"; import { Badge, Button, FormError, LoadingBlock } from "../../components/ui/basics"; import { Field, TextInput } from "../../components/ui/forms"; import { ConfirmDialog } from "../../components/ui/overlays"; +import { BackLink } from "../../components/ui/BackLink"; /** Live answer to "what would someone with this role actually see?" */ function AccessPreview({ permissions, total }: { permissions: Set; total: number }) { @@ -175,12 +176,7 @@ export function RoleEditor() { return (
- - Roles - +
diff --git a/SW.Bitween.Web/ClientApp/src/pages/work-groups/LiveQueueStats.tsx b/SW.Bitween.Web/ClientApp/src/pages/work-groups/LiveQueueStats.tsx index cb51dece..edd0f658 100644 --- a/SW.Bitween.Web/ClientApp/src/pages/work-groups/LiveQueueStats.tsx +++ b/SW.Bitween.Web/ClientApp/src/pages/work-groups/LiveQueueStats.tsx @@ -2,6 +2,7 @@ import type { ReactNode } from "react"; import { keepPreviousData, useQuery } from "@tanstack/react-query"; import { api } from "../../api"; import { Badge } from "../../components/ui/basics"; +import { queueHealthTitle } from "../../components/config/shared"; function LiveStat({ label, value, tone }: { label: string; value: ReactNode; tone?: "warn" | "danger" }) { return ( @@ -39,13 +40,20 @@ export function LiveQueueStats({ groupId }: { groupId: number }) {
{consumer.health === "critical" ? ( - Critical + Critical ) : consumer.health === "warning" ? ( - Warning + Warning ) : ( - Healthy + Healthy + )} + {consumer.isBackpressured && ( + + Backpressure + )} - {consumer.isBackpressured && Backpressure} {consumer.queueName}
diff --git a/SW.Bitween.Web/ClientApp/src/pages/work-groups/WorkGroupPage.tsx b/SW.Bitween.Web/ClientApp/src/pages/work-groups/WorkGroupPage.tsx index cd4cc5ab..52feed74 100644 --- a/SW.Bitween.Web/ClientApp/src/pages/work-groups/WorkGroupPage.tsx +++ b/SW.Bitween.Web/ClientApp/src/pages/work-groups/WorkGroupPage.tsx @@ -1,7 +1,7 @@ import { useEffect, useMemo, useState } from "react"; import { Link, useNavigate, useParams } from "react-router"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; -import { ArrowLeft, ArrowUpRight, Trash2 } from "lucide-react"; +import { ArrowUpRight, Trash2 } from "lucide-react"; import { api } from "../../api"; import { Can, useSessionCan } from "../../auth/guards"; import { Button, EmptyState, LoadingBlock } from "../../components/ui/basics"; @@ -14,6 +14,7 @@ import { import { EditableTitle, Panel, UnsavedBar } from "../../components/ui/Panel"; import { SetupList } from "../../components/config/shared"; import { LiveQueueStats } from "./LiveQueueStats"; +import { BackLink } from "../../components/ui/BackLink"; /** * This group's slice of the live RabbitMQ picture — the same numbers the @@ -73,6 +74,7 @@ export function WorkGroupPage() { // Await the detail refetch before re-syncing the draft (avoids stale-data race). await queryClient.invalidateQueries({ queryKey: ["work-group", groupId] }); void queryClient.invalidateQueries({ queryKey: ["work-groups"] }); + void queryClient.invalidateQueries({ queryKey: ["work-groups-search"] }); setLoaded(false); }, }); @@ -91,12 +93,7 @@ export function WorkGroupPage() { return (
- - Work groups - +
@@ -150,6 +147,7 @@ export function WorkGroupPage() { onConfirm={async () => { await api.deleteWorkGroup(groupId); void queryClient.invalidateQueries({ queryKey: ["work-groups"] }); + void queryClient.invalidateQueries({ queryKey: ["work-groups-search"] }); navigate("/work-groups"); }} onClose={() => setDeleting(false)} diff --git a/SW.Bitween.Web/ClientApp/src/pages/work-groups/WorkGroupsPage.tsx b/SW.Bitween.Web/ClientApp/src/pages/work-groups/WorkGroupsPage.tsx index db59a1cf..a10ea624 100644 --- a/SW.Bitween.Web/ClientApp/src/pages/work-groups/WorkGroupsPage.tsx +++ b/SW.Bitween.Web/ClientApp/src/pages/work-groups/WorkGroupsPage.tsx @@ -1,4 +1,4 @@ -import { useMemo, useState } from "react"; +import { useState } from "react"; import { useNavigate, useSearchParams } from "react-router"; import { keepPreviousData, useQuery } from "@tanstack/react-query"; import { ArrowUpRight, Layers, Plus, Search } from "lucide-react"; @@ -7,8 +7,9 @@ import { Can, useSessionCan } from "../../auth/guards"; import { WorkGroupDialog } from "../../components/config/WorkGroupDialog"; import { PageHeader } from "../../components/layout/PageHeader"; import { Badge, Button, EmptyState, LoadingBlock } from "../../components/ui/basics"; +import { Pagination } from "../../components/ui/Pagination"; import { Table, type Column } from "../../components/ui/Table"; -import { UsedByCell, useIntegrationsCache } from "../../components/config/shared"; +import { UsedByCell, queueHealthTitle, useIntegrationsCache } from "../../components/config/shared"; /** * The live RabbitMQ numbers, as columns rather than a per-row drill-down. @@ -28,11 +29,11 @@ function liveColumns(snapshot: QueueHealthSnapshot | undefined): Column—; return c.health === "critical" ? ( - Critical + Critical ) : c.health === "warning" ? ( - Warning + Warning ) : ( - Healthy + Healthy ); }, }, @@ -46,14 +47,21 @@ function liveColumns(snapshot: QueueHealthSnapshot | undefined): Column api.listWorkGroups() }); + const groups = useQuery({ + queryKey: ["work-groups-search", q, offset], + queryFn: () => api.searchWorkGroups({ search: q, offset, limit: PAGE_SIZE }), + placeholderData: keepPreviousData, + }); const integrations = useIntegrationsCache().data ?? []; const live = useQuery({ queryKey: ["queue-health"], @@ -63,23 +71,20 @@ export function WorkGroupsPage() { enabled: canMonitor, }); - const setParam = (key: string, value: string | null) => + const setParam = (key: string, value: string | null, resetOffset = true) => setSearchParams( (prev) => { const next = new URLSearchParams(prev); if (value) next.set(key, value); else next.delete(key); + if (resetOffset) next.delete("offset"); return next; }, { replace: true }, ); - const filtered = useMemo(() => { - const needle = q.trim().toLowerCase(); - return (groups.data ?? []).filter( - (g) => !needle || g.name.toLowerCase().includes(needle) || g.busMessageName.toLowerCase().includes(needle), - ); - }, [groups.data, q]); + const filtered = groups.data?.result ?? []; + const total = groups.data?.total ?? 0; return (
@@ -133,6 +138,14 @@ export function WorkGroupsPage() { rowKey={(g) => g.id} minWidth="min-w-220" onRowClick={(g) => navigate(`/work-groups/${g.id}`)} + footer={ + setParam("offset", String(o), false)} + /> + } columns={[ { header: "Name", cell: (g) => {g.name} }, { diff --git a/SW.Bitween.Web/ClientApp/src/router.tsx b/SW.Bitween.Web/ClientApp/src/router.tsx index 6ed34124..7556e8f4 100644 --- a/SW.Bitween.Web/ClientApp/src/router.tsx +++ b/SW.Bitween.Web/ClientApp/src/router.tsx @@ -17,6 +17,7 @@ import { ApiGatewayNewPage } from "./pages/api-gateways/ApiGatewayNewPage"; import { ApiGatewaysPage } from "./pages/api-gateways/ApiGatewaysPage"; import { ApiGatewayPage } from "./pages/api-gateways/ApiGatewayPage"; import { AttachPartnerPage } from "./pages/api-gateways/AttachPartnerPage"; +import { NewGatewayIntegrationPage } from "./pages/api-gateways/NewGatewayIntegrationPage"; import { EditAttachmentPage } from "./pages/api-gateways/EditAttachmentPage"; import { BusGatewayNewPage } from "./pages/bus-gateways/BusGatewayNewPage"; import { BusGatewayPage } from "./pages/bus-gateways/BusGatewayPage"; @@ -238,6 +239,14 @@ export const router = createBrowserRouter([ ), }, + { + path: "api-gateways/:id/attach/new-integration", + element: ( + + + + ), + }, { path: "api-gateways/:id/attachments/:partnerId", element: ( diff --git a/SW.Bitween.Web/Startup.cs b/SW.Bitween.Web/Startup.cs index b1312598..09f6918b 100644 --- a/SW.Bitween.Web/Startup.cs +++ b/SW.Bitween.Web/Startup.cs @@ -1,5 +1,6 @@ using System; using System.Text; +using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.EntityFrameworkCore; @@ -70,6 +71,9 @@ public void ConfigureServices(IServiceCollection services) services.AddSingleton(); services.AddSingleton(); services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); @@ -345,12 +349,9 @@ public void ConfigureServices(IServiceCollection services) .AllowAnyMethod() .AllowCredentials(); } - else - { - builder.AllowAnyOrigin(); - builder.AllowAnyHeader(); - builder.AllowAnyMethod(); - } + // When no origins are configured, allow no cross-origin access. + // The SPA is served same-origin, so CORS is only needed for + // split-host / local-dev setups that set CorsOrigins explicitly. }); }); @@ -363,10 +364,70 @@ public void ConfigureServices(IServiceCollection services) } + /// + /// Enforced. Proven Report-Only first across every page in the admin UI — a wrong + /// enforced policy blanks the app, so the order matters. Switch back to + /// "Content-Security-Policy-Report-Only" before widening the policy again. + /// + private const string ContentSecurityPolicyHeader = "Content-Security-Policy"; + + /// Mirrors the policy the legacy UI enforces at nginx, minus its nginx-only bits. + private const string ContentSecurityPolicy = + "default-src 'self'; " + + "script-src 'self'; " + + "style-src 'self' 'unsafe-inline'; " + + "img-src 'self' data:; " + + "connect-src 'self' https://login.microsoftonline.com; " + + "frame-src https://login.microsoftonline.com; " + + "font-src 'self' data:; " + + "form-action 'self' https://login.microsoftonline.com; " + + "frame-ancestors 'none'; " + + "base-uri 'self'; " + + "object-src 'none'"; + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { app.UseForwardedHeaders(); + app.Use(async (context, next) => + { + var headers = context.Response.Headers; + headers["X-Frame-Options"] = "DENY"; + headers["X-Content-Type-Options"] = "nosniff"; + headers["Referrer-Policy"] = "strict-origin-when-cross-origin"; + headers["X-Permitted-Cross-Domain-Policies"] = "none"; + headers["Cross-Origin-Opener-Policy"] = "same-origin-allow-popups"; + + // Content-Security-Policy for the admin UI. + // + // The legacy deployment set this in nginx, which only ever served the SPA. + // Here the same host also serves Swagger UI, which needs inline scripts and + // styles of its own, so the policy is scoped to everything else — applied + // globally it would simply take Swagger down. + // + // 'unsafe-inline' is present for styles only: the brand colour is applied at + // runtime as custom properties. Scripts need no such exemption — the built + // index.html carries no inline script, only the module bundle. + if (!context.Request.Path.StartsWithSegments("/swagger")) + headers[ContentSecurityPolicyHeader] = ContentSecurityPolicy; + + // Sensitive API responses (JSON) must not be cached by the browser or + // intermediaries. Scoped by content type so static assets stay cacheable. + context.Response.OnStarting(() => + { + var contentType = context.Response.ContentType; + if (!string.IsNullOrEmpty(contentType) && + (contentType.Contains("application/json", StringComparison.OrdinalIgnoreCase) || + contentType.Contains("+json", StringComparison.OrdinalIgnoreCase))) + { + context.Response.Headers["Cache-Control"] = "no-store, no-cache, must-revalidate"; + } + return Task.CompletedTask; + }); + + await next(); + }); + if (env.IsDevelopment()) { app.UseDeveloperExceptionPage();
- + {/* Status and the relationship markers read as one thought: diff --git a/SW.Bitween.Web/ClientApp/src/pages/flow/FlowMap.tsx b/SW.Bitween.Web/ClientApp/src/pages/flow/FlowMap.tsx index 77b8adf4..6902cdf4 100644 --- a/SW.Bitween.Web/ClientApp/src/pages/flow/FlowMap.tsx +++ b/SW.Bitween.Web/ClientApp/src/pages/flow/FlowMap.tsx @@ -106,6 +106,15 @@ function NodeCard({ // Everything the card had to truncate, in full — the code and the detail line // are exactly what gets cut at 216px. const summary = [`${label}: ${node.title}`, node.code, node.detail].filter(Boolean).join(" · "); + // The loop icon and the warning dot are otherwise unexplained to a mouse user — + // only a screen reader gets their `aria-label`, since a card can be hovered + // anywhere and this is the one title the browser will show either way. + const extra = [ + onLoop && "on a loop — a message from here eventually feeds back into itself", + node.warning, + ] + .filter(Boolean) + .join(" — "); const border = onLoop ? "border-danger-300 bg-danger-50" : node.warning @@ -117,7 +126,7 @@ function NodeCard({ return ( diff --git a/SW.Bitween.Web/ClientApp/src/pages/flow/model.ts b/SW.Bitween.Web/ClientApp/src/pages/flow/model.ts index 17cfb772..a72d4c6d 100644 --- a/SW.Bitween.Web/ClientApp/src/pages/flow/model.ts +++ b/SW.Bitween.Web/ClientApp/src/pages/flow/model.ts @@ -162,6 +162,17 @@ export function buildFlowGraph({ const from = runBy.get(i.id) ?? standalone(i.id); if (from === null) continue; + // A response is whatever the delivery hands back, so with no delivery there is never + // one to route: `XchangeService.RunHandler` returns null without a handler, and both + // response paths are gated on that file being non-null. Drawing the edges anyway would + // make the map assert a flow the runtime cannot produce — the exact kind of lie it + // exists to catch. The integration's own page hides these fields once the delivery is + // cleared, which is what lets the pair drift apart unnoticed. + if (i.handlerId === null) { + warn(from, `${i.name} routes its response but delivers nothing, so no response is ever produced.`); + continue; + } + if (i.responseMessageTypeName !== null && i.responseMessageTypeName !== "") { const to = messageByName.get(i.responseMessageTypeName.toLowerCase()); if (to) edges.push({ id: `pub:${i.id}`, from, to, kind: "publishes" }); diff --git a/SW.Bitween.Web/ClientApp/src/pages/global-values/GlobalValueSetPage.tsx b/SW.Bitween.Web/ClientApp/src/pages/global-values/GlobalValueSetPage.tsx index a6bbee30..771e3663 100644 --- a/SW.Bitween.Web/ClientApp/src/pages/global-values/GlobalValueSetPage.tsx +++ b/SW.Bitween.Web/ClientApp/src/pages/global-values/GlobalValueSetPage.tsx @@ -1,7 +1,7 @@ import { useEffect, useMemo, useState } from "react"; import { Link, useNavigate, useParams } from "react-router"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; -import { ArrowLeft, Trash2 } from "lucide-react"; +import { Trash2 } from "lucide-react"; import { api, referencesGlobal } from "../../api"; import { Can, useSessionCan } from "../../auth/guards"; import { Badge, Button, EmptyState, LoadingBlock } from "../../components/ui/basics"; @@ -9,6 +9,7 @@ import { ConfirmDialog } from "../../components/ui/overlays"; import { KeyValueEditor, toRecord, toRows, type KvRow } from "../../components/ui/KeyValueEditor"; import { EditableTitle, Panel, UnsavedBar } from "../../components/ui/Panel"; import { MiniTable } from "../../components/ui/Table"; +import { BackLink } from "../../components/ui/BackLink"; import { INTEGRATION_TYPE_LABELS, IntegrationMiniList, @@ -70,12 +71,7 @@ export function GlobalValueSetPage() { return (
- - Global values - +
diff --git a/SW.Bitween.Web/ClientApp/src/pages/information-types/InformationTypePage.tsx b/SW.Bitween.Web/ClientApp/src/pages/information-types/InformationTypePage.tsx index 047d2383..4b909627 100644 --- a/SW.Bitween.Web/ClientApp/src/pages/information-types/InformationTypePage.tsx +++ b/SW.Bitween.Web/ClientApp/src/pages/information-types/InformationTypePage.tsx @@ -1,7 +1,7 @@ import { useEffect, useMemo, useState } from "react"; import { Link, useNavigate, useParams } from "react-router"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; -import { ArrowLeft, Trash2 } from "lucide-react"; +import { Trash2 } from "lucide-react"; import { api } from "../../api"; import { Can, useSessionCan } from "../../auth/guards"; import { Button, EmptyState, LoadingBlock } from "../../components/ui/basics"; @@ -9,6 +9,7 @@ import { ConfirmDialog } from "../../components/ui/overlays"; import { CodeBadge, Panel, UnsavedBar } from "../../components/ui/Panel"; import { MiniTable } from "../../components/ui/Table"; import { ExchangesList, SetupList, TrailTable } from "../../components/config/shared"; +import { BackLink } from "../../components/ui/BackLink"; import { InformationTypeFields, informationTypeChanges, @@ -70,12 +71,7 @@ export function InformationTypePage() { return (
- - Information types - +
@@ -94,12 +90,7 @@ export function InformationTypePage() {
{draft && ( - + )}
diff --git a/SW.Bitween.Web/ClientApp/src/pages/information-types/InformationTypesPage.tsx b/SW.Bitween.Web/ClientApp/src/pages/information-types/InformationTypesPage.tsx index 4ed8a0a8..672ffcf9 100644 --- a/SW.Bitween.Web/ClientApp/src/pages/information-types/InformationTypesPage.tsx +++ b/SW.Bitween.Web/ClientApp/src/pages/information-types/InformationTypesPage.tsx @@ -1,6 +1,6 @@ -import { useMemo, useState } from "react"; +import { useState } from "react"; import { useNavigate, useSearchParams } from "react-router"; -import { useQuery } from "@tanstack/react-query"; +import { keepPreviousData, useQuery } from "@tanstack/react-query"; import { FileText, Plus, Search } from "lucide-react"; import { api } from "../../api"; import { Can } from "../../auth/guards"; @@ -8,36 +8,40 @@ import { InformationTypeDialog } from "../../components/config/InformationTypeDi import { PageHeader } from "../../components/layout/PageHeader"; import { Badge, Button, EmptyState, LoadingBlock } from "../../components/ui/basics"; import { CodeBadge } from "../../components/ui/Panel"; +import { Pagination } from "../../components/ui/Pagination"; import { Table } from "../../components/ui/Table"; import { UsedByCell, useIntegrationsCache } from "../../components/config/shared"; +const PAGE_SIZE = 25; + export function InformationTypesPage() { const [searchParams, setSearchParams] = useSearchParams(); const navigate = useNavigate(); const [creating, setCreating] = useState(false); const q = searchParams.get("q") ?? ""; + const offset = searchParams.get("offset") ? Number(searchParams.get("offset")) : 0; - const types = useQuery({ queryKey: ["information-types"], queryFn: () => api.listInformationTypes() }); + const types = useQuery({ + queryKey: ["information-types-search", q, offset], + queryFn: () => api.searchInformationTypes({ search: q, offset, limit: PAGE_SIZE }), + placeholderData: keepPreviousData, + }); const integrations = useIntegrationsCache().data ?? []; - const setParam = (key: string, value: string | null) => + const setParam = (key: string, value: string | null, resetOffset = true) => setSearchParams( (prev) => { const next = new URLSearchParams(prev); if (value) next.set(key, value); else next.delete(key); + if (resetOffset) next.delete("offset"); return next; }, { replace: key === "q" }, ); - const filtered = useMemo(() => { - const needle = q.trim().toLowerCase(); - return (types.data ?? []).filter( - (t) => - !needle || t.name.toLowerCase().includes(needle) || (t.code ?? "").toLowerCase().includes(needle), - ); - }, [types.data, q]); + const rows = types.data?.result ?? []; + const total = types.data?.total ?? 0; return (
@@ -76,7 +80,7 @@ export function InformationTypesPage() { type="search" value={q} onChange={(e) => setParam("q", e.target.value || null)} - placeholder="Search by name or code" + placeholder="Search by name" aria-label="Search information types" className="h-9 w-full rounded-lg border border-ink-200 bg-white pr-3 pl-9 text-sm placeholder:text-ink-400 focus:border-crimson-400 focus:ring-2 focus:ring-crimson-100 focus:outline-none" /> @@ -84,16 +88,24 @@ export function InformationTypesPage() { {types.isPending ? ( - ) : filtered.length === 0 ? ( + ) : rows.length === 0 ? ( } title={q ? "No information types match" : "No information types yet"}> {q ? "Try a different search." : "Define the first kind of document your integrations will carry."} ) : ( t.id} minWidth="min-w-220" onRowClick={(t) => navigate(`/information-types/${t.id}`)} + footer={ + setParam("offset", String(o), false)} + /> + } columns={[ { header: "Code", cell: (t) => }, { header: "Name", cell: (t) => {t.name} }, diff --git a/SW.Bitween.Web/ClientApp/src/pages/integrations/IntegrationPage.tsx b/SW.Bitween.Web/ClientApp/src/pages/integrations/IntegrationPage.tsx index 9292d884..7e841504 100644 --- a/SW.Bitween.Web/ClientApp/src/pages/integrations/IntegrationPage.tsx +++ b/SW.Bitween.Web/ClientApp/src/pages/integrations/IntegrationPage.tsx @@ -1,7 +1,7 @@ import { useEffect, useMemo, useState } from "react"; import { Link, useNavigate, useParams, useSearchParams } from "react-router"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; -import { ArrowLeft, DownloadCloud, Pause, Play, Trash2, X } from "lucide-react"; +import { DownloadCloud, Pause, Play, Trash2, X } from "lucide-react"; import { api } from "../../api"; import { Can, useSessionCan } from "../../auth/guards"; import { Badge, Button, EmptyState, FormError, LoadingBlock } from "../../components/ui/basics"; @@ -17,6 +17,7 @@ import { faceOf } from "./studio/faces"; import { EntryPointsTable, Overview } from "./studio/Overview"; import { ResponseFields } from "./studio/ResponseFields"; import { draftOf, entryPointsOf, stageDirty, type Draft } from "./studio/model"; +import { BackLink } from "../../components/ui/BackLink"; export function IntegrationPage() { const { id = "" } = useParams(); @@ -106,6 +107,7 @@ export function IntegrationPage() { const invalidate = () => { const detail = queryClient.invalidateQueries({ queryKey: ["integration", integrationId] }); void queryClient.invalidateQueries({ queryKey: ["integration-rows"] }); + void queryClient.invalidateQueries({ queryKey: ["integration-rows-search"] }); void queryClient.invalidateQueries({ queryKey: ["integrations"] }); return detail; }; @@ -311,12 +313,7 @@ export function IntegrationPage() { return (
- - Integrations - +
@@ -453,13 +450,15 @@ export function IntegrationPage() { body={ <> {s.name} and its configuration - will be gone for good. Integrations still wired into a gateway can't be deleted. + will be gone for good. One a gateway or another integration still points at can't be + deleted — it will say which. } confirmLabel="Delete integration" onConfirm={async () => { await api.deleteIntegration(integrationId); void queryClient.invalidateQueries({ queryKey: ["integration-rows"] }); + void queryClient.invalidateQueries({ queryKey: ["integration-rows-search"] }); void queryClient.invalidateQueries({ queryKey: ["integrations"] }); navigate("/subscriptions"); }} diff --git a/SW.Bitween.Web/ClientApp/src/pages/integrations/IntegrationsPage.tsx b/SW.Bitween.Web/ClientApp/src/pages/integrations/IntegrationsPage.tsx index fbb8f422..7a1e1f09 100644 --- a/SW.Bitween.Web/ClientApp/src/pages/integrations/IntegrationsPage.tsx +++ b/SW.Bitween.Web/ClientApp/src/pages/integrations/IntegrationsPage.tsx @@ -1,11 +1,13 @@ -import { useMemo } from "react"; import { Link, useNavigate, useSearchParams } from "react-router"; -import { useQuery } from "@tanstack/react-query"; +import { keepPreviousData, useQuery } from "@tanstack/react-query"; import { Search, Workflow } from "lucide-react"; import { api, type IntegrationRow, type IntegrationType } from "../../api"; import { useSessionCan } from "../../auth/guards"; import { PageHeader } from "../../components/layout/PageHeader"; import { EmptyState, LoadingBlock } from "../../components/ui/basics"; +import { Pagination } from "../../components/ui/Pagination"; +import { SearchSelect } from "../../components/ui/SearchSelect"; +import { Select } from "../../components/ui/forms"; import { Table } from "../../components/ui/Table"; import { HealthBadge, @@ -16,6 +18,12 @@ import { useGatewayPartners, } from "../../components/config/shared"; +const STATUS_OPTIONS = [ + { value: "", label: "Any status" }, + { value: "false", label: "Active" }, + { value: "true", label: "Disabled" }, +]; + /** Filter order: what you'll have most of first, legacy last. */ const TYPE_ORDER: IntegrationType[] = [ "Receiving", @@ -36,15 +44,39 @@ const TYPE_ORDER: IntegrationType[] = [ * Scheduled jobs, deliberately: here for the complete picture, there for the * schedule-specific columns. */ +const PAGE_SIZE = 25; + export function IntegrationsPage() { const [searchParams, setSearchParams] = useSearchParams(); const navigate = useNavigate(); const q = searchParams.get("q") ?? ""; const type = searchParams.get("type") as IntegrationType | null; + const informationTypeId = searchParams.get("informationTypeId") + ? Number(searchParams.get("informationTypeId")) + : null; + const partnerId = searchParams.get("partnerId") ? Number(searchParams.get("partnerId")) : null; + const inactiveParam = searchParams.get("inactive"); + const inactive = inactiveParam === "true" ? true : inactiveParam === "false" ? false : null; + const offset = searchParams.get("offset") ? Number(searchParams.get("offset")) : 0; const canSeeInfoTypes = useSessionCan("documents.view"); - const rows = useQuery({ queryKey: ["integration-rows"], queryFn: () => api.listIntegrationRows() }); + const rows = useQuery({ + queryKey: ["integration-rows-search", q, type, informationTypeId, partnerId, inactive, offset], + queryFn: () => + api.searchIntegrationRows({ + search: q, + type, + informationTypeId, + partnerId, + inactive, + offset, + limit: PAGE_SIZE, + }), + placeholderData: keepPreviousData, + }); const gatewayPartners = useGatewayPartners(); + const infoTypes = useQuery({ queryKey: ["information-types"], queryFn: () => api.listInformationTypes() }).data ?? []; + const partners = useQuery({ queryKey: ["partners"], queryFn: () => api.listPartners() }).data ?? []; /** Its own partner (legacy types) plus any reached through a gateway. */ const partnersFor = (r: IntegrationRow) => { @@ -53,40 +85,25 @@ export function IntegrationsPage() { return [...own, ...viaGateway]; }; - const setParam = (key: string, value: string | null) => + const setParam = (key: string, value: string | null, resetOffset = true) => setSearchParams( (prev) => { const next = new URLSearchParams(prev); if (value) next.set(key, value); else next.delete(key); + if (resetOffset) next.delete("offset"); return next; }, { replace: key === "q" }, ); - // Only offer a type you actually have — an empty filter teaches nothing. - const presentTypes = useMemo(() => { - const present = new Set((rows.data ?? []).map((r) => r.type)); - return TYPE_ORDER.filter((t) => present.has(t)); - }, [rows.data]); - - const filtered = useMemo(() => { - const needle = q.trim().toLowerCase(); - return (rows.data ?? []) - .filter((r) => !type || r.type === type) - .filter( - (r) => - !needle || - r.name.toLowerCase().includes(needle) || - r.informationTypeCode.toLowerCase().includes(needle), - ) - .sort((a, b) => a.name.localeCompare(b.name)); - }, [rows.data, type, q]); + const filtered = rows.data?.result ?? []; + const total = rows.data?.total ?? 0; return (
All - {presentTypes.map((t) => ( + {TYPE_ORDER.map((t) => (
+
+ setParam("informationTypeId", v || null)} + options={infoTypes.map((t) => ({ value: String(t.id), label: t.name, code: t.code }))} + /> + setParam("partnerId", v || null)} + options={partners.map((p) => ({ value: String(p.id), label: p.name }))} + /> + { + setOutcome((e.target.value || null) as ReceiveOutcome | null); + setOffset(0); + }} + options={OUTCOME_OPTIONS} + /> +
+
+ + {attempts.isPending ? ( + + ) : rows.length === 0 ? ( + + {outcome + ? "Try a different filter." + : "This integration hasn't checked for new data since this history started being kept."} + + ) : ( +
a.id} + minWidth="min-w-160" + footer={ + + } + columns={[ + { + header: "When", + cell: (a) => ( + + {timeAgo(a.startedOn)} + + ), + }, + { header: "Result", cell: (a) => }, + { header: "Exchange", cell: (a) => }, + ]} + /> + )} + + ); +} diff --git a/SW.Bitween.Web/ClientApp/src/pages/integrations/studio/ResponseFields.tsx b/SW.Bitween.Web/ClientApp/src/pages/integrations/studio/ResponseFields.tsx index 643f99f7..9538a9d4 100644 --- a/SW.Bitween.Web/ClientApp/src/pages/integrations/studio/ResponseFields.tsx +++ b/SW.Bitween.Web/ClientApp/src/pages/integrations/studio/ResponseFields.tsx @@ -1,11 +1,13 @@ import { useState } from "react"; +import { Link } from "react-router"; import { useQuery } from "@tanstack/react-query"; import { Plus } from "lucide-react"; -import { api } from "../../../api"; +import { api, type IntegrationType } from "../../../api"; import { useSessionCan } from "../../../auth/guards"; -import { Field, TextInput } from "../../../components/ui/forms"; +import { Field } from "../../../components/ui/forms"; import { SearchSelect } from "../../../components/ui/SearchSelect"; import { InformationTypeDialog } from "../../../components/config/InformationTypeDialog"; +import { busMessageNameProblem } from "../../../lib/busMessageName"; /** * The Response stage's body: what happens to whatever the delivery hands back. @@ -13,6 +15,9 @@ import { InformationTypeDialog } from "../../../components/config/InformationTyp * Shared by the studio pages and both create pages so the node means the same * thing wherever it appears — the create rails would otherwise be one node * shorter than the edit rail, which defeats reusing the pipeline at all. + * + * There is one way to pass a response on: publish it on the bus. Feeding it + * straight into a named integration is retired — see {@link FedIntoNotice}. */ export function ResponseFields({ handlerId, @@ -32,8 +37,8 @@ export function ResponseFields({ responseMessageTypeName?: string | null; }) => void; disabled: boolean; - /** Integrations the response can be fed into (excluding this one). */ - candidates: { id: number; name: string }[]; + /** Only used to name an already-saved target; nothing here can choose from them. */ + candidates: { id: number; name: string; type: IntegrationType }[]; idPrefix?: string; }) { if (handlerId === null) @@ -44,27 +49,74 @@ export function ResponseFields({ ); return ( -
- - + {responseIntegrationId !== null && ( + x.id === responseIntegrationId)?.name ?? null} + id={responseIntegrationId} disabled={disabled} - onChange={(v) => onChange({ responseIntegrationId: v === "" ? null : Number(v) })} - clearLabel="Nothing — responses are only recorded" - options={candidates.map((x) => ({ value: String(x.id), label: x.name }))} + onClear={() => onChange({ responseIntegrationId: null })} /> - - onChange({ responseMessageTypeName })} - /> + )} +
+ onChange({ responseMessageTypeName })} + /> +
+
+ ); +} + +/** + * An already-saved "feed the response into this integration", shown so it can be seen + * and undone — and offered nowhere else, because nothing new should acquire one. + * + * It hands the response to exactly one integration with the bus skipped: nothing is + * published, no filter is consulted, and nothing else bound to the same information + * type hears it. Publishing does all of that and is the reason the bus is here, so the + * field is kept only for configuration that already depends on it. Retired rather than + * dropped, because silently ignoring a saved value would change what a live integration + * does without anyone being told. + */ +function FedIntoNotice({ + name, + id, + disabled, + onClear, +}: { + name: string | null; + id: number; + disabled: boolean; + onClear: () => void; +}) { + return ( +
+
+

+ Feeds the response straight into{" "} + + {name ?? `integration ${id}`} + + . +

+ {!disabled && ( + + )} +
+

+ An old setting, kept so it can be cleared — it can't be set again. The bus is skipped, so + nothing is published and no other route bound to that information type hears it. Publish on + the bus below instead. +

); } @@ -78,9 +130,11 @@ export function ResponseFields({ * nobody at all, silently. Listing the real names makes the working answers the * easy ones, and offers to create the type when it doesn't exist yet. * - * Free text is still reachable, because publishing is not Bitween's to police: - * something outside the product may be the consumer. It is just no longer the - * default, and an unrecognised name says so out loud. + * A name of your own is still reachable, because publishing is not Bitween's to + * police: something outside the product may be the consumer. It is offered as the + * last row of the same dropdown rather than behind a separate mode — the mode used + * to be seeded from `unknown`, which is derived from a query that has not resolved + * on first render, so it latched on for every value including the valid ones. */ function BusMessageField({ value, @@ -103,37 +157,9 @@ function BusMessageField({ const known = (informationTypes.data ?? []).filter((t) => t.busEnabled && t.busMessageTypeName); const matched = known.find((t) => t.busMessageTypeName?.toLowerCase() === (value ?? "").toLowerCase()); - // A saved value nobody carries: kept as an option so opening this panel can - // never silently drop what is already configured. - const unknown = value !== null && value !== "" && !matched; - const [freeText, setFreeText] = useState(unknown); - - if (freeText) - return ( - - onChange(e.target.value || null)} - /> - {!disabled && ( - - )} - - ); + // A saved value nobody carries. Only meaningful once the types have actually arrived — + // while the query is pending `known` is empty, so every value looks unknown. + const unknown = !informationTypes.isPending && value !== null && value !== "" && !matched; return ( onChange(v === "" ? null : v)} clearLabel="Nothing — keep responses off the bus" - options={known.map((t) => ({ - value: t.busMessageTypeName!, - label: t.busMessageTypeName!, - code: t.code, - hint: t.name, - }))} + // Publishing is not Bitween's to police — the consumer may be another + // product entirely — so a name nobody carries is offered right here + // rather than dead-ending on "nothing matches". + freeText={(typed) => + busMessageNameProblem(typed) ?? + { value: typed, label: `Publish as “${typed}” — a name of your own` } + } + options={[ + ...known.map((t) => ({ + value: t.busMessageTypeName!, + label: t.busMessageTypeName!, + code: t.code, + hint: t.name, + })), + // A saved or just-accepted name no information type carries. Listed so the + // field can display it, and marked so it doesn't read as a working choice. + ...(unknown ? [{ value: value!, label: value!, hint: "not an information type" }] : []), + ]} /> {!disabled && (
@@ -165,13 +203,7 @@ function BusMessageField({ New information type )} - +
)} {creating && ( diff --git a/SW.Bitween.Web/ClientApp/src/pages/integrations/studio/RetryBudget.tsx b/SW.Bitween.Web/ClientApp/src/pages/integrations/studio/RetryBudget.tsx new file mode 100644 index 00000000..9fa90788 --- /dev/null +++ b/SW.Bitween.Web/ClientApp/src/pages/integrations/studio/RetryBudget.tsx @@ -0,0 +1,102 @@ +import { useState } from "react"; +import { Link } from "react-router"; +import { useQuery, useQueryClient } from "@tanstack/react-query"; +import { RotateCcw } from "lucide-react"; +import { api } from "../../../api"; +import { Button } from "../../../components/ui/basics"; +import { ConfirmDialog } from "../../../components/ui/overlays"; +import { timeAgo } from "../../../lib/dates"; + +/** + * This integration's own retry budgets, asked for from its side rather than its policy's. + * + * Two reasons it is not enough to read this on the retry policy page. An integration can carry + * rules inline instead of a shared policy — those have no policy id, so no policy page reaches + * their counters, and one could sit stopped for good with nothing on screen able to say why or + * hand it back. And even with a shared policy, "why has this stopped retrying?" gets asked + * here, where the integration is, not on a page listing every integration that shares its rules. + * + * Silent until it has something to report: an integration that has never failed does not need + * to be told it has spent none of its budget. + */ +export function RetryBudget({ integrationId, canEdit }: { integrationId: number; canEdit: boolean }) { + const queryClient = useQueryClient(); + const [resetting, setResetting] = useState(false); + + const usage = useQuery({ + queryKey: ["retry-usage", "integration", integrationId], + queryFn: () => api.getIntegrationRetryUsage(integrationId), + }); + + const rows = (usage.data ?? []).filter((r) => r.used > 0); + if (rows.length === 0) return null; + + const stopped = rows.filter((r) => r.exhausted); + const lastFailure = rows + .map((r) => r.lastAttemptOn) + .filter((d): d is string => d !== null) + .sort() + .at(-1); + + return ( +
0 ? "bg-danger-50 text-danger-800" : "bg-ink-50 text-ink-600" + }`} + > +
+

+ {stopped.length > 0 ? ( + <> + Retries have stopped.{" "} + {stopped.map((r) => r.groupName).join(", ")} used up{" "} + {stopped.length === 1 ? "its budget" : "their budgets"} — failures are no longer + retried automatically until the budget is reset, or this integration succeeds again. + + ) : ( + <> + Retry budget:{" "} + {rows.map((r) => `${r.groupName} ${r.used}/${r.total}`).join(", ")} + {lastFailure && ` · last failure ${timeAgo(lastFailure)}`} + + )} +

+ + {stopped.some((r) => r.resolvedHandlerId === null) && ( + Nobody was alerted. + )} + + See failures + + {canEdit && stopped.length > 0 && ( + + )} + +
+ + {resetting && ( + + Retries resume immediately for {stopped.map((r) => r.groupName).join(", ")}. If + whatever they were failing against is still down, the budget will be spent again. + + } + confirmLabel="Reset budgets" + onConfirm={async () => { + // No group id: every group of this integration, which is what the banner reports on. + await api.resetIntegrationRetryUsage(integrationId); + await queryClient.invalidateQueries({ queryKey: ["retry-usage"] }); + }} + onClose={() => setResetting(false)} + /> + )} +
+ ); +} diff --git a/SW.Bitween.Web/ClientApp/src/pages/integrations/studio/faces.ts b/SW.Bitween.Web/ClientApp/src/pages/integrations/studio/faces.ts index 89e53ffe..39168de8 100644 --- a/SW.Bitween.Web/ClientApp/src/pages/integrations/studio/faces.ts +++ b/SW.Bitween.Web/ClientApp/src/pages/integrations/studio/faces.ts @@ -45,6 +45,12 @@ export interface FaceInput { fault?: StageFace["fault"]; /** Names for the "feed the response into" target. */ integrationNames?: { id: number; name: string }[]; + /** + * Set while the integration is being defined and nothing is saved yet. A delivery-less + * integration that already exists is a legal thing that records and stops; one being + * created here cannot be saved without a delivery, so the node has to say so. + */ + unsaved?: boolean; } /** @@ -55,7 +61,7 @@ export interface FaceInput { * while editing is how the two drift apart. */ export function faceOf(stageId: StageId, input: FaceInput): StageFace { - const { type, draft: d, catalogs, saved, entryPoints = [], nextRunOn, fault, integrationNames } = input; + const { type, draft: d, catalogs, saved, entryPoints = [], nextRunOn, fault, integrationNames, unsaved } = input; const dirty = saved ? stageDirty(stageId, d, saved) : false; switch (stageId) { @@ -113,16 +119,16 @@ export function faceOf(stageId: StageId, input: FaceInput): StageFace { state: d.mapperId ? "set" : "none", }; case "delivery": - // "none", not "missing": an integration that records the document and - // stops is a legal configuration. The create page requires a handler - // anyway, but it says so at the Create button rather than by calling a - // saved integration broken. + // "none", not "missing", once it exists: an integration that records the document + // and stops is a legal configuration, and calling a saved one broken would be + // wrong. While it is still being defined the save is blocked on this, so it is + // genuinely missing and the node says which node to go to. return { id: stageId, dirty, - title: labelOf(catalogs.handlers, d.handlerId) ?? "Stops here", + title: labelOf(catalogs.handlers, d.handlerId) ?? (unsaved ? "Needed" : "Stops here"), detail: locationHint(d.handlerProperties), - state: d.handlerId ? "set" : "none", + state: d.handlerId ? "set" : unsaved ? "missing" : "none", }; case "response": if (!d.handlerId) return { id: stageId, dirty, title: "Nothing delivered", state: "none" }; diff --git a/SW.Bitween.Web/ClientApp/src/pages/integrations/studio/model.ts b/SW.Bitween.Web/ClientApp/src/pages/integrations/studio/model.ts index a680cda8..38799b28 100644 --- a/SW.Bitween.Web/ClientApp/src/pages/integrations/studio/model.ts +++ b/SW.Bitween.Web/ClientApp/src/pages/integrations/studio/model.ts @@ -41,6 +41,40 @@ export const draftOf = (d: IntegrationDetail): Draft => ({ responseMessageTypeName: d.responseMessageTypeName, }); +/** + * An integration being defined on a gateway's canvas, before it exists. + * + * The route already worked this way — see `NEW_ROUTE` — and an integration is the + * same problem one level down: asking for it in a modal hides the diagram the + * answers are about. It lives in the studio's state until one save writes it and + * the thing pointing at it together. + */ +export const EMPTY_INTEGRATION: Draft = { + name: "", + enabled: true, + workGroupId: null, + retryPolicyId: null, + receiverId: null, + receiverProperties: {}, + validatorId: null, + validatorProperties: {}, + mapperId: null, + mapperProperties: {}, + handlerId: null, + handlerProperties: {}, + matchExpression: null, + schedules: [], + responseIntegrationId: null, + responseMessageTypeName: null, +}; + +/** + * Stands in for "the integration being defined right here" wherever an id is + * expected. Negative so it can never collide with a real one, and never sent to + * the server: the save swaps it for the inline payload the gateway endpoints take. + */ +export const NEW_INTEGRATION_ID = -1; + /** * Which draft fields each stage owns — only so a card can carry an unsaved dot. * Name, enabled, work group and retry policy belong to no stage; they live on diff --git a/SW.Bitween.Web/ClientApp/src/pages/notifiers/NotifierPage.tsx b/SW.Bitween.Web/ClientApp/src/pages/notifiers/NotifierPage.tsx index 3986fc35..3c0f4f0b 100644 --- a/SW.Bitween.Web/ClientApp/src/pages/notifiers/NotifierPage.tsx +++ b/SW.Bitween.Web/ClientApp/src/pages/notifiers/NotifierPage.tsx @@ -1,17 +1,19 @@ import { useEffect, useMemo, useState } from "react"; -import { Link, useParams } from "react-router"; +import { Link, useNavigate, useParams } from "react-router"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; -import { ArrowLeft, ArrowUpRight, Search } from "lucide-react"; +import { ArrowUpRight, Search } from "lucide-react"; import { api, type NotificationEntry, type Notifier } from "../../api"; -import { useSessionCan } from "../../auth/guards"; -import { Badge, EmptyState, LoadingBlock } from "../../components/ui/basics"; +import { Can, useSessionCan } from "../../auth/guards"; +import { Badge, Button, EmptyState, LoadingBlock } from "../../components/ui/basics"; import { Checkbox, Field, TextInput } from "../../components/ui/forms"; import { EditableTitle, Panel, UnsavedBar } from "../../components/ui/Panel"; +import { ConfirmDialog } from "../../components/ui/overlays"; import { MiniTable } from "../../components/ui/Table"; import { SearchSelect } from "../../components/ui/SearchSelect"; import { useAdapterCatalog } from "../../components/config/AdapterConfig"; import { useIntegrationsCache } from "../../components/config/shared"; import { timeAgo } from "../../lib/dates"; +import { BackLink } from "../../components/ui/BackLink"; type Draft = Omit; @@ -66,6 +68,7 @@ export function NotifierPage() { const { id = "" } = useParams(); const notifierId = Number(id); const queryClient = useQueryClient(); + const navigate = useNavigate(); const canEdit = useSessionCan("notifiers.edit"); const notifier = useQuery({ @@ -79,6 +82,7 @@ export function NotifierPage() { const [draft, setDraft] = useState(null); const [loaded, setLoaded] = useState(false); const [watchSearch, setWatchSearch] = useState(""); + const [deleting, setDeleting] = useState(false); useEffect(() => { if (!loaded && notifier.data) { @@ -100,6 +104,7 @@ export function NotifierPage() { // Await the detail refetch before re-syncing the draft (avoids stale-data race). await queryClient.invalidateQueries({ queryKey: ["notifier", notifierId] }); void queryClient.invalidateQueries({ queryKey: ["notifiers"] }); + void queryClient.invalidateQueries({ queryKey: ["notifiers-search"] }); setLoaded(false); }, }); @@ -140,12 +145,7 @@ export function NotifierPage() { return (
- - Notifiers - +
@@ -171,6 +171,12 @@ export function NotifierPage() { )}
+ + + +
@@ -334,6 +340,27 @@ export function NotifierPage() { onDiscard={() => setLoaded(false)} /> )} + + {deleting && ( + + {n.name} will be gone for good, + along with the list of integrations it watches. The notifications it already sent are + kept. + + } + confirmLabel="Delete notifier" + onConfirm={async () => { + await api.deleteNotifier(notifierId); + void queryClient.invalidateQueries({ queryKey: ["notifiers"] }); + void queryClient.invalidateQueries({ queryKey: ["notifiers-search"] }); + navigate("/notifiers"); + }} + onClose={() => setDeleting(false)} + /> + )}
); } diff --git a/SW.Bitween.Web/ClientApp/src/pages/notifiers/NotifiersPage.tsx b/SW.Bitween.Web/ClientApp/src/pages/notifiers/NotifiersPage.tsx index 35f05835..e7f66c27 100644 --- a/SW.Bitween.Web/ClientApp/src/pages/notifiers/NotifiersPage.tsx +++ b/SW.Bitween.Web/ClientApp/src/pages/notifiers/NotifiersPage.tsx @@ -1,6 +1,6 @@ -import { useMemo, useState, type FormEvent } from "react"; +import { useState, type FormEvent } from "react"; import { useNavigate, useSearchParams } from "react-router"; -import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { keepPreviousData, useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { BellRing, Plus, Search } from "lucide-react"; import { api } from "../../api"; import { Can } from "../../auth/guards"; @@ -9,6 +9,7 @@ import { PageHeader } from "../../components/layout/PageHeader"; import { Badge, Button, EmptyState, FormError, LoadingBlock } from "../../components/ui/basics"; import { Field, TextInput } from "../../components/ui/forms"; import { Dialog } from "../../components/ui/overlays"; +import { Pagination } from "../../components/ui/Pagination"; import { Table } from "../../components/ui/Table"; function CreateNotifierDialog({ onClose }: { onClose: () => void }) { @@ -20,6 +21,7 @@ function CreateNotifierDialog({ onClose }: { onClose: () => void }) { mutationFn: () => api.createNotifier({ name }), onSuccess: (notifier) => { void queryClient.invalidateQueries({ queryKey: ["notifiers"] }); + void queryClient.invalidateQueries({ queryKey: ["notifiers-search"] }); navigate(`/notifiers/${notifier.id}`); }, }); @@ -58,30 +60,36 @@ function CreateNotifierDialog({ onClose }: { onClose: () => void }) { ); } +const PAGE_SIZE = 25; + export function NotifiersPage() { const [searchParams, setSearchParams] = useSearchParams(); const navigate = useNavigate(); const q = searchParams.get("q") ?? ""; const creating = searchParams.get("new") === "1"; + const offset = searchParams.get("offset") ? Number(searchParams.get("offset")) : 0; - const notifiers = useQuery({ queryKey: ["notifiers"], queryFn: () => api.listNotifiers() }); + const notifiers = useQuery({ + queryKey: ["notifiers-search", q, offset], + queryFn: () => api.searchNotifiers({ search: q, offset, limit: PAGE_SIZE }), + placeholderData: keepPreviousData, + }); const channels = useAdapterCatalog("handler"); - const setParam = (key: string, value: string | null) => + const setParam = (key: string, value: string | null, resetOffset = key === "q") => setSearchParams( (prev) => { const next = new URLSearchParams(prev); if (value) next.set(key, value); else next.delete(key); + if (resetOffset) next.delete("offset"); return next; }, { replace: key === "q" }, ); - const filtered = useMemo(() => { - const needle = q.trim().toLowerCase(); - return (notifiers.data ?? []).filter((n) => !needle || n.name.toLowerCase().includes(needle)); - }, [notifiers.data, q]); + const filtered = notifiers.data?.result ?? []; + const total = notifiers.data?.total ?? 0; const channelLabel = (id: string) => channels.data?.find((c) => c.id === id)?.label ?? id; @@ -138,12 +146,23 @@ export function NotifiersPage() { rows={filtered} rowKey={(n) => n.id} onRowClick={(n) => navigate(`/notifiers/${n.id}`)} + footer={ + setParam("offset", String(o), false)} + /> + } columns={[ { header: "Notifier", cell: (n) => {n.name} }, { header: "Sends when", cell: (n) => ( - + {n.onFailed && Failed} {n.onBadResult && Bad result} {n.onSuccess && Success} diff --git a/SW.Bitween.Web/ClientApp/src/pages/partners/PartnerPage.tsx b/SW.Bitween.Web/ClientApp/src/pages/partners/PartnerPage.tsx index 618aec8c..df5b0bc8 100644 --- a/SW.Bitween.Web/ClientApp/src/pages/partners/PartnerPage.tsx +++ b/SW.Bitween.Web/ClientApp/src/pages/partners/PartnerPage.tsx @@ -1,7 +1,7 @@ import { useEffect, useMemo, useState } from "react"; import { Link, useNavigate, useParams } from "react-router"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; -import { ArrowLeft, Trash2 } from "lucide-react"; +import { Trash2 } from "lucide-react"; import { api } from "../../api"; import { Can, useSessionCan } from "../../auth/guards"; import { Badge, Button, EmptyState, LoadingBlock } from "../../components/ui/basics"; @@ -9,6 +9,7 @@ import { ConfirmDialog } from "../../components/ui/overlays"; import { EditableTitle, Panel, UnsavedBar } from "../../components/ui/Panel"; import { MiniTable } from "../../components/ui/Table"; import { ExchangesList, SetupList, usePartnerIntegrations } from "../../components/config/shared"; +import { BackLink } from "../../components/ui/BackLink"; import { PartnerFields, partnerChanges, @@ -93,12 +94,7 @@ export function PartnerPage() { return (
- - Partners - +
diff --git a/SW.Bitween.Web/ClientApp/src/pages/partners/PartnersPage.tsx b/SW.Bitween.Web/ClientApp/src/pages/partners/PartnersPage.tsx index 5ea1d5d5..b99edff1 100644 --- a/SW.Bitween.Web/ClientApp/src/pages/partners/PartnersPage.tsx +++ b/SW.Bitween.Web/ClientApp/src/pages/partners/PartnersPage.tsx @@ -1,39 +1,46 @@ -import { useMemo, useState } from "react"; +import { useState } from "react"; import { useNavigate, useSearchParams } from "react-router"; -import { useQuery } from "@tanstack/react-query"; +import { keepPreviousData, useQuery } from "@tanstack/react-query"; import { Handshake, Plus, Search } from "lucide-react"; import { api } from "../../api"; import { Can } from "../../auth/guards"; import { PartnerDialog } from "../../components/config/PartnerDialog"; import { PageHeader } from "../../components/layout/PageHeader"; import { Badge, Button, EmptyState, LoadingBlock } from "../../components/ui/basics"; +import { Pagination } from "../../components/ui/Pagination"; import { Table } from "../../components/ui/Table"; import { UsedByCell, usePartnerIntegrations } from "../../components/config/shared"; +const PAGE_SIZE = 25; + export function PartnersPage() { const [searchParams, setSearchParams] = useSearchParams(); const navigate = useNavigate(); const [creating, setCreating] = useState(false); const q = searchParams.get("q") ?? ""; + const offset = searchParams.get("offset") ? Number(searchParams.get("offset")) : 0; - const partners = useQuery({ queryKey: ["partners"], queryFn: () => api.listPartners() }); + const partners = useQuery({ + queryKey: ["partners-search", q, offset], + queryFn: () => api.searchPartners({ search: q, offset, limit: PAGE_SIZE }), + placeholderData: keepPreviousData, + }); const partnerIntegrations = usePartnerIntegrations(); - const setParam = (key: string, value: string | null) => + const setParam = (key: string, value: string | null, resetOffset = true) => setSearchParams( (prev) => { const next = new URLSearchParams(prev); if (value) next.set(key, value); else next.delete(key); + if (resetOffset) next.delete("offset"); return next; }, { replace: key === "q" }, ); - const filtered = useMemo(() => { - const needle = q.trim().toLowerCase(); - return (partners.data ?? []).filter((p) => !needle || p.name.toLowerCase().includes(needle)); - }, [partners.data, q]); + const rows = partners.data?.result ?? []; + const total = partners.data?.total ?? 0; return (
@@ -63,23 +70,35 @@ export function PartnersPage() { {partners.isPending ? ( - ) : filtered.length === 0 ? ( + ) : rows.length === 0 ? ( } title={q ? "No partners match" : "No partners yet"}> {q ? "Try a different search." : "Create the first partner you exchange data with."} ) : (
p.id} minWidth="min-w-200" onRowClick={(p) => navigate(`/partners/${p.id}`)} + footer={ + setParam("offset", String(o), false)} + /> + } columns={[ { header: "Partner", cell: (p) => ( {p.name} - {p.isSystem && Built-in} + {p.isSystem && ( + + Built-in + + )} ), }, diff --git a/SW.Bitween.Web/ClientApp/src/pages/queue-health/QueueHealthPage.tsx b/SW.Bitween.Web/ClientApp/src/pages/queue-health/QueueHealthPage.tsx index 32447c91..2d5deb15 100644 --- a/SW.Bitween.Web/ClientApp/src/pages/queue-health/QueueHealthPage.tsx +++ b/SW.Bitween.Web/ClientApp/src/pages/queue-health/QueueHealthPage.tsx @@ -5,6 +5,7 @@ import { AlertTriangle, OctagonAlert } from "lucide-react"; import { api, type ConsumerHealth, type QueueLane, type QueueSeverity } from "../../api"; import { PageHeader } from "../../components/layout/PageHeader"; import { Badge, EmptyState, LoadingBlock } from "../../components/ui/basics"; +import { queueHealthTitle } from "../../components/config/shared"; import { Panel } from "../../components/ui/Panel"; import { timeAgo } from "../../lib/dates"; @@ -67,9 +68,9 @@ const linkFor = (c: ConsumerHealth): string | null => { }; function HealthBadge({ health }: { health: QueueSeverity }) { - if (health === "critical") return Critical; - if (health === "warning") return Warning; - return Healthy; + if (health === "critical") return Critical; + if (health === "warning") return Warning; + return Healthy; } function StatTile({ label, value, sub }: { label: string; value: ReactNode; sub?: ReactNode }) { @@ -254,7 +255,14 @@ export function QueueHealthPage() { diff --git a/SW.Bitween.Web/ClientApp/src/pages/retry-policies/AlertRouting.tsx b/SW.Bitween.Web/ClientApp/src/pages/retry-policies/AlertRouting.tsx new file mode 100644 index 00000000..c8fc296c --- /dev/null +++ b/SW.Bitween.Web/ClientApp/src/pages/retry-policies/AlertRouting.tsx @@ -0,0 +1,90 @@ +import type { ReactNode } from "react"; +import type { RetryAlertConfig, RetryAlertMode } from "../../api"; +import { AdapterConfig } from "../../components/config/AdapterConfig"; + +/** + * Where a "retry budget ran out" alert goes, as set at one level of the hierarchy. + * + * Three levels decide between them — the policy, a group, and one integration-and-group pair — + * resolved most specific first. A level that sends **replaces** the level above rather than + * merging into it, so whichever one wins has to carry the handler *and* every property it needs. + * That is why each level offers the whole adapter form and not just a handler name: a handler + * copied down without its settings saves an alert that only fails at send time, hours later, + * with nobody watching. + */ + +const MODES: { value: RetryAlertMode; label: string; hint: string }[] = [ + { value: "Inherit", label: "Inherit", hint: "Use whatever the level above sends." }, + { value: "Send", label: "Send here", hint: "Send through this level's own handler instead." }, + { value: "Silent", label: "Silent", hint: "Send nothing, even if a level above would." }, +]; + +export function AlertRouting({ + value, + onChange, + inherited, + disabled = false, +}: { + value: RetryAlertConfig; + onChange: (next: RetryAlertConfig) => void; + /** + * What Inherit resolves to right now, spelled out — "Inherit" alone tells you the rule but + * not the outcome, and the outcome is the thing being decided. + */ + inherited: ReactNode; + disabled?: boolean; +}) { + const setMode = (mode: RetryAlertMode) => { + // Leaving Send keeps the handler in state but stops sending it, so flipping to Silent to + // hush an alert overnight and back again doesn't cost you the configuration. + if (mode === "Send") return onChange({ ...value, alertMode: "Send" }); + onChange({ ...value, alertMode: mode }); + }; + + return ( +
+
+ {MODES.map((m) => { + const active = value.alertMode === m.value; + return ( + + ); + })} +
+ +

+ {value.alertMode === "Inherit" ? inherited : MODES.find((m) => m.value === value.alertMode)!.hint} +

+ + {value.alertMode === "Send" && ( +
+ + onChange({ ...value, alertHandlerId, alertHandlerProperties }) + } + /> +
+ )} +
+ ); +} diff --git a/SW.Bitween.Web/ClientApp/src/pages/retry-policies/GroupDialog.tsx b/SW.Bitween.Web/ClientApp/src/pages/retry-policies/GroupDialog.tsx index b574f602..c4a14c57 100644 --- a/SW.Bitween.Web/ClientApp/src/pages/retry-policies/GroupDialog.tsx +++ b/SW.Bitween.Web/ClientApp/src/pages/retry-policies/GroupDialog.tsx @@ -1,9 +1,10 @@ import { useState, type FormEvent } from "react"; import { Plus, Trash2 } from "lucide-react"; -import type { RetryDelay, RetryGroup, RetryMatcher, RetryResultType } from "../../api"; +import type { RetryAlertConfig, RetryDelay, RetryGroup, RetryMatcher, RetryResultType } from "../../api"; import { Button, FormError } from "../../components/ui/basics"; import { Checkbox, Field, Select, TextInput } from "../../components/ui/forms"; import { Dialog } from "../../components/ui/overlays"; +import { AlertRouting } from "./AlertRouting"; const DEFAULT_MATCHER: RetryMatcher = { type: "contains", value: "", caseSensitive: false }; @@ -153,10 +154,13 @@ export function GroupDialog({ initial, onSubmit, onClose, + policyAlertHandlerId, }: { initial?: RetryGroup; onSubmit: (group: RetryGroup) => void; onClose: () => void; + /** The policy default this group inherits when it doesn't route its own alert. */ + policyAlertHandlerId: string | null; }) { const [name, setName] = useState(initial?.name ?? ""); const [priority, setPriority] = useState(initial?.priority ?? 10); @@ -168,6 +172,11 @@ export function GroupDialog({ initial?.budget ?? { maxAttemptsPerError: 3, maxAttemptsTotal: 10, delay: defaultDelayFor("exponential") }, ); const [notes, setNotes] = useState(initial?.notes ?? ""); + const [alert, setAlert] = useState({ + alertMode: initial?.alertMode ?? "Inherit", + alertHandlerId: initial?.alertHandlerId ?? null, + alertHandlerProperties: initial?.alertHandlerProperties ?? {}, + }); const [error, setError] = useState(""); const toggleAppliesTo = (t: RetryResultType) => @@ -186,6 +195,14 @@ export function GroupDialog({ e.preventDefault(); if (!name.trim()) return setError("Give the group a name."); if (appliesTo.length === 0) return setError("Pick at least one failure kind it applies to."); + // The server refuses a group with no conditions, so catching it here saves a round trip + // that would otherwise fail only once the whole page was saved. Groups stored without any + // — from before the rule existed — still match every failure, which is why the table can + // describe one that way while this refuses to make another. + if (matchers.length === 0) + return setError("Add at least one condition — a group with none is rejected when you save."); + if (alert.alertMode === "Send" && !alert.alertHandlerId) + return setError("Pick how the budget-exhausted alert is delivered, or choose Inherit."); onSubmit({ id: initial?.id ?? crypto.randomUUID(), name: name.trim(), @@ -196,6 +213,11 @@ export function GroupDialog({ action, budget: action === "Allow" ? budget : undefined, notes: notes.trim() || undefined, + // A group that blocks can never spend a budget, so it can never exhaust one and never + // alert. Saving routing for it would leave a setting on screen that cannot ever fire. + ...(action === "Allow" + ? alert + : { alertMode: "Inherit" as const, alertHandlerId: null, alertHandlerProperties: {} }), }); onClose(); }; @@ -236,7 +258,7 @@ export function GroupDialog({
- Conditions — any may match; none means every failure + Conditions — any one may match; at least one is needed
{matchers.map((m, i) => ( @@ -268,7 +290,11 @@ export function GroupDialog({ {action === "Allow" && (
- + setBudget({ ...budget, maxAttemptsPerError: Number(e.target.value) })} /> - + )} + {action === "Allow" && ( +
+ + When the budget runs out + + + Sends through the policy default, {policyAlertHandlerId}. + + ) : ( + "The policy sends no alert, so nothing is sent — unless a single integration overrides it." + ) + } + /> +
+ )} + setNotes(e.target.value)} /> diff --git a/SW.Bitween.Web/ClientApp/src/pages/retry-policies/RetryPoliciesPage.tsx b/SW.Bitween.Web/ClientApp/src/pages/retry-policies/RetryPoliciesPage.tsx index c7e897df..95fa788b 100644 --- a/SW.Bitween.Web/ClientApp/src/pages/retry-policies/RetryPoliciesPage.tsx +++ b/SW.Bitween.Web/ClientApp/src/pages/retry-policies/RetryPoliciesPage.tsx @@ -1,6 +1,6 @@ -import { useMemo, useState, type FormEvent } from "react"; +import { useState, type FormEvent } from "react"; import { useNavigate, useSearchParams } from "react-router"; -import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { keepPreviousData, useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { Plus, RotateCcw, Search } from "lucide-react"; import { api } from "../../api"; import { Can } from "../../auth/guards"; @@ -8,6 +8,7 @@ import { PageHeader } from "../../components/layout/PageHeader"; import { Button, EmptyState, FormError, LoadingBlock } from "../../components/ui/basics"; import { Field, TextInput } from "../../components/ui/forms"; import { Dialog } from "../../components/ui/overlays"; +import { Pagination } from "../../components/ui/Pagination"; import { Table } from "../../components/ui/Table"; import { UsedByCell, useIntegrationsCache } from "../../components/config/shared"; @@ -54,30 +55,36 @@ function CreateRetryPolicyDialog({ onClose }: { onClose: () => void }) { ); } +const PAGE_SIZE = 25; + export function RetryPoliciesPage() { const [searchParams, setSearchParams] = useSearchParams(); const navigate = useNavigate(); const q = searchParams.get("q") ?? ""; const creating = searchParams.get("new") === "1"; + const offset = searchParams.get("offset") ? Number(searchParams.get("offset")) : 0; - const policies = useQuery({ queryKey: ["retry-policies"], queryFn: () => api.listRetryPolicies() }); + const policies = useQuery({ + queryKey: ["retry-policies-search", q, offset], + queryFn: () => api.searchRetryPolicies({ search: q, offset, limit: PAGE_SIZE }), + placeholderData: keepPreviousData, + }); const integrations = useIntegrationsCache().data ?? []; - const setParam = (key: string, value: string | null) => + const setParam = (key: string, value: string | null, resetOffset = key === "q") => setSearchParams( (prev) => { const next = new URLSearchParams(prev); if (value) next.set(key, value); else next.delete(key); + if (resetOffset) next.delete("offset"); return next; }, { replace: key === "q" }, ); - const filtered = useMemo(() => { - const needle = q.trim().toLowerCase(); - return (policies.data ?? []).filter((p) => !needle || p.name.toLowerCase().includes(needle)); - }, [policies.data, q]); + const rows = policies.data?.result ?? []; + const total = policies.data?.total ?? 0; return (
@@ -123,16 +130,24 @@ export function RetryPoliciesPage() { {policies.isPending ? ( - ) : filtered.length === 0 ? ( + ) : rows.length === 0 ? ( } title={q ? "No policies match" : "No retry policies yet"}> {q ? "Try a different search." : "Create a policy to control what happens after failures."} ) : (
- {c.isBackpressured && Backpressure} + {c.isBackpressured && ( + + Backpressure + + )}
p.id} minWidth="min-w-130" onRowClick={(p) => navigate(`/retry-policies/${p.id}`)} + footer={ + setParam("offset", String(o), false)} + /> + } columns={[ { header: "Policy", cell: (p) => {p.name} }, { diff --git a/SW.Bitween.Web/ClientApp/src/pages/retry-policies/RetryPolicyPage.tsx b/SW.Bitween.Web/ClientApp/src/pages/retry-policies/RetryPolicyPage.tsx index 1b0d8dd2..52f95d79 100644 --- a/SW.Bitween.Web/ClientApp/src/pages/retry-policies/RetryPolicyPage.tsx +++ b/SW.Bitween.Web/ClientApp/src/pages/retry-policies/RetryPolicyPage.tsx @@ -1,16 +1,18 @@ import { useEffect, useMemo, useState, type FormEvent } from "react"; import { Link, useNavigate, useParams } from "react-router"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; -import { ArrowLeft, FlaskConical, Pencil, Plus, Trash2 } from "lucide-react"; +import { FlaskConical, Pencil, Plus, Trash2 } from "lucide-react"; import { api, type RetryGroup, type RetryMatcher, type RetryResultType } from "../../api"; import { Can, useSessionCan } from "../../auth/guards"; import { Badge, Button, EmptyState, FormError, LoadingBlock } from "../../components/ui/basics"; import { Field, Select, TextInput } from "../../components/ui/forms"; -import { ConfirmDialog } from "../../components/ui/overlays"; +import { ConfirmDialog, Dialog } from "../../components/ui/overlays"; import { EditableTitle, Panel, UnsavedBar } from "../../components/ui/Panel"; import { MiniTable } from "../../components/ui/Table"; -import { SetupList } from "../../components/config/shared"; +import { AdapterConfig } from "../../components/config/AdapterConfig"; import { GroupDialog } from "./GroupDialog"; +import { UsagePanel } from "./UsagePanel"; +import { BackLink } from "../../components/ui/BackLink"; const matcherSummary = (m: RetryMatcher): string => { switch (m.type) { @@ -111,6 +113,111 @@ function TestPanel({ groups }: { groups: RetryGroup[] }) { ); } +/** + * The policy-wide alert, summarised — with its adapter form behind a dialog. + * + * Left open, a mail handler's thirteen fields filled the whole column and left the groups + * table sitting beside a void; two columns of them inside a 360px rail wrapped every address + * onto three lines. It is also set once and rarely revisited, where everything around it is + * read on every visit, so it had the run of the page on the strength of being the longest + * form rather than the most useful one. + * + * A dialog makes the three levels consistent too: a group routes its own alert in the group + * dialog, one integration-and-group pair in the override dialog, and the policy default here. + * All three stage into the same save bar. + */ +function PolicyAlertCard({ + handlerId, + properties, + groups, + canEdit, + onChange, +}: { + handlerId: string | null; + properties: Record; + groups: RetryGroup[]; + canEdit: boolean; + onChange: (handlerId: string | null, properties: Record) => void; +}) { + const [editing, setEditing] = useState(false); + const [draftId, setDraftId] = useState(handlerId); + const [draftProps, setDraftProps] = useState(properties); + + // Only a group that retries can exhaust a budget, so only those can inherit an alert. + const canAlert = groups.filter((g) => g.action === "Allow"); + const inheriting = canAlert.filter((g) => g.alertMode === "Inherit"); + + const open = () => { + setDraftId(handlerId); + setDraftProps(properties); + setEditing(true); + }; + + return ( + + {handlerId ? "Change" : "Set up"} + + ) : undefined + } + > + {handlerId ? ( + <> +

{handlerId}

+

+ {inheriting.length === 0 + ? "No group inherits it — each one routes its own alert, or is silent." + : `${inheriting.length} of ${canAlert.length} ${canAlert.length === 1 ? "group sends" : "groups send"} here.`} +

+ + ) : ( +

+ No alert. Nothing is sent when a budget runs out, unless a group or a single + integration routes one itself. +

+ )} + + {editing && ( + setEditing(false)} wide> +
+

+ Where this policy sends an alert when any of its groups stops retrying. Saved with + the rest of the page. +

+ { + setDraftId(id); + setDraftProps(props); + }} + /> +
+ + +
+
+
+ )} +
+ ); +} + export function RetryPolicyPage() { const { id = "" } = useParams(); const policyId = Number(id); @@ -126,6 +233,8 @@ export function RetryPolicyPage() { const [name, setName] = useState(""); const [groups, setGroups] = useState(null); + const [alertHandlerId, setAlertHandlerId] = useState(null); + const [alertProps, setAlertProps] = useState>({}); const [editingGroup, setEditingGroup] = useState(null); const [deleting, setDeleting] = useState(false); const [loaded, setLoaded] = useState(false); @@ -134,21 +243,36 @@ export function RetryPolicyPage() { if (!loaded && policy.data) { setName(policy.data.name); setGroups(structuredClone(policy.data.groups)); + setAlertHandlerId(policy.data.alertHandlerId); + setAlertProps(structuredClone(policy.data.alertHandlerProperties)); setLoaded(true); } }, [policy.data, loaded]); const dirty = useMemo(() => { if (!policy.data || groups === null) return false; - return name !== policy.data.name || JSON.stringify(groups) !== JSON.stringify(policy.data.groups); - }, [policy.data, name, groups]); + return ( + name !== policy.data.name || + JSON.stringify(groups) !== JSON.stringify(policy.data.groups) || + alertHandlerId !== policy.data.alertHandlerId || + JSON.stringify(alertProps) !== JSON.stringify(policy.data.alertHandlerProperties) + ); + }, [policy.data, name, groups, alertHandlerId, alertProps]); const save = useMutation({ - mutationFn: () => api.updateRetryPolicy(policyId, { name, groups: groups ?? [] }), + mutationFn: () => + api.updateRetryPolicy(policyId, { + name, + groups: groups ?? [], + alertHandlerId, + alertHandlerProperties: alertProps, + }), onSuccess: async () => { // Await the detail refetch before re-syncing the draft (avoids stale-data race). await queryClient.invalidateQueries({ queryKey: ["retry-policy", policyId] }); void queryClient.invalidateQueries({ queryKey: ["retry-policies"] }); + // Editing a group can change which budgets exist, so the usage report is stale too. + void queryClient.invalidateQueries({ queryKey: ["retry-usage"] }); setLoaded(false); }, }); @@ -176,12 +300,7 @@ export function RetryPolicyPage() { return (
- - Retry policies - +
@@ -214,6 +333,8 @@ export function RetryPolicyPage() { g.id} + fitWidth + onRowClick={canEdit ? (g) => setEditingGroup(g) : undefined} empty="No groups yet — failures under this policy are never retried." columns={[ { @@ -225,7 +346,10 @@ export function RetryPolicyPage() { header: "Group", truncate: true, cell: (g) => ( - + {g.name} ), @@ -242,38 +366,57 @@ export function RetryPolicyPage() { { header: "Applies to", truncate: true, - cell: (g) => ( - - {g.appliesTo.map((t) => (t === "Error" ? "errors" : "bad results")).join(" and ")} - {g.matchers.length === 0 - ? " — any failure" - : ` matching ${g.matchers.map((m) => matcherSummary(m)).join(" or ")}`} - - ), + cell: (g) => { + // Scope first and short, conditions second: every row in a policy tends to + // share the scope, so leading with "errors matching " spent the column's + // width on the one part that never tells them apart. + const scope = + g.appliesTo.length === 1 && g.appliesTo[0] === "Error" + ? null + : g.appliesTo.map((t) => (t === "Error" ? "Errors" : "Bad results")).join(" + "); + const conditions = + g.matchers.length === 0 + ? "any failure" + : g.matchers.map((m) => matcherSummary(m)).join(" or "); + return ( + + {scope && {scope} · } + {conditions} + + ); + }, }, { + // Bounded text — two numbers and one of three delay names — so it shrinks to + // fit instead of truncating, leaving the slack to the columns that need it. header: "Budget", - truncate: true, cell: (g) => g.action === "Allow" && g.budget ? ( - - {g.budget.maxAttemptsPerError} tries ({g.budget.maxAttemptsTotal} total),{" "} - {g.budget.delay.type} delay + + {g.budget.maxAttemptsPerError} tries ({g.budget.maxAttemptsTotal} total) ·{" "} + {g.budget.delay.type} ) : ( ), }, { - header: "Notes", + header: "Alert", truncate: true, cell: (g) => - g.notes ? ( - - {g.notes} - - ) : ( + g.action !== "Allow" ? ( + ) : g.alertMode === "Silent" ? ( + Silent + ) : g.alertMode === "Send" && g.alertHandlerId ? ( + {g.alertHandlerId} + ) : ( + + {alertHandlerId ? "Inherited" : "Nobody"} + ), }, { @@ -281,7 +424,7 @@ export function RetryPolicyPage() { align: "right", cell: (g) => canEdit ? ( - + e.stopPropagation()}>
- - - + { + setAlertHandlerId(id); + setAlertProps(props); + }} + />
+
+ + +
+ {canEdit && dirty && ( setEditingGroup(null)} + policyAlertHandlerId={alertHandlerId} /> )} diff --git a/SW.Bitween.Web/ClientApp/src/pages/retry-policies/UsagePanel.tsx b/SW.Bitween.Web/ClientApp/src/pages/retry-policies/UsagePanel.tsx new file mode 100644 index 00000000..6e2a0edc --- /dev/null +++ b/SW.Bitween.Web/ClientApp/src/pages/retry-policies/UsagePanel.tsx @@ -0,0 +1,499 @@ +import { useState } from "react"; +import { Link } from "react-router"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { AlertTriangle, BellOff, ChevronDown, ChevronRight, RotateCcw } from "lucide-react"; +import { api, type IntegrationSetupRef, type RetryAlertLevel, type RetryUsageRow } from "../../api"; +import { Badge, Button, FormError, LoadingBlock } from "../../components/ui/basics"; +import { ConfirmDialog, Dialog } from "../../components/ui/overlays"; +import { Panel } from "../../components/ui/Panel"; +import { formatDateTime, timeAgo } from "../../lib/dates"; +import { AlertRouting } from "./AlertRouting"; + +/** + * What each retry budget of this policy has actually spent, and whether anyone was told when + * one ran out. + * + * One row per integration-and-group pair, because that is how a budget is counted: a shared + * policy gives every integration its own separate total. There is no such thing as "this + * policy's usage" — any single figure here would be an aggregate matching nothing anyone can + * act on — and it is also why resetting and overriding both address one pair. + * + * Everything in this panel happens immediately. The groups above it stage into the save bar; + * a reset here does not, and the panel says so rather than leaving the reader to find out. + */ + +const LEVEL_WORD: Record = { + SubscriptionGroup: "this integration", + Group: "the group", + Policy: "the policy", +}; + +type FilterKey = "attention" | "exhausted" | "silent" | "overridden" | "all"; + +/** + * A pair worth looking at: stopped retrying, alerted nobody when it did, or would alert + * nobody without anyone having chosen that. + * + * A silence someone configured is deliberately *not* here. Both a silenced pair and an + * unrouted one send nothing, but only one of them is a mistake, and a filter that cannot + * tell them apart flags every deliberate silence until nobody reads it any more. + */ +const needsAttention = (r: RetryUsageRow) => + r.exhausted || r.alert?.delivered === false || (r.resolvedHandlerId === null && r.silencedAt === null); + +const FILTERS: { key: FilterKey; label: string; match: (r: RetryUsageRow) => boolean; blurb: string }[] = [ + { + key: "attention", + label: "Needs attention", + match: needsAttention, + blurb: + "Budgets that have run out, alerts that did not arrive, and pairs that would alert nobody without anyone having chosen that.", + }, + { + key: "exhausted", + label: "Exhausted", + match: (r) => r.exhausted, + blurb: "No longer retried at all until the budget is reset, or the integration succeeds.", + }, + { + key: "silent", + label: "No alert", + match: (r) => r.resolvedHandlerId === null, + blurb: "Nothing is sent when these run out — whether that was chosen or simply never set.", + }, + { + key: "overridden", + label: "Overridden", + match: (r) => r.override.alertMode !== "Inherit", + blurb: "Pairs whose alert routing is set on the integration itself, not inherited.", + }, + { key: "all", label: "All", match: () => true, blurb: "Every integration and group using this policy." }, +]; + +/** Where this pair's alert ends up, said as a destination rather than a mode. */ +function AlertCell({ row }: { row: RetryUsageRow }) { + if (row.resolvedHandlerId) + return ( + + {row.resolvedHandlerId} + {row.resolvedFrom && · set by {LEVEL_WORD[row.resolvedFrom]}} + + ); + + return ( + + + {row.silencedAt ? ( + Silenced by {LEVEL_WORD[row.silencedAt]} + ) : ( + Nobody + )} + + ); +} + +/** + * Whether the alert reached anyone. + * + * Kept apart from the fact that one was raised, because those are two different things and + * the page has to be able to say when they disagree: the alert is claimed *before* the send is + * attempted, so a refused login or a failed TLS handshake leaves a budget that stopped + * retrying and a team that was never told. + */ +function AlertedCell({ row }: { row: RetryUsageRow }) { + const { alert } = row; + if (!alert) return ; + if (alert.delivered === false) return Not delivered; + if (alert.delivered === null) + return ( + + Unconfirmed + + ); + return ( + + Sent {timeAgo(alert.claimedOn)} + + ); +} + +/** The failures a pair spent its budget on, fetched only once the row is opened. */ +function Attempts({ policyId, row }: { policyId: number; row: RetryUsageRow }) { + const q = useQuery({ + queryKey: ["retry-attempts", policyId, row.integrationId, row.groupId], + queryFn: () => api.getRetryAttempts(policyId, { integrationId: row.integrationId, groupId: row.groupId }), + }); + + if (q.isPending) return ; + if (q.isError) return {q.error.message}; + + const { total, attempts } = q.data; + if (attempts.length === 0) + return ( +

+ No failures recorded against this group for this integration. +

+ ); + + return ( +
+ {row.alert?.error && ( +
+

+ The budget-exhausted alert could not be delivered. +

+

{row.alert.error}

+
+ )} +
    + {attempts.map((a) => ( +
  1. + + {a.exchangeId.slice(0, 8)} + + + {timeAgo(a.failedOn)} + + {a.retryPending ? ( + Retry due + ) : a.blockedReason ? ( + Stopped + ) : null} + + {a.blockedReason ?? a.error} + +
  2. + ))} +
+ {total > attempts.length && ( +

+ Showing the {attempts.length} most recent of {total} failures.{" "} + + See them all + +

+ )} +
+ ); +} + +/** Sets where one pair's alert goes, overriding the group and the policy behind it. */ +function OverrideDialog({ + policyId, + row, + onClose, +}: { + policyId: number; + row: RetryUsageRow; + onClose: () => void; +}) { + const queryClient = useQueryClient(); + // Seeded from what this pair currently sends, not from an empty form: an override replaces + // the level above rather than merging with it, so starting blank would quietly drop the very + // settings the alert needs to arrive. + const [value, setValue] = useState({ + alertMode: row.override.alertMode, + alertHandlerId: row.override.alertHandlerId ?? row.resolvedHandlerId, + alertHandlerProperties: + Object.keys(row.override.alertHandlerProperties).length > 0 + ? row.override.alertHandlerProperties + : row.resolvedHandlerProperties, + }); + + const save = useMutation({ + mutationFn: () => api.saveRetryAlertOverride(policyId, { ...value, integrationId: row.integrationId, groupId: row.groupId }), + onSuccess: async () => { + await queryClient.invalidateQueries({ queryKey: ["retry-usage"] }); + onClose(); + }, + }); + + return ( + +
+

+ Where this one integration's alert goes when this group's budget runs out. The most + specific of the three levels — it wins over both the group and the policy. +

+ + Sends through {row.resolvedHandlerId}, set by{" "} + {row.resolvedFrom ? LEVEL_WORD[row.resolvedFrom] : "a level above"}. + + ) : row.silencedAt ? ( + `Silenced by ${LEVEL_WORD[row.silencedAt]}, so nothing is sent.` + ) : ( + "No level above sends anything, so nothing is sent." + ) + } + /> + {save.error?.message} +
+ + +
+
+
+ ); +} + +export function UsagePanel({ + policyId, + integrations, + canEdit, +}: { + policyId: number; + /** Used to explain an empty report — the integrations are still following the policy. */ + integrations: IntegrationSetupRef[]; + canEdit: boolean; +}) { + const queryClient = useQueryClient(); + const [filter, setFilter] = useState("attention"); + const [open, setOpen] = useState(null); + const [overriding, setOverriding] = useState(null); + const [resetting, setResetting] = useState(null); + + const usage = useQuery({ queryKey: ["retry-usage", policyId], queryFn: () => api.getRetryUsage(policyId) }); + + const invalidate = () => queryClient.invalidateQueries({ queryKey: ["retry-usage"] }); + + if (usage.isPending) return ; + if (usage.isError) + return ( + + {usage.error.message} + + ); + + const rows = usage.data; + + if (rows.length === 0) + return ( + +

+ {integrations.length === 0 + ? "No integration uses this policy yet." + : "No group in this policy sets a total budget, so there is nothing to spend and nothing that can run out."} +

+
+ ); + + const counts = Object.fromEntries( + FILTERS.map((f) => [f.key, rows.filter(f.match).length]), + ) as Record; + const active = FILTERS.find((f) => f.key === filter)!; + const shown = rows.filter(active.match); + const spent = rows.filter((r) => r.exhausted); + + return ( + 0 ? ( + + ) : undefined + } + > +
+ {FILTERS.map((f) => { + const on = f.key === filter; + return ( + + ); + })} +
+

{active.blurb}

+ + {shown.length === 0 ? ( +

Nothing here — which is the good outcome.

+ ) : ( +
+
+ + + + + + + + + + + + {shown.map((r) => { + const key = `${r.integrationId}:${r.groupId}`; + const isOpen = open === key; + return [ + + + + + + + + + + , + isOpen && ( + + {/* + max-w-0 stops this cell reporting an intrinsic width, the same trick the + shared Table uses. Without it a single unwrapped stack trace widened the + whole table, and every column after Integration was pushed out of view — + opening one row hid the data in all the others. + */} + + + ), + ]; + })} + +
+ IntegrationGroupUsedLast failureAlert goes toAlerted +
+ + + + {r.integrationName} + + + {r.groupName} + + + + {r.used} / {r.total} + + {r.exhausted && ( + + Exhausted + + )} + + + {r.lastAttemptOn ? ( + {timeAgo(r.lastAttemptOn)} + ) : ( + never failed + )} + + + + + + {canEdit && ( + + {r.lastAttemptOn && ( + + )} + + + )} +
+ +
+
+ )} + + {overriding && ( + setOverriding(null)} /> + )} + + {resetting && ( + + {spent.length} {spent.length === 1 ? "budget starts" : "budgets start"} again from zero, + and {spent.length === 1 ? "its" : "their"} integrations begin retrying immediately. If + the downstream is still down, they will spend it again. + + ) : ( + <> + {resetting.integrationName} starts + again from zero in {resetting.groupName}, + and retries resume immediately. + + ) + } + confirmLabel={resetting === "all" ? "Reset them all" : "Reset budget"} + onConfirm={async () => { + // No pair means every pair — which is why the sweep is scoped to what has actually + // run out rather than sent as one policy-wide reset: a budget partway through is + // spending it for a reason, and handing that back is not what was asked for. + if (resetting === "all") + await Promise.all( + spent.map((r) => + api.resetRetryUsage(policyId, { integrationId: r.integrationId, groupId: r.groupId }), + ), + ); + else + await api.resetRetryUsage(policyId, { + integrationId: resetting.integrationId, + groupId: resetting.groupId, + }); + await invalidate(); + }} + onClose={() => setResetting(null)} + /> + )} + + {counts.attention > 0 && filter !== "attention" && ( +

+ + {counts.attention} {counts.attention === 1 ? "pair needs" : "pairs need"} attention. +

+ )} + + ); +} diff --git a/SW.Bitween.Web/ClientApp/src/pages/scheduled-jobs/NewScheduledJobPage.tsx b/SW.Bitween.Web/ClientApp/src/pages/scheduled-jobs/NewScheduledJobPage.tsx index 4737ac9c..efba08fa 100644 --- a/SW.Bitween.Web/ClientApp/src/pages/scheduled-jobs/NewScheduledJobPage.tsx +++ b/SW.Bitween.Web/ClientApp/src/pages/scheduled-jobs/NewScheduledJobPage.tsx @@ -1,7 +1,7 @@ import { useState } from "react"; -import { Link, useNavigate } from "react-router"; +import { useNavigate } from "react-router"; import { useMutation, useQueryClient } from "@tanstack/react-query"; -import { ArrowLeft, X } from "lucide-react"; +import { X } from "lucide-react"; import { Button, FormError } from "../../components/ui/basics"; import { Checkbox, Field, TextInput } from "../../components/ui/forms"; import { Panel } from "../../components/ui/Panel"; @@ -15,6 +15,7 @@ import { StageRail } from "../integrations/studio/StageRail"; import { adapterIncomplete, faceOf } from "../integrations/studio/faces"; import { ResponseFields } from "../integrations/studio/ResponseFields"; import type { Draft as StudioDraft } from "../integrations/studio/model"; +import { BackLink } from "../../components/ui/BackLink"; /** Local draft state with the patch-and-clear shape the form bodies already use. */ function useDraft(initial: T) { @@ -224,12 +225,7 @@ export function NewScheduledJobPage() { return (
- - Scheduled jobs - +

New scheduled job

diff --git a/SW.Bitween.Web/ClientApp/src/pages/scheduled-jobs/ScheduledJobsPage.tsx b/SW.Bitween.Web/ClientApp/src/pages/scheduled-jobs/ScheduledJobsPage.tsx index 173a58f4..7d6689dd 100644 --- a/SW.Bitween.Web/ClientApp/src/pages/scheduled-jobs/ScheduledJobsPage.tsx +++ b/SW.Bitween.Web/ClientApp/src/pages/scheduled-jobs/ScheduledJobsPage.tsx @@ -1,12 +1,13 @@ import { useMemo, useState } from "react"; import { Link, useNavigate, useSearchParams } from "react-router"; -import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { keepPreviousData, useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { CalendarClock, DownloadCloud, Plus, Search } from "lucide-react"; import { api, type IntegrationRow, type ScheduleHealth } from "../../api"; import { Can, useSessionCan } from "../../auth/guards"; import { PageHeader } from "../../components/layout/PageHeader"; import { Badge, Button, EmptyState, LoadingBlock } from "../../components/ui/basics"; import { ConfirmDialog } from "../../components/ui/overlays"; +import { Pagination } from "../../components/ui/Pagination"; import { Table } from "../../components/ui/Table"; import { HealthBadge, @@ -26,6 +27,7 @@ function ReceiveNowButton({ job }: { job: IntegrationRow }) { mutationFn: () => api.receiveNow(job.id), onSuccess: () => { void queryClient.invalidateQueries({ queryKey: ["integration-rows"] }); + void queryClient.invalidateQueries({ queryKey: ["integration-rows-search"] }); void queryClient.invalidateQueries({ queryKey: ["last-runs"] }); }, }); @@ -81,14 +83,21 @@ function ScheduleFault({ health }: { health: ScheduleHealth | undefined }) { * Last run comes from the scheduler's own execution history (kept ~30 days); * next run is Bitween's own `ReceiveOn`. */ +const PAGE_SIZE = 25; + export function ScheduledJobsPage() { const [searchParams, setSearchParams] = useSearchParams(); const navigate = useNavigate(); const q = searchParams.get("q") ?? ""; + const offset = searchParams.get("offset") ? Number(searchParams.get("offset")) : 0; const canOperate = useSessionCan("subscriptions.operate"); const canSeeInfoTypes = useSessionCan("documents.view"); - const rows = useQuery({ queryKey: ["integration-rows"], queryFn: () => api.listIntegrationRows() }); + const rows = useQuery({ + queryKey: ["integration-rows-search", "Receiving", q, offset], + queryFn: () => api.searchIntegrationRows({ search: q, type: "Receiving", offset, limit: PAGE_SIZE }), + placeholderData: keepPreviousData, + }); // The list rows don't carry work group or retry policy; the integrations // cache does, and every page already holds it. const setups = useIntegrationsCache().data ?? []; @@ -102,28 +111,20 @@ export function ScheduledJobsPage() { useQuery({ queryKey: ["schedule-health"], queryFn: () => api.listScheduleHealth() }).data ?? []; const healthById = useMemo(() => new Map(health.map((h) => [h.integrationId, h])), [health]); - const setQ = (value: string) => + const setParam = (key: string, value: string | null, resetOffset = true) => setSearchParams( (prev) => { const next = new URLSearchParams(prev); - if (value) next.set("q", value); - else next.delete("q"); + if (value) next.set(key, value); + else next.delete(key); + if (resetOffset) next.delete("offset"); return next; }, { replace: true }, ); - const filtered = useMemo(() => { - const needle = q.trim().toLowerCase(); - return (rows.data ?? []) - .filter((r) => r.type === "Receiving") - .filter( - (r) => - !needle || - r.name.toLowerCase().includes(needle) || - r.informationTypeCode.toLowerCase().includes(needle), - ); - }, [rows.data, q]); + const filtered = rows.data?.result ?? []; + const total = rows.data?.total ?? 0; return (

@@ -144,7 +145,7 @@ export function ScheduledJobsPage() { setQ(e.target.value)} + onChange={(e) => setParam("q", e.target.value || null)} placeholder="Search jobs" aria-label="Search scheduled jobs" className="h-9 w-full rounded-lg border border-ink-200 bg-white pr-3 pl-9 text-sm placeholder:text-ink-400 focus:border-crimson-400 focus:ring-2 focus:ring-crimson-100 focus:outline-none" @@ -163,6 +164,14 @@ export function ScheduledJobsPage() { rowKey={(r) => r.id} minWidth="min-w-270" onRowClick={(r) => navigate(`/subscriptions/${r.id}`)} + footer={ + setParam("offset", String(o), false)} + /> + } columns={[ { header: "Job", diff --git a/SW.Bitween.Web/ClientApp/src/pages/team/MemberDrawer.tsx b/SW.Bitween.Web/ClientApp/src/pages/team/MemberDrawer.tsx index 47805ea8..697140ba 100644 --- a/SW.Bitween.Web/ClientApp/src/pages/team/MemberDrawer.tsx +++ b/SW.Bitween.Web/ClientApp/src/pages/team/MemberDrawer.tsx @@ -1,6 +1,6 @@ import { useEffect, useState, type ReactNode } from "react"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; -import { Check, KeyRound, Trash2, UserRoundCheck, UserRoundX, X } from "lucide-react"; +import { Check, KeyRound, LockOpen, Trash2, UserRoundCheck, UserRoundX, X } from "lucide-react"; import { api } from "../../api"; import { Can } from "../../auth/guards"; import { useSession } from "../../auth/SessionContext"; @@ -9,7 +9,7 @@ import { CopyField } from "../../components/ui/CopyField"; import { Badge, Button, FormError, LoadingBlock } from "../../components/ui/basics"; import { Checkbox, PasswordInput } from "../../components/ui/forms"; import { ConfirmDialog } from "../../components/ui/overlays"; -import { formatDate, timeAgo } from "../../lib/dates"; +import { formatDate, timeAgo, timeUntil } from "../../lib/dates"; import { statusBadge } from "./MembersTab"; function Section({ title, children }: { title: string; children: ReactNode }) { @@ -58,6 +58,11 @@ export function MemberDrawer({ userId, onClose }: { userId: string; onClose: () mutationFn: (disabled: boolean) => api.setUserDisabled(userId, disabled), onSuccess: invalidate, }); + const unlock = useMutation({ + mutationFn: () => api.unlockUser(userId), + onSuccess: invalidate, + }); + const setPassword = useMutation({ mutationFn: (password: string) => api.setUserPassword(userId, password), onSuccess: (_result, password) => { @@ -91,7 +96,7 @@ export function MemberDrawer({ userId, onClose }: { userId: string; onClose: () {isSelf && You}

{u.email}

-
{statusBadge(u.status)}
+
{statusBadge(u.status, u.lockedUntil)}
@@ -195,6 +200,18 @@ export function MemberDrawer({ userId, onClose }: { userId: string; onClose: () {setPassword.error?.message} )} + {editable && u.lockedUntil && ( +
+ +

+ Locked after repeated failed sign-ins, for another {timeUntil(u.lockedUntil)}. + Unlocking clears it now. +

+ {unlock.error?.message} +
+ )} {editable && (
{statusBadge(user.status)}{statusBadge(user.status, user.lockedUntil)} {user.lastActiveOn ? timeAgo(user.lastActiveOn) : "—"}