diff --git a/src/VirtoCommerce.PushMessages.Core/BackgroundJobs/IPushMessageJobService.cs b/src/VirtoCommerce.PushMessages.Core/BackgroundJobs/IPushMessageJobService.cs index 5fd6b77..c0e176d 100644 --- a/src/VirtoCommerce.PushMessages.Core/BackgroundJobs/IPushMessageJobService.cs +++ b/src/VirtoCommerce.PushMessages.Core/BackgroundJobs/IPushMessageJobService.cs @@ -1,8 +1,8 @@ -using System.Threading.Tasks; +using System.Collections.Generic; namespace VirtoCommerce.PushMessages.Core.BackgroundJobs; -public interface IPushMessageJobService +public interface IPushMessageJobService : IRecurringJobService { - Task StartStopRecurringJobs(); + void EnqueueAddRecipients(IList messageIds = null); } diff --git a/src/VirtoCommerce.PushMessages.Core/BackgroundJobs/IRecurringJobService.cs b/src/VirtoCommerce.PushMessages.Core/BackgroundJobs/IRecurringJobService.cs new file mode 100644 index 0000000..92f3bd1 --- /dev/null +++ b/src/VirtoCommerce.PushMessages.Core/BackgroundJobs/IRecurringJobService.cs @@ -0,0 +1,10 @@ +using System.Threading.Tasks; +using VirtoCommerce.Platform.Core.Events; +using VirtoCommerce.Platform.Core.Settings.Events; + +namespace VirtoCommerce.PushMessages.Core.BackgroundJobs; + +public interface IRecurringJobService : IEventHandler +{ + Task StartStopRecurringJobs(); +} diff --git a/src/VirtoCommerce.PushMessages.Core/BackgroundJobs/RecurringJobDescriptor.cs b/src/VirtoCommerce.PushMessages.Core/BackgroundJobs/RecurringJobDescriptor.cs new file mode 100644 index 0000000..64627c0 --- /dev/null +++ b/src/VirtoCommerce.PushMessages.Core/BackgroundJobs/RecurringJobDescriptor.cs @@ -0,0 +1,15 @@ +using System; +using System.Linq.Expressions; +using System.Reflection; +using System.Threading.Tasks; +using VirtoCommerce.Platform.Core.Settings; + +namespace VirtoCommerce.PushMessages.Core.BackgroundJobs; + +public class RecurringJobDescriptor +{ + public SettingDescriptor EnableSetting { get; set; } + public SettingDescriptor CronSetting { get; set; } + public MethodInfo Method { get; set; } + public Expression> MethodCall { get; set; } +} diff --git a/src/VirtoCommerce.PushMessages.Core/Extensions/ApplicationBuilderExtensions.cs b/src/VirtoCommerce.PushMessages.Core/Extensions/ApplicationBuilderExtensions.cs new file mode 100644 index 0000000..cf7288e --- /dev/null +++ b/src/VirtoCommerce.PushMessages.Core/Extensions/ApplicationBuilderExtensions.cs @@ -0,0 +1,19 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; +using VirtoCommerce.Platform.Core.Events; +using VirtoCommerce.Platform.Core.Settings.Events; +using VirtoCommerce.PushMessages.Core.BackgroundJobs; + +namespace VirtoCommerce.PushMessages.Core.Extensions; + +public static class ApplicationBuilderExtensions +{ + public static void UseRecurringJobService(this IApplicationBuilder appBuilder) + where T : class, IRecurringJobService + { + appBuilder.RegisterEventHandler(); + + var pushMessageJobService = appBuilder.ApplicationServices.GetService(); + pushMessageJobService.StartStopRecurringJobs().GetAwaiter().GetResult(); + } +} diff --git a/src/VirtoCommerce.PushMessages.Core/Extensions/PushMessageExtensions.cs b/src/VirtoCommerce.PushMessages.Core/Extensions/PushMessageExtensions.cs deleted file mode 100644 index 3936bfe..0000000 --- a/src/VirtoCommerce.PushMessages.Core/Extensions/PushMessageExtensions.cs +++ /dev/null @@ -1,15 +0,0 @@ -using VirtoCommerce.Platform.Core.Common; -using VirtoCommerce.Platform.Core.Events; -using VirtoCommerce.PushMessages.Core.Models; - -namespace VirtoCommerce.PushMessages.Core.Extensions; - -public static class PushMessageExtensions -{ - public static bool IsSent(this GenericChangedEntry entry) - { - return entry.NewEntry.Status == PushMessageStatus.Sent && - (entry.EntryState == EntryState.Added || - entry.EntryState == EntryState.Modified && entry.OldEntry.Status != PushMessageStatus.Sent); - } -} diff --git a/src/VirtoCommerce.PushMessages.Core/Extensions/ServiceCollectionExtensions.cs b/src/VirtoCommerce.PushMessages.Core/Extensions/ServiceCollectionExtensions.cs new file mode 100644 index 0000000..7f47468 --- /dev/null +++ b/src/VirtoCommerce.PushMessages.Core/Extensions/ServiceCollectionExtensions.cs @@ -0,0 +1,15 @@ +using Microsoft.Extensions.DependencyInjection; +using VirtoCommerce.PushMessages.Core.BackgroundJobs; + +namespace VirtoCommerce.PushMessages.Core.Extensions; + +public static class ServiceCollectionExtensions +{ + public static void AddRecurringJobService(this IServiceCollection serviceCollection) + where TService : class, IRecurringJobService + where TImplementation : class, TService + { + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); + } +} diff --git a/src/VirtoCommerce.PushMessages.Core/Models/PushMessage.cs b/src/VirtoCommerce.PushMessages.Core/Models/PushMessage.cs index 221faf6..66d7812 100644 --- a/src/VirtoCommerce.PushMessages.Core/Models/PushMessage.cs +++ b/src/VirtoCommerce.PushMessages.Core/Models/PushMessage.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using Newtonsoft.Json; using VirtoCommerce.Platform.Core.Common; namespace VirtoCommerce.PushMessages.Core.Models; @@ -13,15 +12,21 @@ public class PushMessage : AuditableEntity, ICloneable public DateTime? StartDate { get; set; } - public string Status { get; set; } + public string Status { get; set; } = PushMessageStatus.Draft; - public IList MemberIds { get; set; } + public bool TrackNewRecipients { get; set; } + + public string MemberQuery { get; set; } - [JsonIgnore] - public IList UserIds { get; set; } + public IList MemberIds { get; set; } - public object Clone() + public virtual object Clone() { return MemberwiseClone(); } + + public virtual bool HasRecipients() + { + return MemberIds != null && MemberIds.Count > 0 || !string.IsNullOrEmpty(MemberQuery); + } } diff --git a/src/VirtoCommerce.PushMessages.Core/Models/PushMessageMember.cs b/src/VirtoCommerce.PushMessages.Core/Models/PushMessageMember.cs deleted file mode 100644 index a939424..0000000 --- a/src/VirtoCommerce.PushMessages.Core/Models/PushMessageMember.cs +++ /dev/null @@ -1,10 +0,0 @@ -using VirtoCommerce.Platform.Core.Common; - -namespace VirtoCommerce.PushMessages.Core.Models; - -public class PushMessageMember : AuditableEntity -{ - public string MemberId { get; set; } - public string MemberName { get; set; } - public string MemberType { get; set; } -} diff --git a/src/VirtoCommerce.PushMessages.Core/Models/PushMessageSearchCriteria.cs b/src/VirtoCommerce.PushMessages.Core/Models/PushMessageSearchCriteria.cs index dfd825d..8eeed15 100644 --- a/src/VirtoCommerce.PushMessages.Core/Models/PushMessageSearchCriteria.cs +++ b/src/VirtoCommerce.PushMessages.Core/Models/PushMessageSearchCriteria.cs @@ -6,6 +6,9 @@ namespace VirtoCommerce.PushMessages.Core.Models; public class PushMessageSearchCriteria : SearchCriteriaBase { + public bool? IsDraft { get; set; } + public bool? TrackNewRecipients { get; set; } + public DateTime? CreatedDateBefore { get; set; } public DateTime? StartDateBefore { get; set; } public IList Statuses { get; set; } } diff --git a/src/VirtoCommerce.PushMessages.Core/Models/PushMessageStatus.cs b/src/VirtoCommerce.PushMessages.Core/Models/PushMessageStatus.cs index bcde9f7..6ecc63f 100644 --- a/src/VirtoCommerce.PushMessages.Core/Models/PushMessageStatus.cs +++ b/src/VirtoCommerce.PushMessages.Core/Models/PushMessageStatus.cs @@ -2,6 +2,7 @@ namespace VirtoCommerce.PushMessages.Core.Models; public static class PushMessageStatus { + public const string Draft = "Draft"; public const string Scheduled = "Scheduled"; public const string Sent = "Sent"; } diff --git a/src/VirtoCommerce.PushMessages.Core/ModuleConstants.cs b/src/VirtoCommerce.PushMessages.Core/ModuleConstants.cs index b203cc7..fadc9a9 100644 --- a/src/VirtoCommerce.PushMessages.Core/ModuleConstants.cs +++ b/src/VirtoCommerce.PushMessages.Core/ModuleConstants.cs @@ -50,28 +50,46 @@ public static IEnumerable AllGeneralSettings public static class BackgroundJobs { - public static SettingDescriptor Enable { get; } = new() + public static SettingDescriptor SendScheduledMessagesRecurringJobEnable { get; } = new() { - Name = "PushMessages.BackgroundJobs.Enable", + Name = "PushMessages.SendScheduledMessagesRecurringJob.Enable", GroupName = "Push Messages|Background Jobs", ValueType = SettingValueType.Boolean, DefaultValue = true, }; - public static SettingDescriptor CronExpression { get; } = new() + public static SettingDescriptor SendScheduledMessagesRecurringJobCronExpression { get; } = new() { - Name = "PushMessages.BackgroundJobs.CronExpression", + Name = "PushMessages.SendScheduledMessagesRecurringJob.CronExpression", GroupName = "Push Messages|Background Jobs", ValueType = SettingValueType.ShortText, DefaultValue = "0/5 * * * *", }; + public static SettingDescriptor TrackNewRecipientsRecurringJobEnable { get; } = new() + { + Name = "PushMessages.TrackNewRecipientsRecurringJob.Enable", + GroupName = "Push Messages|Background Jobs", + ValueType = SettingValueType.Boolean, + DefaultValue = true, + }; + + public static SettingDescriptor TrackNewRecipientsRecurringJobCronExpression { get; } = new() + { + Name = "PushMessages.TrackNewRecipientsRecurringJob.CronExpression", + GroupName = "Push Messages|Background Jobs", + ValueType = SettingValueType.ShortText, + DefaultValue = "0 0/1 * * *", + }; + public static IEnumerable AllBackgroundJobsSettings { get { - yield return Enable; - yield return CronExpression; + yield return SendScheduledMessagesRecurringJobEnable; + yield return SendScheduledMessagesRecurringJobCronExpression; + yield return TrackNewRecipientsRecurringJobEnable; + yield return TrackNewRecipientsRecurringJobCronExpression; } } } diff --git a/src/VirtoCommerce.PushMessages.Core/Services/IPushMessageService.cs b/src/VirtoCommerce.PushMessages.Core/Services/IPushMessageService.cs index 3915807..78cc940 100644 --- a/src/VirtoCommerce.PushMessages.Core/Services/IPushMessageService.cs +++ b/src/VirtoCommerce.PushMessages.Core/Services/IPushMessageService.cs @@ -1,3 +1,4 @@ +using System.Threading.Tasks; using VirtoCommerce.Platform.Core.GenericCrud; using VirtoCommerce.PushMessages.Core.Models; @@ -5,4 +6,5 @@ namespace VirtoCommerce.PushMessages.Core.Services; public interface IPushMessageService : ICrudService { + Task ChangeTracking(string messageId, bool value); } diff --git a/src/VirtoCommerce.PushMessages.Data.MySql/Migrations/20240502161150_AddMoreFieldsToMessage.Designer.cs b/src/VirtoCommerce.PushMessages.Data.MySql/Migrations/20240502161150_AddMoreFieldsToMessage.Designer.cs new file mode 100644 index 0000000..8a975f1 --- /dev/null +++ b/src/VirtoCommerce.PushMessages.Data.MySql/Migrations/20240502161150_AddMoreFieldsToMessage.Designer.cs @@ -0,0 +1,198 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using VirtoCommerce.PushMessages.Data.Repositories; + +#nullable disable + +namespace VirtoCommerce.PushMessages.Data.MySql.Migrations +{ + [DbContext(typeof(PushMessagesDbContext))] + [Migration("20240502161150_AddMoreFieldsToMessage")] + partial class AddMoreFieldsToMessage + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + modelBuilder.Entity("VirtoCommerce.PushMessages.Data.Models.PushMessageEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasMaxLength(128) + .HasColumnType("varchar(128)"); + + b.Property("CreatedBy") + .HasMaxLength(64) + .HasColumnType("varchar(64)"); + + b.Property("CreatedDate") + .HasColumnType("datetime(6)"); + + b.Property("MemberQuery") + .HasMaxLength(1024) + .HasColumnType("varchar(1024)"); + + b.Property("ModifiedBy") + .HasMaxLength(64) + .HasColumnType("varchar(64)"); + + b.Property("ModifiedDate") + .HasColumnType("datetime(6)"); + + b.Property("ShortMessage") + .HasMaxLength(1024) + .HasColumnType("varchar(1024)"); + + b.Property("StartDate") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .HasMaxLength(64) + .HasColumnType("varchar(64)"); + + b.Property("Topic") + .HasMaxLength(128) + .HasColumnType("varchar(128)"); + + b.Property("TrackNewRecipients") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.ToTable("PushMessage", (string)null); + }); + + modelBuilder.Entity("VirtoCommerce.PushMessages.Data.Models.PushMessageMemberEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasMaxLength(128) + .HasColumnType("varchar(128)"); + + b.Property("CreatedBy") + .HasMaxLength(64) + .HasColumnType("varchar(64)"); + + b.Property("CreatedDate") + .HasColumnType("datetime(6)"); + + b.Property("MemberId") + .HasMaxLength(128) + .HasColumnType("varchar(128)"); + + b.Property("MessageId") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("varchar(128)"); + + b.Property("ModifiedBy") + .HasMaxLength(64) + .HasColumnType("varchar(64)"); + + b.Property("ModifiedDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("MessageId", "MemberId") + .IsUnique() + .HasDatabaseName("IX_PushMessageMember_MessageId_MemberId"); + + b.ToTable("PushMessageMember", (string)null); + }); + + modelBuilder.Entity("VirtoCommerce.PushMessages.Data.Models.PushMessageRecipientEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasMaxLength(128) + .HasColumnType("varchar(128)"); + + b.Property("CreatedBy") + .HasMaxLength(64) + .HasColumnType("varchar(64)"); + + b.Property("CreatedDate") + .HasColumnType("datetime(6)"); + + b.Property("IsHidden") + .HasColumnType("tinyint(1)"); + + b.Property("IsRead") + .HasColumnType("tinyint(1)"); + + b.Property("MemberId") + .HasMaxLength(128) + .HasColumnType("varchar(128)"); + + b.Property("MemberName") + .HasMaxLength(512) + .HasColumnType("varchar(512)"); + + b.Property("MessageId") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("varchar(128)"); + + b.Property("ModifiedBy") + .HasMaxLength(64) + .HasColumnType("varchar(64)"); + + b.Property("ModifiedDate") + .HasColumnType("datetime(6)"); + + b.Property("UserId") + .HasMaxLength(128) + .HasColumnType("varchar(128)"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("MessageId", "UserId") + .IsUnique() + .HasDatabaseName("IX_PushMessageRecipient_MessageId_UserId"); + + b.ToTable("PushMessageRecipient", (string)null); + }); + + modelBuilder.Entity("VirtoCommerce.PushMessages.Data.Models.PushMessageMemberEntity", b => + { + b.HasOne("VirtoCommerce.PushMessages.Data.Models.PushMessageEntity", "Message") + .WithMany("Members") + .HasForeignKey("MessageId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Message"); + }); + + modelBuilder.Entity("VirtoCommerce.PushMessages.Data.Models.PushMessageRecipientEntity", b => + { + b.HasOne("VirtoCommerce.PushMessages.Data.Models.PushMessageEntity", "Message") + .WithMany() + .HasForeignKey("MessageId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Message"); + }); + + modelBuilder.Entity("VirtoCommerce.PushMessages.Data.Models.PushMessageEntity", b => + { + b.Navigation("Members"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/VirtoCommerce.PushMessages.Data.MySql/Migrations/20240502161150_AddMoreFieldsToMessage.cs b/src/VirtoCommerce.PushMessages.Data.MySql/Migrations/20240502161150_AddMoreFieldsToMessage.cs new file mode 100644 index 0000000..5518585 --- /dev/null +++ b/src/VirtoCommerce.PushMessages.Data.MySql/Migrations/20240502161150_AddMoreFieldsToMessage.cs @@ -0,0 +1,41 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace VirtoCommerce.PushMessages.Data.MySql.Migrations +{ + /// + public partial class AddMoreFieldsToMessage : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "MemberQuery", + table: "PushMessage", + type: "varchar(1024)", + maxLength: 1024, + nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.AddColumn( + name: "TrackNewRecipients", + table: "PushMessage", + type: "tinyint(1)", + nullable: false, + defaultValue: false); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "MemberQuery", + table: "PushMessage"); + + migrationBuilder.DropColumn( + name: "TrackNewRecipients", + table: "PushMessage"); + } + } +} diff --git a/src/VirtoCommerce.PushMessages.Data.MySql/Migrations/PushMessagesDbContextModelSnapshot.cs b/src/VirtoCommerce.PushMessages.Data.MySql/Migrations/PushMessagesDbContextModelSnapshot.cs index 77dc2fc..9e7d86e 100644 --- a/src/VirtoCommerce.PushMessages.Data.MySql/Migrations/PushMessagesDbContextModelSnapshot.cs +++ b/src/VirtoCommerce.PushMessages.Data.MySql/Migrations/PushMessagesDbContextModelSnapshot.cs @@ -33,6 +33,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("CreatedDate") .HasColumnType("datetime(6)"); + b.Property("MemberQuery") + .HasMaxLength(1024) + .HasColumnType("varchar(1024)"); + b.Property("ModifiedBy") .HasMaxLength(64) .HasColumnType("varchar(64)"); @@ -55,6 +59,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasMaxLength(128) .HasColumnType("varchar(128)"); + b.Property("TrackNewRecipients") + .HasColumnType("tinyint(1)"); + b.HasKey("Id"); b.ToTable("PushMessage", (string)null); @@ -170,7 +177,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder.Entity("VirtoCommerce.PushMessages.Data.Models.PushMessageRecipientEntity", b => { b.HasOne("VirtoCommerce.PushMessages.Data.Models.PushMessageEntity", "Message") - .WithMany("Recipients") + .WithMany() .HasForeignKey("MessageId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); @@ -181,8 +188,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder.Entity("VirtoCommerce.PushMessages.Data.Models.PushMessageEntity", b => { b.Navigation("Members"); - - b.Navigation("Recipients"); }); #pragma warning restore 612, 618 } diff --git a/src/VirtoCommerce.PushMessages.Data.PostgreSql/Migrations/20240502161141_AddMoreFieldsToMessage.Designer.cs b/src/VirtoCommerce.PushMessages.Data.PostgreSql/Migrations/20240502161141_AddMoreFieldsToMessage.Designer.cs new file mode 100644 index 0000000..cd3849c --- /dev/null +++ b/src/VirtoCommerce.PushMessages.Data.PostgreSql/Migrations/20240502161141_AddMoreFieldsToMessage.Designer.cs @@ -0,0 +1,201 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using VirtoCommerce.PushMessages.Data.Repositories; + +#nullable disable + +namespace VirtoCommerce.PushMessages.Data.PostgreSql.Migrations +{ + [DbContext(typeof(PushMessagesDbContext))] + [Migration("20240502161141_AddMoreFieldsToMessage")] + partial class AddMoreFieldsToMessage + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("VirtoCommerce.PushMessages.Data.Models.PushMessageEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("CreatedBy") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("CreatedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("MemberQuery") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("ModifiedBy") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("ModifiedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ShortMessage") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("StartDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Topic") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("TrackNewRecipients") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.ToTable("PushMessage", (string)null); + }); + + modelBuilder.Entity("VirtoCommerce.PushMessages.Data.Models.PushMessageMemberEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("CreatedBy") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("CreatedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("MemberId") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("MessageId") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("ModifiedBy") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("ModifiedDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("MessageId", "MemberId") + .IsUnique() + .HasDatabaseName("IX_PushMessageMember_MessageId_MemberId"); + + b.ToTable("PushMessageMember", (string)null); + }); + + modelBuilder.Entity("VirtoCommerce.PushMessages.Data.Models.PushMessageRecipientEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("CreatedBy") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("CreatedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("IsHidden") + .HasColumnType("boolean"); + + b.Property("IsRead") + .HasColumnType("boolean"); + + b.Property("MemberId") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("MemberName") + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("MessageId") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("ModifiedBy") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("ModifiedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("MessageId", "UserId") + .IsUnique() + .HasDatabaseName("IX_PushMessageRecipient_MessageId_UserId"); + + b.ToTable("PushMessageRecipient", (string)null); + }); + + modelBuilder.Entity("VirtoCommerce.PushMessages.Data.Models.PushMessageMemberEntity", b => + { + b.HasOne("VirtoCommerce.PushMessages.Data.Models.PushMessageEntity", "Message") + .WithMany("Members") + .HasForeignKey("MessageId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Message"); + }); + + modelBuilder.Entity("VirtoCommerce.PushMessages.Data.Models.PushMessageRecipientEntity", b => + { + b.HasOne("VirtoCommerce.PushMessages.Data.Models.PushMessageEntity", "Message") + .WithMany() + .HasForeignKey("MessageId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Message"); + }); + + modelBuilder.Entity("VirtoCommerce.PushMessages.Data.Models.PushMessageEntity", b => + { + b.Navigation("Members"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/VirtoCommerce.PushMessages.Data.PostgreSql/Migrations/20240502161141_AddMoreFieldsToMessage.cs b/src/VirtoCommerce.PushMessages.Data.PostgreSql/Migrations/20240502161141_AddMoreFieldsToMessage.cs new file mode 100644 index 0000000..e702595 --- /dev/null +++ b/src/VirtoCommerce.PushMessages.Data.PostgreSql/Migrations/20240502161141_AddMoreFieldsToMessage.cs @@ -0,0 +1,40 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace VirtoCommerce.PushMessages.Data.PostgreSql.Migrations +{ + /// + public partial class AddMoreFieldsToMessage : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "MemberQuery", + table: "PushMessage", + type: "character varying(1024)", + maxLength: 1024, + nullable: true); + + migrationBuilder.AddColumn( + name: "TrackNewRecipients", + table: "PushMessage", + type: "boolean", + nullable: false, + defaultValue: false); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "MemberQuery", + table: "PushMessage"); + + migrationBuilder.DropColumn( + name: "TrackNewRecipients", + table: "PushMessage"); + } + } +} diff --git a/src/VirtoCommerce.PushMessages.Data.PostgreSql/Migrations/PushMessagesDbContextModelSnapshot.cs b/src/VirtoCommerce.PushMessages.Data.PostgreSql/Migrations/PushMessagesDbContextModelSnapshot.cs index 3e07fc4..63c4dd2 100644 --- a/src/VirtoCommerce.PushMessages.Data.PostgreSql/Migrations/PushMessagesDbContextModelSnapshot.cs +++ b/src/VirtoCommerce.PushMessages.Data.PostgreSql/Migrations/PushMessagesDbContextModelSnapshot.cs @@ -36,6 +36,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("CreatedDate") .HasColumnType("timestamp with time zone"); + b.Property("MemberQuery") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + b.Property("ModifiedBy") .HasMaxLength(64) .HasColumnType("character varying(64)"); @@ -58,6 +62,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasMaxLength(128) .HasColumnType("character varying(128)"); + b.Property("TrackNewRecipients") + .HasColumnType("boolean"); + b.HasKey("Id"); b.ToTable("PushMessage", (string)null); @@ -173,7 +180,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder.Entity("VirtoCommerce.PushMessages.Data.Models.PushMessageRecipientEntity", b => { b.HasOne("VirtoCommerce.PushMessages.Data.Models.PushMessageEntity", "Message") - .WithMany("Recipients") + .WithMany() .HasForeignKey("MessageId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); @@ -184,8 +191,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder.Entity("VirtoCommerce.PushMessages.Data.Models.PushMessageEntity", b => { b.Navigation("Members"); - - b.Navigation("Recipients"); }); #pragma warning restore 612, 618 } diff --git a/src/VirtoCommerce.PushMessages.Data.SqlServer/Migrations/20240502160835_AddMoreFieldsToMessage.Designer.cs b/src/VirtoCommerce.PushMessages.Data.SqlServer/Migrations/20240502160835_AddMoreFieldsToMessage.Designer.cs new file mode 100644 index 0000000..aa97b9f --- /dev/null +++ b/src/VirtoCommerce.PushMessages.Data.SqlServer/Migrations/20240502160835_AddMoreFieldsToMessage.Designer.cs @@ -0,0 +1,203 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using VirtoCommerce.PushMessages.Data.Repositories; + +#nullable disable + +namespace VirtoCommerce.PushMessages.Data.SqlServer.Migrations +{ + [DbContext(typeof(PushMessagesDbContext))] + [Migration("20240502160835_AddMoreFieldsToMessage")] + partial class AddMoreFieldsToMessage + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("VirtoCommerce.PushMessages.Data.Models.PushMessageEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasMaxLength(128) + .HasColumnType("nvarchar(128)"); + + b.Property("CreatedBy") + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("CreatedDate") + .HasColumnType("datetime2"); + + b.Property("MemberQuery") + .HasMaxLength(1024) + .HasColumnType("nvarchar(1024)"); + + b.Property("ModifiedBy") + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("ModifiedDate") + .HasColumnType("datetime2"); + + b.Property("ShortMessage") + .HasMaxLength(1024) + .HasColumnType("nvarchar(1024)"); + + b.Property("StartDate") + .HasColumnType("datetime2"); + + b.Property("Status") + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("Topic") + .HasMaxLength(128) + .HasColumnType("nvarchar(128)"); + + b.Property("TrackNewRecipients") + .HasColumnType("bit"); + + b.HasKey("Id"); + + b.ToTable("PushMessage", (string)null); + }); + + modelBuilder.Entity("VirtoCommerce.PushMessages.Data.Models.PushMessageMemberEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasMaxLength(128) + .HasColumnType("nvarchar(128)"); + + b.Property("CreatedBy") + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("CreatedDate") + .HasColumnType("datetime2"); + + b.Property("MemberId") + .HasMaxLength(128) + .HasColumnType("nvarchar(128)"); + + b.Property("MessageId") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("nvarchar(128)"); + + b.Property("ModifiedBy") + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("ModifiedDate") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("MessageId", "MemberId") + .IsUnique() + .HasDatabaseName("IX_PushMessageMember_MessageId_MemberId") + .HasFilter("[MemberId] IS NOT NULL"); + + b.ToTable("PushMessageMember", (string)null); + }); + + modelBuilder.Entity("VirtoCommerce.PushMessages.Data.Models.PushMessageRecipientEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasMaxLength(128) + .HasColumnType("nvarchar(128)"); + + b.Property("CreatedBy") + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("CreatedDate") + .HasColumnType("datetime2"); + + b.Property("IsHidden") + .HasColumnType("bit"); + + b.Property("IsRead") + .HasColumnType("bit"); + + b.Property("MemberId") + .HasMaxLength(128) + .HasColumnType("nvarchar(128)"); + + b.Property("MemberName") + .HasMaxLength(512) + .HasColumnType("nvarchar(512)"); + + b.Property("MessageId") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("nvarchar(128)"); + + b.Property("ModifiedBy") + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("ModifiedDate") + .HasColumnType("datetime2"); + + b.Property("UserId") + .HasMaxLength(128) + .HasColumnType("nvarchar(128)"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("MessageId", "UserId") + .IsUnique() + .HasDatabaseName("IX_PushMessageRecipient_MessageId_UserId") + .HasFilter("[UserId] IS NOT NULL"); + + b.ToTable("PushMessageRecipient", (string)null); + }); + + modelBuilder.Entity("VirtoCommerce.PushMessages.Data.Models.PushMessageMemberEntity", b => + { + b.HasOne("VirtoCommerce.PushMessages.Data.Models.PushMessageEntity", "Message") + .WithMany("Members") + .HasForeignKey("MessageId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Message"); + }); + + modelBuilder.Entity("VirtoCommerce.PushMessages.Data.Models.PushMessageRecipientEntity", b => + { + b.HasOne("VirtoCommerce.PushMessages.Data.Models.PushMessageEntity", "Message") + .WithMany() + .HasForeignKey("MessageId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Message"); + }); + + modelBuilder.Entity("VirtoCommerce.PushMessages.Data.Models.PushMessageEntity", b => + { + b.Navigation("Members"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/VirtoCommerce.PushMessages.Data.SqlServer/Migrations/20240502160835_AddMoreFieldsToMessage.cs b/src/VirtoCommerce.PushMessages.Data.SqlServer/Migrations/20240502160835_AddMoreFieldsToMessage.cs new file mode 100644 index 0000000..cd962fb --- /dev/null +++ b/src/VirtoCommerce.PushMessages.Data.SqlServer/Migrations/20240502160835_AddMoreFieldsToMessage.cs @@ -0,0 +1,40 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace VirtoCommerce.PushMessages.Data.SqlServer.Migrations +{ + /// + public partial class AddMoreFieldsToMessage : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "MemberQuery", + table: "PushMessage", + type: "nvarchar(1024)", + maxLength: 1024, + nullable: true); + + migrationBuilder.AddColumn( + name: "TrackNewRecipients", + table: "PushMessage", + type: "bit", + nullable: false, + defaultValue: false); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "MemberQuery", + table: "PushMessage"); + + migrationBuilder.DropColumn( + name: "TrackNewRecipients", + table: "PushMessage"); + } + } +} diff --git a/src/VirtoCommerce.PushMessages.Data.SqlServer/Migrations/PushMessagesDbContextModelSnapshot.cs b/src/VirtoCommerce.PushMessages.Data.SqlServer/Migrations/PushMessagesDbContextModelSnapshot.cs index c58ceb7..c1d7e3b 100644 --- a/src/VirtoCommerce.PushMessages.Data.SqlServer/Migrations/PushMessagesDbContextModelSnapshot.cs +++ b/src/VirtoCommerce.PushMessages.Data.SqlServer/Migrations/PushMessagesDbContextModelSnapshot.cs @@ -36,6 +36,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("CreatedDate") .HasColumnType("datetime2"); + b.Property("MemberQuery") + .HasMaxLength(1024) + .HasColumnType("nvarchar(1024)"); + b.Property("ModifiedBy") .HasMaxLength(64) .HasColumnType("nvarchar(64)"); @@ -58,6 +62,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasMaxLength(128) .HasColumnType("nvarchar(128)"); + b.Property("TrackNewRecipients") + .HasColumnType("bit"); + b.HasKey("Id"); b.ToTable("PushMessage", (string)null); @@ -175,7 +182,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder.Entity("VirtoCommerce.PushMessages.Data.Models.PushMessageRecipientEntity", b => { b.HasOne("VirtoCommerce.PushMessages.Data.Models.PushMessageEntity", "Message") - .WithMany("Recipients") + .WithMany() .HasForeignKey("MessageId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); @@ -186,8 +193,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder.Entity("VirtoCommerce.PushMessages.Data.Models.PushMessageEntity", b => { b.Navigation("Members"); - - b.Navigation("Recipients"); }); #pragma warning restore 612, 618 } diff --git a/src/VirtoCommerce.PushMessages.Data/BackgroundJobs/PushMessageJobService.cs b/src/VirtoCommerce.PushMessages.Data/BackgroundJobs/PushMessageJobService.cs new file mode 100644 index 0000000..f539338 --- /dev/null +++ b/src/VirtoCommerce.PushMessages.Data/BackgroundJobs/PushMessageJobService.cs @@ -0,0 +1,238 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Hangfire; +using VirtoCommerce.CustomerModule.Core.Model; +using VirtoCommerce.CustomerModule.Core.Model.Search; +using VirtoCommerce.CustomerModule.Core.Services; +using VirtoCommerce.Platform.Core.Common; +using VirtoCommerce.Platform.Core.Security; +using VirtoCommerce.Platform.Core.Settings; +using VirtoCommerce.PushMessages.Core.BackgroundJobs; +using VirtoCommerce.PushMessages.Core.Extensions; +using VirtoCommerce.PushMessages.Core.Models; +using VirtoCommerce.PushMessages.Core.Services; +using VirtoCommerce.PushMessages.Data.Extensions; +using GeneralSettings = VirtoCommerce.PushMessages.Core.ModuleConstants.Settings.General; +using JobSettings = VirtoCommerce.PushMessages.Core.ModuleConstants.Settings.BackgroundJobs; + +namespace VirtoCommerce.PushMessages.Data.BackgroundJobs; + +public class PushMessageJobService : RecurringJobService, IPushMessageJobService +{ + private readonly ISettingsManager _settingsManager; + private readonly IPushMessageService _messageService; + private readonly IPushMessageSearchService _messageSearchService; + private readonly IPushMessageRecipientService _recipientService; + private readonly IPushMessageRecipientSearchService _recipientSearchService; + private readonly IMemberService _memberService; + private readonly IMemberSearchService _memberSearchService; + + public PushMessageJobService( + ISettingsManager settingsManager, + IPushMessageService messageService, + IPushMessageSearchService messageSearchService, + IPushMessageRecipientService recipientService, + IPushMessageRecipientSearchService recipientSearchService, + IMemberService memberService, + IMemberSearchService memberSearchService) + : base(settingsManager) + { + _settingsManager = settingsManager; + _messageService = messageService; + _messageSearchService = messageSearchService; + _recipientService = recipientService; + _recipientSearchService = recipientSearchService; + _memberService = memberService; + _memberSearchService = memberSearchService; + + RecurringJobs.Add(new RecurringJobDescriptor + { + EnableSetting = JobSettings.SendScheduledMessagesRecurringJobEnable, + CronSetting = JobSettings.SendScheduledMessagesRecurringJobCronExpression, + Method = typeof(PushMessageJobService).GetMethod(nameof(SendScheduledMessagesRecurringJob)), + MethodCall = x => x.SendScheduledMessagesRecurringJob(JobCancellationToken.Null), + }); + + RecurringJobs.Add(new RecurringJobDescriptor + { + EnableSetting = JobSettings.TrackNewRecipientsRecurringJobEnable, + CronSetting = JobSettings.TrackNewRecipientsRecurringJobCronExpression, + Method = typeof(PushMessageJobService).GetMethod(nameof(TrackNewRecipientsRecurringJob)), + MethodCall = x => x.TrackNewRecipientsRecurringJob(JobCancellationToken.Null), + }); + } + + public void EnqueueAddRecipients(IList messageIds = null) + { + if (messageIds?.Count > 0) + { + BackgroundJob.Enqueue(x => x.AddRecipientsJob(messageIds, JobCancellationToken.Null)); + } + else + { + BackgroundJob.Enqueue(x => x.TrackNewRecipientsRecurringJob(JobCancellationToken.Null)); + } + } + + [DisableConcurrentExecution(10)] + public async Task SendScheduledMessagesRecurringJob(IJobCancellationToken cancellationToken) + { + var searchCriteria = AbstractTypeFactory.TryCreateInstance(); + searchCriteria.Statuses = [PushMessageStatus.Scheduled]; + searchCriteria.StartDateBefore = DateTime.UtcNow; + searchCriteria.Sort = $"{nameof(PushMessage.StartDate)};{nameof(PushMessage.CreatedDate)}"; + searchCriteria.Take = await _settingsManager.GetValueAsync(GeneralSettings.BatchSize); + + await _messageSearchService.SearchWhileResultIsNotEmpty(searchCriteria, async searchResult => + { + cancellationToken.ThrowIfCancellationRequested(); + + searchResult.Results.Apply(x => x.Status = PushMessageStatus.Sent); + await _messageService.SaveChangesAsync(searchResult.Results); + }); + } + + [DisableConcurrentExecution(10)] + public async Task TrackNewRecipientsRecurringJob(IJobCancellationToken cancellationToken) + { + var searchCriteria = AbstractTypeFactory.TryCreateInstance(); + searchCriteria.Statuses = [PushMessageStatus.Sent]; + searchCriteria.TrackNewRecipients = true; + searchCriteria.CreatedDateBefore = DateTime.UtcNow; + searchCriteria.ResponseGroup = PushMessageResponseGroup.WithMembers.ToString(); + searchCriteria.Take = await _settingsManager.GetValueAsync(GeneralSettings.BatchSize); + + await foreach (var searchResult in _messageSearchService.SearchBatchesNoCloneAsync(searchCriteria)) + { + foreach (var message in searchResult.Results) + { + cancellationToken.ThrowIfCancellationRequested(); + + await AddRecipients(message); + } + } + } + + public async Task AddRecipientsJob(IList messageIds, IJobCancellationToken cancellationToken) + { + foreach (var messageId in messageIds) + { + cancellationToken.ThrowIfCancellationRequested(); + + var message = await _messageService.GetNoCloneAsync(messageId, PushMessageResponseGroup.WithMembers.ToString()); + if (message != null) + { + await AddRecipients(message); + } + } + } + + private async Task AddRecipients(PushMessage message) + { + var oldUserIds = await GetExistingRecipientUserIds(message.Id); + var recipients = await GetNewRecipients(message, oldUserIds); + + if (recipients.Count > 0) + { + await _recipientService.SaveChangesAsync(recipients); + } + } + + private async Task> GetExistingRecipientUserIds(string messageId) + { + var searchCriteria = AbstractTypeFactory.TryCreateInstance(); + searchCriteria.MessageId = messageId; + searchCriteria.WithHidden = true; + searchCriteria.Take = await _settingsManager.GetValueAsync(GeneralSettings.BatchSize); + + var userIds = new HashSet(); + + await foreach (var searchResult in _recipientSearchService.SearchBatchesNoCloneAsync(searchCriteria)) + { + searchResult.Results.Apply(x => userIds.Add(x.UserId)); + } + + return userIds; + } + + private async Task> GetNewRecipients(PushMessage message, HashSet userIds) + { + var recipients = new List(); + var memberIds = new HashSet(StringComparer.OrdinalIgnoreCase); + + var searchCriteria = AbstractTypeFactory.TryCreateInstance(); + searchCriteria.ResponseGroup = MemberResponseGroup.WithSecurityAccounts.ToString(); + searchCriteria.Take = await _settingsManager.GetValueAsync(GeneralSettings.BatchSize); + var queue = new Queue(); + + if (!message.MemberIds.IsNullOrEmpty()) + { + var members = await _memberService.GetByIdsAsync(message.MemberIds.ToArray(), searchCriteria.ResponseGroup); + members.Apply(EnqueueMember); + } + + if (!string.IsNullOrEmpty(message.MemberQuery)) + { + await EnqueueMembers(keyword: message.MemberQuery); + } + + while (queue.TryDequeue(out var member)) + { + if (member is IHasSecurityAccounts hasSecurityAccounts) + { + foreach (var user in hasSecurityAccounts.SecurityAccounts) + { + AddRecipient(member, user); + } + } + else + { + await EnqueueMembers(memberId: member.Id); + } + } + + return recipients; + + async Task EnqueueMembers(string keyword = null, string memberId = null) + { + searchCriteria.Keyword = keyword; + searchCriteria.MemberId = memberId; + searchCriteria.DeepSearch = !string.IsNullOrEmpty(keyword); + + await foreach (var searchResult in _memberSearchService.SearchBatchesAsync(searchCriteria)) + { + searchResult.Results.Apply(EnqueueMember); + } + } + + void EnqueueMember(Member member) + { + if (memberIds.Add(member.Id)) + { + queue.Enqueue(member); + } + } + + void AddRecipient(Member member, ApplicationUser user) + { + if (userIds.Add(user.Id)) + { + recipients.Add(GetRecipient(message, member, user)); + } + } + } + + private static PushMessageRecipient GetRecipient(PushMessage message, Member member, ApplicationUser user) + { + var recipient = AbstractTypeFactory.TryCreateInstance(); + recipient.MessageId = message.Id; + recipient.MemberId = member.Id; + recipient.MemberName = member.Name; + recipient.UserId = user.Id; + recipient.UserName = user.UserName; + + return recipient; + } +} diff --git a/src/VirtoCommerce.PushMessages.Data/BackgroundJobs/PushMessageJobs.cs b/src/VirtoCommerce.PushMessages.Data/BackgroundJobs/PushMessageJobs.cs deleted file mode 100644 index 23a6afb..0000000 --- a/src/VirtoCommerce.PushMessages.Data/BackgroundJobs/PushMessageJobs.cs +++ /dev/null @@ -1,90 +0,0 @@ -using System; -using System.Linq; -using System.Reflection; -using System.Threading.Tasks; -using Hangfire; -using VirtoCommerce.Platform.Core.Common; -using VirtoCommerce.Platform.Core.Settings; -using VirtoCommerce.PushMessages.Core.BackgroundJobs; -using VirtoCommerce.PushMessages.Core.Extensions; -using VirtoCommerce.PushMessages.Core.Models; -using VirtoCommerce.PushMessages.Core.Services; -using GeneralSettings = VirtoCommerce.PushMessages.Core.ModuleConstants.Settings.General; -using JobSettings = VirtoCommerce.PushMessages.Core.ModuleConstants.Settings.BackgroundJobs; - -namespace VirtoCommerce.PushMessages.Data.BackgroundJobs; - -public class PushMessageJobs : IPushMessageJobService -{ - private static readonly MethodInfo _recurringJobMethod = typeof(PushMessageJobs).GetMethod(nameof(SendScheduledMessages)); - - private readonly IPushMessageService _crudService; - private readonly IPushMessageSearchService _searchService; - private readonly ISettingsManager _settingsManager; - - public PushMessageJobs( - IPushMessageService crudService, - IPushMessageSearchService searchService, - ISettingsManager settingsManager) - { - _crudService = crudService; - _searchService = searchService; - _settingsManager = settingsManager; - } - - [DisableConcurrentExecution(10)] - public async Task SendScheduledMessages(IJobCancellationToken cancellationToken) - { - var searchCriteria = AbstractTypeFactory.TryCreateInstance(); - searchCriteria.StartDateBefore = DateTime.UtcNow; - searchCriteria.Statuses = [PushMessageStatus.Scheduled]; - searchCriteria.Sort = $"{nameof(PushMessage.StartDate)};{nameof(PushMessage.CreatedDate)}"; - searchCriteria.Take = await _settingsManager.GetValueAsync(GeneralSettings.BatchSize); - - await _searchService.SearchWhileResultIsNotEmpty(searchCriteria, async searchResult => - { - cancellationToken.ThrowIfCancellationRequested(); - - searchResult.Results.Apply(x => x.Status = PushMessageStatus.Sent); - await _crudService.SaveChangesAsync(searchResult.Results); - }); - } - - public async Task StartStopRecurringJobs() - { - const string recurringJobId = $"{nameof(PushMessageJobs)}.{nameof(SendScheduledMessages)}"; - var enableJobs = await _settingsManager.GetValueAsync(JobSettings.Enable); - - if (enableJobs) - { - var cronExpression = await _settingsManager.GetValueAsync(JobSettings.CronExpression); - RecurringJob.AddOrUpdate(recurringJobId, x => x.SendScheduledMessages(JobCancellationToken.Null), cronExpression); - } - else - { - CancelProcessingJobs(_recurringJobMethod); - RecurringJob.RemoveIfExists(recurringJobId); - } - } - - - private static void CancelProcessingJobs(MethodInfo method) - { - var processingJobs = JobStorage.Current.GetMonitoringApi().ProcessingJobs(0, int.MaxValue); - var (jobId, _) = processingJobs.FirstOrDefault(x => x.Value?.Job?.Method == method); - - if (string.IsNullOrEmpty(jobId)) - { - return; - } - - try - { - BackgroundJob.Delete(jobId); - } - catch - { - // Ignore concurrency exceptions, when somebody else cancelled it as well. - } - } -} diff --git a/src/VirtoCommerce.PushMessages.Data/BackgroundJobs/RecurringJobService.cs b/src/VirtoCommerce.PushMessages.Data/BackgroundJobs/RecurringJobService.cs new file mode 100644 index 0000000..b447023 --- /dev/null +++ b/src/VirtoCommerce.PushMessages.Data/BackgroundJobs/RecurringJobService.cs @@ -0,0 +1,78 @@ +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Threading.Tasks; +using Hangfire; +using VirtoCommerce.Platform.Core.Common; +using VirtoCommerce.Platform.Core.Settings; +using VirtoCommerce.Platform.Core.Settings.Events; +using VirtoCommerce.PushMessages.Core.BackgroundJobs; + +namespace VirtoCommerce.PushMessages.Data.BackgroundJobs; + +public abstract class RecurringJobService : IRecurringJobService +{ + private readonly ISettingsManager _settingsManager; + + protected IList> RecurringJobs { get; } = []; + + protected RecurringJobService(ISettingsManager settingsManager) + { + _settingsManager = settingsManager; + } + + public async Task StartStopRecurringJobs() + { + foreach (var job in RecurringJobs) + { + await StartStopRecurringJob(job); + } + } + + public virtual async Task Handle(ObjectSettingChangedEvent message) + { + foreach (var settingName in message.ChangedEntries + .Where(x => x.EntryState is EntryState.Modified or EntryState.Added) + .Select(x => x.NewEntry.Name)) + { + var job = RecurringJobs.FirstOrDefault(job => job.EnableSetting.Name == settingName || job.CronSetting.Name == settingName); + if (job != null) + { + await StartStopRecurringJob(job); + } + } + } + + private async Task StartStopRecurringJob(RecurringJobDescriptor job) + { + var recurringJobId = $"{job.Method.DeclaringType?.Name}.{job.Method.Name}"; + + if (await _settingsManager.GetValueAsync(job.EnableSetting)) + { + var cronExpression = await _settingsManager.GetValueAsync(job.CronSetting); + RecurringJob.AddOrUpdate(recurringJobId, job.MethodCall, cronExpression); + } + else + { + RecurringJob.RemoveIfExists(recurringJobId); + CancelProcessingJobs(job.Method); + } + } + + private static void CancelProcessingJobs(MethodInfo method) + { + var processingJobs = JobStorage.Current.GetMonitoringApi().ProcessingJobs(0, int.MaxValue); + + foreach (var (jobId, _) in processingJobs.Where(x => x.Value?.Job?.Method == method)) + { + try + { + BackgroundJob.Delete(jobId); + } + catch + { + // Ignore concurrency exceptions, when somebody else cancelled it as well. + } + } + } +} diff --git a/src/VirtoCommerce.PushMessages.Data/Handlers/MemberChangedEventHandler.cs b/src/VirtoCommerce.PushMessages.Data/Handlers/MemberChangedEventHandler.cs new file mode 100644 index 0000000..dd18512 --- /dev/null +++ b/src/VirtoCommerce.PushMessages.Data/Handlers/MemberChangedEventHandler.cs @@ -0,0 +1,28 @@ +using System.Linq; +using System.Threading.Tasks; +using VirtoCommerce.CustomerModule.Core.Events; +using VirtoCommerce.Platform.Core.Common; +using VirtoCommerce.Platform.Core.Events; +using VirtoCommerce.PushMessages.Core.BackgroundJobs; + +namespace VirtoCommerce.PushMessages.Data.Handlers; + +public class MemberChangedEventHandler : IEventHandler +{ + private readonly IPushMessageJobService _pushMessageJobService; + + public MemberChangedEventHandler(IPushMessageJobService pushMessageJobService) + { + _pushMessageJobService = pushMessageJobService; + } + + public Task Handle(MemberChangedEvent message) + { + if (message.ChangedEntries.Any(x => x.EntryState is EntryState.Added or EntryState.Modified)) + { + _pushMessageJobService.EnqueueAddRecipients(); + } + + return Task.CompletedTask; + } +} diff --git a/src/VirtoCommerce.PushMessages.Data/Handlers/ObjectSettingEntryChangedEventHandler.cs b/src/VirtoCommerce.PushMessages.Data/Handlers/ObjectSettingEntryChangedEventHandler.cs deleted file mode 100644 index 7da9d8c..0000000 --- a/src/VirtoCommerce.PushMessages.Data/Handlers/ObjectSettingEntryChangedEventHandler.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System.Linq; -using System.Threading.Tasks; -using VirtoCommerce.Platform.Core.Common; -using VirtoCommerce.Platform.Core.Events; -using VirtoCommerce.Platform.Core.Settings.Events; -using VirtoCommerce.PushMessages.Core.BackgroundJobs; -using JobSettings = VirtoCommerce.PushMessages.Core.ModuleConstants.Settings.BackgroundJobs; - -namespace VirtoCommerce.PushMessages.Data.Handlers; - -public class ObjectSettingEntryChangedEventHandler : IEventHandler -{ - private readonly IPushMessageJobService _pushMessageJobService; - - public ObjectSettingEntryChangedEventHandler(IPushMessageJobService pushMessageJobService) - { - _pushMessageJobService = pushMessageJobService; - } - - public virtual async Task Handle(ObjectSettingChangedEvent message) - { - if (message.ChangedEntries.Any(x => x.EntryState is EntryState.Modified or EntryState.Added && - (x.NewEntry.Name == JobSettings.Enable.Name || - x.NewEntry.Name == JobSettings.CronExpression.Name))) - { - await _pushMessageJobService.StartStopRecurringJobs(); - } - } -} diff --git a/src/VirtoCommerce.PushMessages.Data/Handlers/PushMessageChangedEventHandler.cs b/src/VirtoCommerce.PushMessages.Data/Handlers/PushMessageChangedEventHandler.cs new file mode 100644 index 0000000..5ab5355 --- /dev/null +++ b/src/VirtoCommerce.PushMessages.Data/Handlers/PushMessageChangedEventHandler.cs @@ -0,0 +1,43 @@ +using System.Linq; +using System.Threading.Tasks; +using VirtoCommerce.Platform.Core.Common; +using VirtoCommerce.Platform.Core.Events; +using VirtoCommerce.PushMessages.Core.BackgroundJobs; +using VirtoCommerce.PushMessages.Core.Events; +using VirtoCommerce.PushMessages.Core.Models; + +namespace VirtoCommerce.PushMessages.Data.Handlers; + +public class PushMessageChangedEventHandler : IEventHandler +{ + private readonly IPushMessageJobService _pushMessageJobService; + + public PushMessageChangedEventHandler(IPushMessageJobService pushMessageJobService) + { + _pushMessageJobService = pushMessageJobService; + } + + public Task Handle(PushMessageChangedEvent message) + { + // Add recipients if: + // - new message with status Sent + // - modified message with status changed to Sent + // - modified message with status Sent and TrackNewRecipients changed to true + + var messageIds = message.ChangedEntries + .Where(x => + x.NewEntry.Status == PushMessageStatus.Sent && x.NewEntry.HasRecipients() && ( + x.EntryState == EntryState.Added || + x.EntryState == EntryState.Modified && x.OldEntry.Status != PushMessageStatus.Sent || + x.EntryState == EntryState.Modified && x.NewEntry.TrackNewRecipients && !x.OldEntry.TrackNewRecipients)) + .Select(x => x.NewEntry.Id) + .ToList(); + + if (messageIds.Count > 0) + { + _pushMessageJobService.EnqueueAddRecipients(messageIds); + } + + return Task.CompletedTask; + } +} diff --git a/src/VirtoCommerce.PushMessages.Data/Models/PushMessageEntity.cs b/src/VirtoCommerce.PushMessages.Data/Models/PushMessageEntity.cs index 513a162..a7fb030 100644 --- a/src/VirtoCommerce.PushMessages.Data/Models/PushMessageEntity.cs +++ b/src/VirtoCommerce.PushMessages.Data/Models/PushMessageEntity.cs @@ -21,9 +21,12 @@ public class PushMessageEntity : AuditableEntity, IDataEntity Members { get; set; } = new NullCollection(); + public bool TrackNewRecipients { get; set; } + + [StringLength(1024)] + public string MemberQuery { get; set; } - public virtual ObservableCollection Recipients { get; set; } = new NullCollection(); + public virtual ObservableCollection Members { get; set; } = new NullCollection(); public virtual PushMessage ToModel(PushMessage model) { @@ -37,6 +40,8 @@ public virtual PushMessage ToModel(PushMessage model) model.ShortMessage = ShortMessage; model.StartDate = StartDate; model.Status = Status; + model.TrackNewRecipients = TrackNewRecipients; + model.MemberQuery = MemberQuery; model.MemberIds = Members.OrderBy(x => x.MemberId).Select(x => x.MemberId).ToList(); return model; @@ -56,6 +61,8 @@ public virtual PushMessageEntity FromModel(PushMessage model, PrimaryKeyResolvin ShortMessage = model.ShortMessage; StartDate = model.StartDate; Status = model.Status; + TrackNewRecipients = model.TrackNewRecipients; + MemberQuery = model.MemberQuery; if (model.MemberIds != null) { @@ -75,6 +82,8 @@ public virtual void Patch(PushMessageEntity target) target.ShortMessage = ShortMessage; target.StartDate = StartDate; target.Status = Status; + target.TrackNewRecipients = TrackNewRecipients; + target.MemberQuery = MemberQuery; if (!Members.IsNullCollection()) { diff --git a/src/VirtoCommerce.PushMessages.Data/Repositories/PushMessagesDbContext.cs b/src/VirtoCommerce.PushMessages.Data/Repositories/PushMessagesDbContext.cs index 01fbf15..a58837a 100644 --- a/src/VirtoCommerce.PushMessages.Data/Repositories/PushMessagesDbContext.cs +++ b/src/VirtoCommerce.PushMessages.Data/Repositories/PushMessagesDbContext.cs @@ -35,7 +35,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) modelBuilder.Entity().ToTable("PushMessageRecipient").HasKey(x => x.Id); modelBuilder.Entity().Property(x => x.Id).HasMaxLength(128).ValueGeneratedOnAdd(); - modelBuilder.Entity().HasOne(x => x.Message).WithMany(x => x.Recipients) + modelBuilder.Entity().HasOne(x => x.Message).WithMany() .HasForeignKey(x => x.MessageId).OnDelete(DeleteBehavior.Cascade).IsRequired(); modelBuilder.Entity() .HasIndex(x => new { x.MessageId, x.UserId }) diff --git a/src/VirtoCommerce.PushMessages.Data/Services/PushMessageRecipientSearchService.cs b/src/VirtoCommerce.PushMessages.Data/Services/PushMessageRecipientSearchService.cs index d3384fc..684151b 100644 --- a/src/VirtoCommerce.PushMessages.Data/Services/PushMessageRecipientSearchService.cs +++ b/src/VirtoCommerce.PushMessages.Data/Services/PushMessageRecipientSearchService.cs @@ -68,7 +68,7 @@ protected override IList BuildSortExpression(PushMessageRecipientSearc { sortInfos = [ - new SortInfo { SortColumn = nameof(PushMessageEntity.CreatedDate), SortDirection = SortDirection.Descending }, + new SortInfo { SortColumn = nameof(PushMessageRecipientEntity.CreatedDate), SortDirection = SortDirection.Descending }, new SortInfo { SortColumn = nameof(PushMessageRecipientEntity.Id) }, ]; } diff --git a/src/VirtoCommerce.PushMessages.Data/Services/PushMessageSearchService.cs b/src/VirtoCommerce.PushMessages.Data/Services/PushMessageSearchService.cs index b13de1f..953ce58 100644 --- a/src/VirtoCommerce.PushMessages.Data/Services/PushMessageSearchService.cs +++ b/src/VirtoCommerce.PushMessages.Data/Services/PushMessageSearchService.cs @@ -30,9 +30,30 @@ protected override IQueryable BuildQuery(IRepository reposito if (!string.IsNullOrEmpty(criteria.Keyword)) { - query = query.Where(x => x.ShortMessage.Contains(criteria.Keyword) || - x.CreatedBy.Contains(criteria.Keyword) || - x.Id.Contains(criteria.Keyword)); + query = query.Where(x => + x.Topic.Contains(criteria.Keyword) || + x.ShortMessage.Contains(criteria.Keyword) || + x.MemberQuery.Contains(criteria.Keyword) || + x.ModifiedBy.Contains(criteria.Keyword) || + x.CreatedBy.Contains(criteria.Keyword) || + x.Id.Contains(criteria.Keyword)); + } + + if (criteria.IsDraft != null) + { + query = criteria.IsDraft.Value + ? query.Where(x => x.Status == PushMessageStatus.Draft) + : query.Where(x => x.Status != PushMessageStatus.Draft); + } + + if (criteria.TrackNewRecipients != null) + { + query = query.Where(x => x.TrackNewRecipients == criteria.TrackNewRecipients); + } + + if (criteria.CreatedDateBefore != null) + { + query = query.Where(x => x.CreatedDate <= criteria.CreatedDateBefore); } if (criteria.StartDateBefore != null) diff --git a/src/VirtoCommerce.PushMessages.Data/Services/PushMessageService.cs b/src/VirtoCommerce.PushMessages.Data/Services/PushMessageService.cs index 1a93ebd..3994ad9 100644 --- a/src/VirtoCommerce.PushMessages.Data/Services/PushMessageService.cs +++ b/src/VirtoCommerce.PushMessages.Data/Services/PushMessageService.cs @@ -2,20 +2,14 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using VirtoCommerce.CustomerModule.Core.Model; -using VirtoCommerce.CustomerModule.Core.Model.Search; -using VirtoCommerce.CustomerModule.Core.Services; using VirtoCommerce.Platform.Caching; using VirtoCommerce.Platform.Core.Caching; using VirtoCommerce.Platform.Core.Common; using VirtoCommerce.Platform.Core.Events; -using VirtoCommerce.Platform.Core.Security; using VirtoCommerce.Platform.Data.GenericCrud; using VirtoCommerce.PushMessages.Core.Events; -using VirtoCommerce.PushMessages.Core.Extensions; using VirtoCommerce.PushMessages.Core.Models; using VirtoCommerce.PushMessages.Core.Services; -using VirtoCommerce.PushMessages.Data.Extensions; using VirtoCommerce.PushMessages.Data.Models; using VirtoCommerce.PushMessages.Data.Repositories; @@ -23,35 +17,39 @@ namespace VirtoCommerce.PushMessages.Data.Services; public class PushMessageService : CrudService, IPushMessageService { - private readonly IMemberService _memberService; - private readonly IMemberSearchService _memberSearchService; - private readonly IPushMessageRecipientService _recipientService; - public PushMessageService( Func repositoryFactory, IPlatformMemoryCache platformMemoryCache, - IEventPublisher eventPublisher, - IMemberService memberService, - IMemberSearchService memberSearchService, - IPushMessageRecipientService recipientService) + IEventPublisher eventPublisher) : base(repositoryFactory, platformMemoryCache, eventPublisher) { - _memberService = memberService; - _memberSearchService = memberSearchService; - _recipientService = recipientService; + } + + public virtual async Task ChangeTracking(string messageId, bool value) + { + var message = await this.GetByIdAsync(messageId); + + if (message != null && message.TrackNewRecipients != value) + { + message.TrackNewRecipients = value; + + // Skip status validation + await base.SaveChangesAsync([message]); + } + + return message; } public override async Task SaveChangesAsync(IList models) { var ids = models.Where(x => x.Id != null).Select(x => x.Id).ToList(); - var existingModels = await CheckStatusAsync(ids); - UpdateStatus(models, existingModels); + await ValidateStatusAsync(ids); await base.SaveChangesAsync(models); } public override async Task DeleteAsync(IList ids, bool softDelete = false) { - await CheckStatusAsync(ids); + await ValidateStatusAsync(ids); await base.DeleteAsync(ids, softDelete); } @@ -61,12 +59,6 @@ protected override Task> LoadEntities(IRepository repos return ((IPushMessagesRepository)repository).GetMessagesByIdsAsync(ids, responseGroup); } - protected override async Task AfterSaveChangesAsync(IList models, IList> changedEntries) - { - await base.AfterSaveChangesAsync(models, changedEntries); - await AddRecipients(changedEntries); - } - protected override void ClearCache(IList models) { base.ClearCache(models); @@ -76,7 +68,7 @@ protected override void ClearCache(IList models) } - private async Task> CheckStatusAsync(IList ids) + private async Task ValidateStatusAsync(IList ids) { var models = await GetAsync(ids); @@ -84,91 +76,5 @@ private async Task> CheckStatusAsync(IList ids) { throw new InvalidOperationException($"Cannot modify or delete messages with status {PushMessageStatus.Sent}."); } - - return models; - } - - private static void UpdateStatus(IList newModels, IList oldModels) - { - foreach (var newModel in newModels) - { - var oldModel = oldModels.FirstOrDefault(x => x.Id.EqualsInvariant(newModel.Id)); - - if (oldModel is null) - { - newModel.Status = newModel.StartDate > DateTime.UtcNow - ? PushMessageStatus.Scheduled - : PushMessageStatus.Sent; - } - } - } - - private async Task AddRecipients(IList> changedEntries) - { - foreach (var changedEntry in changedEntries.Where(x => - x.IsSent() && - x.NewEntry.MemberIds.Count > 0)) - { - var message = changedEntry.NewEntry; - var recipients = await GetRecipients(message); - - if (recipients.Count > 0) - { - // TODO: Use batches when saving - await _recipientService.SaveChangesAsync(recipients); - - message.UserIds = recipients.Select(x => x.UserId).ToList(); - } - } - } - - private async Task> GetRecipients(PushMessage message) - { - List recipients = []; - var userIds = new HashSet(StringComparer.OrdinalIgnoreCase); - - var searchCriteria = AbstractTypeFactory.TryCreateInstance(); - searchCriteria.ResponseGroup = MemberResponseGroup.WithSecurityAccounts.ToString(); - searchCriteria.Take = 50; - - var members = await _memberService.GetByIdsAsync(message.MemberIds.ToArray(), searchCriteria.ResponseGroup); - var queue = new Queue(members); - - while (queue.TryDequeue(out var member)) - { - if (member is IHasSecurityAccounts hasSecurityAccounts) - { - var users = hasSecurityAccounts.SecurityAccounts.Where(x => !userIds.Contains(x.Id)).ToList(); - - foreach (var user in users) - { - userIds.Add(user.Id); - recipients.Add(GetRecipient(message, member, user)); - } - } - else - { - searchCriteria.MemberId = member.Id; - - await foreach (var searchResult in _memberSearchService.SearchBatchesAsync(searchCriteria)) - { - searchResult.Results.Apply(queue.Enqueue); - } - } - } - - return recipients; - } - - private static PushMessageRecipient GetRecipient(PushMessage message, Member member, ApplicationUser user) - { - var recipient = AbstractTypeFactory.TryCreateInstance(); - recipient.MessageId = message.Id; - recipient.MemberId = member.Id; - recipient.MemberName = member.Name; - recipient.UserId = user.Id; - recipient.UserName = user.UserName; - - return recipient; } } diff --git a/src/VirtoCommerce.PushMessages.ExperienceApi/Handlers/PushMessageChangedEventHandler.cs b/src/VirtoCommerce.PushMessages.ExperienceApi/Handlers/PushMessageChangedEventHandler.cs deleted file mode 100644 index 532eb03..0000000 --- a/src/VirtoCommerce.PushMessages.ExperienceApi/Handlers/PushMessageChangedEventHandler.cs +++ /dev/null @@ -1,43 +0,0 @@ -using System.Linq; -using System.Threading.Tasks; -using VirtoCommerce.Platform.Core.Events; -using VirtoCommerce.PushMessages.Core.Events; -using VirtoCommerce.PushMessages.Core.Extensions; -using VirtoCommerce.PushMessages.ExperienceApi.Models; -using VirtoCommerce.PushMessages.ExperienceApi.Subscriptions; - -namespace VirtoCommerce.PushMessages.ExperienceApi.Handlers -{ - public class PushMessageChangedEventHandler : IEventHandler - { - private readonly IPushMessageHub _eventBroker; - - public PushMessageChangedEventHandler(IPushMessageHub eventBroker) - { - _eventBroker = eventBroker; - } - - public async Task Handle(PushMessageChangedEvent message) - { - foreach (var pushMessage in message.ChangedEntries - .Where(x => - x.IsSent() && - x.NewEntry.UserIds?.Count > 0) - .Select(x => x.NewEntry)) - { - foreach (var userId in pushMessage.UserIds) - { - var expPushMessage = new ExpPushMessage - { - Id = pushMessage.Id, - ShortMessage = pushMessage.ShortMessage, - CreatedDate = pushMessage.CreatedDate, - UserId = userId, - }; - - await _eventBroker.AddMessageAsync(expPushMessage); - } - } - } - } -} diff --git a/src/VirtoCommerce.PushMessages.ExperienceApi/Handlers/PushMessageRecipientChangedEventHandler.cs b/src/VirtoCommerce.PushMessages.ExperienceApi/Handlers/PushMessageRecipientChangedEventHandler.cs new file mode 100644 index 0000000..ba6c26e --- /dev/null +++ b/src/VirtoCommerce.PushMessages.ExperienceApi/Handlers/PushMessageRecipientChangedEventHandler.cs @@ -0,0 +1,41 @@ +using System.Linq; +using System.Threading.Tasks; +using VirtoCommerce.Platform.Core.Common; +using VirtoCommerce.Platform.Core.Events; +using VirtoCommerce.PushMessages.Core.Events; +using VirtoCommerce.PushMessages.Core.Services; +using VirtoCommerce.PushMessages.ExperienceApi.Models; +using VirtoCommerce.PushMessages.ExperienceApi.Subscriptions; + +namespace VirtoCommerce.PushMessages.ExperienceApi.Handlers; + +public class PushMessageRecipientChangedEventHandler : IEventHandler +{ + private readonly IPushMessageService _pushMessageService; + private readonly IPushMessageHub _eventBroker; + + public PushMessageRecipientChangedEventHandler( + IPushMessageService pushMessageService, + IPushMessageHub eventBroker) + { + _pushMessageService = pushMessageService; + _eventBroker = eventBroker; + } + + public async Task Handle(PushMessageRecipientChangedEvent message) + { + foreach (var (messageId, recipients) in message.ChangedEntries + .Where(x => x.EntryState == EntryState.Added) + .GroupBy(x => x.NewEntry.MessageId) + .ToDictionary(g => g.Key, g => g.Select(x => x.NewEntry))) + { + var pushMessage = await _pushMessageService.GetNoCloneAsync(messageId); + + foreach (var recipient in recipients) + { + var expPushMessage = ExpPushMessage.Create(pushMessage, recipient); + await _eventBroker.AddMessageAsync(expPushMessage); + } + } + } +} diff --git a/src/VirtoCommerce.PushMessages.ExperienceApi/Models/ExpPushMessage.cs b/src/VirtoCommerce.PushMessages.ExperienceApi/Models/ExpPushMessage.cs index 64bf8b7..af8be59 100644 --- a/src/VirtoCommerce.PushMessages.ExperienceApi/Models/ExpPushMessage.cs +++ b/src/VirtoCommerce.PushMessages.ExperienceApi/Models/ExpPushMessage.cs @@ -1,4 +1,6 @@ using System; +using VirtoCommerce.Platform.Core.Common; +using VirtoCommerce.PushMessages.Core.Models; namespace VirtoCommerce.PushMessages.ExperienceApi.Models { @@ -15,5 +17,19 @@ public class ExpPushMessage public bool IsRead { get; set; } public bool IsHidden { get; set; } + + public static ExpPushMessage Create(PushMessage message, PushMessageRecipient recipient) + { + var expPushMessage = AbstractTypeFactory.TryCreateInstance(); + + expPushMessage.Id = message.Id; + expPushMessage.ShortMessage = message.ShortMessage; + expPushMessage.CreatedDate = recipient.CreatedDate; + expPushMessage.UserId = recipient.UserId; + expPushMessage.IsRead = recipient.IsRead; + expPushMessage.IsHidden = recipient.IsHidden; + + return expPushMessage; + } } } diff --git a/src/VirtoCommerce.PushMessages.ExperienceApi/Queries/GetPushMessagesQueryHandler.cs b/src/VirtoCommerce.PushMessages.ExperienceApi/Queries/GetPushMessagesQueryHandler.cs index 85f358f..944e62b 100644 --- a/src/VirtoCommerce.PushMessages.ExperienceApi/Queries/GetPushMessagesQueryHandler.cs +++ b/src/VirtoCommerce.PushMessages.ExperienceApi/Queries/GetPushMessagesQueryHandler.cs @@ -24,26 +24,12 @@ public async Task Handle(GetPushMessagesQuery request, var searchResult = await _recipientSearchService.SearchAsync(criteria); var result = AbstractTypeFactory.TryCreateInstance(); - result.Results = searchResult.Results.Select(ToExpPushMessage).ToList(); + result.Results = searchResult.Results.Select(x => ExpPushMessage.Create(x.Message, x)).ToList(); result.TotalCount = searchResult.TotalCount; return result; } - private static ExpPushMessage ToExpPushMessage(PushMessageRecipient recipient) - { - var message = AbstractTypeFactory.TryCreateInstance(); - - message.Id = recipient.Message.Id; - message.ShortMessage = recipient.Message.ShortMessage; - message.CreatedDate = recipient.Message.CreatedDate; - message.UserId = recipient.UserId; - message.IsRead = recipient.IsRead; - message.IsHidden = recipient.IsHidden; - - return message; - } - private static PushMessageRecipientSearchCriteria GetSearchCriteria(GetPushMessagesQuery request) { var criteria = AbstractTypeFactory.TryCreateInstance(); diff --git a/src/VirtoCommerce.PushMessages.Web/App/package.json b/src/VirtoCommerce.PushMessages.Web/App/package.json index b573093..7850cc4 100644 --- a/src/VirtoCommerce.PushMessages.Web/App/package.json +++ b/src/VirtoCommerce.PushMessages.Web/App/package.json @@ -23,9 +23,9 @@ "@types/node": "^20.10.5", "@typescript-eslint/eslint-plugin": "^6.16.0", "@typescript-eslint/parser": "^6.16.0", - "@vc-shell/api-client-generator": "^1.0.178", - "@vc-shell/release-config": "^1.0.178", - "@vc-shell/ts-config": "^1.0.178", + "@vc-shell/api-client-generator": "^1.0.208", + "@vc-shell/release-config": "^1.0.208", + "@vc-shell/ts-config": "^1.0.208", "@vitejs/plugin-vue": "^5.0.3", "@vue/eslint-config-prettier": "^9.0.0", "@vue/eslint-config-typescript": "^12.0.0", @@ -55,8 +55,8 @@ }, "dependencies": { "@fortawesome/fontawesome-free": "^5.15.3", - "@vc-shell/config-generator": "^1.0.188", - "@vc-shell/framework": "^1.0.188", + "@vc-shell/config-generator": "^1.0.208", + "@vc-shell/framework": "^1.0.208", "@vueuse/core": "^10.7.1", "@vueuse/integrations": "^10.7.1", "moment": "^2.30.1", diff --git a/src/VirtoCommerce.PushMessages.Web/App/src/api_client/virtocommerce.pushmessages.ts b/src/VirtoCommerce.PushMessages.Web/App/src/api_client/virtocommerce.pushmessages.ts index 060f33d..97515e2 100644 --- a/src/VirtoCommerce.PushMessages.Web/App/src/api_client/virtocommerce.pushmessages.ts +++ b/src/VirtoCommerce.PushMessages.Web/App/src/api_client/virtocommerce.pushmessages.ts @@ -297,6 +297,59 @@ export class PushMessageClient extends AuthApiBase { return Promise.resolve(null as any); } + /** + * @return Success + */ + changeTracking(id: string, value: boolean): Promise { + let url_ = this.baseUrl + "/api/push-message/{id}/tracking/{value}"; + if (id === undefined || id === null) + throw new Error("The parameter 'id' must be defined."); + url_ = url_.replace("{id}", encodeURIComponent("" + id)); + if (value === undefined || value === null) + throw new Error("The parameter 'value' must be defined."); + url_ = url_.replace("{value}", encodeURIComponent("" + value)); + url_ = url_.replace(/[?&]$/, ""); + + let options_: RequestInit = { + method: "PUT", + headers: { + "Accept": "text/plain" + } + }; + + return this.transformOptions(options_).then(transformedOptions_ => { + return this.http.fetch(url_, transformedOptions_); + }).then((_response: Response) => { + return this.processChangeTracking(_response); + }); + } + + protected processChangeTracking(response: Response): Promise { + const status = response.status; + let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); }; + if (status === 200) { + return response.text().then((_responseText) => { + let result200: any = null; + let resultData200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver); + result200 = PushMessage.fromJS(resultData200); + return result200; + }); + } else if (status === 401) { + return response.text().then((_responseText) => { + return throwException("Unauthorized", status, _responseText, _headers); + }); + } else if (status === 403) { + return response.text().then((_responseText) => { + return throwException("Forbidden", status, _responseText, _headers); + }); + } else if (status !== 200 && status !== 204) { + return response.text().then((_responseText) => { + return throwException("An unexpected server error occurred.", status, _responseText, _headers); + }); + } + return Promise.resolve(null as any); + } + /** * @param responseGroup (optional) * @return Success @@ -358,6 +411,8 @@ export class PushMessage implements IPushMessage { shortMessage?: string | undefined; startDate?: Date | undefined; status?: string | undefined; + trackNewRecipients?: boolean; + memberQuery?: string | undefined; memberIds?: string[] | undefined; createdDate?: Date; modifiedDate?: Date | undefined; @@ -380,6 +435,8 @@ export class PushMessage implements IPushMessage { this.shortMessage = _data["shortMessage"]; this.startDate = _data["startDate"] ? new Date(_data["startDate"].toString()) : undefined; this.status = _data["status"]; + this.trackNewRecipients = _data["trackNewRecipients"]; + this.memberQuery = _data["memberQuery"]; if (Array.isArray(_data["memberIds"])) { this.memberIds = [] as any; for (let item of _data["memberIds"]) @@ -406,6 +463,8 @@ export class PushMessage implements IPushMessage { data["shortMessage"] = this.shortMessage; data["startDate"] = this.startDate ? this.startDate.toISOString() : undefined; data["status"] = this.status; + data["trackNewRecipients"] = this.trackNewRecipients; + data["memberQuery"] = this.memberQuery; if (Array.isArray(this.memberIds)) { data["memberIds"] = []; for (let item of this.memberIds) @@ -425,6 +484,8 @@ export interface IPushMessage { shortMessage?: string | undefined; startDate?: Date | undefined; status?: string | undefined; + trackNewRecipients?: boolean; + memberQuery?: string | undefined; memberIds?: string[] | undefined; createdDate?: Date; modifiedDate?: Date | undefined; @@ -690,6 +751,9 @@ export interface IPushMessageRecipientSearchResult { } export class PushMessageSearchCriteria implements IPushMessageSearchCriteria { + isDraft?: boolean | undefined; + trackNewRecipients?: boolean | undefined; + createdDateBefore?: Date | undefined; startDateBefore?: Date | undefined; statuses?: string[] | undefined; responseGroup?: string | undefined; @@ -719,6 +783,9 @@ export class PushMessageSearchCriteria implements IPushMessageSearchCriteria { init(_data?: any) { if (_data) { + this.isDraft = _data["isDraft"]; + this.trackNewRecipients = _data["trackNewRecipients"]; + this.createdDateBefore = _data["createdDateBefore"] ? new Date(_data["createdDateBefore"].toString()) : undefined; this.startDateBefore = _data["startDateBefore"] ? new Date(_data["startDateBefore"].toString()) : undefined; if (Array.isArray(_data["statuses"])) { this.statuses = [] as any; @@ -760,6 +827,9 @@ export class PushMessageSearchCriteria implements IPushMessageSearchCriteria { toJSON(data?: any) { data = typeof data === 'object' ? data : {}; + data["isDraft"] = this.isDraft; + data["trackNewRecipients"] = this.trackNewRecipients; + data["createdDateBefore"] = this.createdDateBefore ? this.createdDateBefore.toISOString() : undefined; data["startDateBefore"] = this.startDateBefore ? this.startDateBefore.toISOString() : undefined; if (Array.isArray(this.statuses)) { data["statuses"] = []; @@ -794,6 +864,9 @@ export class PushMessageSearchCriteria implements IPushMessageSearchCriteria { } export interface IPushMessageSearchCriteria { + isDraft?: boolean | undefined; + trackNewRecipients?: boolean | undefined; + createdDateBefore?: Date | undefined; startDateBefore?: Date | undefined; statuses?: string[] | undefined; responseGroup?: string | undefined; diff --git a/src/VirtoCommerce.PushMessages.Web/App/src/modules/push-messages/components/widgets/recipients-widget.vue b/src/VirtoCommerce.PushMessages.Web/App/src/modules/push-messages/components/widgets/recipients-widget.vue index 3e178de..8636b17 100644 --- a/src/VirtoCommerce.PushMessages.Web/App/src/modules/push-messages/components/widgets/recipients-widget.vue +++ b/src/VirtoCommerce.PushMessages.Web/App/src/modules/push-messages/components/widgets/recipients-widget.vue @@ -1,6 +1,6 @@ diff --git a/src/VirtoCommerce.PushMessages.Web/App/src/router/routes.ts b/src/VirtoCommerce.PushMessages.Web/App/src/router/routes.ts index d3a4a28..41241ca 100644 --- a/src/VirtoCommerce.PushMessages.Web/App/src/router/routes.ts +++ b/src/VirtoCommerce.PushMessages.Web/App/src/router/routes.ts @@ -18,7 +18,7 @@ export const routes: RouteRecordRaw[] = [ children: [], redirect: (to) => { if (to.name === "App") { - return { path: "/messages", params: to.params }; + return { path: "/all", params: to.params }; } return to.path; }, diff --git a/src/VirtoCommerce.PushMessages.Web/App/yarn.lock b/src/VirtoCommerce.PushMessages.Web/App/yarn.lock index 07cbaf7..dd75475 100644 --- a/src/VirtoCommerce.PushMessages.Web/App/yarn.lock +++ b/src/VirtoCommerce.PushMessages.Web/App/yarn.lock @@ -2693,9 +2693,9 @@ __metadata: languageName: node linkType: hard -"@vc-shell/api-client-generator@npm:^1.0.178": - version: 1.0.178 - resolution: "@vc-shell/api-client-generator@npm:1.0.178" +"@vc-shell/api-client-generator@npm:^1.0.208": + version: 1.0.208 + resolution: "@vc-shell/api-client-generator@npm:1.0.208" dependencies: chalk: "npm:^2.4.2" nswag: "npm:^13.20.0" @@ -2703,26 +2703,26 @@ __metadata: vite-plugin-dts: "npm:^3.6.4" bin: api-client-generator: ./dist/api-client-generator.js - checksum: 7cde292d8cb463aa459e5eb46d91ec27ad6d3e45961f221eb63c19f0d293bb9f48a5bd699b4dfbf357fb369faf7ffa55ee9b6afa459c5c8413ace64b3a49ebba + checksum: ab02b1e08d999da0a7f803161564da8873e65726034d69bc48229a8a99b361f14e813c2c9ad6897ef1fedb2000852812bc39a38cf90261f5f44d0088d295b37a languageName: node linkType: hard -"@vc-shell/config-generator@npm:^1.0.188": - version: 1.0.188 - resolution: "@vc-shell/config-generator@npm:1.0.188" +"@vc-shell/config-generator@npm:^1.0.208": + version: 1.0.208 + resolution: "@vc-shell/config-generator@npm:1.0.208" dependencies: "@vitejs/plugin-vue": "npm:^5.0.3" vite: "npm:^5.0.11" vite-plugin-checker: "npm:^0.6.2" vite-plugin-mkcert: "npm:^1.17.1" vue: "npm:^3.4.15" - checksum: 93540feee9655224a9921496918d8ab7dc5b4187c4442310e189e11b9b591cec9cee14db5b352083c7ce400268e6edd042a3242b07a706a76c18b79c2dcb8f30 + checksum: c8d646cde35b965d87c42058a711e9544d14ce5744078f0e4cf15bbe5bf30dbc3b5709d563d93d7f31ba87d3a440f0676bb8e75b1a03f6981a435dbc1ac73447 languageName: node linkType: hard -"@vc-shell/framework@npm:^1.0.188": - version: 1.0.188 - resolution: "@vc-shell/framework@npm:1.0.188" +"@vc-shell/framework@npm:^1.0.208": + version: 1.0.208 + resolution: "@vc-shell/framework@npm:1.0.208" dependencies: "@floating-ui/vue": "npm:^1.0.6" "@headlessui/vue": "npm:^1.7.19" @@ -2734,12 +2734,14 @@ __metadata: "@vueuse/core": "npm:^10.7.1" "@vueuse/integrations": "npm:^10.7.1" core-js: "npm:^3.35.0" + dompurify: "npm:^3.0.11" iso-639-1: "npm:^3.1.0" moment: "npm:^2.30.1" normalize.css: "npm:^8.0.1" quill: "npm:^1.3.7" quill-image-uploader: "npm:^1.3.0" swiper: "npm:^6.8.4" + truncate-html: "npm:^1.1.1" vee-validate: "npm:^4.12.4" vue: "npm:^3.4.19" vue-currency-input: "npm:^3.0.5" @@ -2747,27 +2749,27 @@ __metadata: vue-router: "npm:^4.2.5" vue3-touch-events: "npm:^4.1.8" whatwg-fetch: "npm:^3.6.19" - checksum: 18daba6186bea5c00dd2b62ab19d3dfd987174c300e83fc6cbefbd6792b82864c2c7569a0cd5062c4aa83c9aab7cf44a329cef7f69ddbc6eeb5e9628f173dee8 + checksum: a9d36c187643acdd9eaa809f10dc148f8e5dcd96f4ba7b17eebbce3d93a671eb177671229742d89b8318874e841d58a83d34d60b48dcde6ee7ecb39d9679f3cb languageName: node linkType: hard -"@vc-shell/release-config@npm:^1.0.178": - version: 1.0.178 - resolution: "@vc-shell/release-config@npm:1.0.178" +"@vc-shell/release-config@npm:^1.0.208": + version: 1.0.208 + resolution: "@vc-shell/release-config@npm:1.0.208" dependencies: chalk: "npm:^2.4.2" mri: "npm:^1.2.0" prompts: "npm:^2.4.2" semver: "npm:^7.5.4" vite: "npm:^5.0.11" - checksum: 755ae1e3822dcbfee69e7da1da4168c958e1deb1015ab11e971f3ff49e3cfccbf14485f3d729b6ca580288a58ace8cb2aec2f67798d2384d7bcce857a0a7f289 + checksum: 1bea804de762f8d58cd1bba6a50183dcdee26cbe92985f6ad0dddcb6501ce632f7c90cec0b45f3502983990f064f26191baa3514febea052dd814b71a53518f2 languageName: node linkType: hard -"@vc-shell/ts-config@npm:^1.0.178": - version: 1.0.178 - resolution: "@vc-shell/ts-config@npm:1.0.178" - checksum: 261121834973961f4db28de89b0f5248c19398cce67108d0693770c751e6497a2a53cb040a3f750ccdc48da2abb030996bc943c42cd532fafaeac8b5ac18c458 +"@vc-shell/ts-config@npm:^1.0.208": + version: 1.0.208 + resolution: "@vc-shell/ts-config@npm:1.0.208" + checksum: 528a498f98f20bff9198d06d6b58ca0387513c6e178ad5fc36d0f2661d614ecca6a14baf2fc62bf1ccf716f90f8a6e3cb3242b73df61bd7249d247a9058c470b languageName: node linkType: hard @@ -3666,6 +3668,35 @@ __metadata: languageName: node linkType: hard +"cheerio-select@npm:^2.1.0": + version: 2.1.0 + resolution: "cheerio-select@npm:2.1.0" + dependencies: + boolbase: "npm:^1.0.0" + css-select: "npm:^5.1.0" + css-what: "npm:^6.1.0" + domelementtype: "npm:^2.3.0" + domhandler: "npm:^5.0.3" + domutils: "npm:^3.0.1" + checksum: b5d89208c23468c3a32d1e04f88b9e8c6e332e3649650c5cd29255e2cebc215071ae18563f58c3dc3f6ef4c234488fc486035490fceb78755572288245e2931a + languageName: node + linkType: hard + +"cheerio@npm:^1.0.0-rc.12": + version: 1.0.0-rc.12 + resolution: "cheerio@npm:1.0.0-rc.12" + dependencies: + cheerio-select: "npm:^2.1.0" + dom-serializer: "npm:^2.0.0" + domhandler: "npm:^5.0.3" + domutils: "npm:^3.0.1" + htmlparser2: "npm:^8.0.1" + parse5: "npm:^7.0.0" + parse5-htmlparser2-tree-adapter: "npm:^7.0.0" + checksum: 812fed61aa4b669bbbdd057d0d7f73ba4649cabfd4fc3a8f1d5c7499e4613b430636102716369cbd6bbed8f1bdcb06387ae8342289fb908b2743184775f94f18 + languageName: node + linkType: hard + "chokidar@npm:>=3.0.0 <4.0.0, chokidar@npm:^3.5.1, chokidar@npm:^3.5.3": version: 3.6.0 resolution: "chokidar@npm:3.6.0" @@ -4104,6 +4135,26 @@ __metadata: languageName: node linkType: hard +"css-select@npm:^5.1.0": + version: 5.1.0 + resolution: "css-select@npm:5.1.0" + dependencies: + boolbase: "npm:^1.0.0" + css-what: "npm:^6.1.0" + domhandler: "npm:^5.0.2" + domutils: "npm:^3.0.1" + nth-check: "npm:^2.0.1" + checksum: d486b1e7eb140468218a5ab5af53257e01f937d2173ac46981f6b7de9c5283d55427a36715dc8decfc0c079cf89259ac5b41ef58f6e1a422eee44ab8bfdc78da + languageName: node + linkType: hard + +"css-what@npm:^6.1.0": + version: 6.1.0 + resolution: "css-what@npm:6.1.0" + checksum: c67a3a2d0d81843af87f8bf0a4d0845b0f952377714abbb2884e48942409d57a2110eabee003609d02ee487b054614bdfcfc59ee265728ff105bd5aa221c1d0e + languageName: node + linkType: hard + "cssesc@npm:^3.0.0": version: 3.0.0 resolution: "cssesc@npm:3.0.0" @@ -4317,6 +4368,17 @@ __metadata: languageName: node linkType: hard +"dom-serializer@npm:^2.0.0": + version: 2.0.0 + resolution: "dom-serializer@npm:2.0.0" + dependencies: + domelementtype: "npm:^2.3.0" + domhandler: "npm:^5.0.2" + entities: "npm:^4.2.0" + checksum: e3bf9027a64450bca0a72297ecdc1e3abb7a2912268a9f3f5d33a2e29c1e2c3502c6e9f860fc6625940bfe0cfb57a44953262b9e94df76872fdfb8151097eeb3 + languageName: node + linkType: hard + "dom7@npm:^3.0.0": version: 3.0.0 resolution: "dom7@npm:3.0.0" @@ -4326,6 +4388,40 @@ __metadata: languageName: node linkType: hard +"domelementtype@npm:^2.3.0": + version: 2.3.0 + resolution: "domelementtype@npm:2.3.0" + checksum: ee837a318ff702622f383409d1f5b25dd1024b692ef64d3096ff702e26339f8e345820f29a68bcdcea8cfee3531776b3382651232fbeae95612d6f0a75efb4f6 + languageName: node + linkType: hard + +"domhandler@npm:^5.0.2, domhandler@npm:^5.0.3": + version: 5.0.3 + resolution: "domhandler@npm:5.0.3" + dependencies: + domelementtype: "npm:^2.3.0" + checksum: 809b805a50a9c6884a29f38aec0a4e1b4537f40e1c861950ed47d10b049febe6b79ab72adaeeebb3cc8fc1cd33f34e97048a72a9265103426d93efafa78d3e96 + languageName: node + linkType: hard + +"dompurify@npm:^3.0.11": + version: 3.1.0 + resolution: "dompurify@npm:3.1.0" + checksum: a8788d3510b0a5e26ae8f1beb3f079be63f417be0f7259918c273bd53f9b9eab50a0708e065caff9904ae97895cc4a7d4c66a1076021a9be0685389ad8ae4d2d + languageName: node + linkType: hard + +"domutils@npm:^3.0.1": + version: 3.1.0 + resolution: "domutils@npm:3.1.0" + dependencies: + dom-serializer: "npm:^2.0.0" + domelementtype: "npm:^2.3.0" + domhandler: "npm:^5.0.3" + checksum: 9a169a6e57ac4c738269a73ab4caf785114ed70e46254139c1bbc8144ac3102aacb28a6149508395ae34aa5d6a40081f4fa5313855dc8319c6d8359866b6dfea + languageName: node + linkType: hard + "dot-prop@npm:^5.1.0": version: 5.3.0 resolution: "dot-prop@npm:5.3.0" @@ -4400,7 +4496,7 @@ __metadata: languageName: node linkType: hard -"entities@npm:^4.5.0": +"entities@npm:^4.2.0, entities@npm:^4.4.0, entities@npm:^4.5.0": version: 4.5.0 resolution: "entities@npm:4.5.0" checksum: ede2a35c9bce1aeccd055a1b445d41c75a14a2bb1cd22e242f20cf04d236cdcd7f9c859eb83f76885327bfae0c25bf03303665ee1ce3d47c5927b98b0e3e3d48 @@ -5606,6 +5702,18 @@ __metadata: languageName: node linkType: hard +"htmlparser2@npm:^8.0.1": + version: 8.0.2 + resolution: "htmlparser2@npm:8.0.2" + dependencies: + domelementtype: "npm:^2.3.0" + domhandler: "npm:^5.0.3" + domutils: "npm:^3.0.1" + entities: "npm:^4.4.0" + checksum: ea5512956eee06f5835add68b4291d313c745e8407efa63848f4b8a90a2dee45f498a698bca8614e436f1ee0cfdd609938b71d67c693794545982b76e53e6f11 + languageName: node + linkType: hard + "http-cache-semantics@npm:^4.1.1": version: 4.1.1 resolution: "http-cache-semantics@npm:4.1.1" @@ -7082,7 +7190,7 @@ __metadata: languageName: node linkType: hard -"nth-check@npm:^2.1.1": +"nth-check@npm:^2.0.1, nth-check@npm:^2.1.1": version: 2.1.1 resolution: "nth-check@npm:2.1.1" dependencies: @@ -7328,6 +7436,25 @@ __metadata: languageName: node linkType: hard +"parse5-htmlparser2-tree-adapter@npm:^7.0.0": + version: 7.0.0 + resolution: "parse5-htmlparser2-tree-adapter@npm:7.0.0" + dependencies: + domhandler: "npm:^5.0.2" + parse5: "npm:^7.0.0" + checksum: 23dbe45fdd338fe726cf5c55b236e1f403aeb0c1b926e18ab8ef0aa580980a25f8492d160fe2ed0ec906c3c8e38b51e68ef5620a3b9460d9458ea78946a3f7c0 + languageName: node + linkType: hard + +"parse5@npm:^7.0.0": + version: 7.1.2 + resolution: "parse5@npm:7.1.2" + dependencies: + entities: "npm:^4.4.0" + checksum: 3c86806bb0fb1e9a999ff3a4c883b1ca243d99f45a619a0898dbf021a95a0189ed955c31b07fe49d342b54e814f33f2c9d7489198e8630dacd5477d413ec5782 + languageName: node + linkType: hard + "path-browserify@npm:^1.0.1": version: 1.0.1 resolution: "path-browserify@npm:1.0.1" @@ -7619,11 +7746,11 @@ __metadata: "@types/node": "npm:^20.10.5" "@typescript-eslint/eslint-plugin": "npm:^6.16.0" "@typescript-eslint/parser": "npm:^6.16.0" - "@vc-shell/api-client-generator": "npm:^1.0.178" - "@vc-shell/config-generator": "npm:^1.0.188" - "@vc-shell/framework": "npm:^1.0.188" - "@vc-shell/release-config": "npm:^1.0.178" - "@vc-shell/ts-config": "npm:^1.0.178" + "@vc-shell/api-client-generator": "npm:^1.0.208" + "@vc-shell/config-generator": "npm:^1.0.208" + "@vc-shell/framework": "npm:^1.0.208" + "@vc-shell/release-config": "npm:^1.0.208" + "@vc-shell/ts-config": "npm:^1.0.208" "@vitejs/plugin-vue": "npm:^5.0.3" "@vue/eslint-config-prettier": "npm:^9.0.0" "@vue/eslint-config-typescript": "npm:^12.0.0" @@ -8926,6 +9053,15 @@ __metadata: languageName: node linkType: hard +"truncate-html@npm:^1.1.1": + version: 1.1.1 + resolution: "truncate-html@npm:1.1.1" + dependencies: + cheerio: "npm:^1.0.0-rc.12" + checksum: 0360ccf6d8a0758a1097c221f29c63ef91aa1bdeb2103eecf4410d8815b24c69e4c8f91b0995840502e69ccea07b3d1ac650636ec022bb96000ed763cc09d463 + languageName: node + linkType: hard + "ts-api-utils@npm:^1.0.1": version: 1.3.0 resolution: "ts-api-utils@npm:1.3.0" diff --git a/src/VirtoCommerce.PushMessages.Web/Content/logo.png b/src/VirtoCommerce.PushMessages.Web/Content/logo.png deleted file mode 100644 index ca22993..0000000 Binary files a/src/VirtoCommerce.PushMessages.Web/Content/logo.png and /dev/null differ diff --git a/src/VirtoCommerce.PushMessages.Web/Controllers/Api/PushMessageController.cs b/src/VirtoCommerce.PushMessages.Web/Controllers/Api/PushMessageController.cs index 23f502e..a1df1a6 100644 --- a/src/VirtoCommerce.PushMessages.Web/Controllers/Api/PushMessageController.cs +++ b/src/VirtoCommerce.PushMessages.Web/Controllers/Api/PushMessageController.cs @@ -31,7 +31,6 @@ public PushMessageController( public async Task> SearchRecipients([FromBody] PushMessageRecipientSearchCriteria criteria) { var result = await _recipientSearchService.SearchAsync(criteria); - return Ok(result); } @@ -40,7 +39,6 @@ public async Task> SearchRecipien public async Task> Search([FromBody] PushMessageSearchCriteria criteria) { var result = await _messageSearchService.SearchAsync(criteria); - return Ok(result); } @@ -57,15 +55,22 @@ public Task> Create([FromBody] PushMessage model) public async Task> Update([FromBody] PushMessage model) { await _messageService.SaveChangesAsync([model]); - return Ok(model); + return Ok(await _messageService.GetNoCloneAsync(model.Id)); + } + + [HttpPut("{id}/tracking/{value}")] + [Authorize(ModuleConstants.Security.Permissions.Update)] + public async Task> ChangeTracking([FromRoute] string id, [FromRoute] bool value) + { + await _messageService.ChangeTracking(id, value); + return Ok(await _messageService.GetNoCloneAsync(id)); } [HttpGet("{id}")] [Authorize(ModuleConstants.Security.Permissions.Read)] public async Task> Get([FromRoute] string id, [FromQuery] string responseGroup = null) { - var retVal = await _messageService.GetNoCloneAsync(id, responseGroup); - return Ok(retVal); + return Ok(await _messageService.GetNoCloneAsync(id, responseGroup)); } [HttpDelete] diff --git a/src/VirtoCommerce.PushMessages.Web/Localizations/en.PushMessages.json b/src/VirtoCommerce.PushMessages.Web/Localizations/en.PushMessages.json index f43b455..9a0d9a9 100644 --- a/src/VirtoCommerce.PushMessages.Web/Localizations/en.PushMessages.json +++ b/src/VirtoCommerce.PushMessages.Web/Localizations/en.PushMessages.json @@ -11,12 +11,20 @@ "title": "Batch size", "description": "" }, - "PushMessages.BackgroundJobs.Enable": { - "title": "Background jobs enable", + "PushMessages.SendScheduledMessagesRecurringJob.Enable": { + "title": "Send scheduled messages recurring job: enable", "description": "" }, - "PushMessages.BackgroundJobs.CronExpression": { - "title": "Background jobs cron expression", + "PushMessages.SendScheduledMessagesRecurringJob.CronExpression": { + "title": "Send scheduled messages recurring job: cron expression", + "description": "" + }, + "PushMessages.TrackNewRecipientsRecurringJob.Enable": { + "title": "Track new recipients recurring job: enable", + "description": "" + }, + "PushMessages.TrackNewRecipientsRecurringJob.CronExpression": { + "title": "Track new recipients recurring job: cron expression", "description": "" } } diff --git a/src/VirtoCommerce.PushMessages.Web/Module.cs b/src/VirtoCommerce.PushMessages.Web/Module.cs index 10b5f0a..668cc6e 100644 --- a/src/VirtoCommerce.PushMessages.Web/Module.cs +++ b/src/VirtoCommerce.PushMessages.Web/Module.cs @@ -6,16 +6,17 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using VirtoCommerce.CustomerModule.Core.Events; using VirtoCommerce.ExperienceApiModule.Core.Extensions; using VirtoCommerce.ExperienceApiModule.Core.Infrastructure; using VirtoCommerce.Platform.Core.Events; using VirtoCommerce.Platform.Core.Modularity; using VirtoCommerce.Platform.Core.Security; using VirtoCommerce.Platform.Core.Settings; -using VirtoCommerce.Platform.Core.Settings.Events; using VirtoCommerce.PushMessages.Core; using VirtoCommerce.PushMessages.Core.BackgroundJobs; using VirtoCommerce.PushMessages.Core.Events; +using VirtoCommerce.PushMessages.Core.Extensions; using VirtoCommerce.PushMessages.Core.Services; using VirtoCommerce.PushMessages.Data.BackgroundJobs; using VirtoCommerce.PushMessages.Data.Handlers; @@ -66,8 +67,10 @@ public void Initialize(IServiceCollection serviceCollection) serviceCollection.AddTransient(); serviceCollection.AddTransient(); - serviceCollection.AddSingleton(); - serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); + + serviceCollection.AddRecurringJobService(); // GraphQL var assemblyMarker = typeof(AssemblyMarker); @@ -77,7 +80,7 @@ public void Initialize(IServiceCollection serviceCollection) serviceCollection.AddAutoMapper(assemblyMarker); serviceCollection.AddSchemaBuilders(assemblyMarker); serviceCollection.AddDistributedMessageService(Configuration); - serviceCollection.AddTransient(); + serviceCollection.AddTransient(); serviceCollection.AddSingleton(); } @@ -99,12 +102,11 @@ public void PostInitialize(IApplicationBuilder appBuilder) dbContext.Database.Migrate(); // Register event handlers - appBuilder.RegisterEventHandler(); + appBuilder.RegisterEventHandler(); appBuilder.RegisterEventHandler(); + appBuilder.RegisterEventHandler(); - // Schedule background jobs - var pushMessageJobService = serviceProvider.GetService(); - pushMessageJobService.StartStopRecurringJobs().GetAwaiter().GetResult(); + appBuilder.UseRecurringJobService(); } public void Uninstall() diff --git a/src/VirtoCommerce.PushMessages.Web/module.manifest b/src/VirtoCommerce.PushMessages.Web/module.manifest index c536a4e..de7b266 100644 --- a/src/VirtoCommerce.PushMessages.Web/module.manifest +++ b/src/VirtoCommerce.PushMessages.Web/module.manifest @@ -29,7 +29,7 @@ https://github.com/VirtoCommerce/vc-module-push-messages - Modules/$(VirtoCommerce.PushMessages)/Content/logo.png + Modules/$(VirtoCommerce.PushMessages)/Content/push-messages/img/icons/apple-touch-icon.png false VirtoCommerce.PushMessages.Web.dll