From 3d060cc3fc22fe63b581d0fd540d59feb0442466 Mon Sep 17 00:00:00 2001 From: support Date: Thu, 22 Oct 2026 20:45:08 +0200 Subject: [PATCH 1/3] Split MessageProviderService into one service per message area MessageProviderService was a 2100-line class behind a 50-member interface, so every caller (order handlers, account controllers, scheduled tasks, plugins) depended on every message the platform sends. The typed Send*Message methods move verbatim into seven services: Customer, Order, Newsletter, Catalog, MerchandiseReturn, Vendor and Cms (IMessageProviderService). The lookups they share (store, active template, email account, language) move to IMessageLookupService, and IMessageProviderService keeps only the generic SendNotification. Each caller now takes the one interface it uses. Breaking change: code outside the repository that calls a typed send method on IMessageProviderService, or overrides one in a subclass of MessageProviderService, must switch to the area interface. Template names, tokens and DI lifetimes are unchanged. --- ...ValueForPurchasedVouchersCommandHandler.cs | 8 +- .../Orders/OrderNotificationCommandHandler.cs | 12 +- .../Orders/OrderStatusCommandHandler.cs | 14 +- .../Orders/PartiallyRefundCommandHandler.cs | 10 +- .../PartiallyRefundOfflineCommandHandler.cs | 10 +- .../Orders/PlaceOrderCommandHandler.cs | 8 +- .../Orders/ProcessOrderPaidCommandHandler.cs | 12 +- .../Handlers/Orders/RefundCommandHandler.cs | 10 +- .../Orders/RefundOfflineCommandHandler.cs | 10 +- .../Shipping/DeliveryCommandHandler.cs | 8 +- .../Handlers/Shipping/ShipCommandHandler.cs | 8 +- .../ICatalogMessageProviderService.cs | 141 ++ .../Messages/ICmsMessageProviderService.cs | 45 + .../ICustomerMessageProviderService.cs | 113 + ...MerchandiseReturnMessageProviderService.cs | 53 + .../Messages/IMessageLookupService.cs | 31 + .../Messages/IMessageProviderService.cs | 619 +---- .../INewsletterMessageProviderService.cs | 31 + .../Messages/IOrderMessageProviderService.cs | 188 ++ .../Messages/IVendorMessageProviderService.cs | 64 + ...otificationsToSubscribersCommandHandler.cs | 8 +- ...utBidCustomerNotificationCommandHandler.cs | 8 +- ...lowStoreOwnerNotificationCommandHandler.cs | 10 +- .../LiquidObjectBuilderExtensions.cs | 17 + .../Services/CatalogMessageProviderService.cs | 605 +++++ .../Services/CmsMessageProviderService.cs | 174 ++ .../CustomerMessageProviderService.cs | 301 +++ ...MerchandiseReturnMessageProviderService.cs | 234 ++ .../Services/MessageLookupService.cs | 89 + .../Services/MessageProviderService.cs | 2022 +---------------- .../NewsletterMessageProviderService.cs | 110 + .../Services/OrderMessageProviderService.cs | 522 +++++ .../Services/VendorMessageProviderService.cs | 252 ++ .../Startup/StartupApplication.cs | 8 + .../BackgroundServices/EndAuctionsTask.cs | 14 +- .../FacebookAuthenticationEventConsumer.cs | 10 +- .../GoogleAuthenticationEventConsumer.cs | 10 +- ...urchasedGiftVouchersCommandHandlerTests.cs | 4 +- .../OrderNotificationCommandHandlerTests.cs | 8 +- .../Orders/OrderStatusCommandHandlerTests.cs | 6 +- .../PartiallyRefundCommandHandlerTests.cs | 6 +- ...rtiallyRefundOfflineCommandHandlerTests.cs | 6 +- .../Orders/PlaceOrderCommandHandlerTests.cs | 2 +- .../Orders/RefundCommandHandlerTests.cs | 6 +- .../Orders/VoidCommandHandlerTests.cs | 4 +- .../Shipping/DeliveryCommandHandlerTests.cs | 6 +- .../Shipping/ShipCommandHandlerTests.cs | 6 +- .../Services/MessageProviderServiceTest.cs | 78 +- ...CustomerNotificationCommandHandlerTests.cs | 4 +- ...oreOwnerNotificationCommandHandlerTests.cs | 4 +- .../BackgroundService/EndAuctionsTaskTests.cs | 4 +- .../BaseCustomerControllerTests.cs | 8 +- .../BaseCustomerManagementControllerTests.cs | 6 +- .../LoginControllerExternalSignInTests.cs | 2 +- .../LoginControllerProviderOnlyTests.cs | 2 +- .../MerchandiseReturnViewModelServiceTests.cs | 8 +- .../PanelPasswordRecoveryServiceTests.cs | 10 +- .../Controllers/CustomerControllerTests.cs | 6 +- ...PasswordRecoverySendCommandHandlerTests.cs | 6 +- .../AccountControllerExternalRegisterTests.cs | 4 +- .../AccountControllerProviderOnlyTests.cs | 2 +- ...ccountControllerTwoFactorReturnUrlTests.cs | 2 +- .../Controllers/ContentPreviewTests.cs | 2 +- .../Controllers/CustomerController.cs | 4 +- .../Controllers/LoginController.cs | 2 +- .../Controllers/BaseCustomerController.cs | 8 +- .../BaseCustomerManagementController.cs | 4 +- .../Controllers/BaseLoginController.cs | 8 +- .../Services/CustomerViewModelService.cs | 6 +- .../Services/GiftVoucherViewModelService.cs | 8 +- .../MerchandiseReturnViewModelService.cs | 6 +- .../Services/OrderViewModelService.cs | 8 +- .../Services/PanelPasswordRecoveryService.cs | 4 +- .../Controllers/CustomerController.cs | 4 +- .../Controllers/LoginController.cs | 2 +- .../Controllers/LoginController.cs | 2 +- .../Blogs/InsertBlogCommentCommandHandler.cs | 8 +- .../Contact/ContactUsSendCommandHandler.cs | 8 +- .../CustomerRegisteredCommandHandler.cs | 8 +- .../Customers/DeleteAccountCommandHandler.cs | 8 +- .../PasswordRecoverySendCommandHandler.cs | 8 +- .../News/InsertNewsCommentCommandHandler.cs | 8 +- .../Newsletter/SubscribeNewsletterHandler.cs | 12 +- .../Orders/InsertOrderNoteCommandHandler.cs | 8 +- .../MerchandiseReturnSubmitCommandHandler.cs | 10 +- .../InsertProductReviewCommandHandler.cs | 8 +- ...ProductAskQuestionMessageCommandHandler.cs | 8 +- ...roductEmailAFriendMessageCommandHandler.cs | 8 +- .../Vendors/ContactVendorCommandHandler.cs | 8 +- .../InsertVendorReviewCommandHandler.cs | 8 +- .../Controllers/AccountController.cs | 12 +- .../Controllers/KnowledgebaseController.cs | 8 +- .../Grand.Web/Controllers/VendorController.cs | 8 +- .../Controllers/WishlistController.cs | 4 +- .../GetTwoFactorAuthenticationHandler.cs | 8 +- 95 files changed, 3311 insertions(+), 2904 deletions(-) create mode 100644 src/Business/Grand.Business.Core/Interfaces/Messages/ICatalogMessageProviderService.cs create mode 100644 src/Business/Grand.Business.Core/Interfaces/Messages/ICmsMessageProviderService.cs create mode 100644 src/Business/Grand.Business.Core/Interfaces/Messages/ICustomerMessageProviderService.cs create mode 100644 src/Business/Grand.Business.Core/Interfaces/Messages/IMerchandiseReturnMessageProviderService.cs create mode 100644 src/Business/Grand.Business.Core/Interfaces/Messages/IMessageLookupService.cs create mode 100644 src/Business/Grand.Business.Core/Interfaces/Messages/INewsletterMessageProviderService.cs create mode 100644 src/Business/Grand.Business.Core/Interfaces/Messages/IOrderMessageProviderService.cs create mode 100644 src/Business/Grand.Business.Core/Interfaces/Messages/IVendorMessageProviderService.cs create mode 100644 src/Business/Grand.Business.Messages/DotLiquidDrops/LiquidObjectBuilderExtensions.cs create mode 100644 src/Business/Grand.Business.Messages/Services/CatalogMessageProviderService.cs create mode 100644 src/Business/Grand.Business.Messages/Services/CmsMessageProviderService.cs create mode 100644 src/Business/Grand.Business.Messages/Services/CustomerMessageProviderService.cs create mode 100644 src/Business/Grand.Business.Messages/Services/MerchandiseReturnMessageProviderService.cs create mode 100644 src/Business/Grand.Business.Messages/Services/MessageLookupService.cs create mode 100644 src/Business/Grand.Business.Messages/Services/NewsletterMessageProviderService.cs create mode 100644 src/Business/Grand.Business.Messages/Services/OrderMessageProviderService.cs create mode 100644 src/Business/Grand.Business.Messages/Services/VendorMessageProviderService.cs diff --git a/src/Business/Grand.Business.Checkout/Commands/Handlers/Orders/ActivatedValueForPurchasedVouchersCommandHandler.cs b/src/Business/Grand.Business.Checkout/Commands/Handlers/Orders/ActivatedValueForPurchasedVouchersCommandHandler.cs index a58a51574f..24d775bca3 100644 --- a/src/Business/Grand.Business.Checkout/Commands/Handlers/Orders/ActivatedValueForPurchasedVouchersCommandHandler.cs +++ b/src/Business/Grand.Business.Checkout/Commands/Handlers/Orders/ActivatedValueForPurchasedVouchersCommandHandler.cs @@ -13,16 +13,16 @@ public class { private readonly IGiftVoucherService _giftVoucherService; private readonly ILanguageService _languageService; - private readonly IMessageProviderService _messageProviderService; + private readonly IOrderMessageProviderService _orderMessageProviderService; public ActivatedValueForPurchasedGiftVouchersCommandHandler( IGiftVoucherService giftVoucherService, ILanguageService languageService, - IMessageProviderService messageProviderService) + IOrderMessageProviderService orderMessageProviderService) { _giftVoucherService = giftVoucherService; _languageService = languageService; - _messageProviderService = messageProviderService; + _orderMessageProviderService = orderMessageProviderService; } public async Task Handle(ActivatedValueForPurchasedGiftVouchersCommand request, @@ -48,7 +48,7 @@ await _languageService.GetLanguageById(request.Order.CustomerLanguageId) ?? (await _languageService.GetAllLanguages()).FirstOrDefault(); if (customerLang == null) throw new Exception("No languages could be loaded"); - _ = await _messageProviderService.SendGiftVoucherMessage(gc, request.Order, + _ = await _orderMessageProviderService.SendGiftVoucherMessage(gc, request.Order, customerLang.Id); gc.IsRecipientNotified = true; } diff --git a/src/Business/Grand.Business.Checkout/Commands/Handlers/Orders/OrderNotificationCommandHandler.cs b/src/Business/Grand.Business.Checkout/Commands/Handlers/Orders/OrderNotificationCommandHandler.cs index dd5618da91..115ecd51e9 100644 --- a/src/Business/Grand.Business.Checkout/Commands/Handlers/Orders/OrderNotificationCommandHandler.cs +++ b/src/Business/Grand.Business.Checkout/Commands/Handlers/Orders/OrderNotificationCommandHandler.cs @@ -13,7 +13,7 @@ namespace Grand.Business.Checkout.Commands.Handlers.Orders; public class OrderNotificationCommandHandler : IRequestHandler { private readonly IOrderService _orderService; - private readonly IMessageProviderService _messageProviderService; + private readonly IOrderMessageProviderService _orderMessageProviderService; private readonly OrderSettings _orderSettings; private readonly LanguageSettings _languageSettings; private readonly IPdfService _pdfService; @@ -22,7 +22,7 @@ public class OrderNotificationCommandHandler : IRequestHandler(); - await _messageProviderService + await _orderMessageProviderService .SendOrderCompletedCustomerMessage(request.Order, customer, request.Order.CustomerLanguageId, orderCompletedAttachmentFilePath, orderCompletedAttachmentFileName, orderCompletedAttachments); @@ -90,7 +90,7 @@ await _messageProviderService && request.NotifyCustomer) { //notification customer - await _messageProviderService.SendOrderCancelledCustomerMessage(request.Order, customer, + await _orderMessageProviderService.SendOrderCancelledCustomerMessage(request.Order, customer, request.Order.CustomerLanguageId); //notification for vendor @@ -103,7 +103,7 @@ await _messageProviderService.SendOrderCancelledCustomerMessage(request.Order, c { //notification store owner var orderCancelledStoreOwnerNotificationQueuedEmailId = - await _messageProviderService.SendOrderCancelledStoreOwnerMessage(request.Order, customer, + await _orderMessageProviderService.SendOrderCancelledStoreOwnerMessage(request.Order, customer, request.Order.CustomerLanguageId); if (orderCancelledStoreOwnerNotificationQueuedEmailId > 0) await _orderService.InsertOrderNote(new OrderNote { @@ -147,7 +147,7 @@ private async Task VendorNotification(Order order) if (string.IsNullOrEmpty(orderItem.VendorId)) continue; var vendor = await _vendorService.GetVendorById(orderItem.VendorId); if (vendor is { Deleted: false, Active: true }) - await _messageProviderService.SendOrderCancelledVendorMessage(order, vendor, order.CustomerLanguageId); + await _orderMessageProviderService.SendOrderCancelledVendorMessage(order, vendor, order.CustomerLanguageId); } } } \ No newline at end of file diff --git a/src/Business/Grand.Business.Checkout/Commands/Handlers/Orders/PartiallyRefundCommandHandler.cs b/src/Business/Grand.Business.Checkout/Commands/Handlers/Orders/PartiallyRefundCommandHandler.cs index e70b434b07..600add5ee5 100644 --- a/src/Business/Grand.Business.Checkout/Commands/Handlers/Orders/PartiallyRefundCommandHandler.cs +++ b/src/Business/Grand.Business.Checkout/Commands/Handlers/Orders/PartiallyRefundCommandHandler.cs @@ -18,7 +18,7 @@ public class PartiallyRefundCommandHandler : IRequestHandler _logger; private readonly IMediator _mediator; - private readonly IMessageProviderService _messageProviderService; + private readonly IOrderMessageProviderService _orderMessageProviderService; private readonly IOrderService _orderService; private readonly IPaymentService _paymentService; private readonly IPaymentTransactionService _paymentTransactionService; @@ -27,7 +27,7 @@ public PartiallyRefundCommandHandler( IPaymentService paymentService, IPaymentTransactionService paymentTransactionService, IMediator mediator, - IMessageProviderService messageProviderService, + IOrderMessageProviderService orderMessageProviderService, ILogger logger, IOrderService orderService, LanguageSettings languageSettings) @@ -36,7 +36,7 @@ public PartiallyRefundCommandHandler( _paymentTransactionService = paymentTransactionService; _orderService = orderService; _mediator = mediator; - _messageProviderService = messageProviderService; + _orderMessageProviderService = orderMessageProviderService; _logger = logger; _languageSettings = languageSettings; } @@ -90,10 +90,10 @@ await _mediator.Send( await _mediator.Send(new CheckOrderStatusCommand { Order = order }, cancellationToken); //notifications - await _messageProviderService.SendOrderRefundedStoreOwnerMessage(order, amountToRefund, + await _orderMessageProviderService.SendOrderRefundedStoreOwnerMessage(order, amountToRefund, _languageSettings.DefaultAdminLanguageId); - await _messageProviderService.SendOrderRefundedCustomerMessage(order, amountToRefund, + await _orderMessageProviderService.SendOrderRefundedCustomerMessage(order, amountToRefund, order.CustomerLanguageId); } diff --git a/src/Business/Grand.Business.Checkout/Commands/Handlers/Orders/PartiallyRefundOfflineCommandHandler.cs b/src/Business/Grand.Business.Checkout/Commands/Handlers/Orders/PartiallyRefundOfflineCommandHandler.cs index efc467951d..21641606ac 100644 --- a/src/Business/Grand.Business.Checkout/Commands/Handlers/Orders/PartiallyRefundOfflineCommandHandler.cs +++ b/src/Business/Grand.Business.Checkout/Commands/Handlers/Orders/PartiallyRefundOfflineCommandHandler.cs @@ -15,7 +15,7 @@ public class PartiallyRefundOfflineCommandHandler : IRequestHandler _logger; private readonly IMediator _mediator; - private readonly IMessageProviderService _messageProviderService; + private readonly ICatalogMessageProviderService _catalogMessageProviderService; private readonly IOrderService _orderService; private readonly OrderSettings _orderSettings; private readonly OidcLoginConfig _oidcLoginConfig; @@ -84,7 +84,7 @@ public PlaceOrderCommandHandler( ICustomerService customerService, IDiscountService discountService, IGroupService groupService, - IMessageProviderService messageProviderService, + ICatalogMessageProviderService catalogMessageProviderService, IVendorService vendorService, ISalesEmployeeService salesEmployeeService, ICurrencyService currencyService, @@ -117,7 +117,7 @@ public PlaceOrderCommandHandler( _customerService = customerService; _groupService = groupService; _discountService = discountService; - _messageProviderService = messageProviderService; + _catalogMessageProviderService = catalogMessageProviderService; _vendorService = vendorService; _salesEmployeeService = salesEmployeeService; _currencyService = currencyService; @@ -812,7 +812,7 @@ protected virtual async Task UpdateAuctionEnded(ShoppingCartItem sc, Product pro { await _auctionService.UpdateAuctionEnded(product, true, true); await _auctionService.UpdateHighestBid(product, product.Price, order.CustomerId); - await _messageProviderService.SendAuctionEndedBinCustomerMessage(product, order.CustomerId, + await _catalogMessageProviderService.SendAuctionEndedBinCustomerMessage(product, order.CustomerId, order.CustomerLanguageId, order.StoreId); await _auctionService.InsertBid(new Bid { CustomerId = order.CustomerId, diff --git a/src/Business/Grand.Business.Checkout/Commands/Handlers/Orders/ProcessOrderPaidCommandHandler.cs b/src/Business/Grand.Business.Checkout/Commands/Handlers/Orders/ProcessOrderPaidCommandHandler.cs index 419c773a9f..32a83cdc4a 100644 --- a/src/Business/Grand.Business.Checkout/Commands/Handlers/Orders/ProcessOrderPaidCommandHandler.cs +++ b/src/Business/Grand.Business.Checkout/Commands/Handlers/Orders/ProcessOrderPaidCommandHandler.cs @@ -15,19 +15,19 @@ public class ProcessOrderPaidCommandHandler : IRequestHandler(); - await _messageProviderService.SendOrderPaidCustomerMessage(order, customer, order.CustomerLanguageId, + await _orderMessageProviderService.SendOrderPaidCustomerMessage(order, customer, order.CustomerLanguageId, orderPaidAttachmentFilePath, orderPaidAttachmentFileName, orderPaidAttachments); - await _messageProviderService.SendOrderPaidStoreOwnerMessage(order, customer, + await _orderMessageProviderService.SendOrderPaidStoreOwnerMessage(order, customer, _languageSettings.DefaultAdminLanguageId); if (order.OrderItems.Any(x => !string.IsNullOrEmpty(x.VendorId))) { var vendors = await _mediator.Send(new GetVendorsInOrderQuery { Order = order }); foreach (var vendor in vendors) - await _messageProviderService.SendOrderPaidVendorMessage(order, vendor, + await _orderMessageProviderService.SendOrderPaidVendorMessage(order, vendor, _languageSettings.DefaultAdminLanguageId); } } diff --git a/src/Business/Grand.Business.Checkout/Commands/Handlers/Orders/RefundCommandHandler.cs b/src/Business/Grand.Business.Checkout/Commands/Handlers/Orders/RefundCommandHandler.cs index abe5eee38b..f439f4361e 100644 --- a/src/Business/Grand.Business.Checkout/Commands/Handlers/Orders/RefundCommandHandler.cs +++ b/src/Business/Grand.Business.Checkout/Commands/Handlers/Orders/RefundCommandHandler.cs @@ -16,7 +16,7 @@ public class RefundCommandHandler : IRequestHandler private readonly LanguageSettings _languageSettings; private readonly ILogger _logger; private readonly IMediator _mediator; - private readonly IMessageProviderService _messageProviderService; + private readonly IOrderMessageProviderService _orderMessageProviderService; private readonly IOrderService _orderService; private readonly IPaymentService _paymentService; private readonly IPaymentTransactionService _paymentTransactionService; @@ -26,7 +26,7 @@ public RefundCommandHandler( IPaymentService paymentService, IPaymentTransactionService paymentTransactionService, IOrderService orderService, - IMessageProviderService messageProviderService, + IOrderMessageProviderService orderMessageProviderService, ILogger logger, LanguageSettings languageSettings) { @@ -34,7 +34,7 @@ public RefundCommandHandler( _paymentService = paymentService; _paymentTransactionService = paymentTransactionService; _orderService = orderService; - _messageProviderService = messageProviderService; + _orderMessageProviderService = orderMessageProviderService; _logger = logger; _languageSettings = languageSettings; } @@ -75,11 +75,11 @@ public async Task> Handle(RefundCommand command, CancellationToken await _mediator.Send(new CheckOrderStatusCommand { Order = order }, cancellationToken); //notifications for store owner - await _messageProviderService.SendOrderRefundedStoreOwnerMessage(order, request.AmountToRefund, + await _orderMessageProviderService.SendOrderRefundedStoreOwnerMessage(order, request.AmountToRefund, _languageSettings.DefaultAdminLanguageId); //notifications for customer - await _messageProviderService.SendOrderRefundedCustomerMessage(order, request.AmountToRefund, + await _orderMessageProviderService.SendOrderRefundedCustomerMessage(order, request.AmountToRefund, order.CustomerLanguageId); //raise event diff --git a/src/Business/Grand.Business.Checkout/Commands/Handlers/Orders/RefundOfflineCommandHandler.cs b/src/Business/Grand.Business.Checkout/Commands/Handlers/Orders/RefundOfflineCommandHandler.cs index 89b145a4fa..c2445a0b50 100644 --- a/src/Business/Grand.Business.Checkout/Commands/Handlers/Orders/RefundOfflineCommandHandler.cs +++ b/src/Business/Grand.Business.Checkout/Commands/Handlers/Orders/RefundOfflineCommandHandler.cs @@ -15,7 +15,7 @@ public class RefundOfflineCommandHandler : IRequestHandler { private readonly IMediator _mediator; - private readonly IMessageProviderService _messageProviderService; + private readonly IOrderMessageProviderService _orderMessageProviderService; private readonly IOrderService _orderService; private readonly IShipmentService _shipmentService; @@ -21,12 +21,12 @@ public DeliveryCommandHandler( IMediator mediator, IOrderService orderService, IShipmentService shipmentService, - IMessageProviderService messageProviderService) + IOrderMessageProviderService orderMessageProviderService) { _mediator = mediator; _orderService = orderService; _shipmentService = shipmentService; - _messageProviderService = messageProviderService; + _orderMessageProviderService = orderMessageProviderService; } public async Task Handle(DeliveryCommand request, CancellationToken cancellationToken) @@ -61,7 +61,7 @@ await _orderService.InsertOrderNote(new OrderNote { }); if (request.NotifyCustomer) //send email notification - await _messageProviderService.SendShipmentDeliveredCustomerMessage(request.Shipment, order); + await _orderMessageProviderService.SendShipmentDeliveredCustomerMessage(request.Shipment, order); //check order status await _mediator.Send(new CheckOrderStatusCommand { Order = order }, cancellationToken); diff --git a/src/Business/Grand.Business.Checkout/Commands/Handlers/Shipping/ShipCommandHandler.cs b/src/Business/Grand.Business.Checkout/Commands/Handlers/Shipping/ShipCommandHandler.cs index 17d24fd0dd..75df0956b7 100644 --- a/src/Business/Grand.Business.Checkout/Commands/Handlers/Shipping/ShipCommandHandler.cs +++ b/src/Business/Grand.Business.Checkout/Commands/Handlers/Shipping/ShipCommandHandler.cs @@ -13,7 +13,7 @@ namespace Grand.Business.Checkout.Commands.Handlers.Shipping; public class ShipCommandHandler : IRequestHandler { private readonly IMediator _mediator; - private readonly IMessageProviderService _messageProviderService; + private readonly IOrderMessageProviderService _orderMessageProviderService; private readonly IOrderService _orderService; private readonly IShipmentService _shipmentService; @@ -21,12 +21,12 @@ public ShipCommandHandler( IMediator mediator, IOrderService orderService, IShipmentService shipmentService, - IMessageProviderService messageProviderService) + IOrderMessageProviderService orderMessageProviderService) { _mediator = mediator; _orderService = orderService; _shipmentService = shipmentService; - _messageProviderService = messageProviderService; + _orderMessageProviderService = orderMessageProviderService; } public async Task Handle(ShipCommand request, CancellationToken cancellationToken) @@ -67,7 +67,7 @@ await _orderService.InsertOrderNote(new OrderNote { if (request.NotifyCustomer) //notify customer - await _messageProviderService.SendShipmentSentCustomerMessage(request.Shipment, order); + await _orderMessageProviderService.SendShipmentSentCustomerMessage(request.Shipment, order); //check order status await _mediator.Send(new CheckOrderStatusCommand { Order = order }, cancellationToken); diff --git a/src/Business/Grand.Business.Core/Interfaces/Messages/ICatalogMessageProviderService.cs b/src/Business/Grand.Business.Core/Interfaces/Messages/ICatalogMessageProviderService.cs new file mode 100644 index 0000000000..bf00c85c93 --- /dev/null +++ b/src/Business/Grand.Business.Core/Interfaces/Messages/ICatalogMessageProviderService.cs @@ -0,0 +1,141 @@ +using Grand.Domain.Catalog; +using Grand.Domain.Customers; +using Grand.Domain.Stores; + +namespace Grand.Business.Core.Interfaces.Messages; + +public interface ICatalogMessageProviderService +{ + /// + /// Sends "email a friend" message + /// + /// Customer instance + /// Store + /// Message language identifier + /// Product instance + /// Customer's email + /// Friend's email + /// Personal message + /// Queued email identifier + /// + /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain + /// + Task SendProductEmailAFriendMessage(Customer customer, Store store, string languageId, + Product product, string customerEmail, string friendsEmail, string personalMessage, DomainHost host = null); + + /// + /// Sends wishlist "email a friend" message + /// + /// Customer + /// Store + /// Message language identifier + /// Customer's email + /// Friend's email + /// Personal message + /// Queued email identifier + /// + /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain + /// + Task SendWishlistEmailAFriendMessage(Customer customer, Store store, string languageId, + string customerEmail, string friendsEmail, string personalMessage, DomainHost host = null); + + /// + /// Sends "email a friend" message + /// + /// Queued email identifier + /// + /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain + /// + Task SendProductQuestionMessage(Customer customer, Store store, string languageId, + Product product, string customerEmail, string fullName, string phone, string message, string ipaddress, + DomainHost host = null); + + /// + /// Sends a product review notification message to a store owner + /// + /// Product + /// Product review + /// Store + /// Message language identifier + /// Queued email identifier + /// + /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain + /// + Task SendProductReviewMessage(Product product, ProductReview productReview, Store store, string languageId, + DomainHost host = null); + + /// + /// Sends a "quantity below" notification to a store owner + /// + /// Product + /// Message language identifier + /// Queued email identifier + Task SendQuantityBelowStoreOwnerMessage(Product product, string languageId); + + /// + /// Sends a "quantity below" notification to a store owner + /// + /// Product + /// Attribute combination + /// Message language identifier + /// Queued email identifier + Task SendQuantityBelowStoreOwnerMessage(Product product, ProductAttributeCombination combination, + string languageId); + + /// + /// Sends a 'Back in stock' notification message to a customer + /// + /// Customer + /// Product + /// Subscription + /// Message language identifier + /// Queued email identifier + Task SendBackInStockMessage(Customer customer, Product product, OutOfStockSubscription subscription, + string languageId); + + /// + /// Sends auction ended notification to a customer (winner) + /// + /// Auction + /// Message language identifier + /// Bid + /// Queued email identifier + Task SendAuctionWinEndedCustomerMessage(Product product, string languageId, Bid bid); + + /// + /// Sends auction ended notification to a customer (loser) + /// + /// Auction + /// Message language identifier + /// + /// Queued email identifier + Task SendAuctionEndedLostCustomerMessage(Product product, string languageId, Bid bid); + + /// + /// Sends auction ended notification to a customer (loser - bin) + /// + /// Auction + /// Customer identifier + /// Message language identifier + /// Store identifier + /// Queued email identifier + Task SendAuctionEndedBinCustomerMessage(Product product, string customerId, string languageId, string storeId); + + /// + /// Sends auction ended notification to a store owner + /// + /// Message language identifier + /// Auction + /// Bid + /// Queued email identifier + Task SendAuctionEndedStoreOwnerMessage(Product product, string languageId, Bid bid); + + /// + /// Send outbid notification to a customer + /// + /// Message language identifier + /// Product + /// Bid + /// Queued email identifier + Task SendOutBidCustomerMessage(Product product, string languageId, Bid bid); +} diff --git a/src/Business/Grand.Business.Core/Interfaces/Messages/ICmsMessageProviderService.cs b/src/Business/Grand.Business.Core/Interfaces/Messages/ICmsMessageProviderService.cs new file mode 100644 index 0000000000..4cb4539ebd --- /dev/null +++ b/src/Business/Grand.Business.Core/Interfaces/Messages/ICmsMessageProviderService.cs @@ -0,0 +1,45 @@ +using Grand.Domain.Blogs; +using Grand.Domain.Knowledgebase; +using Grand.Domain.News; +using Grand.Domain.Stores; + +namespace Grand.Business.Core.Interfaces.Messages; + +public interface ICmsMessageProviderService +{ + /// + /// Sends a blog comment notification message to a store owner + /// + /// Queued email identifier + /// + /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain + /// + Task SendBlogCommentMessage(BlogPost blogPost, BlogComment blogComment, string languageId, + DomainHost host = null); + + /// + /// Sends an article comment notification message to a store owner + /// + /// Article + /// Article comment + /// Message language identifier + /// Queued email identifier + /// + /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain + /// + Task SendArticleCommentMessage(KnowledgebaseArticle article, KnowledgebaseArticleComment articleComment, + string languageId, DomainHost host = null); + + /// + /// Sends a news comment notification message to a store owner + /// + /// News item + /// News comment + /// Message language identifier + /// Queued email identifier + /// + /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain + /// + Task SendNewsCommentMessage(NewsItem newsItem, NewsComment newsComment, string languageId, + DomainHost host = null); +} diff --git a/src/Business/Grand.Business.Core/Interfaces/Messages/ICustomerMessageProviderService.cs b/src/Business/Grand.Business.Core/Interfaces/Messages/ICustomerMessageProviderService.cs new file mode 100644 index 0000000000..edb8d64102 --- /dev/null +++ b/src/Business/Grand.Business.Core/Interfaces/Messages/ICustomerMessageProviderService.cs @@ -0,0 +1,113 @@ +using Grand.Domain.Common; +using Grand.Domain.Customers; +using Grand.Domain.Stores; + +namespace Grand.Business.Core.Interfaces.Messages; + +public interface ICustomerMessageProviderService +{ + /// + /// Sends 'New customer' notification message to a store owner + /// + /// Customer instance + /// Store + /// Message language identifier + /// Queued email identifier + /// + /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain + /// + Task SendCustomerRegisteredMessage(Customer customer, Store store, string languageId, DomainHost host = null); + + /// + /// Sends a welcome message to a customer + /// + /// Customer instance + /// Store + /// Message language identifier + /// Queued email identifier + /// + /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain + /// + Task SendCustomerWelcomeMessage(Customer customer, Store store, string languageId, DomainHost host = null); + + /// + /// Sends an email validation message to a customer + /// + /// Customer instance + /// Store + /// Message language identifier + /// Queued email identifier + /// + /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain + /// + Task SendCustomerEmailValidationMessage(Customer customer, Store store, string languageId, + DomainHost host = null); + + /// + /// Sends password recovery message to a customer + /// + /// Customer instance + /// Store + /// Message language identifier + /// Queued email identifier + /// + /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain + /// + Task SendCustomerPasswordRecoveryMessage(Customer customer, Store store, string languageId, + DomainHost host = null); + + /// + /// Sends a new customer note added notification to a customer + /// + /// Customer instance + /// Customer note + /// Store + /// Message language identifier + /// Queued email identifier + Task SendNewCustomerNoteMessage(CustomerNote customerNote, Customer customer, Store store, string languageId); + + /// + /// Send an email token validation message to a customer + /// + /// Customer instance + /// Store instance + /// Message language identifier + /// Queued email identifier + /// + /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain + /// + Task SendCustomerEmailTokenValidationMessage(Customer customer, Store store, string languageId, + DomainHost host = null); + + /// + /// Sends a "customer delete" notification to a store owner + /// + /// Customer + /// Message language identifier + /// Queued email identifier + /// + /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain + /// + Task SendCustomerDeleteStoreOwnerMessage(Customer customer, string languageId, DomainHost host = null); + + /// + /// Sends "contact us" message + /// + /// Customer + /// Store + /// Message language identifier + /// Sender email + /// Sender name + /// Email subject. Pass null if you want a message template subject to be used. + /// Email body + /// Attr info + /// Custom Attributes + /// ip address + /// Queued email identifier + /// + /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain + /// + Task SendContactUsMessage(Customer customer, Store store, string languageId, string senderEmail, + string senderName, string subject, string body, string attrInfo, IList customAttributes, + string ipaddress, DomainHost host = null); +} diff --git a/src/Business/Grand.Business.Core/Interfaces/Messages/IMerchandiseReturnMessageProviderService.cs b/src/Business/Grand.Business.Core/Interfaces/Messages/IMerchandiseReturnMessageProviderService.cs new file mode 100644 index 0000000000..1d9e7acbba --- /dev/null +++ b/src/Business/Grand.Business.Core/Interfaces/Messages/IMerchandiseReturnMessageProviderService.cs @@ -0,0 +1,53 @@ +using Grand.Domain.Orders; +using Grand.Domain.Stores; + +namespace Grand.Business.Core.Interfaces.Messages; + +public interface IMerchandiseReturnMessageProviderService +{ + /// + /// Sends 'New Merchandise Return' message to a store owner + /// + /// Merchandise return + /// Order + /// Message language identifier + /// Queued email identifier + /// + /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain + /// + Task SendNewMerchandiseReturnStoreOwnerMessage(MerchandiseReturn merchandiseReturn, Order order, + string languageId, DomainHost host = null); + + /// + /// Sends 'Merchandise Return status changed' message to a customer + /// + /// Merchandise return + /// Order item + /// Message language identifier + /// Queued email identifier + Task SendMerchandiseReturnStatusChangedCustomerMessage(MerchandiseReturn merchandiseReturn, Order order, + string languageId); + + /// + /// Sends 'New Merchandise Return' message to a customer + /// + /// + /// + /// + /// + /// + /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain + /// + Task SendNewMerchandiseReturnCustomerMessage(MerchandiseReturn merchandiseReturn, Order order, + string languageId, DomainHost host = null); + + /// + /// Sends a new merchandise return note added notification to a customer + /// + /// Merchandise return + /// Merchandise return note + /// Order + /// Queued email identifier + Task SendNewMerchandiseReturnNoteAddedCustomerMessage(MerchandiseReturn merchandiseReturn, + MerchandiseReturnNote merchandiseReturnNote, Order order); +} diff --git a/src/Business/Grand.Business.Core/Interfaces/Messages/IMessageLookupService.cs b/src/Business/Grand.Business.Core/Interfaces/Messages/IMessageLookupService.cs new file mode 100644 index 0000000000..bd035377f8 --- /dev/null +++ b/src/Business/Grand.Business.Core/Interfaces/Messages/IMessageLookupService.cs @@ -0,0 +1,31 @@ +using Grand.Domain.Localization; +using Grand.Domain.Messages; +using Grand.Domain.Stores; + +namespace Grand.Business.Core.Interfaces.Messages; + +/// +/// Resolves what a message needs before it is sent: store, active template, email account and language +/// +public interface IMessageLookupService +{ + /// + /// Gets the store by identifier, or the first store when it does not exist + /// + Task GetStore(string storeId); + + /// + /// Gets an active message template by name, or null when it is missing or inactive + /// + Task GetMessageTemplate(string messageTemplateName, string storeId); + + /// + /// Gets the email account of the message template, falling back to the default and then to any account + /// + Task GetEmailAccountOfMessageTemplate(MessageTemplate messageTemplate, string languageId); + + /// + /// Gets the language if published, else a published language of the store, else any language + /// + Task EnsureLanguageIsActive(string languageId, string storeId); +} diff --git a/src/Business/Grand.Business.Core/Interfaces/Messages/IMessageProviderService.cs b/src/Business/Grand.Business.Core/Interfaces/Messages/IMessageProviderService.cs index f28f90038a..6f12440117 100644 --- a/src/Business/Grand.Business.Core/Interfaces/Messages/IMessageProviderService.cs +++ b/src/Business/Grand.Business.Core/Interfaces/Messages/IMessageProviderService.cs @@ -1,623 +1,10 @@ -using Grand.Domain.Blogs; -using Grand.Domain.Catalog; -using Grand.Domain.Common; -using Grand.Domain.Customers; -using Grand.Domain.Knowledgebase; +using Grand.Domain.Common; using Grand.Domain.Messages; -using Grand.Domain.News; -using Grand.Domain.Orders; -using Grand.Domain.Shipping; -using Grand.Domain.Stores; -using Grand.Domain.Vendors; namespace Grand.Business.Core.Interfaces.Messages; public interface IMessageProviderService { - #region Customer messages - - /// - /// Sends 'New customer' notification message to a store owner - /// - /// Customer instance - /// Store - /// Message language identifier - /// Queued email identifier - /// - /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain - /// - Task SendCustomerRegisteredMessage(Customer customer, Store store, string languageId, DomainHost host = null); - - /// - /// Sends a welcome message to a customer - /// - /// Customer instance - /// Store - /// Message language identifier - /// Queued email identifier - /// - /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain - /// - Task SendCustomerWelcomeMessage(Customer customer, Store store, string languageId, DomainHost host = null); - - /// - /// Sends an email validation message to a customer - /// - /// Customer instance - /// Store - /// Message language identifier - /// Queued email identifier - /// - /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain - /// - Task SendCustomerEmailValidationMessage(Customer customer, Store store, string languageId, - DomainHost host = null); - - /// - /// Sends password recovery message to a customer - /// - /// Customer instance - /// Store - /// Message language identifier - /// Queued email identifier - /// - /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain - /// - Task SendCustomerPasswordRecoveryMessage(Customer customer, Store store, string languageId, - DomainHost host = null); - - /// - /// Sends a new customer note added notification to a customer - /// - /// Customer instance - /// Customer note - /// Store - /// Message language identifier - /// Queued email identifier - Task SendNewCustomerNoteMessage(CustomerNote customerNote, Customer customer, Store store, string languageId); - - /// - /// Send an email token validation message to a customer - /// - /// Customer instance - /// Store instance - /// Message language identifier - /// Queued email identifier - /// - /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain - /// - Task SendCustomerEmailTokenValidationMessage(Customer customer, Store store, string languageId, - DomainHost host = null); - - #endregion - - #region Order messages - - /// - /// Sends an order placed notification to a vendor - /// - /// Order instance - /// Customer instance - /// Vendor instance - /// Message language identifier - /// Queued email identifier - /// - /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain - /// - Task SendOrderPlacedVendorMessage(Order order, Customer customer, Vendor vendor, string languageId, - DomainHost host = null); - - /// - /// Sends an order placed notification to a store owner - /// - /// Order instance - /// Customer instance - /// Message language identifier - /// Queued email identifier - /// - /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain - /// - Task SendOrderPlacedStoreOwnerMessage(Order order, Customer customer, string languageId, - DomainHost host = null); - - /// - /// Sends an order paid notification to a store owner - /// - /// Order instance - /// Customer instance - /// Message language identifier - /// Queued email identifier - Task SendOrderPaidStoreOwnerMessage(Order order, Customer customer, string languageId); - - /// - /// Sends an order paid notification to a customer - /// - /// Order instance - /// Customer instance - /// Message language identifier - /// Attachment file path - /// - /// Attachment file name. If specified, then this file name will be sent to a recipient. - /// Otherwise, "AttachmentFilePath" name will be used. - /// - /// Attachments ident - /// Queued email identifier - Task SendOrderPaidCustomerMessage(Order order, Customer customer, string languageId, - string attachmentFilePath = null, string attachmentFileName = null, IEnumerable attachments = null); - - /// - /// Sends an order paid notification to a vendor - /// - /// Order instance - /// Vendor instance - /// Message language identifier - /// Queued email identifier - Task SendOrderPaidVendorMessage(Order order, Vendor vendor, string languageId); - - /// - /// Sends an order placed notification to a customer - /// - /// Order instance - /// Customer - /// Message language identifier - /// Attachment file path - /// - /// Attachment file name. If specified, then this file name will be sent to a recipient. - /// Otherwise, "AttachmentFilePath" name will be used. - /// - /// Attachments ident - /// Queued email identifier - /// - /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain - /// - Task SendOrderPlacedCustomerMessage(Order order, Customer customer, string languageId, - string attachmentFilePath = null, string attachmentFileName = null, IEnumerable attachments = null, - DomainHost host = null); - - /// - /// Sends a shipment sent notification to a customer - /// - /// Shipment - /// Order - /// Queued email identifier - Task SendShipmentSentCustomerMessage(Shipment shipment, Order order); - - /// - /// Sends a shipment delivered notification to a customer - /// - /// Shipment - /// Order - /// Queued email identifier - Task SendShipmentDeliveredCustomerMessage(Shipment shipment, Order order); - - /// - /// Sends an order completed notification to a customer - /// - /// Order instance - /// Customer instance - /// Message language identifier - /// Attachment file path - /// - /// Attachment file name. If specified, then this file name will be sent to a recipient. - /// Otherwise, "AttachmentFilePath" name will be used. - /// - /// Attachments ident - /// Queued email identifier - Task SendOrderCompletedCustomerMessage(Order order, Customer customer, string languageId, - string attachmentFilePath = null, string attachmentFileName = null, IEnumerable attachments = null); - - /// - /// Sends an order cancelled notification to a customer - /// - /// Order instance - /// Customer instance - /// Message language identifier - /// Queued email identifier - Task SendOrderCancelledCustomerMessage(Order order, Customer customer, string languageId); - - /// - /// Sends an order cancelled notification to a store owner - /// - /// Order instance - /// Customer instance - /// Message language identifier - /// Queued email identifier - Task SendOrderCancelledStoreOwnerMessage(Order order, Customer customer, string languageId); - - /// - /// Sends an order cancel notification to a vendor - /// - /// Order instance - /// Vendor instance - /// Message language identifier - /// Queued email identifier - Task SendOrderCancelledVendorMessage(Order order, Vendor vendor, string languageId); - - /// - /// Sends an order refunded notification to a store owner - /// - /// Order instance - /// Amount refunded - /// Message language identifier - /// Queued email identifier - Task SendOrderRefundedStoreOwnerMessage(Order order, double refundedAmount, string languageId); - - /// - /// Sends an order refunded notification to a customer - /// - /// Order instance - /// Amount refunded - /// Message language identifier - /// Queued email identifier - Task SendOrderRefundedCustomerMessage(Order order, double refundedAmount, string languageId); - - /// - /// Sends a new order note added notification to a customer - /// - /// Order - /// Order note - /// Queued email identifier - /// - /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain - /// - Task SendNewOrderNoteAddedCustomerMessage(Order order, OrderNote orderNote, DomainHost host = null); - - #endregion - - #region Newsletter messages - - /// - /// Sends a newsletter subscription activation message - /// - /// Newsletter subscription - /// Language identifier - /// Queued email identifier - /// - /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain - /// - Task SendNewsLetterSubscriptionActivationMessage(NewsLetterSubscription subscription, - string languageId, DomainHost host = null); - - /// - /// Sends a newsletter subscription deactivation message - /// - /// Newsletter subscription - /// Language identifier - /// Queued email identifier - /// - /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain - /// - Task SendNewsLetterSubscriptionDeactivationMessage(NewsLetterSubscription subscription, - string languageId, DomainHost host = null); - - #endregion - - #region Send a message to a friend, ask question - - /// - /// Sends "email a friend" message - /// - /// Customer instance - /// Store - /// Message language identifier - /// Product instance - /// Customer's email - /// Friend's email - /// Personal message - /// Queued email identifier - /// - /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain - /// - Task SendProductEmailAFriendMessage(Customer customer, Store store, string languageId, - Product product, string customerEmail, string friendsEmail, string personalMessage, DomainHost host = null); - - /// - /// Sends wishlist "email a friend" message - /// - /// Customer - /// Store - /// Message language identifier - /// Customer's email - /// Friend's email - /// Personal message - /// Queued email identifier - /// - /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain - /// - Task SendWishlistEmailAFriendMessage(Customer customer, Store store, string languageId, - string customerEmail, string friendsEmail, string personalMessage, DomainHost host = null); - - - /// - /// Sends "email a friend" message - /// - /// Queued email identifier - /// - /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain - /// - Task SendProductQuestionMessage(Customer customer, Store store, string languageId, - Product product, string customerEmail, string fullName, string phone, string message, string ipaddress, - DomainHost host = null); - - #endregion - - #region Merchandise returns - - /// - /// Sends 'New Merchandise Return' message to a store owner - /// - /// Merchandise return - /// Order - /// Message language identifier - /// Queued email identifier - /// - /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain - /// - Task SendNewMerchandiseReturnStoreOwnerMessage(MerchandiseReturn merchandiseReturn, Order order, - string languageId, DomainHost host = null); - - - /// - /// Sends 'Merchandise Return status changed' message to a customer - /// - /// Merchandise return - /// Order item - /// Message language identifier - /// Queued email identifier - Task SendMerchandiseReturnStatusChangedCustomerMessage(MerchandiseReturn merchandiseReturn, Order order, - string languageId); - - /// - /// Sends 'New Merchandise Return' message to a customer - /// - /// - /// - /// - /// - /// - /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain - /// - Task SendNewMerchandiseReturnCustomerMessage(MerchandiseReturn merchandiseReturn, Order order, - string languageId, DomainHost host = null); - - /// - /// Sends a new merchandise return note added notification to a customer - /// - /// Merchandise return - /// Merchandise return note - /// Order - /// Queued email identifier - Task SendNewMerchandiseReturnNoteAddedCustomerMessage(MerchandiseReturn merchandiseReturn, - MerchandiseReturnNote merchandiseReturnNote, Order order); - - #endregion - - #region Misc - - /// - /// Sends 'New vendor account submitted' message to a store owner - /// - /// Customer - /// Vendor - /// Store - /// Message language identifier - /// Queued email identifier - /// - /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain - /// - Task SendNewVendorAccountApplyStoreOwnerMessage(Customer customer, Vendor vendor, Store store, - string languageId, DomainHost host = null); - - - /// - /// Sends 'Vendor information change' message to a store owner - /// - /// Vendor - /// Store - /// Message language identifier - /// Queued email identifier - Task SendVendorInformationChangeMessage(Vendor vendor, Store store, string languageId); - - /// - /// Sends a product review notification message to a store owner - /// - /// Product - /// Product review - /// Store - /// Message language identifier - /// Queued email identifier - /// - /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain - /// - Task SendProductReviewMessage(Product product, ProductReview productReview, Store store, string languageId, - DomainHost host = null); - - /// - /// Sends a vendor review notification message to a store owner - /// - /// Vendor review - /// Store - /// Message language identifier - /// Queued email identifier - /// - /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain - /// - Task SendVendorReviewMessage(VendorReview vendorReview, Store store, string languageId, - DomainHost host = null); - - /// - /// Sends a gift voucher notification - /// - /// Gift voucher - /// Order - /// Message language identifier - /// Queued email identifier - Task SendGiftVoucherMessage(GiftVoucher giftVoucher, Order order, string languageId); - - - /// - /// Sends a "quantity below" notification to a store owner - /// - /// Product - /// Message language identifier - /// Queued email identifier - Task SendQuantityBelowStoreOwnerMessage(Product product, string languageId); - - /// - /// Sends a "quantity below" notification to a store owner - /// - /// Product - /// Attribute combination - /// Message language identifier - /// Queued email identifier - Task SendQuantityBelowStoreOwnerMessage(Product product, ProductAttributeCombination combination, - string languageId); - - /// - /// Sends a "customer delete" notification to a store owner - /// - /// Customer - /// Message language identifier - /// Queued email identifier - /// - /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain - /// - Task SendCustomerDeleteStoreOwnerMessage(Customer customer, string languageId, DomainHost host = null); - - /// - /// Sends a blog comment notification message to a store owner - /// - /// Queued email identifier - /// - /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain - /// - Task SendBlogCommentMessage(BlogPost blogPost, BlogComment blogComment, string languageId, - DomainHost host = null); - - /// - /// Sends an article comment notification message to a store owner - /// - /// Article - /// Article comment - /// Message language identifier - /// Queued email identifier - /// - /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain - /// - Task SendArticleCommentMessage(KnowledgebaseArticle article, KnowledgebaseArticleComment articleComment, - string languageId, DomainHost host = null); - - /// - /// Sends a news comment notification message to a store owner - /// - /// News item - /// News comment - /// Message language identifier - /// Queued email identifier - /// - /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain - /// - Task SendNewsCommentMessage(NewsItem newsItem, NewsComment newsComment, string languageId, - DomainHost host = null); - - /// - /// Sends a 'Back in stock' notification message to a customer - /// - /// Customer - /// Product - /// Subscription - /// Message language identifier - /// Queued email identifier - Task SendBackInStockMessage(Customer customer, Product product, OutOfStockSubscription subscription, - string languageId); - - - /// - /// Sends "contact us" message - /// - /// Customer - /// Store - /// Message language identifier - /// Sender email - /// Sender name - /// Email subject. Pass null if you want a message template subject to be used. - /// Email body - /// Attr info - /// Custom Attributes - /// ip address - /// Queued email identifier - /// - /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain - /// - Task SendContactUsMessage(Customer customer, Store store, string languageId, string senderEmail, - string senderName, string subject, string body, string attrInfo, IList customAttributes, - string ipaddress, DomainHost host = null); - - /// - /// Sends "contact vendor" message - /// - /// Customer - /// Store - /// Vendor - /// Message language identifier - /// Sender email - /// Sender name - /// Email subject. Pass null if you want a message template subject to be used. - /// Email body - /// Ip Address - /// Queued email identifier - /// - /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain - /// - Task SendContactVendorMessage(Customer customer, Store store, Vendor vendor, string languageId, - string senderEmail, string senderName, string subject, string body, string ipaddress, DomainHost host = null); - - /// - /// Sends auction ended notification to a customer (winner) - /// - /// Auction - /// Message language identifier - /// Bid - /// Queued email identifier - Task SendAuctionWinEndedCustomerMessage(Product product, string languageId, Bid bid); - - /// - /// Sends auction ended notification to a customer (loser) - /// - /// Auction - /// Message language identifier - /// - /// Queued email identifier - Task SendAuctionEndedLostCustomerMessage(Product product, string languageId, Bid bid); - - /// - /// Sends auction ended notification to a customer (loser - bin) - /// - /// Auction - /// Customer identifier - /// Message language identifier - /// Store identifier - /// Queued email identifier - Task SendAuctionEndedBinCustomerMessage(Product product, string customerId, string languageId, string storeId); - - /// - /// Sends auction ended notification to a store owner - /// - /// Message language identifier - /// Auction - /// Bid - /// Queued email identifier - Task SendAuctionEndedStoreOwnerMessage(Product product, string languageId, Bid bid); - - /// - /// Send outbid notification to a customer - /// - /// Message language identifier - /// Product - /// Bid - /// Queued email identifier - Task SendOutBidCustomerMessage(Product product, string languageId, Bid bid); - /// /// Send notification /// @@ -646,6 +33,4 @@ Task SendNotification(MessageTemplate messageTemplate, string replyToEmailAddress = null, string replyToName = null, string fromEmail = null, string fromName = null, string subject = null, Reference reference = Reference.None, string objectId = ""); - - #endregion -} \ No newline at end of file +} diff --git a/src/Business/Grand.Business.Core/Interfaces/Messages/INewsletterMessageProviderService.cs b/src/Business/Grand.Business.Core/Interfaces/Messages/INewsletterMessageProviderService.cs new file mode 100644 index 0000000000..517fcaf3d1 --- /dev/null +++ b/src/Business/Grand.Business.Core/Interfaces/Messages/INewsletterMessageProviderService.cs @@ -0,0 +1,31 @@ +using Grand.Domain.Messages; +using Grand.Domain.Stores; + +namespace Grand.Business.Core.Interfaces.Messages; + +public interface INewsletterMessageProviderService +{ + /// + /// Sends a newsletter subscription activation message + /// + /// Newsletter subscription + /// Language identifier + /// Queued email identifier + /// + /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain + /// + Task SendNewsLetterSubscriptionActivationMessage(NewsLetterSubscription subscription, + string languageId, DomainHost host = null); + + /// + /// Sends a newsletter subscription deactivation message + /// + /// Newsletter subscription + /// Language identifier + /// Queued email identifier + /// + /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain + /// + Task SendNewsLetterSubscriptionDeactivationMessage(NewsLetterSubscription subscription, + string languageId, DomainHost host = null); +} diff --git a/src/Business/Grand.Business.Core/Interfaces/Messages/IOrderMessageProviderService.cs b/src/Business/Grand.Business.Core/Interfaces/Messages/IOrderMessageProviderService.cs new file mode 100644 index 0000000000..5734434ed9 --- /dev/null +++ b/src/Business/Grand.Business.Core/Interfaces/Messages/IOrderMessageProviderService.cs @@ -0,0 +1,188 @@ +using Grand.Domain.Customers; +using Grand.Domain.Orders; +using Grand.Domain.Shipping; +using Grand.Domain.Stores; +using Grand.Domain.Vendors; + +namespace Grand.Business.Core.Interfaces.Messages; + +public interface IOrderMessageProviderService +{ + /// + /// Sends an order placed notification to a vendor + /// + /// Order instance + /// Customer instance + /// Vendor instance + /// Message language identifier + /// Queued email identifier + /// + /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain + /// + Task SendOrderPlacedVendorMessage(Order order, Customer customer, Vendor vendor, string languageId, + DomainHost host = null); + + /// + /// Sends an order placed notification to a store owner + /// + /// Order instance + /// Customer instance + /// Message language identifier + /// Queued email identifier + /// + /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain + /// + Task SendOrderPlacedStoreOwnerMessage(Order order, Customer customer, string languageId, + DomainHost host = null); + + /// + /// Sends an order paid notification to a store owner + /// + /// Order instance + /// Customer instance + /// Message language identifier + /// Queued email identifier + Task SendOrderPaidStoreOwnerMessage(Order order, Customer customer, string languageId); + + /// + /// Sends an order paid notification to a customer + /// + /// Order instance + /// Customer instance + /// Message language identifier + /// Attachment file path + /// + /// Attachment file name. If specified, then this file name will be sent to a recipient. + /// Otherwise, "AttachmentFilePath" name will be used. + /// + /// Attachments ident + /// Queued email identifier + Task SendOrderPaidCustomerMessage(Order order, Customer customer, string languageId, + string attachmentFilePath = null, string attachmentFileName = null, IEnumerable attachments = null); + + /// + /// Sends an order paid notification to a vendor + /// + /// Order instance + /// Vendor instance + /// Message language identifier + /// Queued email identifier + Task SendOrderPaidVendorMessage(Order order, Vendor vendor, string languageId); + + /// + /// Sends an order placed notification to a customer + /// + /// Order instance + /// Customer + /// Message language identifier + /// Attachment file path + /// + /// Attachment file name. If specified, then this file name will be sent to a recipient. + /// Otherwise, "AttachmentFilePath" name will be used. + /// + /// Attachments ident + /// Queued email identifier + /// + /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain + /// + Task SendOrderPlacedCustomerMessage(Order order, Customer customer, string languageId, + string attachmentFilePath = null, string attachmentFileName = null, IEnumerable attachments = null, + DomainHost host = null); + + /// + /// Sends a shipment sent notification to a customer + /// + /// Shipment + /// Order + /// Queued email identifier + Task SendShipmentSentCustomerMessage(Shipment shipment, Order order); + + /// + /// Sends a shipment delivered notification to a customer + /// + /// Shipment + /// Order + /// Queued email identifier + Task SendShipmentDeliveredCustomerMessage(Shipment shipment, Order order); + + /// + /// Sends an order completed notification to a customer + /// + /// Order instance + /// Customer instance + /// Message language identifier + /// Attachment file path + /// + /// Attachment file name. If specified, then this file name will be sent to a recipient. + /// Otherwise, "AttachmentFilePath" name will be used. + /// + /// Attachments ident + /// Queued email identifier + Task SendOrderCompletedCustomerMessage(Order order, Customer customer, string languageId, + string attachmentFilePath = null, string attachmentFileName = null, IEnumerable attachments = null); + + /// + /// Sends an order cancelled notification to a customer + /// + /// Order instance + /// Customer instance + /// Message language identifier + /// Queued email identifier + Task SendOrderCancelledCustomerMessage(Order order, Customer customer, string languageId); + + /// + /// Sends an order cancelled notification to a store owner + /// + /// Order instance + /// Customer instance + /// Message language identifier + /// Queued email identifier + Task SendOrderCancelledStoreOwnerMessage(Order order, Customer customer, string languageId); + + /// + /// Sends an order cancel notification to a vendor + /// + /// Order instance + /// Vendor instance + /// Message language identifier + /// Queued email identifier + Task SendOrderCancelledVendorMessage(Order order, Vendor vendor, string languageId); + + /// + /// Sends an order refunded notification to a store owner + /// + /// Order instance + /// Amount refunded + /// Message language identifier + /// Queued email identifier + Task SendOrderRefundedStoreOwnerMessage(Order order, double refundedAmount, string languageId); + + /// + /// Sends an order refunded notification to a customer + /// + /// Order instance + /// Amount refunded + /// Message language identifier + /// Queued email identifier + Task SendOrderRefundedCustomerMessage(Order order, double refundedAmount, string languageId); + + /// + /// Sends a new order note added notification to a customer + /// + /// Order + /// Order note + /// Queued email identifier + /// + /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain + /// + Task SendNewOrderNoteAddedCustomerMessage(Order order, OrderNote orderNote, DomainHost host = null); + + /// + /// Sends a gift voucher notification + /// + /// Gift voucher + /// Order + /// Message language identifier + /// Queued email identifier + Task SendGiftVoucherMessage(GiftVoucher giftVoucher, Order order, string languageId); +} diff --git a/src/Business/Grand.Business.Core/Interfaces/Messages/IVendorMessageProviderService.cs b/src/Business/Grand.Business.Core/Interfaces/Messages/IVendorMessageProviderService.cs new file mode 100644 index 0000000000..7652ae7079 --- /dev/null +++ b/src/Business/Grand.Business.Core/Interfaces/Messages/IVendorMessageProviderService.cs @@ -0,0 +1,64 @@ +using Grand.Domain.Common; +using Grand.Domain.Customers; +using Grand.Domain.Stores; +using Grand.Domain.Vendors; + +namespace Grand.Business.Core.Interfaces.Messages; + +public interface IVendorMessageProviderService +{ + /// + /// Sends 'New vendor account submitted' message to a store owner + /// + /// Customer + /// Vendor + /// Store + /// Message language identifier + /// Queued email identifier + /// + /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain + /// + Task SendNewVendorAccountApplyStoreOwnerMessage(Customer customer, Vendor vendor, Store store, + string languageId, DomainHost host = null); + + /// + /// Sends 'Vendor information change' message to a store owner + /// + /// Vendor + /// Store + /// Message language identifier + /// Queued email identifier + Task SendVendorInformationChangeMessage(Vendor vendor, Store store, string languageId); + + /// + /// Sends a vendor review notification message to a store owner + /// + /// Vendor review + /// Store + /// Message language identifier + /// Queued email identifier + /// + /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain + /// + Task SendVendorReviewMessage(VendorReview vendorReview, Store store, string languageId, + DomainHost host = null); + + /// + /// Sends "contact vendor" message + /// + /// Customer + /// Store + /// Vendor + /// Message language identifier + /// Sender email + /// Sender name + /// Email subject. Pass null if you want a message template subject to be used. + /// Email body + /// Ip Address + /// Queued email identifier + /// + /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain + /// + Task SendContactVendorMessage(Customer customer, Store store, Vendor vendor, string languageId, + string senderEmail, string senderName, string subject, string body, string ipaddress, DomainHost host = null); +} diff --git a/src/Business/Grand.Business.Messages/Commands/Handlers/Catalog/SendNotificationsToSubscribersCommandHandler.cs b/src/Business/Grand.Business.Messages/Commands/Handlers/Catalog/SendNotificationsToSubscribersCommandHandler.cs index 7babf68260..bbcc92ad6a 100644 --- a/src/Business/Grand.Business.Messages/Commands/Handlers/Catalog/SendNotificationsToSubscribersCommandHandler.cs +++ b/src/Business/Grand.Business.Messages/Commands/Handlers/Catalog/SendNotificationsToSubscribersCommandHandler.cs @@ -15,16 +15,16 @@ public class IList> { private readonly ICustomerService _customerService; - private readonly IMessageProviderService _messageProviderService; + private readonly ICatalogMessageProviderService _catalogMessageProviderService; private readonly IRepository _outOfStockSubscriptionRepository; public SendNotificationsToSubscribersCommandHandler( ICustomerService customerService, - IMessageProviderService messageProviderService, + ICatalogMessageProviderService catalogMessageProviderService, IRepository outOfStockSubscriptionRepository) { _customerService = customerService; - _messageProviderService = messageProviderService; + _catalogMessageProviderService = catalogMessageProviderService; _outOfStockSubscriptionRepository = outOfStockSubscriptionRepository; } @@ -42,7 +42,7 @@ public async Task> Handle(SendNotificationsToSubsc if (customer == null || !CommonHelper.IsValidEmail(customer.Email)) continue; var customerLanguageId = customer.GetUserFieldFromEntity(SystemCustomerFieldNames.LanguageId, subscription.StoreId); - await _messageProviderService.SendBackInStockMessage(customer, request.Product, subscription, + await _catalogMessageProviderService.SendBackInStockMessage(customer, request.Product, subscription, customerLanguageId); } diff --git a/src/Business/Grand.Business.Messages/Commands/Handlers/Catalog/SendOutBidCustomerNotificationCommandHandler.cs b/src/Business/Grand.Business.Messages/Commands/Handlers/Catalog/SendOutBidCustomerNotificationCommandHandler.cs index 599567da1a..9dddab3d2b 100644 --- a/src/Business/Grand.Business.Messages/Commands/Handlers/Catalog/SendOutBidCustomerNotificationCommandHandler.cs +++ b/src/Business/Grand.Business.Messages/Commands/Handlers/Catalog/SendOutBidCustomerNotificationCommandHandler.cs @@ -6,16 +6,16 @@ namespace Grand.Module.ScheduledTasks.Commands.Handlers.Catalog; public class SendOutBidCustomerNotificationCommandHandler : IRequestHandler { - private readonly IMessageProviderService _messageProviderService; + private readonly ICatalogMessageProviderService _catalogMessageProviderService; - public SendOutBidCustomerNotificationCommandHandler(IMessageProviderService messageProviderService) + public SendOutBidCustomerNotificationCommandHandler(ICatalogMessageProviderService catalogMessageProviderService) { - _messageProviderService = messageProviderService; + _catalogMessageProviderService = catalogMessageProviderService; } public async Task Handle(SendOutBidCustomerCommand request, CancellationToken cancellationToken) { - await _messageProviderService.SendOutBidCustomerMessage(request.Product, request.Language.Id, request.Bid); + await _catalogMessageProviderService.SendOutBidCustomerMessage(request.Product, request.Language.Id, request.Bid); return true; } } \ No newline at end of file diff --git a/src/Business/Grand.Business.Messages/Commands/Handlers/Catalog/SendQuantityBelowStoreOwnerNotificationCommandHandler.cs b/src/Business/Grand.Business.Messages/Commands/Handlers/Catalog/SendQuantityBelowStoreOwnerNotificationCommandHandler.cs index 4dc66b0afc..c148d5dfcd 100644 --- a/src/Business/Grand.Business.Messages/Commands/Handlers/Catalog/SendQuantityBelowStoreOwnerNotificationCommandHandler.cs +++ b/src/Business/Grand.Business.Messages/Commands/Handlers/Catalog/SendQuantityBelowStoreOwnerNotificationCommandHandler.cs @@ -9,23 +9,23 @@ public class SendQuantityBelowStoreOwnerNotificationCommandHandler : IRequestHandler { private readonly LanguageSettings _languageSettings; - private readonly IMessageProviderService _messageProviderService; + private readonly ICatalogMessageProviderService _catalogMessageProviderService; public SendQuantityBelowStoreOwnerNotificationCommandHandler( - IMessageProviderService messageProviderService, + ICatalogMessageProviderService catalogMessageProviderService, LanguageSettings languageSettings) { - _messageProviderService = messageProviderService; + _catalogMessageProviderService = catalogMessageProviderService; _languageSettings = languageSettings; } public async Task Handle(SendQuantityBelowStoreOwnerCommand request, CancellationToken cancellationToken) { if (request.ProductAttributeCombination == null) - await _messageProviderService.SendQuantityBelowStoreOwnerMessage(request.Product, + await _catalogMessageProviderService.SendQuantityBelowStoreOwnerMessage(request.Product, _languageSettings.DefaultAdminLanguageId); else - await _messageProviderService.SendQuantityBelowStoreOwnerMessage(request.Product, + await _catalogMessageProviderService.SendQuantityBelowStoreOwnerMessage(request.Product, request.ProductAttributeCombination, _languageSettings.DefaultAdminLanguageId); return true; diff --git a/src/Business/Grand.Business.Messages/DotLiquidDrops/LiquidObjectBuilderExtensions.cs b/src/Business/Grand.Business.Messages/DotLiquidDrops/LiquidObjectBuilderExtensions.cs new file mode 100644 index 0000000000..954506d11e --- /dev/null +++ b/src/Business/Grand.Business.Messages/DotLiquidDrops/LiquidObjectBuilderExtensions.cs @@ -0,0 +1,17 @@ +using Grand.Domain.Customers; +using Grand.Domain.Localization; +using Grand.Domain.Stores; + +namespace Grand.Business.Messages.DotLiquidDrops; + +internal static class LiquidObjectBuilderExtensions +{ + public static void AddCustomerTokensIfNotNull(this LiquidObjectBuilder builder, Customer customer, Store store, + DomainHost host, Language language) + { + if (customer != null) + { + builder.AddCustomerTokens(customer, store, host, language); + } + } +} diff --git a/src/Business/Grand.Business.Messages/Services/CatalogMessageProviderService.cs b/src/Business/Grand.Business.Messages/Services/CatalogMessageProviderService.cs new file mode 100644 index 0000000000..39c214c965 --- /dev/null +++ b/src/Business/Grand.Business.Messages/Services/CatalogMessageProviderService.cs @@ -0,0 +1,605 @@ +using Grand.Business.Core.Commands.Messages.Common; +using Grand.Business.Core.Extensions; +using Grand.Business.Core.Interfaces.Common.Stores; +using Grand.Business.Core.Interfaces.Messages; +using Grand.Business.Core.Queries.Messages; +using Grand.Business.Messages.DotLiquidDrops; +using Grand.Domain.Catalog; +using Grand.Domain.Common; +using Grand.Domain.Customers; +using Grand.Domain.Localization; +using Grand.Domain.Messages; +using Grand.Domain.Stores; +using Grand.Mediator; + +namespace Grand.Business.Messages.Services; + +public class CatalogMessageProviderService : ICatalogMessageProviderService +{ + #region Ctor + + public CatalogMessageProviderService( + IMessageLookupService messageLookupService, + IMessageProviderService messageProviderService, + IMediator mediator, + IStoreService storeService, + CommonSettings commonSettings) + { + _messageLookupService = messageLookupService; + _messageProviderService = messageProviderService; + _mediator = mediator; + _storeService = storeService; + _commonSettings = commonSettings; + } + + #endregion + + #region Fields + + private readonly IMessageLookupService _messageLookupService; + private readonly IMessageProviderService _messageProviderService; + private readonly IMediator _mediator; + private readonly IStoreService _storeService; + private readonly CommonSettings _commonSettings; + + #endregion + + #region Methods + + /// + /// Sends "email a friend" message + /// + /// Customer instance + /// Store + /// Message language identifier + /// Product instance + /// Customer's email + /// Friend's email + /// Personal message + /// Queued email identifier + /// + /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain + /// + public virtual async Task SendProductEmailAFriendMessage(Customer customer, Store store, string languageId, + Product product, string customerEmail, string friendsEmail, string personalMessage, DomainHost host = null) + { + ArgumentNullException.ThrowIfNull(customer); + ArgumentNullException.ThrowIfNull(product); + + var language = await _messageLookupService.EnsureLanguageIsActive(languageId, store.Id); + + var messageTemplate = await _messageLookupService.GetMessageTemplate(MessageTemplateNames.SendProductEmailAFriendMessage, store.Id); + if (messageTemplate == null) + return 0; + + //email account + var emailAccount = await _messageLookupService.GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); + + host = store.LinkHost(host); + var builder = new LiquidObjectBuilder(_mediator); + builder.AddStoreTokens(store, language, emailAccount) + .AddEmailAFriendTokens(personalMessage, customerEmail, friendsEmail) + .AddCustomerTokens(customer, store, host, language) + .AddProductTokens(product, language, store, host); + var liquidObject = await builder.BuildAsync(); + + //event notification + await _mediator.MessageTokensAdded(messageTemplate, liquidObject); + + var toEmail = friendsEmail; + var toName = ""; + return await _messageProviderService.SendNotification(messageTemplate, emailAccount, + languageId, liquidObject, + toEmail, toName, + reference: Reference.Customer, objectId: customer.Id); + } + + /// + /// Sends wishlist "email a friend" message + /// + /// Customer + /// Store + /// Message language identifier + /// Customer's email + /// Friend's email + /// Personal message + /// Queued email identifier + /// + /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain + /// + public virtual async Task SendWishlistEmailAFriendMessage(Customer customer, Store store, string languageId, + string customerEmail, string friendsEmail, string personalMessage, DomainHost host = null) + { + ArgumentNullException.ThrowIfNull(customer); + + var language = await _messageLookupService.EnsureLanguageIsActive(languageId, store.Id); + + var messageTemplate = await _messageLookupService.GetMessageTemplate(MessageTemplateNames.SendWishlistEmailAFriendMessage, store.Id); + if (messageTemplate == null) + return 0; + + //email account + var emailAccount = await _messageLookupService.GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); + + + host = store.LinkHost(host); + var builder = new LiquidObjectBuilder(_mediator); + builder.AddStoreTokens(store, language, emailAccount) + .AddCustomerTokens(customer, store, host, language) + .AddEmailAFriendTokens(personalMessage, customerEmail, friendsEmail); + + var liquidObject = await builder.BuildAsync(); + + //event notification + await _mediator.MessageTokensAdded(messageTemplate, liquidObject); + + var toEmail = friendsEmail; + var toName = ""; + return await _messageProviderService.SendNotification(messageTemplate, emailAccount, + languageId, liquidObject, + toEmail, toName, + reference: Reference.Customer, objectId: customer.Id); + } + + /// + /// Sends "email a friend" message + /// + /// Queued email identifier + /// + /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain + /// + public virtual async Task SendProductQuestionMessage(Customer customer, Store store, string languageId, + Product product, string customerEmail, string fullName, string phone, string message, string ipaddress, + DomainHost host = null) + { + ArgumentNullException.ThrowIfNull(customer); + ArgumentNullException.ThrowIfNull(product); + + var language = await _messageLookupService.EnsureLanguageIsActive(languageId, store.Id); + + var messageTemplate = await _messageLookupService.GetMessageTemplate(MessageTemplateNames.SendProductQuestionMessage, store.Id); + if (messageTemplate == null) + return 0; + + //email account + var emailAccount = await _messageLookupService.GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); + + host = store.LinkHost(host); + var builder = new LiquidObjectBuilder(_mediator); + builder.AddStoreTokens(store, language, emailAccount) + .AddCustomerTokens(customer, store, host, language) + .AddProductTokens(product, language, store, host); + var liquidObject = await builder.BuildAsync(); + liquidObject.AskQuestion = new LiquidAskQuestion(message, customerEmail, fullName, phone); + + //event notification + await _mediator.MessageTokensAdded(messageTemplate, liquidObject); + + //store in database + if (_commonSettings.StoreInDatabaseContactUsForm) + { + var subject = messageTemplate.GetTranslation(mt => mt.Subject, languageId); + var body = messageTemplate.GetTranslation(mt => mt.Body, languageId); + + var subjectReplaced = LiquidExtensions.Render(liquidObject, subject); + var bodyReplaced = LiquidExtensions.Render(liquidObject, body); + + await _mediator.Send(new InsertContactUsCommand { + CustomerId = customer.Id, + StoreId = store.Id, + VendorId = product.VendorId, + Email = customerEmail, + Enquiry = bodyReplaced, + FullName = fullName, + Subject = subjectReplaced, + EmailAccountId = emailAccount.Id, + RemoteIpAddress = ipaddress + }); + } + + var toEmail = emailAccount.Email; + var toName = ""; + + if (!string.IsNullOrEmpty(product.VendorId)) + { + var vendor = await _mediator.Send(new GetVendorByIdQuery { Id = product.VendorId }); + if (vendor != null) + { + toEmail = vendor.Email; + toName = vendor.Name; + } + } + + return await _messageProviderService.SendNotification(messageTemplate, emailAccount, + languageId, liquidObject, + toEmail, toName, replyToEmailAddress: customerEmail, + reference: Reference.Customer, objectId: customer.Id); + } + + /// + /// Sends a product review notification message to a store owner + /// + /// Product + /// Product review + /// Store + /// Message language identifier + /// + /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain + /// + public virtual async Task SendProductReviewMessage(Product product, ProductReview productReview, + Store store, string languageId, DomainHost host = null) + { + ArgumentNullException.ThrowIfNull(productReview); + + var language = await _messageLookupService.EnsureLanguageIsActive(languageId, store.Id); + + var messageTemplate = await _messageLookupService.GetMessageTemplate("Product.ProductReview", store.Id); + if (messageTemplate == null) + return 0; + + //email account + var emailAccount = await _messageLookupService.GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); + + var customer = await _mediator.Send(new GetCustomerByIdQuery { Id = productReview.CustomerId }); + + host = store.LinkHost(host); + var builder = new LiquidObjectBuilder(_mediator); + builder.AddStoreTokens(store, language, emailAccount) + .AddProductReviewTokens(product, productReview); + builder.AddCustomerTokensIfNotNull(customer, store, host, language); + + var liquidObject = await builder.BuildAsync(); + //event notification + await _mediator.MessageTokensAdded(messageTemplate, liquidObject); + + var toEmail = emailAccount.Email; + var toName = emailAccount.DisplayName; + return await _messageProviderService.SendNotification(messageTemplate, emailAccount, + languageId, liquidObject, + toEmail, toName, + reference: Reference.Product, objectId: product.Id); + } + + /// + /// Sends a "quantity below" notification to a store owner + /// + /// Product + /// Message language identifier + public virtual async Task SendQuantityBelowStoreOwnerMessage(Product product, string languageId) + { + ArgumentNullException.ThrowIfNull(product); + + var store = await _messageLookupService.GetStore(""); + var language = await _messageLookupService.EnsureLanguageIsActive(languageId, store.Id); + + var messageTemplate = await _messageLookupService.GetMessageTemplate("QuantityBelow.StoreOwnerNotification", store.Id); + if (messageTemplate == null) + return 0; + + //email account + var emailAccount = await _messageLookupService.GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); + + var host = store.PrimaryHost(); + var builder = new LiquidObjectBuilder(_mediator); + builder.AddStoreTokens(store, language, emailAccount) + .AddProductTokens(product, language, store, host); + var liquidObject = await builder.BuildAsync(); + //event notification + await _mediator.MessageTokensAdded(messageTemplate, liquidObject); + + var toEmail = emailAccount.Email; + var toName = emailAccount.DisplayName; + return await _messageProviderService.SendNotification(messageTemplate, emailAccount, + languageId, liquidObject, + toEmail, toName, + reference: Reference.Product, objectId: product.Id); + } + + /// + /// Sends a "quantity below" notification to a store owner + /// + /// + /// Attribute combination + /// Message language identifier + public virtual async Task SendQuantityBelowStoreOwnerMessage(Product product, + ProductAttributeCombination combination, string languageId) + { + ArgumentNullException.ThrowIfNull(combination); + + var store = await _messageLookupService.GetStore(""); + var language = await _messageLookupService.EnsureLanguageIsActive(languageId, store.Id); + + var messageTemplate = + await _messageLookupService.GetMessageTemplate("QuantityBelow.AttributeCombination.StoreOwnerNotification", store.Id); + if (messageTemplate == null) + return 0; + + //email account + var emailAccount = await _messageLookupService.GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); + + var host = store.PrimaryHost(); + var builder = new LiquidObjectBuilder(_mediator); + builder.AddStoreTokens(store, language, emailAccount) + .AddProductTokens(product, language, store, host) + .AddAttributeCombinationTokens(product, combination, language, host); + var liquidObject = await builder.BuildAsync(); + //event notification + await _mediator.MessageTokensAdded(messageTemplate, liquidObject); + + var toEmail = emailAccount.Email; + var toName = emailAccount.DisplayName; + return await _messageProviderService.SendNotification(messageTemplate, emailAccount, + languageId, liquidObject, + toEmail, toName, + reference: Reference.Product, objectId: product.Id); + } + + /// + /// Sends a 'Out of stock' notification message to a customer + /// + /// Customer + /// Product + /// Subscription + /// Message language identifier + public virtual async Task SendBackInStockMessage(Customer customer, Product product, + OutOfStockSubscription subscription, string languageId) + { + ArgumentNullException.ThrowIfNull(subscription); + + var store = await _messageLookupService.GetStore(subscription.StoreId); + var language = await _messageLookupService.EnsureLanguageIsActive(languageId, store.Id); + + var messageTemplate = await _messageLookupService.GetMessageTemplate("Customer.OutOfStock", store.Id); + if (messageTemplate == null) + return 0; + + //email account + var emailAccount = await _messageLookupService.GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); + + var host = store.PrimaryHost(); + var builder = new LiquidObjectBuilder(_mediator); + builder.AddStoreTokens(store, language, emailAccount); + builder.AddCustomerTokensIfNotNull(customer, store, host, language); + + builder.AddOutOfStockTokens(product, subscription, store, host, language); + var liquidObject = await builder.BuildAsync(); + //event notification + await _mediator.MessageTokensAdded(messageTemplate, liquidObject); + + var toEmail = customer?.Email; + var toName = customer.GetFullName(); + return await _messageProviderService.SendNotification(messageTemplate, emailAccount, + languageId, liquidObject, + toEmail, toName, + reference: Reference.Customer, objectId: customer?.Id); + } + + public virtual async Task SendAuctionWinEndedCustomerMessage(Product product, string languageId, Bid bid) + { + ArgumentNullException.ThrowIfNull(product); + + var customer = await _mediator.Send(new GetCustomerByIdQuery { Id = bid.CustomerId }); + if (customer == null) return 0; + if (string.IsNullOrEmpty(languageId)) + languageId = customer.GetUserFieldFromEntity(SystemCustomerFieldNames.LanguageId); + + var store = await _messageLookupService.GetStore(bid.StoreId); + var language = await _messageLookupService.EnsureLanguageIsActive(languageId, store.Id); + + var messageTemplate = await _messageLookupService.GetMessageTemplate("AuctionEnded.CustomerNotificationWin", store.Id); + if (messageTemplate == null) + return 0; + + var emailAccount = await _messageLookupService.GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); + + var host = store.PrimaryHost(); + var builder = new LiquidObjectBuilder(_mediator); + builder.AddAuctionTokens(product, bid) + .AddCustomerTokens(customer, store, host, language) + .AddProductTokens(product, language, store, host) + .AddStoreTokens(store, language, emailAccount); + + var liquidObject = await builder.BuildAsync(); + //event notification + await _mediator.MessageTokensAdded(messageTemplate, liquidObject); + + var toEmail = customer.Email; + var toName = customer.GetFullName(); + return await _messageProviderService.SendNotification(messageTemplate, emailAccount, + languageId, liquidObject, + toEmail, toName, + reference: Reference.Customer, objectId: customer.Id); + } + + public virtual async Task SendAuctionEndedLostCustomerMessage(Product product, string languageId, Bid bid) + { + ArgumentNullException.ThrowIfNull(product); + + var winner = await _mediator.Send(new GetCustomerByIdQuery { Id = bid.CustomerId }); + if (winner == null) return 0; + + var store = await _messageLookupService.GetStore(bid.StoreId); + + var language = await _messageLookupService.EnsureLanguageIsActive(languageId, store.Id); + + var messageTemplate = await _messageLookupService.GetMessageTemplate("AuctionEnded.CustomerNotificationLost", store.Id); + if (messageTemplate == null) + return 0; + + var emailAccount = await _messageLookupService.GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); + + var host = store.PrimaryHost(); + var builder = new LiquidObjectBuilder(_mediator); + builder.AddAuctionTokens(product, bid) + .AddProductTokens(product, language, store, host) + .AddStoreTokens(store, language, emailAccount); + var liquidObject = await builder.BuildAsync(); + + var bids = (await _mediator.Send(new GetBidsByProductIdQuery { ProductId = bid.ProductId })) + .Where(x => x.CustomerId != bid.CustomerId).GroupBy(x => x.CustomerId); + foreach (var item in bids) + { + var builder2 = new LiquidObjectBuilder(_mediator, liquidObject); + var customer = await _mediator.Send(new GetCustomerByIdQuery { Id = item.Key }); + if (string.IsNullOrEmpty(languageId)) + languageId = customer.GetUserFieldFromEntity(SystemCustomerFieldNames.LanguageId); + builder2.AddCustomerTokens(customer, store, host, language); + var liquidObject2 = await builder2.BuildAsync(); + + //event notification + await _mediator.MessageTokensAdded(messageTemplate, liquidObject2); + + var toEmail = customer.Email; + var toName = customer.GetFullName(); + await _messageProviderService.SendNotification(messageTemplate, emailAccount, + languageId, liquidObject2, + toEmail, toName, + reference: Reference.Customer, objectId: customer.Id); + } + + return 0; + } + + public virtual async Task SendAuctionEndedBinCustomerMessage(Product product, string customerId, + string languageId, string storeId) + { + ArgumentNullException.ThrowIfNull(product); + + var store = await _messageLookupService.GetStore(storeId); + + var messageTemplate = await _messageLookupService.GetMessageTemplate("AuctionEnded.CustomerNotificationBin", storeId); + if (messageTemplate == null) + return 0; + + var language = await _messageLookupService.EnsureLanguageIsActive(languageId, store.Id); + var emailAccount = await _messageLookupService.GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); + + var host = store.PrimaryHost(); + var builder = new LiquidObjectBuilder(_mediator); + builder.AddProductTokens(product, language, store, host) + .AddStoreTokens(store, language, emailAccount); + + var liquidObject = await builder.BuildAsync(); + var bids = (await _mediator.Send(new GetBidsByProductIdQuery { ProductId = product.Id })) + .Where(x => x.CustomerId != customerId).GroupBy(x => x.CustomerId); + foreach (var item in bids) + { + var builder2 = new LiquidObjectBuilder(_mediator, liquidObject); + var customer = await _mediator.Send(new GetCustomerByIdQuery { Id = item.Key }); + if (customer != null) + { + if (string.IsNullOrEmpty(languageId)) + languageId = customer.GetUserFieldFromEntity(SystemCustomerFieldNames.LanguageId); + + builder2.AddCustomerTokensIfNotNull(customer, store, host, language); + var liquidObject2 = await builder2.BuildAsync(); + //event notification + await _mediator.MessageTokensAdded(messageTemplate, liquidObject2); + + var toEmail = customer.Email; + var toName = customer.GetFullName(); + await _messageProviderService.SendNotification(messageTemplate, emailAccount, + languageId, liquidObject2, + toEmail, toName, + reference: Reference.Customer, objectId: customer.Id); + } + } + + return 0; + } + + public virtual async Task SendAuctionEndedStoreOwnerMessage(Product product, string languageId, Bid bid) + { + ArgumentNullException.ThrowIfNull(product); + + var builder = new LiquidObjectBuilder(_mediator); + MessageTemplate messageTemplate; + EmailAccount emailAccount; + + if (bid != null) + { + var customer = await _mediator.Send(new GetCustomerByIdQuery { Id = bid.CustomerId }); + if (string.IsNullOrEmpty(languageId)) + languageId = customer.GetUserFieldFromEntity(SystemCustomerFieldNames.LanguageId); + + var store = await _messageLookupService.GetStore(bid.StoreId); + + var language = await _messageLookupService.EnsureLanguageIsActive(languageId, store.Id); + + messageTemplate = await _messageLookupService.GetMessageTemplate("AuctionEnded.StoreOwnerNotification", store.Id); + if (messageTemplate == null) + return 0; + + emailAccount = await _messageLookupService.GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); + builder.AddAuctionTokens(product, bid) + .AddCustomerTokens(customer, store, store.PrimaryHost(), language) + .AddStoreTokens(store, language, emailAccount); + } + else + { + var store = (await _storeService.GetAllStores()).FirstOrDefault(); + var language = await _messageLookupService.EnsureLanguageIsActive(languageId, store?.Id); + messageTemplate = await _messageLookupService.GetMessageTemplate("AuctionExpired.StoreOwnerNotification", ""); + if (messageTemplate == null) + return 0; + + emailAccount = await _messageLookupService.GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); + builder.AddProductTokens(product, language, store, store.PrimaryHost()); + } + + var liquidObject = await builder.BuildAsync(); + //event notification + await _mediator.MessageTokensAdded(messageTemplate, liquidObject); + + var toEmail = emailAccount.Email; + var toName = emailAccount.DisplayName; + + return await _messageProviderService.SendNotification(messageTemplate, emailAccount, + languageId, liquidObject, + toEmail, toName); + } + + /// + /// Send outbid notification to a customer + /// + /// Message language identifier + /// Product + /// + public virtual async Task SendOutBidCustomerMessage(Product product, string languageId, Bid bid) + { + ArgumentNullException.ThrowIfNull(product); + + var customer = await _mediator.Send(new GetCustomerByIdQuery { Id = bid.CustomerId }); + if (string.IsNullOrEmpty(languageId)) + languageId = customer.GetUserFieldFromEntity(SystemCustomerFieldNames.LanguageId); + + var store = await _messageLookupService.GetStore(bid.StoreId); + + var language = await _messageLookupService.EnsureLanguageIsActive(languageId, store.Id); + + var messageTemplate = await _messageLookupService.GetMessageTemplate("BidUp.CustomerNotification", store.Id); + if (messageTemplate == null) + return 0; + + var emailAccount = await _messageLookupService.GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); + + var host = store.PrimaryHost(); + var builder = new LiquidObjectBuilder(_mediator); + builder.AddAuctionTokens(product, bid) + .AddCustomerTokens(customer, store, host, language) + .AddStoreTokens(store, language, emailAccount); + + var liquidObject = await builder.BuildAsync(); + //event notification + await _mediator.MessageTokensAdded(messageTemplate, liquidObject); + + var toEmail = customer.Email; + var toName = customer.GetFullName(); + return await _messageProviderService.SendNotification(messageTemplate, emailAccount, + languageId, liquidObject, + toEmail, toName, + reference: Reference.Customer, objectId: customer.Id); + } + + #endregion +} diff --git a/src/Business/Grand.Business.Messages/Services/CmsMessageProviderService.cs b/src/Business/Grand.Business.Messages/Services/CmsMessageProviderService.cs new file mode 100644 index 0000000000..81d20695cb --- /dev/null +++ b/src/Business/Grand.Business.Messages/Services/CmsMessageProviderService.cs @@ -0,0 +1,174 @@ +using Grand.Business.Core.Extensions; +using Grand.Business.Core.Interfaces.Common.Directory; +using Grand.Business.Core.Interfaces.Messages; +using Grand.Business.Core.Queries.Messages; +using Grand.Business.Messages.DotLiquidDrops; +using Grand.Domain.Blogs; +using Grand.Domain.Common; +using Grand.Domain.Customers; +using Grand.Domain.Knowledgebase; +using Grand.Domain.Localization; +using Grand.Domain.News; +using Grand.Domain.Stores; +using Grand.Mediator; + +namespace Grand.Business.Messages.Services; + +public class CmsMessageProviderService : ICmsMessageProviderService +{ + #region Ctor + + public CmsMessageProviderService( + IMessageLookupService messageLookupService, + IMessageProviderService messageProviderService, + IMediator mediator, + IGroupService groupService) + { + _messageLookupService = messageLookupService; + _messageProviderService = messageProviderService; + _mediator = mediator; + _groupService = groupService; + } + + #endregion + + #region Fields + + private readonly IMessageLookupService _messageLookupService; + private readonly IMessageProviderService _messageProviderService; + private readonly IMediator _mediator; + private readonly IGroupService _groupService; + + #endregion + + #region Methods + + /// + /// Sends a blog comment notification message to a store owner + /// + /// + /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain + /// + public virtual async Task SendBlogCommentMessage(BlogPost blogPost, BlogComment blogComment, string languageId, + DomainHost host = null) + { + ArgumentNullException.ThrowIfNull(blogComment); + + var store = await _messageLookupService.GetStore(blogComment.StoreId); + var language = await _messageLookupService.EnsureLanguageIsActive(languageId, store.Id); + + var messageTemplate = await _messageLookupService.GetMessageTemplate("Blog.BlogComment", store.Id); + if (messageTemplate == null) + return 0; + + //email account + var emailAccount = await _messageLookupService.GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); + + host = store.LinkHost(host); + var builder = new LiquidObjectBuilder(_mediator); + builder.AddStoreTokens(store, language, emailAccount) + .AddBlogCommentTokens(blogPost, blogComment, store, host, language); + + var customer = await _mediator.Send(new GetCustomerByIdQuery { Id = blogComment.CustomerId }); + if (customer != null && await _groupService.IsRegistered(customer)) + builder.AddCustomerTokens(customer, store, host, language); + + var liquidObject = await builder.BuildAsync(); + //event notification + await _mediator.MessageTokensAdded(messageTemplate, liquidObject); + + var toEmail = emailAccount.Email; + var toName = emailAccount.DisplayName; + return await _messageProviderService.SendNotification(messageTemplate, emailAccount, + languageId, liquidObject, + toEmail, toName, + reference: Reference.Blog, objectId: blogPost.Id); + } + + /// + /// Sends an article comment notification message to a store owner + /// + /// + /// Article comment + /// Message language identifier + /// + /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain + /// + public virtual async Task SendArticleCommentMessage(KnowledgebaseArticle article, + KnowledgebaseArticleComment articleComment, string languageId, DomainHost host = null) + { + ArgumentNullException.ThrowIfNull(articleComment); + + var store = await _messageLookupService.GetStore(""); + var language = await _messageLookupService.EnsureLanguageIsActive(languageId, store.Id); + + var messageTemplate = await _messageLookupService.GetMessageTemplate("Knowledgebase.ArticleComment", store.Id); + if (messageTemplate == null) + return 0; + + //email account + var emailAccount = await _messageLookupService.GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); + + host = store.LinkHost(host); + var builder = new LiquidObjectBuilder(_mediator); + builder.AddStoreTokens(store, language, emailAccount) + .AddArticleCommentTokens(article, articleComment, store, host, language); + + var customer = await _mediator.Send(new GetCustomerByIdQuery { Id = articleComment.CustomerId }); + if (customer != null && await _groupService.IsRegistered(customer)) + builder.AddCustomerTokens(customer, store, host, language); + var liquidObject = await builder.BuildAsync(); + //event notification + await _mediator.MessageTokensAdded(messageTemplate, liquidObject); + + var toEmail = emailAccount.Email; + var toName = emailAccount.DisplayName; + return await _messageProviderService.SendNotification(messageTemplate, emailAccount, + languageId, liquidObject, + toEmail, toName); + } + + /// + /// Sends a news comment notification message to a store owner + /// + /// News item + /// News comment + /// Message language identifier + /// + /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain + /// + public virtual async Task SendNewsCommentMessage(NewsItem newsItem, NewsComment newsComment, string languageId, + DomainHost host = null) + { + ArgumentNullException.ThrowIfNull(newsComment); + + var store = await _messageLookupService.GetStore(newsComment.StoreId); + var language = await _messageLookupService.EnsureLanguageIsActive(languageId, store.Id); + + var messageTemplate = await _messageLookupService.GetMessageTemplate("News.NewsComment", store.Id); + if (messageTemplate == null) + return 0; + + //email account + var emailAccount = await _messageLookupService.GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); + + host = store.LinkHost(host); + var builder = new LiquidObjectBuilder(_mediator); + builder.AddStoreTokens(store, language, emailAccount) + .AddNewsCommentTokens(newsItem, newsComment, store, host, language); + var customer = await _mediator.Send(new GetCustomerByIdQuery { Id = newsComment.CustomerId }); + builder.AddCustomerTokensIfNotNull(customer, store, host, language); + + var liquidObject = await builder.BuildAsync(); + //event notification + await _mediator.MessageTokensAdded(messageTemplate, liquidObject); + + var toEmail = emailAccount.Email; + var toName = emailAccount.DisplayName; + return await _messageProviderService.SendNotification(messageTemplate, emailAccount, + languageId, liquidObject, + toEmail, toName); + } + + #endregion +} diff --git a/src/Business/Grand.Business.Messages/Services/CustomerMessageProviderService.cs b/src/Business/Grand.Business.Messages/Services/CustomerMessageProviderService.cs new file mode 100644 index 0000000000..5b4d2803e0 --- /dev/null +++ b/src/Business/Grand.Business.Messages/Services/CustomerMessageProviderService.cs @@ -0,0 +1,301 @@ +using Grand.Business.Core.Commands.Messages.Common; +using Grand.Business.Core.Extensions; +using Grand.Business.Core.Interfaces.Messages; +using Grand.Business.Messages.DotLiquidDrops; +using Grand.Domain.Common; +using Grand.Domain.Customers; +using Grand.Domain.Messages; +using Grand.Domain.Stores; +using Grand.Mediator; +using Grand.SharedKernel.Extensions; +using System.Net; + +namespace Grand.Business.Messages.Services; + +public class CustomerMessageProviderService : ICustomerMessageProviderService +{ + #region Ctor + + public CustomerMessageProviderService( + IMessageLookupService messageLookupService, + IMessageProviderService messageProviderService, + IMediator mediator, + CommonSettings commonSettings) + { + _messageLookupService = messageLookupService; + _messageProviderService = messageProviderService; + _mediator = mediator; + _commonSettings = commonSettings; + } + + #endregion + + #region Fields + + private readonly IMessageLookupService _messageLookupService; + private readonly IMessageProviderService _messageProviderService; + private readonly IMediator _mediator; + private readonly CommonSettings _commonSettings; + + #endregion + + #region Methods + + /// + /// Send a message to a customer + /// + /// Customer instance + /// Store + /// Message language identifier + /// Message template name + /// Send email to email account + /// Customer note + /// + /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain + /// + protected virtual async Task SendCustomerMessage(Customer customer, Store store, string languageId, + string templateName, bool toEmailAccount = false, CustomerNote customerNote = null, DomainHost host = null) + { + ArgumentNullException.ThrowIfNull(customer); + + var language = await _messageLookupService.EnsureLanguageIsActive(languageId, store.Id); + + var messageTemplate = await _messageLookupService.GetMessageTemplate(templateName, store.Id); + if (messageTemplate == null) + return 0; + + //email account + var emailAccount = await _messageLookupService.GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); + + host = store.LinkHost(host); + var builder = new LiquidObjectBuilder(_mediator); + builder.AddStoreTokens(store, language, emailAccount) + .AddCustomerTokens(customer, store, host, language, customerNote); + + var liquidObject = await builder.BuildAsync(); + //event notification + await _mediator.MessageTokensAdded(messageTemplate, liquidObject); + + var toEmail = toEmailAccount ? emailAccount.Email : customer.Email; + var toName = toEmailAccount ? emailAccount.DisplayName : customer.GetFullName(); + return await _messageProviderService.SendNotification(messageTemplate, emailAccount, + languageId, liquidObject, + toEmail, toName, reference: Reference.Customer, objectId: customer.Id); + } + + /// + /// Sends 'New customer' notification message to a store owner + /// + /// Customer instance + /// Store identifier + /// Message language identifier + /// Queued email identifier + /// + /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain + /// + public virtual async Task SendCustomerRegisteredMessage(Customer customer, Store store, string languageId, + DomainHost host = null) + { + return await SendCustomerMessage(customer, store, languageId, MessageTemplateNames.CustomerRegistered, true, + host: host); + } + + /// + /// Sends a welcome message to a customer + /// + /// Customer instance + /// Store + /// Message language identifier + /// + /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain + /// + public virtual async Task SendCustomerWelcomeMessage(Customer customer, Store store, string languageId, + DomainHost host = null) + { + return await SendCustomerMessage(customer, store, languageId, MessageTemplateNames.CustomerWelcome, host: host); + } + + /// + /// Sends an email validation message to a customer + /// + /// Customer instance + /// Store + /// Message language identifier + /// + /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain + /// + public virtual async Task SendCustomerEmailValidationMessage(Customer customer, Store store, string languageId, + DomainHost host = null) + { + return await SendCustomerMessage(customer, store, languageId, MessageTemplateNames.CustomerEmailValidation, + host: host); + } + + /// + /// Sends password recovery message to a customer + /// + /// Customer instance + /// Store + /// Message language identifier + /// + /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain + /// + public virtual async Task SendCustomerPasswordRecoveryMessage(Customer customer, Store store, + string languageId, DomainHost host = null) + { + return await SendCustomerMessage(customer, store, languageId, MessageTemplateNames.CustomerPasswordRecovery, + host: host); + } + + /// + /// Sends a new customer note added notification to a customer + /// + /// Customer note + /// Customer + /// Store + /// Message language identifier + /// Queued email identifier + public virtual async Task SendNewCustomerNoteMessage(CustomerNote customerNote, Customer customer, Store store, + string languageId) + { + return await SendCustomerMessage(customer, store, languageId, MessageTemplateNames.CustomerNewCustomerNote, + customerNote: customerNote); + } + + /// + /// Send an email token validation message to a customer + /// + /// Customer instance + /// Store instance + /// Message language identifier + /// + /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain + /// + public virtual async Task SendCustomerEmailTokenValidationMessage(Customer customer, Store store, + string languageId, DomainHost host = null) + { + return await SendCustomerMessage(customer, store, languageId, + MessageTemplateNames.CustomerEmailTokenValidationMessage, host: host); + } + + /// + /// Sends a "new VAT" notification to a store owner + /// + /// Customer + /// Message language identifier + /// + /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain + /// + public virtual async Task SendCustomerDeleteStoreOwnerMessage(Customer customer, string languageId, + DomainHost host = null) + { + ArgumentNullException.ThrowIfNull(customer); + + var store = await _messageLookupService.GetStore(""); + var language = await _messageLookupService.EnsureLanguageIsActive(languageId, store.Id); + + var messageTemplate = await _messageLookupService.GetMessageTemplate("CustomerDelete.StoreOwnerNotification", store.Id); + if (messageTemplate == null) + return 0; + + //email account + var emailAccount = await _messageLookupService.GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); + + host = store.LinkHost(host); + var builder = new LiquidObjectBuilder(_mediator); + builder.AddStoreTokens(store, language, emailAccount) + .AddCustomerTokens(customer, store, host, language); + + var liquidObject = await builder.BuildAsync(); + + var toEmail = emailAccount.Email; + var toName = emailAccount.DisplayName; + return await _messageProviderService.SendNotification(messageTemplate, emailAccount, + languageId, liquidObject, + toEmail, toName, + reference: Reference.Customer, objectId: customer.Id); + } + + /// + /// Sends "contact us" message + /// + /// Customer + /// Store + /// Message language identifier + /// Sender email + /// Sender name + /// Email subject. Pass null if you want a message template subject to be used. + /// Email body + /// Attr info + /// CustomAttributes + /// Ip address + /// + /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain + /// + public virtual async Task SendContactUsMessage(Customer customer, Store store, string languageId, + string senderEmail, + string senderName, string subject, string body, string attrInfo, IList customAttributes, + string ipaddress, DomainHost host = null) + { + var language = await _messageLookupService.EnsureLanguageIsActive(languageId, store.Id); + var messageTemplate = await _messageLookupService.GetMessageTemplate("Service.ContactUs", store.Id); + if (messageTemplate == null) + return 0; + + //email account + var emailAccount = await _messageLookupService.GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); + + string fromEmail; + string fromName; + senderName = WebUtility.HtmlEncode(senderName); + senderEmail = WebUtility.HtmlEncode(senderEmail); + //required for some SMTP servers + if (_commonSettings.UseSystemEmailForContactUsForm) + { + fromEmail = emailAccount.Email; + fromName = emailAccount.DisplayName; + } + else + { + fromEmail = senderEmail; + fromName = senderName; + } + + host = store.LinkHost(host); + var builder = new LiquidObjectBuilder(_mediator); + builder.AddStoreTokens(store, language, emailAccount) + .AddCustomerTokens(customer, store, host, language); + + var liquidObject = await builder.BuildAsync(); + liquidObject.ContactUs = new LiquidContactUs(senderEmail, senderName, body, attrInfo); + //event notification + await _mediator.MessageTokensAdded(messageTemplate, liquidObject); + + var toEmail = emailAccount.Email; + var toName = emailAccount.DisplayName; + + //store in database + if (_commonSettings.StoreInDatabaseContactUsForm) + await _mediator.Send(new InsertContactUsCommand { + CustomerId = customer.Id, + StoreId = store.Id, + VendorId = "", + Email = senderEmail, + Enquiry = body, + FullName = senderName, + Subject = string.IsNullOrEmpty(subject) ? "Contact Us (form)" : subject, + ContactAttributeDescription = attrInfo, + ContactAttributes = customAttributes, + EmailAccountId = emailAccount.Id, + RemoteIpAddress = ipaddress + }); + return await _messageProviderService.SendNotification(messageTemplate, emailAccount, languageId, liquidObject, toEmail, toName, + fromEmail: fromEmail, + fromName: fromName, + subject: subject, + replyToEmailAddress: senderEmail, + replyToName: senderName, reference: Reference.Customer, objectId: customer?.Id); + } + + #endregion +} diff --git a/src/Business/Grand.Business.Messages/Services/MerchandiseReturnMessageProviderService.cs b/src/Business/Grand.Business.Messages/Services/MerchandiseReturnMessageProviderService.cs new file mode 100644 index 0000000000..f666de2e66 --- /dev/null +++ b/src/Business/Grand.Business.Messages/Services/MerchandiseReturnMessageProviderService.cs @@ -0,0 +1,234 @@ +using Grand.Business.Core.Extensions; +using Grand.Business.Core.Interfaces.Messages; +using Grand.Business.Core.Queries.Messages; +using Grand.Business.Messages.DotLiquidDrops; +using Grand.Domain.Common; +using Grand.Domain.Customers; +using Grand.Domain.Localization; +using Grand.Domain.Orders; +using Grand.Domain.Stores; +using Grand.Mediator; + +namespace Grand.Business.Messages.Services; + +public class MerchandiseReturnMessageProviderService : IMerchandiseReturnMessageProviderService +{ + #region Ctor + + public MerchandiseReturnMessageProviderService( + IMessageLookupService messageLookupService, + IMessageProviderService messageProviderService, + IMediator mediator) + { + _messageLookupService = messageLookupService; + _messageProviderService = messageProviderService; + _mediator = mediator; + } + + #endregion + + #region Fields + + private readonly IMessageLookupService _messageLookupService; + private readonly IMessageProviderService _messageProviderService; + private readonly IMediator _mediator; + + #endregion + + #region Methods + + /// + /// Sends 'New Merchandise Return' message to a store owner + /// + /// Merchandise return + /// Order + /// Message language identifier + /// + /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain + /// + public virtual async Task SendNewMerchandiseReturnStoreOwnerMessage(MerchandiseReturn merchandiseReturn, + Order order, string languageId, DomainHost host = null) + { + ArgumentNullException.ThrowIfNull(merchandiseReturn); + + var store = await _messageLookupService.GetStore(order.StoreId); + var language = await _messageLookupService.EnsureLanguageIsActive(languageId, store.Id); + + var messageTemplate = + await _messageLookupService.GetMessageTemplate(MessageTemplateNames.SendNewMerchandiseReturnStoreOwnerMessage, store.Id); + if (messageTemplate == null) + return 0; + + //email account + var emailAccount = await _messageLookupService.GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); + + var customer = await _mediator.Send(new GetCustomerByIdQuery { Id = merchandiseReturn.CustomerId }); + + host = store.LinkHost(host); + var builder = new LiquidObjectBuilder(_mediator); + builder.AddStoreTokens(store, language, emailAccount); + + builder.AddCustomerTokensIfNotNull(customer, store, host, language); + + builder.AddMerchandiseReturnTokens(merchandiseReturn, store, host, order, language); + + var liquidObject = await builder.BuildAsync(); + + //event notification + await _mediator.MessageTokensAdded(messageTemplate, liquidObject); + + if (!string.IsNullOrEmpty(merchandiseReturn.VendorId)) + { + var vendor = await _mediator.Send(new GetVendorByIdQuery { Id = merchandiseReturn.VendorId }); + if (vendor != null) + { + var vendorEmail = vendor.Email; + var vendorName = vendor.Name; + await _messageProviderService.SendNotification(messageTemplate, emailAccount, + languageId, liquidObject, + vendorEmail, vendorName); + } + } + + var toEmail = emailAccount.Email; + var toName = emailAccount.DisplayName; + return await _messageProviderService.SendNotification(messageTemplate, emailAccount, + languageId, liquidObject, + toEmail, toName, + reference: Reference.MerchandiseReturn, objectId: merchandiseReturn.Id); + } + + /// + /// Sends 'Merchandise Return status changed' message to a customer + /// + /// Merchandise return + /// Order + /// Message language identifier + public virtual async Task SendMerchandiseReturnStatusChangedCustomerMessage( + MerchandiseReturn merchandiseReturn, Order order, string languageId) + { + ArgumentNullException.ThrowIfNull(merchandiseReturn); + + var store = await _messageLookupService.GetStore(order.StoreId); + var language = await _messageLookupService.EnsureLanguageIsActive(languageId, store.Id); + + var messageTemplate = + await _messageLookupService.GetMessageTemplate(MessageTemplateNames.SendMerchandiseReturnStatusChangedCustomerMessage, store.Id); + if (messageTemplate == null) + return 0; + + //email account + var emailAccount = await _messageLookupService.GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); + + var customer = await _mediator.Send(new GetCustomerByIdQuery { Id = merchandiseReturn.CustomerId }); + + var host = store.PrimaryHost(); + var builder = new LiquidObjectBuilder(_mediator); + builder.AddStoreTokens(store, language, emailAccount); + + builder.AddCustomerTokensIfNotNull(customer, store, host, language); + + builder.AddMerchandiseReturnTokens(merchandiseReturn, store, host, order, language); + var liquidObject = await builder.BuildAsync(); + //event notification + await _mediator.MessageTokensAdded(messageTemplate, liquidObject); + + var toEmail = string.IsNullOrEmpty(customer?.Email) ? order.BillingAddress.Email : customer.Email; + var toName = string.IsNullOrEmpty(customer?.Email) ? order.BillingAddress.FirstName : customer.GetFullName(); + return await _messageProviderService.SendNotification(messageTemplate, emailAccount, + languageId, liquidObject, + toEmail, toName, + reference: Reference.MerchandiseReturn, objectId: merchandiseReturn.Id); + } + + /// + /// Sends 'New Merchandise Return' message to a customer + /// + /// Merchandise return + /// Order + /// Message language identifier + /// + /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain + /// + public virtual async Task SendNewMerchandiseReturnCustomerMessage(MerchandiseReturn merchandiseReturn, + Order order, string languageId, DomainHost host = null) + { + ArgumentNullException.ThrowIfNull(merchandiseReturn); + + var store = await _messageLookupService.GetStore(order.StoreId); + var language = await _messageLookupService.EnsureLanguageIsActive(languageId, store.Id); + + var messageTemplate = + await _messageLookupService.GetMessageTemplate(MessageTemplateNames.SendNewMerchandiseReturnCustomerMessage, store.Id); + if (messageTemplate == null) + return 0; + + //email account + var emailAccount = await _messageLookupService.GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); + + var customer = await _mediator.Send(new GetCustomerByIdQuery { Id = merchandiseReturn.CustomerId }); + + host = store.LinkHost(host); + var builder = new LiquidObjectBuilder(_mediator); + builder.AddStoreTokens(store, language, emailAccount); + builder.AddCustomerTokensIfNotNull(customer, store, host, language); + + builder.AddMerchandiseReturnTokens(merchandiseReturn, store, host, order, language); + var liquidObject = await builder.BuildAsync(); + //event notification + await _mediator.MessageTokensAdded(messageTemplate, liquidObject); + + var toEmail = string.IsNullOrEmpty(customer?.Email) ? order.BillingAddress.Email : customer.Email; + var toName = string.IsNullOrEmpty(customer?.Email) ? order.BillingAddress.FirstName : customer.GetFullName(); + return await _messageProviderService.SendNotification(messageTemplate, emailAccount, + languageId, liquidObject, + toEmail, toName, + reference: Reference.MerchandiseReturn, objectId: merchandiseReturn.Id); + } + + /// + /// Sends a new merchandise return note added notification to a customer + /// + /// Merchandise return + /// Merchandise return note + /// Order + public virtual async Task SendNewMerchandiseReturnNoteAddedCustomerMessage(MerchandiseReturn merchandiseReturn, + MerchandiseReturnNote merchandiseReturnNote, Order order) + { + ArgumentNullException.ThrowIfNull(merchandiseReturnNote); + ArgumentNullException.ThrowIfNull(merchandiseReturn); + + var store = await _messageLookupService.GetStore(order.StoreId); + var language = await _messageLookupService.EnsureLanguageIsActive(order.CustomerLanguageId, store.Id); + + var messageTemplate = + await _messageLookupService.GetMessageTemplate(MessageTemplateNames.SendNewMerchandiseReturnNoteAddedCustomerMessage, store.Id); + if (messageTemplate == null) + return 0; + + //email account + var emailAccount = await _messageLookupService.GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); + + var customer = await _mediator.Send(new GetCustomerByIdQuery { Id = merchandiseReturn.CustomerId }); + + var host = store.PrimaryHost(); + var builder = new LiquidObjectBuilder(_mediator); + builder.AddStoreTokens(store, language, emailAccount); + builder.AddCustomerTokensIfNotNull(customer, store, host, language); + + builder.AddMerchandiseReturnTokens(merchandiseReturn, store, host, order, language, + merchandiseReturnNote); + var liquidObject = await builder.BuildAsync(); + //event notification + await _mediator.MessageTokensAdded(messageTemplate, liquidObject); + + var toEmail = string.IsNullOrEmpty(customer?.Email) ? order.BillingAddress.Email : customer.Email; + var toName = string.IsNullOrEmpty(customer?.Email) ? order.BillingAddress.FirstName : customer.GetFullName(); + return await _messageProviderService.SendNotification(messageTemplate, emailAccount, + language.Id, liquidObject, + toEmail, toName, + reference: Reference.MerchandiseReturn, objectId: merchandiseReturn.Id); + } + + #endregion +} diff --git a/src/Business/Grand.Business.Messages/Services/MessageLookupService.cs b/src/Business/Grand.Business.Messages/Services/MessageLookupService.cs new file mode 100644 index 0000000000..1e63326c14 --- /dev/null +++ b/src/Business/Grand.Business.Messages/Services/MessageLookupService.cs @@ -0,0 +1,89 @@ +using Grand.Business.Core.Extensions; +using Grand.Business.Core.Interfaces.Common.Localization; +using Grand.Business.Core.Interfaces.Common.Stores; +using Grand.Business.Core.Interfaces.Messages; +using Grand.Domain.Localization; +using Grand.Domain.Messages; +using Grand.Domain.Stores; + +namespace Grand.Business.Messages.Services; + +public class MessageLookupService : IMessageLookupService +{ + #region Ctor + + public MessageLookupService( + IMessageTemplateService messageTemplateService, + ILanguageService languageService, + IEmailAccountService emailAccountService, + IStoreService storeService, + EmailAccountSettings emailAccountSettings) + { + _messageTemplateService = messageTemplateService; + _languageService = languageService; + _emailAccountService = emailAccountService; + _storeService = storeService; + _emailAccountSettings = emailAccountSettings; + } + + #endregion + + #region Fields + + private readonly IMessageTemplateService _messageTemplateService; + private readonly ILanguageService _languageService; + private readonly IEmailAccountService _emailAccountService; + private readonly IStoreService _storeService; + private readonly EmailAccountSettings _emailAccountSettings; + + #endregion + + #region Methods + + public virtual async Task GetStore(string storeId) + { + return await _storeService.GetStoreById(storeId) ?? (await _storeService.GetAllStores()).FirstOrDefault(); + } + + public virtual async Task GetMessageTemplate(string messageTemplateName, string storeId) + { + var messageTemplate = await _messageTemplateService.GetMessageTemplateByName(messageTemplateName, storeId); + + //no template found + if (messageTemplate == null) + return null; + + //ensure it's active + var isActive = messageTemplate.IsActive; + return !isActive ? null : messageTemplate; + } + + public virtual async Task GetEmailAccountOfMessageTemplate(MessageTemplate messageTemplate, + string languageId) + { + var emailAccounId = messageTemplate.GetTranslation(mt => mt.EmailAccountId, languageId); + var emailAccount = await _emailAccountService.GetEmailAccountById(emailAccounId); + emailAccount ??= await _emailAccountService.GetEmailAccountById(_emailAccountSettings.DefaultEmailAccountId); + emailAccount ??= (await _emailAccountService.GetAllEmailAccounts()).FirstOrDefault(); + return emailAccount; + } + + public virtual async Task EnsureLanguageIsActive(string languageId, string storeId) + { + //load language by specified ID + var language = await _languageService.GetLanguageById(languageId); + + if (language is not { Published: true }) + //load any language from the specified store + language = (await _languageService.GetAllLanguages(storeId: storeId)).FirstOrDefault(); + if (language is not { Published: true }) + //load any language + language = (await _languageService.GetAllLanguages()).FirstOrDefault(); + + if (language == null) + throw new Exception("No active language could be loaded"); + return language; + } + + #endregion +} diff --git a/src/Business/Grand.Business.Messages/Services/MessageProviderService.cs b/src/Business/Grand.Business.Messages/Services/MessageProviderService.cs index ec09d5c108..c707833e1f 100644 --- a/src/Business/Grand.Business.Messages/Services/MessageProviderService.cs +++ b/src/Business/Grand.Business.Messages/Services/MessageProviderService.cs @@ -1,26 +1,9 @@ -using Grand.Business.Core.Commands.Messages.Common; -using Grand.Business.Core.Extensions; -using Grand.Business.Core.Interfaces.Common.Directory; -using Grand.Business.Core.Interfaces.Common.Localization; -using Grand.Business.Core.Interfaces.Common.Stores; +using Grand.Business.Core.Extensions; using Grand.Business.Core.Interfaces.Messages; -using Grand.Business.Core.Queries.Messages; using Grand.Business.Messages.DotLiquidDrops; -using Grand.Domain.Blogs; -using Grand.Domain.Catalog; using Grand.Domain.Common; -using Grand.Domain.Customers; -using Grand.Domain.Knowledgebase; -using Grand.Domain.Localization; using Grand.Domain.Messages; -using Grand.Domain.News; -using Grand.Domain.Orders; -using Grand.Domain.Shipping; -using Grand.Domain.Stores; -using Grand.Domain.Vendors; -using Grand.Mediator; using Grand.SharedKernel.Extensions; -using System.Net; namespace Grand.Business.Messages.Services; @@ -28,2016 +11,21 @@ public class MessageProviderService : IMessageProviderService { #region Ctor - public MessageProviderService( - IMessageTemplateService messageTemplateService, - IQueuedEmailService queuedEmailService, - ILanguageService languageService, - IEmailAccountService emailAccountService, - IStoreService storeService, - IGroupService groupService, - IMediator mediator, - EmailAccountSettings emailAccountSettings, - CommonSettings commonSettings) + public MessageProviderService(IQueuedEmailService queuedEmailService) { - _messageTemplateService = messageTemplateService; _queuedEmailService = queuedEmailService; - _languageService = languageService; - _emailAccountService = emailAccountService; - _storeService = storeService; - _groupService = groupService; - _emailAccountSettings = emailAccountSettings; - _commonSettings = commonSettings; - _mediator = mediator; } #endregion #region Fields - private readonly IMessageTemplateService _messageTemplateService; - private readonly IQueuedEmailService _queuedEmailService; - private readonly ILanguageService _languageService; - private readonly IEmailAccountService _emailAccountService; - private readonly IStoreService _storeService; - private readonly IGroupService _groupService; - private readonly IMediator _mediator; - - private readonly EmailAccountSettings _emailAccountSettings; - private readonly CommonSettings _commonSettings; - - #endregion - - #region Utilities - - protected virtual async Task GetStore(string storeId) - { - return await _storeService.GetStoreById(storeId) ?? (await _storeService.GetAllStores()).FirstOrDefault(); - } - - protected virtual async Task GetMessageTemplate(string messageTemplateName, string storeId) - { - var messageTemplate = await _messageTemplateService.GetMessageTemplateByName(messageTemplateName, storeId); - - //no template found - if (messageTemplate == null) - return null; - - //ensure it's active - var isActive = messageTemplate.IsActive; - return !isActive ? null : messageTemplate; - } - - protected virtual async Task GetEmailAccountOfMessageTemplate(MessageTemplate messageTemplate, - string languageId) - { - var emailAccounId = messageTemplate.GetTranslation(mt => mt.EmailAccountId, languageId); - var emailAccount = await _emailAccountService.GetEmailAccountById(emailAccounId); - emailAccount ??= await _emailAccountService.GetEmailAccountById(_emailAccountSettings.DefaultEmailAccountId); - emailAccount ??= (await _emailAccountService.GetAllEmailAccounts()).FirstOrDefault(); - return emailAccount; - } - protected virtual async Task EnsureLanguageIsActive(string languageId, string storeId) - { - //load language by specified ID - var language = await _languageService.GetLanguageById(languageId); - - if (language is not { Published: true }) - //load any language from the specified store - language = (await _languageService.GetAllLanguages(storeId: storeId)).FirstOrDefault(); - if (language is not { Published: true }) - //load any language - language = (await _languageService.GetAllLanguages()).FirstOrDefault(); - - if (language == null) - throw new Exception("No active language could be loaded"); - return language; - } - - private void AddCustomerTokensIfNotNull(LiquidObjectBuilder builder, Customer customer, Store store, - DomainHost host, Language language) - { - if (customer != null) - { - builder.AddCustomerTokens(customer, store, host, language); - } - } + private readonly IQueuedEmailService _queuedEmailService; #endregion #region Methods - #region Customer messages - - /// - /// Send a message to a customer - /// - /// Customer instance - /// Store - /// Message language identifier - /// Message template name - /// Send email to email account - /// Customer note - /// - /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain - /// - protected virtual async Task SendCustomerMessage(Customer customer, Store store, string languageId, - string templateName, bool toEmailAccount = false, CustomerNote customerNote = null, DomainHost host = null) - { - ArgumentNullException.ThrowIfNull(customer); - - var language = await EnsureLanguageIsActive(languageId, store.Id); - - var messageTemplate = await GetMessageTemplate(templateName, store.Id); - if (messageTemplate == null) - return 0; - - //email account - var emailAccount = await GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); - - host = store.LinkHost(host); - var builder = new LiquidObjectBuilder(_mediator); - builder.AddStoreTokens(store, language, emailAccount) - .AddCustomerTokens(customer, store, host, language, customerNote); - - var liquidObject = await builder.BuildAsync(); - //event notification - await _mediator.MessageTokensAdded(messageTemplate, liquidObject); - - var toEmail = toEmailAccount ? emailAccount.Email : customer.Email; - var toName = toEmailAccount ? emailAccount.DisplayName : customer.GetFullName(); - return await SendNotification(messageTemplate, emailAccount, - languageId, liquidObject, - toEmail, toName, reference: Reference.Customer, objectId: customer.Id); - } - - /// - /// Sends 'New customer' notification message to a store owner - /// - /// Customer instance - /// Store identifier - /// Message language identifier - /// Queued email identifier - /// - /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain - /// - public virtual async Task SendCustomerRegisteredMessage(Customer customer, Store store, string languageId, - DomainHost host = null) - { - return await SendCustomerMessage(customer, store, languageId, MessageTemplateNames.CustomerRegistered, true, - host: host); - } - - /// - /// Sends a welcome message to a customer - /// - /// Customer instance - /// Store - /// Message language identifier - /// - /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain - /// - public virtual async Task SendCustomerWelcomeMessage(Customer customer, Store store, string languageId, - DomainHost host = null) - { - return await SendCustomerMessage(customer, store, languageId, MessageTemplateNames.CustomerWelcome, host: host); - } - - /// - /// Sends an email validation message to a customer - /// - /// Customer instance - /// Store - /// Message language identifier - /// - /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain - /// - public virtual async Task SendCustomerEmailValidationMessage(Customer customer, Store store, string languageId, - DomainHost host = null) - { - return await SendCustomerMessage(customer, store, languageId, MessageTemplateNames.CustomerEmailValidation, - host: host); - } - - /// - /// Sends password recovery message to a customer - /// - /// Customer instance - /// Store - /// Message language identifier - /// - /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain - /// - public virtual async Task SendCustomerPasswordRecoveryMessage(Customer customer, Store store, - string languageId, DomainHost host = null) - { - return await SendCustomerMessage(customer, store, languageId, MessageTemplateNames.CustomerPasswordRecovery, - host: host); - } - - /// - /// Sends a new customer note added notification to a customer - /// - /// Customer note - /// Customer - /// Store - /// Message language identifier - /// Queued email identifier - public virtual async Task SendNewCustomerNoteMessage(CustomerNote customerNote, Customer customer, Store store, - string languageId) - { - return await SendCustomerMessage(customer, store, languageId, MessageTemplateNames.CustomerNewCustomerNote, - customerNote: customerNote); - } - - /// - /// Send an email token validation message to a customer - /// - /// Customer instance - /// Store instance - /// Message language identifier - /// - /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain - /// - public virtual async Task SendCustomerEmailTokenValidationMessage(Customer customer, Store store, - string languageId, DomainHost host = null) - { - return await SendCustomerMessage(customer, store, languageId, - MessageTemplateNames.CustomerEmailTokenValidationMessage, host: host); - } - - #endregion - - #region Order messages - - /// - /// Sends an order placed notification to a store owner - /// - /// Order instance - /// Customer - /// Message language identifier - /// - /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain - /// - public virtual async Task SendOrderPlacedStoreOwnerMessage(Order order, Customer customer, string languageId, - DomainHost host = null) - { - return await SendOrderStoreOwnerMessage(MessageTemplateNames.SendOrderPlacedStoreOwnerMessage, order, customer, - languageId, host: host); - } - - /// - /// Sends an order paid notification to a store owner - /// - /// Order instance - /// Customer instance - /// Message language identifier - public virtual async Task SendOrderPaidStoreOwnerMessage(Order order, Customer customer, string languageId) - { - return await SendOrderStoreOwnerMessage(MessageTemplateNames.SendOrderPaidStoreOwnerMessage, order, customer, - languageId); - } - - /// - /// Sends an order cancelled notification to an admin - /// - /// Order instance - /// Customer instance - /// Message language identifier - /// Queued email identifier - public virtual async Task SendOrderCancelledStoreOwnerMessage(Order order, Customer customer, - string languageId) - { - return await SendOrderStoreOwnerMessage(MessageTemplateNames.SendOrderCancelledStoreOwnerMessage, order, - customer, languageId); - } - - /// Sends an order refunded notification to a store owner - /// Order instance - /// Amount refunded - /// Message language identifier - public virtual async Task SendOrderRefundedStoreOwnerMessage(Order order, double refundedAmount, - string languageId) - { - var customer = await _mediator.Send(new GetCustomerByIdQuery { Id = order.CustomerId }); - return await SendOrderStoreOwnerMessage(MessageTemplateNames.SendOrderRefundedStoreOwnerMessage, order, - customer, languageId, refundedAmount); - } - - /// - /// Sends an order notification to a store owner - /// - /// Message template - /// Order instance - /// Customer instance - /// Message language identifier - /// - /// - /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain - /// - private async Task SendOrderStoreOwnerMessage(string template, Order order, Customer customer, - string languageId, double refundedAmount = 0, DomainHost host = null) - { - ArgumentNullException.ThrowIfNull(order); - - var store = await GetStore(order.StoreId); - var language = await EnsureLanguageIsActive(languageId, store.Id); - - var messageTemplate = await GetMessageTemplate(template, store.Id); - if (messageTemplate == null) - return 0; - - //email account - var emailAccount = await GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); - - host = store.LinkHost(host); - var liquidBuilder = new LiquidObjectBuilder(_mediator); - liquidBuilder.AddStoreTokens(store, language, emailAccount) - .AddOrderTokens(order, customer, store, host); - AddCustomerTokensIfNotNull(liquidBuilder, customer, store, host, language); - - var liquidObject = await liquidBuilder.BuildAsync(); - //event notification - await _mediator.MessageTokensAdded(messageTemplate, liquidObject); - - var toEmail = emailAccount.Email; - var toName = emailAccount.DisplayName; - return await SendNotification(messageTemplate, emailAccount, - languageId, liquidObject, - toEmail, toName, - reference: Reference.Order, objectId: order.Id); - } - - /// - /// Sends an order placed notification to a customer - /// - /// Order instance - /// - /// Message language identifier - /// Attachment file path - /// - /// Attachment file name. If specified, then this file name will be sent to a recipient. - /// Otherwise, "AttachmentFilePath" name will be used. - /// - /// Attachments - /// - /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain - /// - public virtual async Task SendOrderPlacedCustomerMessage(Order order, Customer customer, string languageId, - string attachmentFilePath = null, string attachmentFileName = null, IEnumerable attachments = null, - DomainHost host = null) - { - return await SendOrderCustomerMessage(MessageTemplateNames.SendOrderPlacedCustomerMessage, order, customer, - languageId, attachmentFilePath, attachmentFileName, attachments, host: host); - } - - /// - /// Sends an order paid notification to a customer - /// - /// Order instance - /// Customer instance - /// Message language identifier - /// Attachment file path - /// - /// Attachment file name. If specified, then this file name will be sent to a recipient. - /// Otherwise, "AttachmentFilePath" name will be used. - /// - /// Attachments ident - public virtual async Task SendOrderPaidCustomerMessage(Order order, Customer customer, string languageId, - string attachmentFilePath = null, string attachmentFileName = null, IEnumerable attachments = null) - { - return await SendOrderCustomerMessage(MessageTemplateNames.SendOrderPaidCustomerMessage, order, customer, - languageId, attachmentFilePath, attachmentFileName, attachments); - } - - /// - /// Sends an order completed notification to a customer - /// - /// Order instance - /// Customer instance - /// Message language identifier - /// Attachment file path - /// - /// Attachment file name. If specified, then this file name will be sent to a recipient. - /// Otherwise, "AttachmentFilePath" name will be used. - /// - /// Attachments ident - public virtual async Task SendOrderCompletedCustomerMessage(Order order, Customer customer, string languageId, - string attachmentFilePath = null, string attachmentFileName = null, IEnumerable attachments = null) - { - return await SendOrderCustomerMessage(MessageTemplateNames.SendOrderCompletedCustomerMessage, order, customer, - languageId, attachmentFilePath, attachmentFileName, attachments); - } - - /// - /// Sends an order cancelled notification to a customer - /// - /// Order instance - /// Customer instance - /// Message language identifier - public virtual async Task SendOrderCancelledCustomerMessage(Order order, Customer customer, string languageId) - { - return await SendOrderCustomerMessage(MessageTemplateNames.SendOrderCancelledCustomerMessage, order, customer, - languageId); - } - - /// - /// Sends an order refunded notification to a customer - /// - /// Order instance - /// Amount refunded - /// Message language identifier - /// Queued email identifier - public virtual async Task SendOrderRefundedCustomerMessage(Order order, double refundedAmount, - string languageId) - { - var customer = await _mediator.Send(new GetCustomerByIdQuery { Id = order.CustomerId }); - return await SendOrderCustomerMessage(MessageTemplateNames.SendOrderRefundedCustomerMessage, order, customer, - languageId, refundedAmount: refundedAmount); - } - - /// - /// Sends an order notification to a customer - /// - /// Message template - /// Order instance - /// Customer instance - /// Message language identifier - /// Attachment file path - /// - /// Attachment file name. If specified, then this file name will be sent to a recipient. - /// Otherwise, "AttachmentFilePath" name will be used. - /// - /// Attachments ident - /// - /// - /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain - /// - private async Task SendOrderCustomerMessage(string message, Order order, Customer customer, string languageId, - string attachmentFilePath = null, string attachmentFileName = null, IEnumerable attachments = null, - double refundedAmount = 0, DomainHost host = null) - { - ArgumentNullException.ThrowIfNull(order); - - var store = await GetStore(order.StoreId); - var language = await EnsureLanguageIsActive(languageId, store.Id); - - var messageTemplate = await GetMessageTemplate(message, store.Id); - if (messageTemplate == null) - return 0; - - //email account - var emailAccount = await GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); - - host = store.LinkHost(host); - var builder = new LiquidObjectBuilder(_mediator); - builder.AddStoreTokens(store, language, emailAccount) - .AddOrderTokens(order, customer, store, host); - AddCustomerTokensIfNotNull(builder, customer, store, host, language); - - var liquidObject = await builder.BuildAsync(); - //event notification - await _mediator.MessageTokensAdded(messageTemplate, liquidObject); - - var toEmail = order.BillingAddress.Email; - var toName = $"{order.BillingAddress.FirstName} {order.BillingAddress.LastName}"; - return await SendNotification(messageTemplate, emailAccount, - languageId, liquidObject, - toEmail, toName, - attachmentFilePath, - attachmentFileName, - attachments, - reference: Reference.Order, objectId: order.Id); - } - - /// - /// Sends an order placed notification to a vendor - /// - /// Order instance - /// Customer instance - /// Vendor instance - /// Message language identifier - /// - /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain - /// - public virtual async Task SendOrderPlacedVendorMessage(Order order, Customer customer, Vendor vendor, - string languageId, DomainHost host = null) - { - return await SendOrderVendorMessage(MessageTemplateNames.SendOrderPlacedVendorMessage, order, vendor, - languageId, host: host); - } - - /// - /// Sends an order paid notification to a vendor - /// - /// Order instance - /// Vendor instance - /// Message language identifier - public virtual async Task SendOrderPaidVendorMessage(Order order, Vendor vendor, string languageId) - { - return await SendOrderVendorMessage(MessageTemplateNames.SendOrderPaidVendorMessage, order, vendor, languageId); - } - - /// - /// Sends an order cancel notification to a vendor - /// - /// Order instance - /// Vendor instance - /// Message language identifier - public virtual async Task SendOrderCancelledVendorMessage(Order order, Vendor vendor, string languageId) - { - return await SendOrderVendorMessage(MessageTemplateNames.SendOrderCancelledVendorMessage, order, vendor, - languageId); - } - - /// - /// Sends an order notification to a vendor - /// - /// Message template - /// Order instance - /// Vendor instance - /// Message language identifier - /// - /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain - /// - private async Task SendOrderVendorMessage(string message, Order order, Vendor vendor, string languageId, - DomainHost host = null) - { - ArgumentNullException.ThrowIfNull(order); - ArgumentNullException.ThrowIfNull(vendor); - - var store = await GetStore(order.StoreId); - var language = await EnsureLanguageIsActive(languageId, store.Id); - - var messageTemplate = await GetMessageTemplate(message, store.Id); - if (messageTemplate == null) - return 0; - - //email account - var emailAccount = await GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); - - var customer = await _mediator.Send(new GetCustomerByIdQuery { Id = order.CustomerId }); - - host = store.LinkHost(host); - var builder = new LiquidObjectBuilder(_mediator); - builder.AddStoreTokens(store, language, emailAccount) - .AddOrderTokens(order, customer, store, host, vendor: vendor); - - AddCustomerTokensIfNotNull(builder, customer, store, host, language); - - var liquidObject = await builder.BuildAsync(); - //event notification - await _mediator.MessageTokensAdded(messageTemplate, liquidObject); - - var toEmail = vendor.Email; - var toName = vendor.Name; - return await SendNotification(messageTemplate, emailAccount, - languageId, liquidObject, - toEmail, toName, - reference: Reference.Order, objectId: order.Id); - } - - - /// - /// Sends a shipment sent notification to a customer - /// - /// Shipment - /// Order - public virtual async Task SendShipmentSentCustomerMessage(Shipment shipment, Order order) - { - return await SendShipmentCustomerMessage(MessageTemplateNames.SendShipmentSentCustomerMessage, shipment, order); - } - - /// - /// Sends a shipment delivered notification to a customer - /// - /// Shipment - /// Order - public virtual async Task SendShipmentDeliveredCustomerMessage(Shipment shipment, Order order) - { - return await SendShipmentCustomerMessage(MessageTemplateNames.SendShipmentDeliveredCustomerMessage, shipment, - order); - } - - /// - /// Send a shipment notification to a customer - /// - /// Message template - /// Shipment - /// Order - private async Task SendShipmentCustomerMessage(string message, Shipment shipment, Order order) - { - ArgumentNullException.ThrowIfNull(shipment); - - if (order == null) - throw new Exception("Order cannot be loaded"); - - var store = await GetStore(order.StoreId); - var language = await EnsureLanguageIsActive(order.CustomerLanguageId, store.Id); - - var messageTemplate = await GetMessageTemplate(message, store.Id); - if (messageTemplate == null) - return 0; - - //email account - var emailAccount = await GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); - - var customer = await _mediator.Send(new GetCustomerByIdQuery { Id = order.CustomerId }); - - var host = store.PrimaryHost(); - var builder = new LiquidObjectBuilder(_mediator); - builder.AddStoreTokens(store, language, emailAccount) - .AddShipmentTokens(shipment, order, store, host, language) - .AddOrderTokens(order, customer, store, host); - - AddCustomerTokensIfNotNull(builder, customer, store, host, language); - - var liquidObject = await builder.BuildAsync(); - //event notification - await _mediator.MessageTokensAdded(messageTemplate, liquidObject); - - var toEmail = order.BillingAddress.Email; - var toName = $"{order.BillingAddress.FirstName} {order.BillingAddress.LastName}"; - return await SendNotification(messageTemplate, emailAccount, - language.Id, liquidObject, - toEmail, toName, - reference: Reference.Shipment, objectId: shipment.Id); - } - - - /// - /// Sends a new order note added notification to a customer - /// - /// Order - /// Order note - /// - /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain - /// - public virtual async Task SendNewOrderNoteAddedCustomerMessage(Order order, OrderNote orderNote, - DomainHost host = null) - { - ArgumentNullException.ThrowIfNull(orderNote); - ArgumentNullException.ThrowIfNull(order); - - var store = await GetStore(order.StoreId); - var language = await EnsureLanguageIsActive(order.CustomerLanguageId, store.Id); - - var messageTemplate = - await GetMessageTemplate(MessageTemplateNames.SendNewOrderNoteAddedCustomerMessage, store.Id); - if (messageTemplate == null) - return 0; - - //email account - var emailAccount = await GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); - - var customer = await _mediator.Send(new GetCustomerByIdQuery { Id = order.CustomerId }); - - host = store.LinkHost(host); - var builder = new LiquidObjectBuilder(_mediator); - builder.AddStoreTokens(store, language, emailAccount) - .AddOrderTokens(order, customer, store, host, orderNote); - - AddCustomerTokensIfNotNull(builder, customer, store, host, language); - - var liquidObject = await builder.BuildAsync(); - //event notification - await _mediator.MessageTokensAdded(messageTemplate, liquidObject); - - var toEmail = order.BillingAddress.Email; - var toName = $"{order.BillingAddress.FirstName} {order.BillingAddress.LastName}"; - return await SendNotification(messageTemplate, emailAccount, - language.Id, liquidObject, - toEmail, toName, - reference: Reference.Order, objectId: order.Id); - } - - #endregion - - #region Newsletter messages - - /// - /// Sends a newsletter subscription activation message - /// - /// Newsletter subscription - /// Language identifier - /// - /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain - /// - public virtual async Task SendNewsLetterSubscriptionActivationMessage(NewsLetterSubscription subscription, - string languageId, DomainHost host = null) - { - return await SendNewsLetterSubscriptionMessage(MessageTemplateNames.SendNewsLetterSubscriptionActivationMessage, - subscription, languageId, host: host); - } - - /// - /// Sends a newsletter subscription deactivation message - /// - /// Newsletter subscription - /// Language identifier - /// - /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain - /// - public virtual async Task SendNewsLetterSubscriptionDeactivationMessage(NewsLetterSubscription subscription, - string languageId, DomainHost host = null) - { - return await SendNewsLetterSubscriptionMessage( - MessageTemplateNames.SendNewsLetterSubscriptionDeactivationMessage, subscription, languageId, host: host); - } - - /// - /// Send a newsletter subscription message - /// - /// Message template - /// Newsletter subscription - /// Language identifier - /// - /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain - /// - private async Task SendNewsLetterSubscriptionMessage(string message, NewsLetterSubscription subscription, - string languageId, DomainHost host = null) - { - ArgumentNullException.ThrowIfNull(subscription); - - var store = await GetStore(subscription.StoreId); - var language = await EnsureLanguageIsActive(languageId, store.Id); - - var messageTemplate = await GetMessageTemplate(message, store.Id); - if (messageTemplate == null) - return 0; - - //email account - var emailAccount = await GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); - - host = store.LinkHost(host); - var builder = new LiquidObjectBuilder(_mediator); - builder.AddStoreTokens(store, language, emailAccount) - .AddNewsLetterSubscriptionTokens(subscription, store, host); - - var liquidObject = await builder.BuildAsync(); - //event notification - await _mediator.MessageTokensAdded(messageTemplate, liquidObject); - - var toEmail = subscription.Email; - return await SendNotification(messageTemplate, emailAccount, - languageId, liquidObject, - toEmail, string.Empty, - reference: Reference.Customer, objectId: subscription.CustomerId); - } - - #endregion - - #region Send a message to a friend, ask question - - /// - /// Sends "email a friend" message - /// - /// Customer instance - /// Store - /// Message language identifier - /// Product instance - /// Customer's email - /// Friend's email - /// Personal message - /// Queued email identifier - /// - /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain - /// - public virtual async Task SendProductEmailAFriendMessage(Customer customer, Store store, string languageId, - Product product, string customerEmail, string friendsEmail, string personalMessage, DomainHost host = null) - { - ArgumentNullException.ThrowIfNull(customer); - ArgumentNullException.ThrowIfNull(product); - - var language = await EnsureLanguageIsActive(languageId, store.Id); - - var messageTemplate = await GetMessageTemplate(MessageTemplateNames.SendProductEmailAFriendMessage, store.Id); - if (messageTemplate == null) - return 0; - - //email account - var emailAccount = await GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); - - host = store.LinkHost(host); - var builder = new LiquidObjectBuilder(_mediator); - builder.AddStoreTokens(store, language, emailAccount) - .AddEmailAFriendTokens(personalMessage, customerEmail, friendsEmail) - .AddCustomerTokens(customer, store, host, language) - .AddProductTokens(product, language, store, host); - var liquidObject = await builder.BuildAsync(); - - //event notification - await _mediator.MessageTokensAdded(messageTemplate, liquidObject); - - var toEmail = friendsEmail; - var toName = ""; - return await SendNotification(messageTemplate, emailAccount, - languageId, liquidObject, - toEmail, toName, - reference: Reference.Customer, objectId: customer.Id); - } - - /// - /// Sends wishlist "email a friend" message - /// - /// Customer - /// Store - /// Message language identifier - /// Customer's email - /// Friend's email - /// Personal message - /// Queued email identifier - /// - /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain - /// - public virtual async Task SendWishlistEmailAFriendMessage(Customer customer, Store store, string languageId, - string customerEmail, string friendsEmail, string personalMessage, DomainHost host = null) - { - ArgumentNullException.ThrowIfNull(customer); - - var language = await EnsureLanguageIsActive(languageId, store.Id); - - var messageTemplate = await GetMessageTemplate(MessageTemplateNames.SendWishlistEmailAFriendMessage, store.Id); - if (messageTemplate == null) - return 0; - - //email account - var emailAccount = await GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); - - - host = store.LinkHost(host); - var builder = new LiquidObjectBuilder(_mediator); - builder.AddStoreTokens(store, language, emailAccount) - .AddCustomerTokens(customer, store, host, language) - .AddEmailAFriendTokens(personalMessage, customerEmail, friendsEmail); - - var liquidObject = await builder.BuildAsync(); - - //event notification - await _mediator.MessageTokensAdded(messageTemplate, liquidObject); - - var toEmail = friendsEmail; - var toName = ""; - return await SendNotification(messageTemplate, emailAccount, - languageId, liquidObject, - toEmail, toName, - reference: Reference.Customer, objectId: customer.Id); - } - - - /// - /// Sends "email a friend" message - /// - /// Queued email identifier - /// - /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain - /// - public virtual async Task SendProductQuestionMessage(Customer customer, Store store, string languageId, - Product product, string customerEmail, string fullName, string phone, string message, string ipaddress, - DomainHost host = null) - { - ArgumentNullException.ThrowIfNull(customer); - ArgumentNullException.ThrowIfNull(product); - - var language = await EnsureLanguageIsActive(languageId, store.Id); - - var messageTemplate = await GetMessageTemplate(MessageTemplateNames.SendProductQuestionMessage, store.Id); - if (messageTemplate == null) - return 0; - - //email account - var emailAccount = await GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); - - host = store.LinkHost(host); - var builder = new LiquidObjectBuilder(_mediator); - builder.AddStoreTokens(store, language, emailAccount) - .AddCustomerTokens(customer, store, host, language) - .AddProductTokens(product, language, store, host); - var liquidObject = await builder.BuildAsync(); - liquidObject.AskQuestion = new LiquidAskQuestion(message, customerEmail, fullName, phone); - - //event notification - await _mediator.MessageTokensAdded(messageTemplate, liquidObject); - - //store in database - if (_commonSettings.StoreInDatabaseContactUsForm) - { - var subject = messageTemplate.GetTranslation(mt => mt.Subject, languageId); - var body = messageTemplate.GetTranslation(mt => mt.Body, languageId); - - var subjectReplaced = LiquidExtensions.Render(liquidObject, subject); - var bodyReplaced = LiquidExtensions.Render(liquidObject, body); - - await _mediator.Send(new InsertContactUsCommand { - CustomerId = customer.Id, - StoreId = store.Id, - VendorId = product.VendorId, - Email = customerEmail, - Enquiry = bodyReplaced, - FullName = fullName, - Subject = subjectReplaced, - EmailAccountId = emailAccount.Id, - RemoteIpAddress = ipaddress - }); - } - - var toEmail = emailAccount.Email; - var toName = ""; - - if (!string.IsNullOrEmpty(product.VendorId)) - { - var vendor = await _mediator.Send(new GetVendorByIdQuery { Id = product.VendorId }); - if (vendor != null) - { - toEmail = vendor.Email; - toName = vendor.Name; - } - } - - return await SendNotification(messageTemplate, emailAccount, - languageId, liquidObject, - toEmail, toName, replyToEmailAddress: customerEmail, - reference: Reference.Customer, objectId: customer.Id); - } - - #endregion - - #region Merchandise returns - - /// - /// Sends 'New Merchandise Return' message to a store owner - /// - /// Merchandise return - /// Order - /// Message language identifier - /// - /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain - /// - public virtual async Task SendNewMerchandiseReturnStoreOwnerMessage(MerchandiseReturn merchandiseReturn, - Order order, string languageId, DomainHost host = null) - { - ArgumentNullException.ThrowIfNull(merchandiseReturn); - - var store = await GetStore(order.StoreId); - var language = await EnsureLanguageIsActive(languageId, store.Id); - - var messageTemplate = - await GetMessageTemplate(MessageTemplateNames.SendNewMerchandiseReturnStoreOwnerMessage, store.Id); - if (messageTemplate == null) - return 0; - - //email account - var emailAccount = await GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); - - var customer = await _mediator.Send(new GetCustomerByIdQuery { Id = merchandiseReturn.CustomerId }); - - host = store.LinkHost(host); - var builder = new LiquidObjectBuilder(_mediator); - builder.AddStoreTokens(store, language, emailAccount); - - AddCustomerTokensIfNotNull(builder, customer, store, host, language); - - builder.AddMerchandiseReturnTokens(merchandiseReturn, store, host, order, language); - - var liquidObject = await builder.BuildAsync(); - - //event notification - await _mediator.MessageTokensAdded(messageTemplate, liquidObject); - - if (!string.IsNullOrEmpty(merchandiseReturn.VendorId)) - { - var vendor = await _mediator.Send(new GetVendorByIdQuery { Id = merchandiseReturn.VendorId }); - if (vendor != null) - { - var vendorEmail = vendor.Email; - var vendorName = vendor.Name; - await SendNotification(messageTemplate, emailAccount, - languageId, liquidObject, - vendorEmail, vendorName); - } - } - - var toEmail = emailAccount.Email; - var toName = emailAccount.DisplayName; - return await SendNotification(messageTemplate, emailAccount, - languageId, liquidObject, - toEmail, toName, - reference: Reference.MerchandiseReturn, objectId: merchandiseReturn.Id); - } - - /// - /// Sends 'Merchandise Return status changed' message to a customer - /// - /// Merchandise return - /// Order - /// Message language identifier - public virtual async Task SendMerchandiseReturnStatusChangedCustomerMessage( - MerchandiseReturn merchandiseReturn, Order order, string languageId) - { - ArgumentNullException.ThrowIfNull(merchandiseReturn); - - var store = await GetStore(order.StoreId); - var language = await EnsureLanguageIsActive(languageId, store.Id); - - var messageTemplate = - await GetMessageTemplate(MessageTemplateNames.SendMerchandiseReturnStatusChangedCustomerMessage, store.Id); - if (messageTemplate == null) - return 0; - - //email account - var emailAccount = await GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); - - var customer = await _mediator.Send(new GetCustomerByIdQuery { Id = merchandiseReturn.CustomerId }); - - var host = store.PrimaryHost(); - var builder = new LiquidObjectBuilder(_mediator); - builder.AddStoreTokens(store, language, emailAccount); - - AddCustomerTokensIfNotNull(builder, customer, store, host, language); - - builder.AddMerchandiseReturnTokens(merchandiseReturn, store, host, order, language); - var liquidObject = await builder.BuildAsync(); - //event notification - await _mediator.MessageTokensAdded(messageTemplate, liquidObject); - - var toEmail = string.IsNullOrEmpty(customer?.Email) ? order.BillingAddress.Email : customer.Email; - var toName = string.IsNullOrEmpty(customer?.Email) ? order.BillingAddress.FirstName : customer.GetFullName(); - return await SendNotification(messageTemplate, emailAccount, - languageId, liquidObject, - toEmail, toName, - reference: Reference.MerchandiseReturn, objectId: merchandiseReturn.Id); - } - - /// - /// Sends 'New Merchandise Return' message to a customer - /// - /// Merchandise return - /// Order - /// Message language identifier - /// - /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain - /// - public virtual async Task SendNewMerchandiseReturnCustomerMessage(MerchandiseReturn merchandiseReturn, - Order order, string languageId, DomainHost host = null) - { - ArgumentNullException.ThrowIfNull(merchandiseReturn); - - var store = await GetStore(order.StoreId); - var language = await EnsureLanguageIsActive(languageId, store.Id); - - var messageTemplate = - await GetMessageTemplate(MessageTemplateNames.SendNewMerchandiseReturnCustomerMessage, store.Id); - if (messageTemplate == null) - return 0; - - //email account - var emailAccount = await GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); - - var customer = await _mediator.Send(new GetCustomerByIdQuery { Id = merchandiseReturn.CustomerId }); - - host = store.LinkHost(host); - var builder = new LiquidObjectBuilder(_mediator); - builder.AddStoreTokens(store, language, emailAccount); - AddCustomerTokensIfNotNull(builder, customer, store, host, language); - - builder.AddMerchandiseReturnTokens(merchandiseReturn, store, host, order, language); - var liquidObject = await builder.BuildAsync(); - //event notification - await _mediator.MessageTokensAdded(messageTemplate, liquidObject); - - var toEmail = string.IsNullOrEmpty(customer?.Email) ? order.BillingAddress.Email : customer.Email; - var toName = string.IsNullOrEmpty(customer?.Email) ? order.BillingAddress.FirstName : customer.GetFullName(); - return await SendNotification(messageTemplate, emailAccount, - languageId, liquidObject, - toEmail, toName, - reference: Reference.MerchandiseReturn, objectId: merchandiseReturn.Id); - } - - /// - /// Sends a new merchandise return note added notification to a customer - /// - /// Merchandise return - /// Merchandise return note - /// Order - public virtual async Task SendNewMerchandiseReturnNoteAddedCustomerMessage(MerchandiseReturn merchandiseReturn, - MerchandiseReturnNote merchandiseReturnNote, Order order) - { - ArgumentNullException.ThrowIfNull(merchandiseReturnNote); - ArgumentNullException.ThrowIfNull(merchandiseReturn); - - var store = await GetStore(order.StoreId); - var language = await EnsureLanguageIsActive(order.CustomerLanguageId, store.Id); - - var messageTemplate = - await GetMessageTemplate(MessageTemplateNames.SendNewMerchandiseReturnNoteAddedCustomerMessage, store.Id); - if (messageTemplate == null) - return 0; - - //email account - var emailAccount = await GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); - - var customer = await _mediator.Send(new GetCustomerByIdQuery { Id = merchandiseReturn.CustomerId }); - - var host = store.PrimaryHost(); - var builder = new LiquidObjectBuilder(_mediator); - builder.AddStoreTokens(store, language, emailAccount); - AddCustomerTokensIfNotNull(builder, customer, store, host, language); - - builder.AddMerchandiseReturnTokens(merchandiseReturn, store, host, order, language, - merchandiseReturnNote); - var liquidObject = await builder.BuildAsync(); - //event notification - await _mediator.MessageTokensAdded(messageTemplate, liquidObject); - - var toEmail = string.IsNullOrEmpty(customer?.Email) ? order.BillingAddress.Email : customer.Email; - var toName = string.IsNullOrEmpty(customer?.Email) ? order.BillingAddress.FirstName : customer.GetFullName(); - return await SendNotification(messageTemplate, emailAccount, - language.Id, liquidObject, - toEmail, toName, - reference: Reference.MerchandiseReturn, objectId: merchandiseReturn.Id); - } - - #endregion - - #region Misc - - /// - /// Sends 'New vendor account submitted' message to a store owner - /// - /// Customer - /// Vendor - /// Store - /// Message language identifier - /// - /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain - /// - public virtual async Task SendNewVendorAccountApplyStoreOwnerMessage(Customer customer, Vendor vendor, - Store store, string languageId, DomainHost host = null) - { - ArgumentNullException.ThrowIfNull(customer); - ArgumentNullException.ThrowIfNull(vendor); - - var language = await EnsureLanguageIsActive(languageId, store.Id); - - var messageTemplate = await GetMessageTemplate("VendorAccountApply.StoreOwnerNotification", store.Id); - if (messageTemplate == null) - return 0; - - //email account - var emailAccount = await GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); - - host = store.LinkHost(host); - var builder = new LiquidObjectBuilder(_mediator).AddStoreTokens(store, language, emailAccount) - .AddCustomerTokens(customer, store, host, language) - .AddVendorTokens(vendor, language); - - var liquidObject = await builder.BuildAsync(); - //event notification - await _mediator.MessageTokensAdded(messageTemplate, liquidObject); - - var toEmail = emailAccount.Email; - var toName = emailAccount.DisplayName; - return await SendNotification(messageTemplate, emailAccount, - languageId, liquidObject, - toEmail, toName, - reference: Reference.Customer, objectId: customer.Id); - } - - /// - /// Sends 'Vendor information changed' message to a store owner - /// - /// Vendor - /// Store - /// Message language identifier - public virtual async Task SendVendorInformationChangeMessage(Vendor vendor, Store store, string languageId) - { - ArgumentNullException.ThrowIfNull(vendor); - - var language = await EnsureLanguageIsActive(languageId, store.Id); - - var messageTemplate = await GetMessageTemplate("VendorInformationChange.StoreOwnerNotification", store.Id); - if (messageTemplate == null) - return 0; - - //email account - var emailAccount = await GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); - - var builder = new LiquidObjectBuilder(_mediator); - builder.AddStoreTokens(store, language, emailAccount) - .AddVendorTokens(vendor, language); - var liquidObject = await builder.BuildAsync(); - //event notification - await _mediator.MessageTokensAdded(messageTemplate, liquidObject); - - var toEmail = emailAccount.Email; - var toName = emailAccount.DisplayName; - - return await SendNotification(messageTemplate, emailAccount, languageId, liquidObject, toEmail, toName, - reference: Reference.Vendor, objectId: vendor.Id); - } - - - /// - /// Sends a gift voucher notification - /// - /// Gift voucher - /// Order - /// Message language identifier - public virtual async Task SendGiftVoucherMessage(GiftVoucher giftVoucher, Order order, string languageId) - { - ArgumentNullException.ThrowIfNull(giftVoucher); - - Store store = null; - if (order != null) store = await _storeService.GetStoreById(order.StoreId); - store ??= (await _storeService.GetAllStores()).FirstOrDefault(); - - var language = await EnsureLanguageIsActive(languageId, store?.Id); - - var messageTemplate = await GetMessageTemplate("GiftVoucher.Notification", store?.Id); - if (messageTemplate == null) - return 0; - - //email account - var emailAccount = await GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); - - var builder = new LiquidObjectBuilder(_mediator); - builder.AddStoreTokens(store, language, emailAccount) - .AddGiftVoucherTokens(giftVoucher, language); - var liquidObject = await builder.BuildAsync(); - //event notification - await _mediator.MessageTokensAdded(messageTemplate, liquidObject); - var toEmail = giftVoucher.RecipientEmail; - var toName = giftVoucher.RecipientName; - return await SendNotification(messageTemplate, emailAccount, - languageId, liquidObject, - toEmail, toName); - } - - /// - /// Sends a product review notification message to a store owner - /// - /// Product - /// Product review - /// Store - /// Message language identifier - /// - /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain - /// - public virtual async Task SendProductReviewMessage(Product product, ProductReview productReview, - Store store, string languageId, DomainHost host = null) - { - ArgumentNullException.ThrowIfNull(productReview); - - var language = await EnsureLanguageIsActive(languageId, store.Id); - - var messageTemplate = await GetMessageTemplate("Product.ProductReview", store.Id); - if (messageTemplate == null) - return 0; - - //email account - var emailAccount = await GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); - - var customer = await _mediator.Send(new GetCustomerByIdQuery { Id = productReview.CustomerId }); - - host = store.LinkHost(host); - var builder = new LiquidObjectBuilder(_mediator); - builder.AddStoreTokens(store, language, emailAccount) - .AddProductReviewTokens(product, productReview); - AddCustomerTokensIfNotNull(builder, customer, store, host, language); - - var liquidObject = await builder.BuildAsync(); - //event notification - await _mediator.MessageTokensAdded(messageTemplate, liquidObject); - - var toEmail = emailAccount.Email; - var toName = emailAccount.DisplayName; - return await SendNotification(messageTemplate, emailAccount, - languageId, liquidObject, - toEmail, toName, - reference: Reference.Product, objectId: product.Id); - } - - - /// - /// Sends a vendor review notification message to a store owner - /// - /// Vendor review - /// Store - /// Message language identifier - /// - /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain - /// - public virtual async Task SendVendorReviewMessage(VendorReview vendorReview, Store store, - string languageId, DomainHost host = null) - { - ArgumentNullException.ThrowIfNull(vendorReview); - - var language = await EnsureLanguageIsActive(languageId, store.Id); - - var messageTemplate = await GetMessageTemplate("Vendor.VendorReview", store.Id); - if (messageTemplate == null) - return 0; - - //email account - var emailAccount = await GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); - //customer - var customer = await _mediator.Send(new GetCustomerByIdQuery { Id = vendorReview.CustomerId }); - //vendor - var vendor = await _mediator.Send(new GetVendorByIdQuery { Id = vendorReview.VendorId }); - - host = store.LinkHost(host); - var builder = new LiquidObjectBuilder(_mediator); - builder.AddStoreTokens(store, language, emailAccount) - .AddVendorReviewTokens(vendor, vendorReview); - AddCustomerTokensIfNotNull(builder, customer, store, host, language); - - builder.AddVendorTokens(vendor, language); - var liquidObject = await builder.BuildAsync(); - - //event notification - await _mediator.MessageTokensAdded(messageTemplate, liquidObject); - - var toEmail = vendor.Email; - var toName = vendor.Name; - return await SendNotification(messageTemplate, emailAccount, - languageId, liquidObject, - toEmail, toName, - reference: Reference.Customer, objectId: customer?.Id); - } - - /// - /// Sends a "quantity below" notification to a store owner - /// - /// Product - /// Message language identifier - public virtual async Task SendQuantityBelowStoreOwnerMessage(Product product, string languageId) - { - ArgumentNullException.ThrowIfNull(product); - - var store = await GetStore(""); - var language = await EnsureLanguageIsActive(languageId, store.Id); - - var messageTemplate = await GetMessageTemplate("QuantityBelow.StoreOwnerNotification", store.Id); - if (messageTemplate == null) - return 0; - - //email account - var emailAccount = await GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); - - var host = store.PrimaryHost(); - var builder = new LiquidObjectBuilder(_mediator); - builder.AddStoreTokens(store, language, emailAccount) - .AddProductTokens(product, language, store, host); - var liquidObject = await builder.BuildAsync(); - //event notification - await _mediator.MessageTokensAdded(messageTemplate, liquidObject); - - var toEmail = emailAccount.Email; - var toName = emailAccount.DisplayName; - return await SendNotification(messageTemplate, emailAccount, - languageId, liquidObject, - toEmail, toName, - reference: Reference.Product, objectId: product.Id); - } - - /// - /// Sends a "quantity below" notification to a store owner - /// - /// - /// Attribute combination - /// Message language identifier - public virtual async Task SendQuantityBelowStoreOwnerMessage(Product product, - ProductAttributeCombination combination, string languageId) - { - ArgumentNullException.ThrowIfNull(combination); - - var store = await GetStore(""); - var language = await EnsureLanguageIsActive(languageId, store.Id); - - var messageTemplate = - await GetMessageTemplate("QuantityBelow.AttributeCombination.StoreOwnerNotification", store.Id); - if (messageTemplate == null) - return 0; - - //email account - var emailAccount = await GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); - - var host = store.PrimaryHost(); - var builder = new LiquidObjectBuilder(_mediator); - builder.AddStoreTokens(store, language, emailAccount) - .AddProductTokens(product, language, store, host) - .AddAttributeCombinationTokens(product, combination, language, host); - var liquidObject = await builder.BuildAsync(); - //event notification - await _mediator.MessageTokensAdded(messageTemplate, liquidObject); - - var toEmail = emailAccount.Email; - var toName = emailAccount.DisplayName; - return await SendNotification(messageTemplate, emailAccount, - languageId, liquidObject, - toEmail, toName, - reference: Reference.Product, objectId: product.Id); - } - - /// - /// Sends a "new VAT" notification to a store owner - /// - /// Customer - /// Message language identifier - /// - /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain - /// - public virtual async Task SendCustomerDeleteStoreOwnerMessage(Customer customer, string languageId, - DomainHost host = null) - { - ArgumentNullException.ThrowIfNull(customer); - - var store = await GetStore(""); - var language = await EnsureLanguageIsActive(languageId, store.Id); - - var messageTemplate = await GetMessageTemplate("CustomerDelete.StoreOwnerNotification", store.Id); - if (messageTemplate == null) - return 0; - - //email account - var emailAccount = await GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); - - host = store.LinkHost(host); - var builder = new LiquidObjectBuilder(_mediator); - builder.AddStoreTokens(store, language, emailAccount) - .AddCustomerTokens(customer, store, host, language); - - var liquidObject = await builder.BuildAsync(); - - var toEmail = emailAccount.Email; - var toName = emailAccount.DisplayName; - return await SendNotification(messageTemplate, emailAccount, - languageId, liquidObject, - toEmail, toName, - reference: Reference.Customer, objectId: customer.Id); - } - - /// - /// Sends a blog comment notification message to a store owner - /// - /// - /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain - /// - public virtual async Task SendBlogCommentMessage(BlogPost blogPost, BlogComment blogComment, string languageId, - DomainHost host = null) - { - ArgumentNullException.ThrowIfNull(blogComment); - - var store = await GetStore(blogComment.StoreId); - var language = await EnsureLanguageIsActive(languageId, store.Id); - - var messageTemplate = await GetMessageTemplate("Blog.BlogComment", store.Id); - if (messageTemplate == null) - return 0; - - //email account - var emailAccount = await GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); - - host = store.LinkHost(host); - var builder = new LiquidObjectBuilder(_mediator); - builder.AddStoreTokens(store, language, emailAccount) - .AddBlogCommentTokens(blogPost, blogComment, store, host, language); - - var customer = await _mediator.Send(new GetCustomerByIdQuery { Id = blogComment.CustomerId }); - if (customer != null && await _groupService.IsRegistered(customer)) - builder.AddCustomerTokens(customer, store, host, language); - - var liquidObject = await builder.BuildAsync(); - //event notification - await _mediator.MessageTokensAdded(messageTemplate, liquidObject); - - var toEmail = emailAccount.Email; - var toName = emailAccount.DisplayName; - return await SendNotification(messageTemplate, emailAccount, - languageId, liquidObject, - toEmail, toName, - reference: Reference.Blog, objectId: blogPost.Id); - } - - /// - /// Sends an article comment notification message to a store owner - /// - /// - /// Article comment - /// Message language identifier - /// - /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain - /// - public virtual async Task SendArticleCommentMessage(KnowledgebaseArticle article, - KnowledgebaseArticleComment articleComment, string languageId, DomainHost host = null) - { - ArgumentNullException.ThrowIfNull(articleComment); - - var store = await GetStore(""); - var language = await EnsureLanguageIsActive(languageId, store.Id); - - var messageTemplate = await GetMessageTemplate("Knowledgebase.ArticleComment", store.Id); - if (messageTemplate == null) - return 0; - - //email account - var emailAccount = await GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); - - host = store.LinkHost(host); - var builder = new LiquidObjectBuilder(_mediator); - builder.AddStoreTokens(store, language, emailAccount) - .AddArticleCommentTokens(article, articleComment, store, host, language); - - var customer = await _mediator.Send(new GetCustomerByIdQuery { Id = articleComment.CustomerId }); - if (customer != null && await _groupService.IsRegistered(customer)) - builder.AddCustomerTokens(customer, store, host, language); - var liquidObject = await builder.BuildAsync(); - //event notification - await _mediator.MessageTokensAdded(messageTemplate, liquidObject); - - var toEmail = emailAccount.Email; - var toName = emailAccount.DisplayName; - return await SendNotification(messageTemplate, emailAccount, - languageId, liquidObject, - toEmail, toName); - } - - /// - /// Sends a news comment notification message to a store owner - /// - /// News item - /// News comment - /// Message language identifier - /// - /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain - /// - public virtual async Task SendNewsCommentMessage(NewsItem newsItem, NewsComment newsComment, string languageId, - DomainHost host = null) - { - ArgumentNullException.ThrowIfNull(newsComment); - - var store = await GetStore(newsComment.StoreId); - var language = await EnsureLanguageIsActive(languageId, store.Id); - - var messageTemplate = await GetMessageTemplate("News.NewsComment", store.Id); - if (messageTemplate == null) - return 0; - - //email account - var emailAccount = await GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); - - host = store.LinkHost(host); - var builder = new LiquidObjectBuilder(_mediator); - builder.AddStoreTokens(store, language, emailAccount) - .AddNewsCommentTokens(newsItem, newsComment, store, host, language); - var customer = await _mediator.Send(new GetCustomerByIdQuery { Id = newsComment.CustomerId }); - AddCustomerTokensIfNotNull(builder, customer, store, host, language); - - var liquidObject = await builder.BuildAsync(); - //event notification - await _mediator.MessageTokensAdded(messageTemplate, liquidObject); - - var toEmail = emailAccount.Email; - var toName = emailAccount.DisplayName; - return await SendNotification(messageTemplate, emailAccount, - languageId, liquidObject, - toEmail, toName); - } - - /// - /// Sends a 'Out of stock' notification message to a customer - /// - /// Customer - /// Product - /// Subscription - /// Message language identifier - public virtual async Task SendBackInStockMessage(Customer customer, Product product, - OutOfStockSubscription subscription, string languageId) - { - ArgumentNullException.ThrowIfNull(subscription); - - var store = await GetStore(subscription.StoreId); - var language = await EnsureLanguageIsActive(languageId, store.Id); - - var messageTemplate = await GetMessageTemplate("Customer.OutOfStock", store.Id); - if (messageTemplate == null) - return 0; - - //email account - var emailAccount = await GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); - - var host = store.PrimaryHost(); - var builder = new LiquidObjectBuilder(_mediator); - builder.AddStoreTokens(store, language, emailAccount); - AddCustomerTokensIfNotNull(builder, customer, store, host, language); - - builder.AddOutOfStockTokens(product, subscription, store, host, language); - var liquidObject = await builder.BuildAsync(); - //event notification - await _mediator.MessageTokensAdded(messageTemplate, liquidObject); - - var toEmail = customer?.Email; - var toName = customer.GetFullName(); - return await SendNotification(messageTemplate, emailAccount, - languageId, liquidObject, - toEmail, toName, - reference: Reference.Customer, objectId: customer?.Id); - } - - /// - /// Sends "contact us" message - /// - /// Customer - /// Store - /// Message language identifier - /// Sender email - /// Sender name - /// Email subject. Pass null if you want a message template subject to be used. - /// Email body - /// Attr info - /// CustomAttributes - /// Ip address - /// - /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain - /// - public virtual async Task SendContactUsMessage(Customer customer, Store store, string languageId, - string senderEmail, - string senderName, string subject, string body, string attrInfo, IList customAttributes, - string ipaddress, DomainHost host = null) - { - var language = await EnsureLanguageIsActive(languageId, store.Id); - var messageTemplate = await GetMessageTemplate("Service.ContactUs", store.Id); - if (messageTemplate == null) - return 0; - - //email account - var emailAccount = await GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); - - string fromEmail; - string fromName; - senderName = WebUtility.HtmlEncode(senderName); - senderEmail = WebUtility.HtmlEncode(senderEmail); - //required for some SMTP servers - if (_commonSettings.UseSystemEmailForContactUsForm) - { - fromEmail = emailAccount.Email; - fromName = emailAccount.DisplayName; - } - else - { - fromEmail = senderEmail; - fromName = senderName; - } - - host = store.LinkHost(host); - var builder = new LiquidObjectBuilder(_mediator); - builder.AddStoreTokens(store, language, emailAccount) - .AddCustomerTokens(customer, store, host, language); - - var liquidObject = await builder.BuildAsync(); - liquidObject.ContactUs = new LiquidContactUs(senderEmail, senderName, body, attrInfo); - //event notification - await _mediator.MessageTokensAdded(messageTemplate, liquidObject); - - var toEmail = emailAccount.Email; - var toName = emailAccount.DisplayName; - - //store in database - if (_commonSettings.StoreInDatabaseContactUsForm) - await _mediator.Send(new InsertContactUsCommand { - CustomerId = customer.Id, - StoreId = store.Id, - VendorId = "", - Email = senderEmail, - Enquiry = body, - FullName = senderName, - Subject = string.IsNullOrEmpty(subject) ? "Contact Us (form)" : subject, - ContactAttributeDescription = attrInfo, - ContactAttributes = customAttributes, - EmailAccountId = emailAccount.Id, - RemoteIpAddress = ipaddress - }); - return await SendNotification(messageTemplate, emailAccount, languageId, liquidObject, toEmail, toName, - fromEmail: fromEmail, - fromName: fromName, - subject: subject, - replyToEmailAddress: senderEmail, - replyToName: senderName, reference: Reference.Customer, objectId: customer?.Id); - } - - /// - /// Sends "contact vendor" message - /// - /// Customer - /// Store - /// Vendor - /// Message language identifier - /// Sender email - /// Sender name - /// Email subject. Pass null if you want a message template subject to be used. - /// Email body - /// Ip address - /// - /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain - /// - public virtual async Task SendContactVendorMessage(Customer customer, Store store, Vendor vendor, - string languageId, string senderEmail, - string senderName, string subject, string body, string ipaddress, DomainHost host = null) - { - ArgumentNullException.ThrowIfNull(vendor); - - var language = await EnsureLanguageIsActive(languageId, store.Id); - - var messageTemplate = await GetMessageTemplate("Service.ContactVendor", store.Id); - if (messageTemplate == null) - return 0; - - //email account - var emailAccount = await GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); - - string fromEmail; - string fromName; - senderName = WebUtility.HtmlEncode(senderName); - senderEmail = WebUtility.HtmlEncode(senderEmail); - - //required for some SMTP servers - if (_commonSettings.UseSystemEmailForContactUsForm) - { - fromEmail = emailAccount.Email; - fromName = emailAccount.DisplayName; - } - else - { - fromEmail = senderEmail; - fromName = senderName; - } - - host = store.LinkHost(host); - var builder = new LiquidObjectBuilder(_mediator); - builder.AddStoreTokens(store, language, emailAccount) - .AddCustomerTokens(customer, store, host, language); - var liquidObject = await builder.BuildAsync(); - liquidObject.ContactUs = new LiquidContactUs(senderEmail, senderName, body, ""); - //event notification - await _mediator.MessageTokensAdded(messageTemplate, liquidObject); - - var toEmail = vendor.Email; - var toName = vendor.Name; - - //store in database - if (_commonSettings.StoreInDatabaseContactUsForm) - await _mediator.Send(new InsertContactUsCommand { - CustomerId = customer.Id, - StoreId = store.Id, - VendorId = vendor.Id, - Email = senderEmail, - Enquiry = body, - RemoteIpAddress = ipaddress, - FullName = senderName, - Subject = string.IsNullOrEmpty(subject) ? "Contact Us (form)" : subject, - EmailAccountId = emailAccount.Id - }); - - return await SendNotification(messageTemplate, emailAccount, languageId, liquidObject, toEmail, toName, - fromEmail: fromEmail, - fromName: fromName, - subject: subject, - replyToEmailAddress: senderEmail, - replyToName: senderName, - reference: Reference.Vendor, objectId: vendor.Id); - } - - #region Auction notification - - public virtual async Task SendAuctionWinEndedCustomerMessage(Product product, string languageId, Bid bid) - { - ArgumentNullException.ThrowIfNull(product); - - var customer = await _mediator.Send(new GetCustomerByIdQuery { Id = bid.CustomerId }); - if (customer == null) return 0; - if (string.IsNullOrEmpty(languageId)) - languageId = customer.GetUserFieldFromEntity(SystemCustomerFieldNames.LanguageId); - - var store = await GetStore(bid.StoreId); - var language = await EnsureLanguageIsActive(languageId, store.Id); - - var messageTemplate = await GetMessageTemplate("AuctionEnded.CustomerNotificationWin", store.Id); - if (messageTemplate == null) - return 0; - - var emailAccount = await GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); - - var host = store.PrimaryHost(); - var builder = new LiquidObjectBuilder(_mediator); - builder.AddAuctionTokens(product, bid) - .AddCustomerTokens(customer, store, host, language) - .AddProductTokens(product, language, store, host) - .AddStoreTokens(store, language, emailAccount); - - var liquidObject = await builder.BuildAsync(); - //event notification - await _mediator.MessageTokensAdded(messageTemplate, liquidObject); - - var toEmail = customer.Email; - var toName = customer.GetFullName(); - return await SendNotification(messageTemplate, emailAccount, - languageId, liquidObject, - toEmail, toName, - reference: Reference.Customer, objectId: customer.Id); - } - - public virtual async Task SendAuctionEndedLostCustomerMessage(Product product, string languageId, Bid bid) - { - ArgumentNullException.ThrowIfNull(product); - - var winner = await _mediator.Send(new GetCustomerByIdQuery { Id = bid.CustomerId }); - if (winner == null) return 0; - - var store = await GetStore(bid.StoreId); - - var language = await EnsureLanguageIsActive(languageId, store.Id); - - var messageTemplate = await GetMessageTemplate("AuctionEnded.CustomerNotificationLost", store.Id); - if (messageTemplate == null) - return 0; - - var emailAccount = await GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); - - var host = store.PrimaryHost(); - var builder = new LiquidObjectBuilder(_mediator); - builder.AddAuctionTokens(product, bid) - .AddProductTokens(product, language, store, host) - .AddStoreTokens(store, language, emailAccount); - var liquidObject = await builder.BuildAsync(); - - var bids = (await _mediator.Send(new GetBidsByProductIdQuery { ProductId = bid.ProductId })) - .Where(x => x.CustomerId != bid.CustomerId).GroupBy(x => x.CustomerId); - foreach (var item in bids) - { - var builder2 = new LiquidObjectBuilder(_mediator, liquidObject); - var customer = await _mediator.Send(new GetCustomerByIdQuery { Id = item.Key }); - if (string.IsNullOrEmpty(languageId)) - languageId = customer.GetUserFieldFromEntity(SystemCustomerFieldNames.LanguageId); - builder2.AddCustomerTokens(customer, store, host, language); - var liquidObject2 = await builder2.BuildAsync(); - - //event notification - await _mediator.MessageTokensAdded(messageTemplate, liquidObject2); - - var toEmail = customer.Email; - var toName = customer.GetFullName(); - await SendNotification(messageTemplate, emailAccount, - languageId, liquidObject2, - toEmail, toName, - reference: Reference.Customer, objectId: customer.Id); - } - - return 0; - } - - public virtual async Task SendAuctionEndedBinCustomerMessage(Product product, string customerId, - string languageId, string storeId) - { - ArgumentNullException.ThrowIfNull(product); - - var store = await GetStore(storeId); - - var messageTemplate = await GetMessageTemplate("AuctionEnded.CustomerNotificationBin", storeId); - if (messageTemplate == null) - return 0; - - var language = await EnsureLanguageIsActive(languageId, store.Id); - var emailAccount = await GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); - - var host = store.PrimaryHost(); - var builder = new LiquidObjectBuilder(_mediator); - builder.AddProductTokens(product, language, store, host) - .AddStoreTokens(store, language, emailAccount); - - var liquidObject = await builder.BuildAsync(); - var bids = (await _mediator.Send(new GetBidsByProductIdQuery { ProductId = product.Id })) - .Where(x => x.CustomerId != customerId).GroupBy(x => x.CustomerId); - foreach (var item in bids) - { - var builder2 = new LiquidObjectBuilder(_mediator, liquidObject); - var customer = await _mediator.Send(new GetCustomerByIdQuery { Id = item.Key }); - if (customer != null) - { - if (string.IsNullOrEmpty(languageId)) - languageId = customer.GetUserFieldFromEntity(SystemCustomerFieldNames.LanguageId); - - AddCustomerTokensIfNotNull(builder2, customer, store, host, language); - var liquidObject2 = await builder2.BuildAsync(); - //event notification - await _mediator.MessageTokensAdded(messageTemplate, liquidObject2); - - var toEmail = customer.Email; - var toName = customer.GetFullName(); - await SendNotification(messageTemplate, emailAccount, - languageId, liquidObject2, - toEmail, toName, - reference: Reference.Customer, objectId: customer.Id); - } - } - - return 0; - } - - public virtual async Task SendAuctionEndedStoreOwnerMessage(Product product, string languageId, Bid bid) - { - ArgumentNullException.ThrowIfNull(product); - - var builder = new LiquidObjectBuilder(_mediator); - MessageTemplate messageTemplate; - EmailAccount emailAccount; - - if (bid != null) - { - var customer = await _mediator.Send(new GetCustomerByIdQuery { Id = bid.CustomerId }); - if (string.IsNullOrEmpty(languageId)) - languageId = customer.GetUserFieldFromEntity(SystemCustomerFieldNames.LanguageId); - - var store = await GetStore(bid.StoreId); - - var language = await EnsureLanguageIsActive(languageId, store.Id); - - messageTemplate = await GetMessageTemplate("AuctionEnded.StoreOwnerNotification", store.Id); - if (messageTemplate == null) - return 0; - - emailAccount = await GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); - builder.AddAuctionTokens(product, bid) - .AddCustomerTokens(customer, store, store.PrimaryHost(), language) - .AddStoreTokens(store, language, emailAccount); - } - else - { - var store = (await _storeService.GetAllStores()).FirstOrDefault(); - var language = await EnsureLanguageIsActive(languageId, store?.Id); - messageTemplate = await GetMessageTemplate("AuctionExpired.StoreOwnerNotification", ""); - if (messageTemplate == null) - return 0; - - emailAccount = await GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); - builder.AddProductTokens(product, language, store, store.PrimaryHost()); - } - - var liquidObject = await builder.BuildAsync(); - //event notification - await _mediator.MessageTokensAdded(messageTemplate, liquidObject); - - var toEmail = emailAccount.Email; - var toName = emailAccount.DisplayName; - - return await SendNotification(messageTemplate, emailAccount, - languageId, liquidObject, - toEmail, toName); - } - - - /// - /// Send outbid notification to a customer - /// - /// Message language identifier - /// Product - /// - public virtual async Task SendOutBidCustomerMessage(Product product, string languageId, Bid bid) - { - ArgumentNullException.ThrowIfNull(product); - - var customer = await _mediator.Send(new GetCustomerByIdQuery { Id = bid.CustomerId }); - if (string.IsNullOrEmpty(languageId)) - languageId = customer.GetUserFieldFromEntity(SystemCustomerFieldNames.LanguageId); - - var store = await GetStore(bid.StoreId); - - var language = await EnsureLanguageIsActive(languageId, store.Id); - - var messageTemplate = await GetMessageTemplate("BidUp.CustomerNotification", store.Id); - if (messageTemplate == null) - return 0; - - var emailAccount = await GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); - - var host = store.PrimaryHost(); - var builder = new LiquidObjectBuilder(_mediator); - builder.AddAuctionTokens(product, bid) - .AddCustomerTokens(customer, store, host, language) - .AddStoreTokens(store, language, emailAccount); - - var liquidObject = await builder.BuildAsync(); - //event notification - await _mediator.MessageTokensAdded(messageTemplate, liquidObject); - - var toEmail = customer.Email; - var toName = customer.GetFullName(); - return await SendNotification(messageTemplate, emailAccount, - languageId, liquidObject, - toEmail, toName, - reference: Reference.Customer, objectId: customer.Id); - } - - #endregion - public virtual async Task SendNotification(MessageTemplate messageTemplate, EmailAccount emailAccount, string languageId, LiquidObject liquidObject, string toEmailAddress, string toName, @@ -2099,6 +87,4 @@ public virtual async Task SendNotification(MessageTemplate messageTemplate, } #endregion - - #endregion -} \ No newline at end of file +} diff --git a/src/Business/Grand.Business.Messages/Services/NewsletterMessageProviderService.cs b/src/Business/Grand.Business.Messages/Services/NewsletterMessageProviderService.cs new file mode 100644 index 0000000000..8307761605 --- /dev/null +++ b/src/Business/Grand.Business.Messages/Services/NewsletterMessageProviderService.cs @@ -0,0 +1,110 @@ +using Grand.Business.Core.Extensions; +using Grand.Business.Core.Interfaces.Messages; +using Grand.Business.Messages.DotLiquidDrops; +using Grand.Domain.Common; +using Grand.Domain.Customers; +using Grand.Domain.Localization; +using Grand.Domain.Messages; +using Grand.Domain.Stores; +using Grand.Mediator; + +namespace Grand.Business.Messages.Services; + +public class NewsletterMessageProviderService : INewsletterMessageProviderService +{ + #region Ctor + + public NewsletterMessageProviderService( + IMessageLookupService messageLookupService, + IMessageProviderService messageProviderService, + IMediator mediator) + { + _messageLookupService = messageLookupService; + _messageProviderService = messageProviderService; + _mediator = mediator; + } + + #endregion + + #region Fields + + private readonly IMessageLookupService _messageLookupService; + private readonly IMessageProviderService _messageProviderService; + private readonly IMediator _mediator; + + #endregion + + #region Methods + + /// + /// Sends a newsletter subscription activation message + /// + /// Newsletter subscription + /// Language identifier + /// + /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain + /// + public virtual async Task SendNewsLetterSubscriptionActivationMessage(NewsLetterSubscription subscription, + string languageId, DomainHost host = null) + { + return await SendNewsLetterSubscriptionMessage(MessageTemplateNames.SendNewsLetterSubscriptionActivationMessage, + subscription, languageId, host: host); + } + + /// + /// Sends a newsletter subscription deactivation message + /// + /// Newsletter subscription + /// Language identifier + /// + /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain + /// + public virtual async Task SendNewsLetterSubscriptionDeactivationMessage(NewsLetterSubscription subscription, + string languageId, DomainHost host = null) + { + return await SendNewsLetterSubscriptionMessage( + MessageTemplateNames.SendNewsLetterSubscriptionDeactivationMessage, subscription, languageId, host: host); + } + + /// + /// Send a newsletter subscription message + /// + /// Message template + /// Newsletter subscription + /// Language identifier + /// + /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain + /// + private async Task SendNewsLetterSubscriptionMessage(string message, NewsLetterSubscription subscription, + string languageId, DomainHost host = null) + { + ArgumentNullException.ThrowIfNull(subscription); + + var store = await _messageLookupService.GetStore(subscription.StoreId); + var language = await _messageLookupService.EnsureLanguageIsActive(languageId, store.Id); + + var messageTemplate = await _messageLookupService.GetMessageTemplate(message, store.Id); + if (messageTemplate == null) + return 0; + + //email account + var emailAccount = await _messageLookupService.GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); + + host = store.LinkHost(host); + var builder = new LiquidObjectBuilder(_mediator); + builder.AddStoreTokens(store, language, emailAccount) + .AddNewsLetterSubscriptionTokens(subscription, store, host); + + var liquidObject = await builder.BuildAsync(); + //event notification + await _mediator.MessageTokensAdded(messageTemplate, liquidObject); + + var toEmail = subscription.Email; + return await _messageProviderService.SendNotification(messageTemplate, emailAccount, + languageId, liquidObject, + toEmail, string.Empty, + reference: Reference.Customer, objectId: subscription.CustomerId); + } + + #endregion +} diff --git a/src/Business/Grand.Business.Messages/Services/OrderMessageProviderService.cs b/src/Business/Grand.Business.Messages/Services/OrderMessageProviderService.cs new file mode 100644 index 0000000000..f6d1e6333b --- /dev/null +++ b/src/Business/Grand.Business.Messages/Services/OrderMessageProviderService.cs @@ -0,0 +1,522 @@ +using Grand.Business.Core.Extensions; +using Grand.Business.Core.Interfaces.Common.Stores; +using Grand.Business.Core.Interfaces.Messages; +using Grand.Business.Core.Queries.Messages; +using Grand.Business.Messages.DotLiquidDrops; +using Grand.Domain.Common; +using Grand.Domain.Customers; +using Grand.Domain.Localization; +using Grand.Domain.Orders; +using Grand.Domain.Shipping; +using Grand.Domain.Stores; +using Grand.Domain.Vendors; +using Grand.Mediator; + +namespace Grand.Business.Messages.Services; + +public class OrderMessageProviderService : IOrderMessageProviderService +{ + #region Ctor + + public OrderMessageProviderService( + IMessageLookupService messageLookupService, + IMessageProviderService messageProviderService, + IMediator mediator, + IStoreService storeService) + { + _messageLookupService = messageLookupService; + _messageProviderService = messageProviderService; + _mediator = mediator; + _storeService = storeService; + } + + #endregion + + #region Fields + + private readonly IMessageLookupService _messageLookupService; + private readonly IMessageProviderService _messageProviderService; + private readonly IMediator _mediator; + private readonly IStoreService _storeService; + + #endregion + + #region Methods + + /// + /// Sends an order placed notification to a store owner + /// + /// Order instance + /// Customer + /// Message language identifier + /// + /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain + /// + public virtual async Task SendOrderPlacedStoreOwnerMessage(Order order, Customer customer, string languageId, + DomainHost host = null) + { + return await SendOrderStoreOwnerMessage(MessageTemplateNames.SendOrderPlacedStoreOwnerMessage, order, customer, + languageId, host: host); + } + + /// + /// Sends an order paid notification to a store owner + /// + /// Order instance + /// Customer instance + /// Message language identifier + public virtual async Task SendOrderPaidStoreOwnerMessage(Order order, Customer customer, string languageId) + { + return await SendOrderStoreOwnerMessage(MessageTemplateNames.SendOrderPaidStoreOwnerMessage, order, customer, + languageId); + } + + /// + /// Sends an order cancelled notification to an admin + /// + /// Order instance + /// Customer instance + /// Message language identifier + /// Queued email identifier + public virtual async Task SendOrderCancelledStoreOwnerMessage(Order order, Customer customer, + string languageId) + { + return await SendOrderStoreOwnerMessage(MessageTemplateNames.SendOrderCancelledStoreOwnerMessage, order, + customer, languageId); + } + + /// Sends an order refunded notification to a store owner + /// Order instance + /// Amount refunded + /// Message language identifier + public virtual async Task SendOrderRefundedStoreOwnerMessage(Order order, double refundedAmount, + string languageId) + { + var customer = await _mediator.Send(new GetCustomerByIdQuery { Id = order.CustomerId }); + return await SendOrderStoreOwnerMessage(MessageTemplateNames.SendOrderRefundedStoreOwnerMessage, order, + customer, languageId, refundedAmount); + } + + /// + /// Sends an order notification to a store owner + /// + /// Message template + /// Order instance + /// Customer instance + /// Message language identifier + /// + /// + /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain + /// + private async Task SendOrderStoreOwnerMessage(string template, Order order, Customer customer, + string languageId, double refundedAmount = 0, DomainHost host = null) + { + ArgumentNullException.ThrowIfNull(order); + + var store = await _messageLookupService.GetStore(order.StoreId); + var language = await _messageLookupService.EnsureLanguageIsActive(languageId, store.Id); + + var messageTemplate = await _messageLookupService.GetMessageTemplate(template, store.Id); + if (messageTemplate == null) + return 0; + + //email account + var emailAccount = await _messageLookupService.GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); + + host = store.LinkHost(host); + var liquidBuilder = new LiquidObjectBuilder(_mediator); + liquidBuilder.AddStoreTokens(store, language, emailAccount) + .AddOrderTokens(order, customer, store, host); + liquidBuilder.AddCustomerTokensIfNotNull(customer, store, host, language); + + var liquidObject = await liquidBuilder.BuildAsync(); + //event notification + await _mediator.MessageTokensAdded(messageTemplate, liquidObject); + + var toEmail = emailAccount.Email; + var toName = emailAccount.DisplayName; + return await _messageProviderService.SendNotification(messageTemplate, emailAccount, + languageId, liquidObject, + toEmail, toName, + reference: Reference.Order, objectId: order.Id); + } + + /// + /// Sends an order placed notification to a customer + /// + /// Order instance + /// + /// Message language identifier + /// Attachment file path + /// + /// Attachment file name. If specified, then this file name will be sent to a recipient. + /// Otherwise, "AttachmentFilePath" name will be used. + /// + /// Attachments + /// + /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain + /// + public virtual async Task SendOrderPlacedCustomerMessage(Order order, Customer customer, string languageId, + string attachmentFilePath = null, string attachmentFileName = null, IEnumerable attachments = null, + DomainHost host = null) + { + return await SendOrderCustomerMessage(MessageTemplateNames.SendOrderPlacedCustomerMessage, order, customer, + languageId, attachmentFilePath, attachmentFileName, attachments, host: host); + } + + /// + /// Sends an order paid notification to a customer + /// + /// Order instance + /// Customer instance + /// Message language identifier + /// Attachment file path + /// + /// Attachment file name. If specified, then this file name will be sent to a recipient. + /// Otherwise, "AttachmentFilePath" name will be used. + /// + /// Attachments ident + public virtual async Task SendOrderPaidCustomerMessage(Order order, Customer customer, string languageId, + string attachmentFilePath = null, string attachmentFileName = null, IEnumerable attachments = null) + { + return await SendOrderCustomerMessage(MessageTemplateNames.SendOrderPaidCustomerMessage, order, customer, + languageId, attachmentFilePath, attachmentFileName, attachments); + } + + /// + /// Sends an order completed notification to a customer + /// + /// Order instance + /// Customer instance + /// Message language identifier + /// Attachment file path + /// + /// Attachment file name. If specified, then this file name will be sent to a recipient. + /// Otherwise, "AttachmentFilePath" name will be used. + /// + /// Attachments ident + public virtual async Task SendOrderCompletedCustomerMessage(Order order, Customer customer, string languageId, + string attachmentFilePath = null, string attachmentFileName = null, IEnumerable attachments = null) + { + return await SendOrderCustomerMessage(MessageTemplateNames.SendOrderCompletedCustomerMessage, order, customer, + languageId, attachmentFilePath, attachmentFileName, attachments); + } + + /// + /// Sends an order cancelled notification to a customer + /// + /// Order instance + /// Customer instance + /// Message language identifier + public virtual async Task SendOrderCancelledCustomerMessage(Order order, Customer customer, string languageId) + { + return await SendOrderCustomerMessage(MessageTemplateNames.SendOrderCancelledCustomerMessage, order, customer, + languageId); + } + + /// + /// Sends an order refunded notification to a customer + /// + /// Order instance + /// Amount refunded + /// Message language identifier + /// Queued email identifier + public virtual async Task SendOrderRefundedCustomerMessage(Order order, double refundedAmount, + string languageId) + { + var customer = await _mediator.Send(new GetCustomerByIdQuery { Id = order.CustomerId }); + return await SendOrderCustomerMessage(MessageTemplateNames.SendOrderRefundedCustomerMessage, order, customer, + languageId, refundedAmount: refundedAmount); + } + + /// + /// Sends an order notification to a customer + /// + /// Message template + /// Order instance + /// Customer instance + /// Message language identifier + /// Attachment file path + /// + /// Attachment file name. If specified, then this file name will be sent to a recipient. + /// Otherwise, "AttachmentFilePath" name will be used. + /// + /// Attachments ident + /// + /// + /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain + /// + private async Task SendOrderCustomerMessage(string message, Order order, Customer customer, string languageId, + string attachmentFilePath = null, string attachmentFileName = null, IEnumerable attachments = null, + double refundedAmount = 0, DomainHost host = null) + { + ArgumentNullException.ThrowIfNull(order); + + var store = await _messageLookupService.GetStore(order.StoreId); + var language = await _messageLookupService.EnsureLanguageIsActive(languageId, store.Id); + + var messageTemplate = await _messageLookupService.GetMessageTemplate(message, store.Id); + if (messageTemplate == null) + return 0; + + //email account + var emailAccount = await _messageLookupService.GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); + + host = store.LinkHost(host); + var builder = new LiquidObjectBuilder(_mediator); + builder.AddStoreTokens(store, language, emailAccount) + .AddOrderTokens(order, customer, store, host); + builder.AddCustomerTokensIfNotNull(customer, store, host, language); + + var liquidObject = await builder.BuildAsync(); + //event notification + await _mediator.MessageTokensAdded(messageTemplate, liquidObject); + + var toEmail = order.BillingAddress.Email; + var toName = $"{order.BillingAddress.FirstName} {order.BillingAddress.LastName}"; + return await _messageProviderService.SendNotification(messageTemplate, emailAccount, + languageId, liquidObject, + toEmail, toName, + attachmentFilePath, + attachmentFileName, + attachments, + reference: Reference.Order, objectId: order.Id); + } + + /// + /// Sends an order placed notification to a vendor + /// + /// Order instance + /// Customer instance + /// Vendor instance + /// Message language identifier + /// + /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain + /// + public virtual async Task SendOrderPlacedVendorMessage(Order order, Customer customer, Vendor vendor, + string languageId, DomainHost host = null) + { + return await SendOrderVendorMessage(MessageTemplateNames.SendOrderPlacedVendorMessage, order, vendor, + languageId, host: host); + } + + /// + /// Sends an order paid notification to a vendor + /// + /// Order instance + /// Vendor instance + /// Message language identifier + public virtual async Task SendOrderPaidVendorMessage(Order order, Vendor vendor, string languageId) + { + return await SendOrderVendorMessage(MessageTemplateNames.SendOrderPaidVendorMessage, order, vendor, languageId); + } + + /// + /// Sends an order cancel notification to a vendor + /// + /// Order instance + /// Vendor instance + /// Message language identifier + public virtual async Task SendOrderCancelledVendorMessage(Order order, Vendor vendor, string languageId) + { + return await SendOrderVendorMessage(MessageTemplateNames.SendOrderCancelledVendorMessage, order, vendor, + languageId); + } + + /// + /// Sends an order notification to a vendor + /// + /// Message template + /// Order instance + /// Vendor instance + /// Message language identifier + /// + /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain + /// + private async Task SendOrderVendorMessage(string message, Order order, Vendor vendor, string languageId, + DomainHost host = null) + { + ArgumentNullException.ThrowIfNull(order); + ArgumentNullException.ThrowIfNull(vendor); + + var store = await _messageLookupService.GetStore(order.StoreId); + var language = await _messageLookupService.EnsureLanguageIsActive(languageId, store.Id); + + var messageTemplate = await _messageLookupService.GetMessageTemplate(message, store.Id); + if (messageTemplate == null) + return 0; + + //email account + var emailAccount = await _messageLookupService.GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); + + var customer = await _mediator.Send(new GetCustomerByIdQuery { Id = order.CustomerId }); + + host = store.LinkHost(host); + var builder = new LiquidObjectBuilder(_mediator); + builder.AddStoreTokens(store, language, emailAccount) + .AddOrderTokens(order, customer, store, host, vendor: vendor); + + builder.AddCustomerTokensIfNotNull(customer, store, host, language); + + var liquidObject = await builder.BuildAsync(); + //event notification + await _mediator.MessageTokensAdded(messageTemplate, liquidObject); + + var toEmail = vendor.Email; + var toName = vendor.Name; + return await _messageProviderService.SendNotification(messageTemplate, emailAccount, + languageId, liquidObject, + toEmail, toName, + reference: Reference.Order, objectId: order.Id); + } + + /// + /// Sends a shipment sent notification to a customer + /// + /// Shipment + /// Order + public virtual async Task SendShipmentSentCustomerMessage(Shipment shipment, Order order) + { + return await SendShipmentCustomerMessage(MessageTemplateNames.SendShipmentSentCustomerMessage, shipment, order); + } + + /// + /// Sends a shipment delivered notification to a customer + /// + /// Shipment + /// Order + public virtual async Task SendShipmentDeliveredCustomerMessage(Shipment shipment, Order order) + { + return await SendShipmentCustomerMessage(MessageTemplateNames.SendShipmentDeliveredCustomerMessage, shipment, + order); + } + + /// + /// Send a shipment notification to a customer + /// + /// Message template + /// Shipment + /// Order + private async Task SendShipmentCustomerMessage(string message, Shipment shipment, Order order) + { + ArgumentNullException.ThrowIfNull(shipment); + + if (order == null) + throw new Exception("Order cannot be loaded"); + + var store = await _messageLookupService.GetStore(order.StoreId); + var language = await _messageLookupService.EnsureLanguageIsActive(order.CustomerLanguageId, store.Id); + + var messageTemplate = await _messageLookupService.GetMessageTemplate(message, store.Id); + if (messageTemplate == null) + return 0; + + //email account + var emailAccount = await _messageLookupService.GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); + + var customer = await _mediator.Send(new GetCustomerByIdQuery { Id = order.CustomerId }); + + var host = store.PrimaryHost(); + var builder = new LiquidObjectBuilder(_mediator); + builder.AddStoreTokens(store, language, emailAccount) + .AddShipmentTokens(shipment, order, store, host, language) + .AddOrderTokens(order, customer, store, host); + + builder.AddCustomerTokensIfNotNull(customer, store, host, language); + + var liquidObject = await builder.BuildAsync(); + //event notification + await _mediator.MessageTokensAdded(messageTemplate, liquidObject); + + var toEmail = order.BillingAddress.Email; + var toName = $"{order.BillingAddress.FirstName} {order.BillingAddress.LastName}"; + return await _messageProviderService.SendNotification(messageTemplate, emailAccount, + language.Id, liquidObject, + toEmail, toName, + reference: Reference.Shipment, objectId: shipment.Id); + } + + /// + /// Sends a new order note added notification to a customer + /// + /// Order + /// Order note + /// + /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain + /// + public virtual async Task SendNewOrderNoteAddedCustomerMessage(Order order, OrderNote orderNote, + DomainHost host = null) + { + ArgumentNullException.ThrowIfNull(orderNote); + ArgumentNullException.ThrowIfNull(order); + + var store = await _messageLookupService.GetStore(order.StoreId); + var language = await _messageLookupService.EnsureLanguageIsActive(order.CustomerLanguageId, store.Id); + + var messageTemplate = + await _messageLookupService.GetMessageTemplate(MessageTemplateNames.SendNewOrderNoteAddedCustomerMessage, store.Id); + if (messageTemplate == null) + return 0; + + //email account + var emailAccount = await _messageLookupService.GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); + + var customer = await _mediator.Send(new GetCustomerByIdQuery { Id = order.CustomerId }); + + host = store.LinkHost(host); + var builder = new LiquidObjectBuilder(_mediator); + builder.AddStoreTokens(store, language, emailAccount) + .AddOrderTokens(order, customer, store, host, orderNote); + + builder.AddCustomerTokensIfNotNull(customer, store, host, language); + + var liquidObject = await builder.BuildAsync(); + //event notification + await _mediator.MessageTokensAdded(messageTemplate, liquidObject); + + var toEmail = order.BillingAddress.Email; + var toName = $"{order.BillingAddress.FirstName} {order.BillingAddress.LastName}"; + return await _messageProviderService.SendNotification(messageTemplate, emailAccount, + language.Id, liquidObject, + toEmail, toName, + reference: Reference.Order, objectId: order.Id); + } + + /// + /// Sends a gift voucher notification + /// + /// Gift voucher + /// Order + /// Message language identifier + public virtual async Task SendGiftVoucherMessage(GiftVoucher giftVoucher, Order order, string languageId) + { + ArgumentNullException.ThrowIfNull(giftVoucher); + + Store store = null; + if (order != null) store = await _storeService.GetStoreById(order.StoreId); + store ??= (await _storeService.GetAllStores()).FirstOrDefault(); + + var language = await _messageLookupService.EnsureLanguageIsActive(languageId, store?.Id); + + var messageTemplate = await _messageLookupService.GetMessageTemplate("GiftVoucher.Notification", store?.Id); + if (messageTemplate == null) + return 0; + + //email account + var emailAccount = await _messageLookupService.GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); + + var builder = new LiquidObjectBuilder(_mediator); + builder.AddStoreTokens(store, language, emailAccount) + .AddGiftVoucherTokens(giftVoucher, language); + var liquidObject = await builder.BuildAsync(); + //event notification + await _mediator.MessageTokensAdded(messageTemplate, liquidObject); + var toEmail = giftVoucher.RecipientEmail; + var toName = giftVoucher.RecipientName; + return await _messageProviderService.SendNotification(messageTemplate, emailAccount, + languageId, liquidObject, + toEmail, toName); + } + + #endregion +} diff --git a/src/Business/Grand.Business.Messages/Services/VendorMessageProviderService.cs b/src/Business/Grand.Business.Messages/Services/VendorMessageProviderService.cs new file mode 100644 index 0000000000..41918c8b1f --- /dev/null +++ b/src/Business/Grand.Business.Messages/Services/VendorMessageProviderService.cs @@ -0,0 +1,252 @@ +using Grand.Business.Core.Commands.Messages.Common; +using Grand.Business.Core.Extensions; +using Grand.Business.Core.Interfaces.Messages; +using Grand.Business.Core.Queries.Messages; +using Grand.Business.Messages.DotLiquidDrops; +using Grand.Domain.Common; +using Grand.Domain.Customers; +using Grand.Domain.Localization; +using Grand.Domain.Messages; +using Grand.Domain.Stores; +using Grand.Domain.Vendors; +using Grand.Mediator; +using Grand.SharedKernel.Extensions; +using System.Net; + +namespace Grand.Business.Messages.Services; + +public class VendorMessageProviderService : IVendorMessageProviderService +{ + #region Ctor + + public VendorMessageProviderService( + IMessageLookupService messageLookupService, + IMessageProviderService messageProviderService, + IMediator mediator, + CommonSettings commonSettings) + { + _messageLookupService = messageLookupService; + _messageProviderService = messageProviderService; + _mediator = mediator; + _commonSettings = commonSettings; + } + + #endregion + + #region Fields + + private readonly IMessageLookupService _messageLookupService; + private readonly IMessageProviderService _messageProviderService; + private readonly IMediator _mediator; + private readonly CommonSettings _commonSettings; + + #endregion + + #region Methods + + /// + /// Sends 'New vendor account submitted' message to a store owner + /// + /// Customer + /// Vendor + /// Store + /// Message language identifier + /// + /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain + /// + public virtual async Task SendNewVendorAccountApplyStoreOwnerMessage(Customer customer, Vendor vendor, + Store store, string languageId, DomainHost host = null) + { + ArgumentNullException.ThrowIfNull(customer); + ArgumentNullException.ThrowIfNull(vendor); + + var language = await _messageLookupService.EnsureLanguageIsActive(languageId, store.Id); + + var messageTemplate = await _messageLookupService.GetMessageTemplate("VendorAccountApply.StoreOwnerNotification", store.Id); + if (messageTemplate == null) + return 0; + + //email account + var emailAccount = await _messageLookupService.GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); + + host = store.LinkHost(host); + var builder = new LiquidObjectBuilder(_mediator).AddStoreTokens(store, language, emailAccount) + .AddCustomerTokens(customer, store, host, language) + .AddVendorTokens(vendor, language); + + var liquidObject = await builder.BuildAsync(); + //event notification + await _mediator.MessageTokensAdded(messageTemplate, liquidObject); + + var toEmail = emailAccount.Email; + var toName = emailAccount.DisplayName; + return await _messageProviderService.SendNotification(messageTemplate, emailAccount, + languageId, liquidObject, + toEmail, toName, + reference: Reference.Customer, objectId: customer.Id); + } + + /// + /// Sends 'Vendor information changed' message to a store owner + /// + /// Vendor + /// Store + /// Message language identifier + public virtual async Task SendVendorInformationChangeMessage(Vendor vendor, Store store, string languageId) + { + ArgumentNullException.ThrowIfNull(vendor); + + var language = await _messageLookupService.EnsureLanguageIsActive(languageId, store.Id); + + var messageTemplate = await _messageLookupService.GetMessageTemplate("VendorInformationChange.StoreOwnerNotification", store.Id); + if (messageTemplate == null) + return 0; + + //email account + var emailAccount = await _messageLookupService.GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); + + var builder = new LiquidObjectBuilder(_mediator); + builder.AddStoreTokens(store, language, emailAccount) + .AddVendorTokens(vendor, language); + var liquidObject = await builder.BuildAsync(); + //event notification + await _mediator.MessageTokensAdded(messageTemplate, liquidObject); + + var toEmail = emailAccount.Email; + var toName = emailAccount.DisplayName; + + return await _messageProviderService.SendNotification(messageTemplate, emailAccount, languageId, liquidObject, toEmail, toName, + reference: Reference.Vendor, objectId: vendor.Id); + } + + /// + /// Sends a vendor review notification message to a store owner + /// + /// Vendor review + /// Store + /// Message language identifier + /// + /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain + /// + public virtual async Task SendVendorReviewMessage(VendorReview vendorReview, Store store, + string languageId, DomainHost host = null) + { + ArgumentNullException.ThrowIfNull(vendorReview); + + var language = await _messageLookupService.EnsureLanguageIsActive(languageId, store.Id); + + var messageTemplate = await _messageLookupService.GetMessageTemplate("Vendor.VendorReview", store.Id); + if (messageTemplate == null) + return 0; + + //email account + var emailAccount = await _messageLookupService.GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); + //customer + var customer = await _mediator.Send(new GetCustomerByIdQuery { Id = vendorReview.CustomerId }); + //vendor + var vendor = await _mediator.Send(new GetVendorByIdQuery { Id = vendorReview.VendorId }); + + host = store.LinkHost(host); + var builder = new LiquidObjectBuilder(_mediator); + builder.AddStoreTokens(store, language, emailAccount) + .AddVendorReviewTokens(vendor, vendorReview); + builder.AddCustomerTokensIfNotNull(customer, store, host, language); + + builder.AddVendorTokens(vendor, language); + var liquidObject = await builder.BuildAsync(); + + //event notification + await _mediator.MessageTokensAdded(messageTemplate, liquidObject); + + var toEmail = vendor.Email; + var toName = vendor.Name; + return await _messageProviderService.SendNotification(messageTemplate, emailAccount, + languageId, liquidObject, + toEmail, toName, + reference: Reference.Customer, objectId: customer?.Id); + } + + /// + /// Sends "contact vendor" message + /// + /// Customer + /// Store + /// Vendor + /// Message language identifier + /// Sender email + /// Sender name + /// Email subject. Pass null if you want a message template subject to be used. + /// Email body + /// Ip address + /// + /// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain + /// + public virtual async Task SendContactVendorMessage(Customer customer, Store store, Vendor vendor, + string languageId, string senderEmail, + string senderName, string subject, string body, string ipaddress, DomainHost host = null) + { + ArgumentNullException.ThrowIfNull(vendor); + + var language = await _messageLookupService.EnsureLanguageIsActive(languageId, store.Id); + + var messageTemplate = await _messageLookupService.GetMessageTemplate("Service.ContactVendor", store.Id); + if (messageTemplate == null) + return 0; + + //email account + var emailAccount = await _messageLookupService.GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); + + string fromEmail; + string fromName; + senderName = WebUtility.HtmlEncode(senderName); + senderEmail = WebUtility.HtmlEncode(senderEmail); + + //required for some SMTP servers + if (_commonSettings.UseSystemEmailForContactUsForm) + { + fromEmail = emailAccount.Email; + fromName = emailAccount.DisplayName; + } + else + { + fromEmail = senderEmail; + fromName = senderName; + } + + host = store.LinkHost(host); + var builder = new LiquidObjectBuilder(_mediator); + builder.AddStoreTokens(store, language, emailAccount) + .AddCustomerTokens(customer, store, host, language); + var liquidObject = await builder.BuildAsync(); + liquidObject.ContactUs = new LiquidContactUs(senderEmail, senderName, body, ""); + //event notification + await _mediator.MessageTokensAdded(messageTemplate, liquidObject); + + var toEmail = vendor.Email; + var toName = vendor.Name; + + //store in database + if (_commonSettings.StoreInDatabaseContactUsForm) + await _mediator.Send(new InsertContactUsCommand { + CustomerId = customer.Id, + StoreId = store.Id, + VendorId = vendor.Id, + Email = senderEmail, + Enquiry = body, + RemoteIpAddress = ipaddress, + FullName = senderName, + Subject = string.IsNullOrEmpty(subject) ? "Contact Us (form)" : subject, + EmailAccountId = emailAccount.Id + }); + + return await _messageProviderService.SendNotification(messageTemplate, emailAccount, languageId, liquidObject, toEmail, toName, + fromEmail: fromEmail, + fromName: fromName, + subject: subject, + replyToEmailAddress: senderEmail, + replyToName: senderName, + reference: Reference.Vendor, objectId: vendor.Id); + } + + #endregion +} diff --git a/src/Business/Grand.Business.Messages/Startup/StartupApplication.cs b/src/Business/Grand.Business.Messages/Startup/StartupApplication.cs index 08e82f523a..bb9b1f3de0 100644 --- a/src/Business/Grand.Business.Messages/Startup/StartupApplication.cs +++ b/src/Business/Grand.Business.Messages/Startup/StartupApplication.cs @@ -17,6 +17,14 @@ public void ConfigureServices(IServiceCollection services, IConfiguration config services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); diff --git a/src/Modules/Grand.Module.ScheduledTasks/BackgroundServices/EndAuctionsTask.cs b/src/Modules/Grand.Module.ScheduledTasks/BackgroundServices/EndAuctionsTask.cs index da64e606ed..055f5f7273 100644 --- a/src/Modules/Grand.Module.ScheduledTasks/BackgroundServices/EndAuctionsTask.cs +++ b/src/Modules/Grand.Module.ScheduledTasks/BackgroundServices/EndAuctionsTask.cs @@ -22,17 +22,17 @@ public class EndAuctionsTask : IScheduleTask private readonly ILanguageService _languageService; private readonly LanguageSettings _languageSettings; private readonly ILogger _logger; - private readonly IMessageProviderService _messageProviderService; + private readonly ICatalogMessageProviderService _catalogMessageProviderService; private readonly IShoppingCartService _shoppingCartService; public EndAuctionsTask(IAuctionService auctionService, - IMessageProviderService messageProviderService, LanguageSettings translationService, + ICatalogMessageProviderService catalogMessageProviderService, LanguageSettings translationService, IShoppingCartService shoppingCartService, ICustomerService customerService, ILanguageService languageService, ILogger logger) { _languageService = languageService; _auctionService = auctionService; - _messageProviderService = messageProviderService; + _catalogMessageProviderService = catalogMessageProviderService; _languageSettings = translationService; _shoppingCartService = shoppingCartService; _customerService = customerService; @@ -59,7 +59,7 @@ public async Task Execute(CancellationToken cancellationToken) if (bid == null) { await _auctionService.UpdateAuctionEnded(auctionToEnd, true); - await _messageProviderService.SendAuctionEndedStoreOwnerMessage(auctionToEnd, + await _catalogMessageProviderService.SendAuctionEndedStoreOwnerMessage(auctionToEnd, _languageSettings.DefaultAdminLanguageId, null); continue; } @@ -81,10 +81,10 @@ await _messageProviderService.SendAuctionEndedStoreOwnerMessage(auctionToEnd, { bid.Win = true; await _auctionService.UpdateBid(bid); - await _messageProviderService.SendAuctionEndedStoreOwnerMessage(auctionToEnd, + await _catalogMessageProviderService.SendAuctionEndedStoreOwnerMessage(auctionToEnd, _languageSettings.DefaultAdminLanguageId, bid); - await _messageProviderService.SendAuctionWinEndedCustomerMessage(auctionToEnd, null, bid); - await _messageProviderService.SendAuctionEndedLostCustomerMessage(auctionToEnd, null, bid); + await _catalogMessageProviderService.SendAuctionWinEndedCustomerMessage(auctionToEnd, null, bid); + await _catalogMessageProviderService.SendAuctionEndedLostCustomerMessage(auctionToEnd, null, bid); await _auctionService.UpdateAuctionEnded(auctionToEnd, true); } else diff --git a/src/Plugins/Authentication.Facebook/Infrastructure/Cache/FacebookAuthenticationEventConsumer.cs b/src/Plugins/Authentication.Facebook/Infrastructure/Cache/FacebookAuthenticationEventConsumer.cs index 2d5c75209c..ceea1df9e9 100644 --- a/src/Plugins/Authentication.Facebook/Infrastructure/Cache/FacebookAuthenticationEventConsumer.cs +++ b/src/Plugins/Authentication.Facebook/Infrastructure/Cache/FacebookAuthenticationEventConsumer.cs @@ -17,7 +17,7 @@ public class FacebookAuthenticationEventConsumer : INotificationHandler _contextAccessor.WorkContext.WorkingLanguage.Id; @@ -29,13 +29,13 @@ public class GoogleAuthenticationEventConsumer : INotificationHandler _giftVoucherMock; private ActivatedValueForPurchasedGiftVouchersCommandHandler _handler; private Mock _langService; - private Mock _messageProviderMock; + private Mock _messageProviderMock; [TestInitialize] public void Init() { _giftVoucherMock = new Mock(); _langService = new Mock(); - _messageProviderMock = new Mock(); + _messageProviderMock = new Mock(); _handler = new ActivatedValueForPurchasedGiftVouchersCommandHandler(_giftVoucherMock.Object, _langService.Object, _messageProviderMock.Object); } diff --git a/src/Tests/Grand.Business.Checkout.Tests/Commands/Handlers/Orders/OrderNotificationCommandHandlerTests.cs b/src/Tests/Grand.Business.Checkout.Tests/Commands/Handlers/Orders/OrderNotificationCommandHandlerTests.cs index fc0213c050..7c869832c1 100644 --- a/src/Tests/Grand.Business.Checkout.Tests/Commands/Handlers/Orders/OrderNotificationCommandHandlerTests.cs +++ b/src/Tests/Grand.Business.Checkout.Tests/Commands/Handlers/Orders/OrderNotificationCommandHandlerTests.cs @@ -17,15 +17,15 @@ namespace Grand.Business.Checkout.Tests.Commands.Handlers.Orders; public class OrderNotificationCommandHandlerTests { private OrderNotificationCommandHandler _handler; - private Mock _messageProviderServiceMock; + private Mock _orderMessageProviderServiceMock; private Mock _orderServiceMock; [TestInitialize] public void Init() { _orderServiceMock = new Mock(); - _messageProviderServiceMock = new Mock(); - _handler = new OrderNotificationCommandHandler(_orderServiceMock.Object, _messageProviderServiceMock.Object, + _orderMessageProviderServiceMock = new Mock(); + _handler = new OrderNotificationCommandHandler(_orderServiceMock.Object, _orderMessageProviderServiceMock.Object, new OrderSettings(), new LanguageSettings(), new Mock().Object, new Mock>().Object, new Mock().Object); } @@ -57,7 +57,7 @@ public async Task Handle_SendsTheCustomerMessageForTheCommandCustomer() await _handler.Handle(new OrderNotificationCommand { Order = order, Customer = customer }, CancellationToken.None); //Assert - _messageProviderServiceMock.Verify(x => x.SendOrderPlacedCustomerMessage(order, customer, "en", + _orderMessageProviderServiceMock.Verify(x => x.SendOrderPlacedCustomerMessage(order, customer, "en", It.IsAny(), It.IsAny(), It.IsAny>()), Times.Once); } } diff --git a/src/Tests/Grand.Business.Checkout.Tests/Commands/Handlers/Orders/OrderStatusCommandHandlerTests.cs b/src/Tests/Grand.Business.Checkout.Tests/Commands/Handlers/Orders/OrderStatusCommandHandlerTests.cs index 41ad3261ce..a633ae22ab 100644 --- a/src/Tests/Grand.Business.Checkout.Tests/Commands/Handlers/Orders/OrderStatusCommandHandlerTests.cs +++ b/src/Tests/Grand.Business.Checkout.Tests/Commands/Handlers/Orders/OrderStatusCommandHandlerTests.cs @@ -18,7 +18,7 @@ public class OrderStatusCommandHandlerTests private OrderStatusCommandHandler _handler; private LoyaltyPointsSettings _loyaltyPointsSettings; private Mock _mediatorMock; - private Mock _messageProviderServiceMock; + private Mock _orderMessageProviderServiceMock; private Mock _orderServiceMock; private OrderSettings _orderSettings; private Mock _pdfServiceMock; @@ -31,13 +31,13 @@ public void Init() _orderServiceMock = new Mock(); _customerServiceMock = new Mock(); _pdfServiceMock = new Mock(); - _messageProviderServiceMock = new Mock(); + _orderMessageProviderServiceMock = new Mock(); _vendorServiceMock = new Mock(); _loyaltyPointsSettings = new LoyaltyPointsSettings(); _orderSettings = new OrderSettings(); _handler = new OrderStatusCommandHandler(_orderServiceMock.Object, _customerServiceMock.Object, - _pdfServiceMock.Object, _messageProviderServiceMock.Object, _vendorServiceMock.Object, _mediatorMock.Object, + _pdfServiceMock.Object, _orderMessageProviderServiceMock.Object, _vendorServiceMock.Object, _mediatorMock.Object, _orderSettings, _loyaltyPointsSettings); } diff --git a/src/Tests/Grand.Business.Checkout.Tests/Commands/Handlers/Orders/PartiallyRefundCommandHandlerTests.cs b/src/Tests/Grand.Business.Checkout.Tests/Commands/Handlers/Orders/PartiallyRefundCommandHandlerTests.cs index 230fae2a74..d1eb575abb 100644 --- a/src/Tests/Grand.Business.Checkout.Tests/Commands/Handlers/Orders/PartiallyRefundCommandHandlerTests.cs +++ b/src/Tests/Grand.Business.Checkout.Tests/Commands/Handlers/Orders/PartiallyRefundCommandHandlerTests.cs @@ -21,7 +21,7 @@ public class PartiallyRefundCommandHandlerTests private PartiallyRefundCommandHandler _handler; private Mock> _loggerMock; private Mock _mediatorMock; - private Mock _messageProviderServiceMock; + private Mock _orderMessageProviderServiceMock; private Mock _orderServiceMock; private Mock _paymentServiceMock; private Mock _paymentTransactionMock; @@ -32,12 +32,12 @@ public void Init() _mediatorMock = new Mock(); _orderServiceMock = new Mock(); _paymentTransactionMock = new Mock(); - _messageProviderServiceMock = new Mock(); + _orderMessageProviderServiceMock = new Mock(); _paymentServiceMock = new Mock(); _loggerMock = new Mock>(); _handler = new PartiallyRefundCommandHandler(_paymentServiceMock.Object, _paymentTransactionMock.Object, - _mediatorMock.Object, _messageProviderServiceMock.Object, _loggerMock.Object, _orderServiceMock.Object, + _mediatorMock.Object, _orderMessageProviderServiceMock.Object, _loggerMock.Object, _orderServiceMock.Object, new LanguageSettings()); } diff --git a/src/Tests/Grand.Business.Checkout.Tests/Commands/Handlers/Orders/PartiallyRefundOfflineCommandHandlerTests.cs b/src/Tests/Grand.Business.Checkout.Tests/Commands/Handlers/Orders/PartiallyRefundOfflineCommandHandlerTests.cs index 85b4b24923..a95e00caca 100644 --- a/src/Tests/Grand.Business.Checkout.Tests/Commands/Handlers/Orders/PartiallyRefundOfflineCommandHandlerTests.cs +++ b/src/Tests/Grand.Business.Checkout.Tests/Commands/Handlers/Orders/PartiallyRefundOfflineCommandHandlerTests.cs @@ -18,7 +18,7 @@ public class PartiallyRefundOfflineCommandHandlerTests { private PartiallyRefundOfflineCommandHandler _handler; private Mock _mediatorMock; - private Mock _messageProviderServiceMock; + private Mock _orderMessageProviderServiceMock; private Mock _orderServiceMock; private Mock _paymentTransactionMock; @@ -28,10 +28,10 @@ public void Init() _mediatorMock = new Mock(); _orderServiceMock = new Mock(); _paymentTransactionMock = new Mock(); - _messageProviderServiceMock = new Mock(); + _orderMessageProviderServiceMock = new Mock(); _handler = new PartiallyRefundOfflineCommandHandler(_orderServiceMock.Object, _paymentTransactionMock.Object, - _mediatorMock.Object, _messageProviderServiceMock.Object, new LanguageSettings()); + _mediatorMock.Object, _orderMessageProviderServiceMock.Object, new LanguageSettings()); } [TestMethod] diff --git a/src/Tests/Grand.Business.Checkout.Tests/Commands/Handlers/Orders/PlaceOrderCommandHandlerTests.cs b/src/Tests/Grand.Business.Checkout.Tests/Commands/Handlers/Orders/PlaceOrderCommandHandlerTests.cs index 3a9600a974..7ded646e45 100644 --- a/src/Tests/Grand.Business.Checkout.Tests/Commands/Handlers/Orders/PlaceOrderCommandHandlerTests.cs +++ b/src/Tests/Grand.Business.Checkout.Tests/Commands/Handlers/Orders/PlaceOrderCommandHandlerTests.cs @@ -50,7 +50,7 @@ public void Init() new Mock().Object, new Mock().Object, new Mock().Object, _checkoutAttributeFormatterMock.Object, new Mock().Object, new Mock().Object, new Mock().Object, - new Mock().Object, new Mock().Object, + new Mock().Object, new Mock().Object, new Mock().Object, new Mock().Object, currencyService.Object, new Mock().Object, new Mock().Object, new Mock().Object, new Mock().Object, diff --git a/src/Tests/Grand.Business.Checkout.Tests/Commands/Handlers/Orders/RefundCommandHandlerTests.cs b/src/Tests/Grand.Business.Checkout.Tests/Commands/Handlers/Orders/RefundCommandHandlerTests.cs index e5e8cf9c64..abee1b20e6 100644 --- a/src/Tests/Grand.Business.Checkout.Tests/Commands/Handlers/Orders/RefundCommandHandlerTests.cs +++ b/src/Tests/Grand.Business.Checkout.Tests/Commands/Handlers/Orders/RefundCommandHandlerTests.cs @@ -21,7 +21,7 @@ public class RefundCommandHandlerTests private RefundCommandHandler _handler; private Mock> _loggerMock; private Mock _mediatorMock; - private Mock _messageProviderServiceMock; + private Mock _orderMessageProviderServiceMock; private Mock _orderServiceMock; private Mock _paymentServiceMock; private Mock _paymentTransactionMock; @@ -32,12 +32,12 @@ public void Init() _mediatorMock = new Mock(); _orderServiceMock = new Mock(); _paymentTransactionMock = new Mock(); - _messageProviderServiceMock = new Mock(); + _orderMessageProviderServiceMock = new Mock(); _paymentServiceMock = new Mock(); _loggerMock = new Mock>(); _handler = new RefundCommandHandler(_mediatorMock.Object, _paymentServiceMock.Object, - _paymentTransactionMock.Object, _orderServiceMock.Object, _messageProviderServiceMock.Object, + _paymentTransactionMock.Object, _orderServiceMock.Object, _orderMessageProviderServiceMock.Object, _loggerMock.Object, new LanguageSettings()); } diff --git a/src/Tests/Grand.Business.Checkout.Tests/Commands/Handlers/Orders/VoidCommandHandlerTests.cs b/src/Tests/Grand.Business.Checkout.Tests/Commands/Handlers/Orders/VoidCommandHandlerTests.cs index 15e6ba97a7..1cba39df3f 100644 --- a/src/Tests/Grand.Business.Checkout.Tests/Commands/Handlers/Orders/VoidCommandHandlerTests.cs +++ b/src/Tests/Grand.Business.Checkout.Tests/Commands/Handlers/Orders/VoidCommandHandlerTests.cs @@ -20,7 +20,7 @@ public class VoidCommandHandlerTests private VoidCommandHandler _handler; private Mock> _loggerMock; private Mock _mediatorMock; - private Mock _messageProviderServiceMock; + private Mock _orderMessageProviderServiceMock; private Mock _orderServiceMock; private Mock _paymentServiceMock; private Mock _paymentTransactionMock; @@ -31,7 +31,7 @@ public void Init() _mediatorMock = new Mock(); _orderServiceMock = new Mock(); _paymentTransactionMock = new Mock(); - _messageProviderServiceMock = new Mock(); + _orderMessageProviderServiceMock = new Mock(); _paymentServiceMock = new Mock(); _loggerMock = new Mock>(); diff --git a/src/Tests/Grand.Business.Checkout.Tests/Commands/Handlers/Shipping/DeliveryCommandHandlerTests.cs b/src/Tests/Grand.Business.Checkout.Tests/Commands/Handlers/Shipping/DeliveryCommandHandlerTests.cs index b146cc3e7e..cf5ee4fb3d 100644 --- a/src/Tests/Grand.Business.Checkout.Tests/Commands/Handlers/Shipping/DeliveryCommandHandlerTests.cs +++ b/src/Tests/Grand.Business.Checkout.Tests/Commands/Handlers/Shipping/DeliveryCommandHandlerTests.cs @@ -16,7 +16,7 @@ public class DeliveryCommandHandlerTests { private DeliveryCommandHandler _deliveryCommandHandler; private Mock _mediatorMock; - private Mock _messageProviderServiceMock; + private Mock _orderMessageProviderServiceMock; private Mock _orderServiceMock; private Mock _shipmentServiceMock; @@ -26,10 +26,10 @@ public void Init() _mediatorMock = new Mock(); _orderServiceMock = new Mock(); _shipmentServiceMock = new Mock(); - _messageProviderServiceMock = new Mock(); + _orderMessageProviderServiceMock = new Mock(); _deliveryCommandHandler = new DeliveryCommandHandler(_mediatorMock.Object, _orderServiceMock.Object, - _shipmentServiceMock.Object, _messageProviderServiceMock.Object); + _shipmentServiceMock.Object, _orderMessageProviderServiceMock.Object); } [TestMethod] diff --git a/src/Tests/Grand.Business.Checkout.Tests/Commands/Handlers/Shipping/ShipCommandHandlerTests.cs b/src/Tests/Grand.Business.Checkout.Tests/Commands/Handlers/Shipping/ShipCommandHandlerTests.cs index bd91dd75aa..40b9fc0b64 100644 --- a/src/Tests/Grand.Business.Checkout.Tests/Commands/Handlers/Shipping/ShipCommandHandlerTests.cs +++ b/src/Tests/Grand.Business.Checkout.Tests/Commands/Handlers/Shipping/ShipCommandHandlerTests.cs @@ -15,7 +15,7 @@ namespace Grand.Business.Checkout.Tests.Commands.Handlers.Shipping; public class ShipCommandHandlerTests { private Mock _mediatorMock; - private Mock _messageProviderServiceMock; + private Mock _orderMessageProviderServiceMock; private Mock _orderServiceMock; private ShipCommandHandler _shipCommandHandler; private Mock _shipmentServiceMock; @@ -26,10 +26,10 @@ public void Init() _mediatorMock = new Mock(); _orderServiceMock = new Mock(); _shipmentServiceMock = new Mock(); - _messageProviderServiceMock = new Mock(); + _orderMessageProviderServiceMock = new Mock(); _shipCommandHandler = new ShipCommandHandler(_mediatorMock.Object, _orderServiceMock.Object, - _shipmentServiceMock.Object, _messageProviderServiceMock.Object); + _shipmentServiceMock.Object, _orderMessageProviderServiceMock.Object); } [TestMethod] diff --git a/src/Tests/Grand.Business.Messages.Tests/Services/MessageProviderServiceTest.cs b/src/Tests/Grand.Business.Messages.Tests/Services/MessageProviderServiceTest.cs index 9b826f3720..a615ec51e0 100644 --- a/src/Tests/Grand.Business.Messages.Tests/Services/MessageProviderServiceTest.cs +++ b/src/Tests/Grand.Business.Messages.Tests/Services/MessageProviderServiceTest.cs @@ -7,11 +7,13 @@ using Grand.Business.Messages.DotLiquidDrops; using Grand.Business.Messages.Services; using Grand.Domain; +using Grand.Domain.Blogs; using Grand.Domain.Catalog; using Grand.Domain.Common; using Grand.Domain.Customers; using Grand.Domain.Localization; using Grand.Domain.Messages; +using Grand.Domain.Orders; using Grand.Domain.Stores; using Grand.Domain.Vendors; using Grand.Mediator; @@ -30,6 +32,13 @@ public class MessageProviderServiceTest private Mock _languageServiceMock; private Mock _mediatorMock; private MessageProviderService _messageService; + private CustomerMessageProviderService _customerMessageService; + private CatalogMessageProviderService _catalogMessageService; + private VendorMessageProviderService _vendorMessageService; + private OrderMessageProviderService _orderMessageService; + private NewsletterMessageProviderService _newsletterMessageService; + private MerchandiseReturnMessageProviderService _merchandiseReturnMessageService; + private CmsMessageProviderService _cmsMessageService; private Mock _messageTemplateServiceMock; private Mock _messageTokenProviderMock; private Product _product; @@ -70,37 +79,48 @@ public void Init() _emailAccountSettingsMock = new Mock(); _commonSetting = new Mock(); - _messageService = new MessageProviderService( + _messageService = new MessageProviderService(_queuedEmailServiceMock.Object); + var messageLookupService = new MessageLookupService( _messageTemplateServiceMock.Object, - _queuedEmailServiceMock.Object, _languageServiceMock.Object, _emailAccountServiceMock.Object, _storeServiceMock.Object, - _groupService.Object, - _mediatorMock.Object, - _emailAccountSettingsMock.Object, - _commonSetting.Object); + _emailAccountSettingsMock.Object); + _customerMessageService = new CustomerMessageProviderService(messageLookupService, _messageService, + _mediatorMock.Object, _commonSetting.Object); + _catalogMessageService = new CatalogMessageProviderService(messageLookupService, _messageService, + _mediatorMock.Object, _storeServiceMock.Object, _commonSetting.Object); + _vendorMessageService = new VendorMessageProviderService(messageLookupService, _messageService, + _mediatorMock.Object, _commonSetting.Object); + _orderMessageService = new OrderMessageProviderService(messageLookupService, _messageService, + _mediatorMock.Object, _storeServiceMock.Object); + _newsletterMessageService = new NewsletterMessageProviderService(messageLookupService, _messageService, + _mediatorMock.Object); + _merchandiseReturnMessageService = new MerchandiseReturnMessageProviderService(messageLookupService, + _messageService, _mediatorMock.Object); + _cmsMessageService = new CmsMessageProviderService(messageLookupService, _messageService, + _mediatorMock.Object, _groupService.Object); _product = new Product(); } [TestMethod] public async Task CallInsertQueuedEmailMethod() { - var result = await _messageService.SendOutBidCustomerMessage(new Product(), "123", new Bid()); + var result = await _catalogMessageService.SendOutBidCustomerMessage(new Product(), "123", new Bid()); _queuedEmailServiceMock.Verify(x => x.InsertQueuedEmail(It.IsAny()), Times.Once()); } [TestMethod] public async Task SendOutBidCustomerNotificationMethodReturnCorrectResult() { - var result = await _messageService.SendOutBidCustomerMessage(new Product(), "123", new Bid()); + var result = await _catalogMessageService.SendOutBidCustomerMessage(new Product(), "123", new Bid()); Assert.AreEqual(1, result); } [TestMethod] public async Task SendNewVendorAccountApplyStoreOwnerNotificationRetunsCorrectResult() { - var result = await _messageService.SendNewVendorAccountApplyStoreOwnerMessage(new Customer(), new Vendor(), + var result = await _vendorMessageService.SendNewVendorAccountApplyStoreOwnerMessage(new Customer(), new Vendor(), new Store { Url = "https://localhost:44350/" }, "123"); Assert.AreEqual(1, result); } @@ -118,6 +138,44 @@ public async Task SendNotificationMethodToEmailIsNull() Assert.AreEqual(0, result); } + [TestMethod] + public async Task SendOrderPaidStoreOwnerMessage_QueuesEmail() + { + var result = await _orderMessageService.SendOrderPaidStoreOwnerMessage(new Order(), new Customer(), "123"); + + Assert.AreEqual(1, result); + _queuedEmailServiceMock.Verify(x => x.InsertQueuedEmail(It.IsAny()), Times.Once()); + } + + [TestMethod] + public async Task SendNewsLetterSubscriptionActivationMessage_QueuesEmail() + { + var result = await _newsletterMessageService.SendNewsLetterSubscriptionActivationMessage( + new NewsLetterSubscription { Email = "a@b.c" }, "123"); + + Assert.AreEqual(1, result); + _queuedEmailServiceMock.Verify(x => x.InsertQueuedEmail(It.IsAny()), Times.Once()); + } + + [TestMethod] + public async Task SendNewMerchandiseReturnStoreOwnerMessage_QueuesEmail() + { + var result = await _merchandiseReturnMessageService.SendNewMerchandiseReturnStoreOwnerMessage( + new MerchandiseReturn(), new Order(), "123"); + + Assert.AreEqual(1, result); + _queuedEmailServiceMock.Verify(x => x.InsertQueuedEmail(It.IsAny()), Times.Once()); + } + + [TestMethod] + public async Task SendBlogCommentMessage_QueuesEmail() + { + var result = await _cmsMessageService.SendBlogCommentMessage(new BlogPost(), new BlogComment(), "123"); + + Assert.AreEqual(1, result); + _queuedEmailServiceMock.Verify(x => x.InsertQueuedEmail(It.IsAny()), Times.Once()); + } + [TestMethod] public async Task SendCustomerMessage_LinksPointToThePrimaryDomainOfTheStore() { @@ -149,7 +207,7 @@ private async Task PasswordRecoveryUrl(Store store) .Callback, CancellationToken>((e, _) => liquidCustomer = (LiquidCustomer)e.LiquidDrop); - await _messageService.SendCustomerPasswordRecoveryMessage(new Customer { Email = "a@b.c" }, store, ""); + await _customerMessageService.SendCustomerPasswordRecoveryMessage(new Customer { Email = "a@b.c" }, store, ""); Assert.IsNotNull(liquidCustomer); return liquidCustomer.PasswordRecoveryURL; diff --git a/src/Tests/Grand.Modules.Tests/Commands/SendOutBidCustomerNotificationCommandHandlerTests.cs b/src/Tests/Grand.Modules.Tests/Commands/SendOutBidCustomerNotificationCommandHandlerTests.cs index cf8d07f5b8..725b7b8bf0 100644 --- a/src/Tests/Grand.Modules.Tests/Commands/SendOutBidCustomerNotificationCommandHandlerTests.cs +++ b/src/Tests/Grand.Modules.Tests/Commands/SendOutBidCustomerNotificationCommandHandlerTests.cs @@ -11,12 +11,12 @@ namespace Grand.Modules.Tests.Commands; public class SendOutBidCustomerNotificationCommandHandlerTests { private SendOutBidCustomerNotificationCommandHandler _handler; - private Mock _messageProviderMock; + private Mock _messageProviderMock; [TestInitialize] public void Init() { - _messageProviderMock = new Mock(); + _messageProviderMock = new Mock(); _handler = new SendOutBidCustomerNotificationCommandHandler(_messageProviderMock.Object); } diff --git a/src/Tests/Grand.Modules.Tests/Commands/SendQuantityBelowStoreOwnerNotificationCommandHandlerTests.cs b/src/Tests/Grand.Modules.Tests/Commands/SendQuantityBelowStoreOwnerNotificationCommandHandlerTests.cs index d611621672..b80368cca5 100644 --- a/src/Tests/Grand.Modules.Tests/Commands/SendQuantityBelowStoreOwnerNotificationCommandHandlerTests.cs +++ b/src/Tests/Grand.Modules.Tests/Commands/SendQuantityBelowStoreOwnerNotificationCommandHandlerTests.cs @@ -12,13 +12,13 @@ namespace Grand.Modules.Tests.Commands; public class SendQuantityBelowStoreOwnerNotificationCommandHandlerTests { private SendQuantityBelowStoreOwnerNotificationCommandHandler _handler; - private Mock _messageProviderMock; + private Mock _messageProviderMock; private LanguageSettings _settings; [TestInitialize] public void Init() { - _messageProviderMock = new Mock(); + _messageProviderMock = new Mock(); _settings = new LanguageSettings(); _handler = new SendQuantityBelowStoreOwnerNotificationCommandHandler(_messageProviderMock.Object, _settings); } diff --git a/src/Tests/Grand.Modules.Tests/Services/BackgroundService/EndAuctionsTaskTests.cs b/src/Tests/Grand.Modules.Tests/Services/BackgroundService/EndAuctionsTaskTests.cs index 76fb460d6f..2e41403097 100644 --- a/src/Tests/Grand.Modules.Tests/Services/BackgroundService/EndAuctionsTaskTests.cs +++ b/src/Tests/Grand.Modules.Tests/Services/BackgroundService/EndAuctionsTaskTests.cs @@ -24,7 +24,7 @@ public class EndAuctionsTaskTests private Mock _customerServiceMock; private Mock _languageServiceMock; private Mock> _loggerMock; - private Mock _messageProviderMock; + private Mock _messageProviderMock; private LanguageSettings _settings; private Mock _shoppingCartMock; private EndAuctionsTask _task; @@ -33,7 +33,7 @@ public class EndAuctionsTaskTests public void Init() { _auctionMock = new Mock(); - _messageProviderMock = new Mock(); + _messageProviderMock = new Mock(); _shoppingCartMock = new Mock(); _customerServiceMock = new Mock(); _loggerMock = new Mock>(); diff --git a/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseCustomerControllerTests.cs b/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseCustomerControllerTests.cs index 29fcf6fce4..9cc8e03cbe 100644 --- a/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseCustomerControllerTests.cs +++ b/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseCustomerControllerTests.cs @@ -41,7 +41,7 @@ protected class TestCustomerController( ICustomerAttributeService customerAttributeService, IAddressAttributeParser addressAttributeParser, IAddressAttributeService addressAttributeService, - Grand.Business.Core.Interfaces.Messages.IMessageProviderService messageProviderService, + Grand.Business.Core.Interfaces.Messages.ICustomerMessageProviderService customerMessageProviderService, IGroupService groupService, IRequestTranslationService translationService, IContextAccessor contextAccessor, @@ -50,7 +50,7 @@ protected class TestCustomerController( : BaseCustomerController(customerService, customerViewModelService, customerManagerService, customerProductService, productReviewService, productReviewViewModelService, productViewModelService, customerAttributeParser, customerAttributeService, - addressAttributeParser, addressAttributeService, messageProviderService, groupService, + addressAttributeParser, addressAttributeService, customerMessageProviderService, groupService, translationService, contextAccessor, customerSettings, scope) { public Task<(Customer, IActionResult)> LoadAuthorizedCustomerPublic(string id) => LoadAuthorizedCustomer(id); @@ -98,7 +98,7 @@ public void Setup() new Mock().Object, new Mock().Object, AddressAttributeServiceMock.Object, - new Mock().Object, + new Mock().Object, GroupServiceMock.Object, TranslationServiceMock.Object, contextAccessorMock.Object, @@ -361,7 +361,7 @@ public async Task Delete_SelfDelete_ErrorsAndRedirectsWithoutDeleting() new Mock().Object, new Mock().Object, new Mock().Object, new Mock().Object, new Mock().Object, - new Mock().Object, + new Mock().Object, GroupServiceMock.Object, TranslationServiceMock.Object, contextAccessorMock.Object, new CustomerSettings(), ScopeMock.Object) { diff --git a/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseCustomerManagementControllerTests.cs b/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseCustomerManagementControllerTests.cs index 7fd6b31677..fbf4369e4f 100644 --- a/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseCustomerManagementControllerTests.cs +++ b/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseCustomerManagementControllerTests.cs @@ -33,14 +33,14 @@ private class TestManagementController( IProductReviewService productReviewService, IProductReviewViewModelService productReviewViewModelService, IProductViewModelService productViewModelService, ICustomerAttributeParser customerAttributeParser, ICustomerAttributeService customerAttributeService, IAddressAttributeParser addressAttributeParser, - IAddressAttributeService addressAttributeService, IMessageProviderService messageProviderService, + IAddressAttributeService addressAttributeService, ICustomerMessageProviderService customerMessageProviderService, IGroupService groupService, IRequestTranslationService translationService, IContextAccessor contextAccessor, CustomerSettings customerSettings, IAdminDataScope scope, IRequestPermissionService permissionService, IExportManager exportManager) : BaseCustomerManagementController(customerService, customerViewModelService, customerManagerService, customerProductService, productReviewService, productReviewViewModelService, productViewModelService, customerAttributeParser, customerAttributeService, addressAttributeParser, addressAttributeService, - messageProviderService, groupService, translationService, contextAccessor, customerSettings, scope, + customerMessageProviderService, groupService, translationService, contextAccessor, customerSettings, scope, permissionService, exportManager) { public void CheckTwoFactorEnabledWarningPublic(Customer existingCustomer, CustomerModel model) => @@ -75,7 +75,7 @@ public void Setup() new Mock().Object, new Mock().Object, new Mock().Object, new Mock().Object, new Mock().Object, new Mock().Object, - new Mock().Object, _groupServiceMock.Object, translationServiceMock.Object, + new Mock().Object, _groupServiceMock.Object, translationServiceMock.Object, _contextAccessorMock.Object, new CustomerSettings(), scopeMock.Object, _permissionServiceMock.Object, new Mock>().Object); diff --git a/src/Tests/Grand.Web.Admin.Tests/Controllers/LoginControllerExternalSignInTests.cs b/src/Tests/Grand.Web.Admin.Tests/Controllers/LoginControllerExternalSignInTests.cs index aff51054d2..49cf1272c9 100644 --- a/src/Tests/Grand.Web.Admin.Tests/Controllers/LoginControllerExternalSignInTests.cs +++ b/src/Tests/Grand.Web.Admin.Tests/Controllers/LoginControllerExternalSignInTests.cs @@ -35,7 +35,7 @@ public void Setup() _controller = new LoginController(new CustomerSettings(), new CaptchaSettings(), new Mock().Object, new Mock().Object, new Mock().Object, new Mock().Object, - new Mock().Object, new Mock { DefaultValue = DefaultValue.Mock }.Object, + new Mock().Object, new Mock { DefaultValue = DefaultValue.Mock }.Object, new Mock().Object); var httpContext = new DefaultHttpContext(); httpContext.Request.PathBase = "/shop"; diff --git a/src/Tests/Grand.Web.Admin.Tests/Controllers/LoginControllerProviderOnlyTests.cs b/src/Tests/Grand.Web.Admin.Tests/Controllers/LoginControllerProviderOnlyTests.cs index f46550f2cb..ec7ee28f8d 100644 --- a/src/Tests/Grand.Web.Admin.Tests/Controllers/LoginControllerProviderOnlyTests.cs +++ b/src/Tests/Grand.Web.Admin.Tests/Controllers/LoginControllerProviderOnlyTests.cs @@ -45,7 +45,7 @@ private LoginController CreateController(string area) var controller = new LoginController(new CustomerSettings(), new CaptchaSettings(), new Mock().Object, _customerManagerService.Object, new Mock().Object, new Mock().Object, - new Mock().Object, _contextAccessor.Object, + new Mock().Object, _contextAccessor.Object, new Mock().Object); var httpContext = new DefaultHttpContext(); httpContext.Request.PathBase = "/shop"; diff --git a/src/Tests/Grand.Web.Admin.Tests/Controllers/MerchandiseReturnViewModelServiceTests.cs b/src/Tests/Grand.Web.Admin.Tests/Controllers/MerchandiseReturnViewModelServiceTests.cs index f1c2bbfb1b..de2dd7690d 100644 --- a/src/Tests/Grand.Web.Admin.Tests/Controllers/MerchandiseReturnViewModelServiceTests.cs +++ b/src/Tests/Grand.Web.Admin.Tests/Controllers/MerchandiseReturnViewModelServiceTests.cs @@ -122,7 +122,7 @@ private static MerchandiseReturnViewModelService Build(ICustomerService customer var dateTimeServiceMock = new Mock(); dateTimeServiceMock.Setup(d => d.CurrentTimeZone).Returns(TimeZoneInfo.Utc); var translationServiceMock = new Mock(); - var messageProviderServiceMock = new Mock(); + var merchandiseReturnMessageProviderServiceMock = new Mock(); var languageSettings = new LanguageSettings(); var merchandiseReturnServiceMock = new Mock(); merchandiseReturnServiceMock @@ -149,7 +149,7 @@ private static MerchandiseReturnViewModelService Build(ICustomerService customer customerService, dateTimeServiceMock.Object, translationServiceMock.Object, - messageProviderServiceMock.Object, + merchandiseReturnMessageProviderServiceMock.Object, languageSettings, merchandiseReturnServiceMock.Object, priceFormatterMock.Object, @@ -176,7 +176,7 @@ private static MerchandiseReturnViewModelService BuildWithCustomMocks( var dateTimeServiceMock = new Mock(); dateTimeServiceMock.Setup(d => d.CurrentTimeZone).Returns(TimeZoneInfo.Utc); var translationServiceMock = new Mock(); - var messageProviderServiceMock = new Mock(); + var merchandiseReturnMessageProviderServiceMock = new Mock(); var languageSettings = new LanguageSettings(); var priceFormatterMock = new Mock(); var addressSettings = new AddressSettings(); @@ -194,7 +194,7 @@ private static MerchandiseReturnViewModelService BuildWithCustomMocks( customerServiceMock.Object, dateTimeServiceMock.Object, translationServiceMock.Object, - messageProviderServiceMock.Object, + merchandiseReturnMessageProviderServiceMock.Object, languageSettings, merchandiseReturnService, priceFormatterMock.Object, diff --git a/src/Tests/Grand.Web.Admin.Tests/Services/PanelPasswordRecoveryServiceTests.cs b/src/Tests/Grand.Web.Admin.Tests/Services/PanelPasswordRecoveryServiceTests.cs index 881588f89f..24c7554771 100644 --- a/src/Tests/Grand.Web.Admin.Tests/Services/PanelPasswordRecoveryServiceTests.cs +++ b/src/Tests/Grand.Web.Admin.Tests/Services/PanelPasswordRecoveryServiceTests.cs @@ -31,7 +31,7 @@ public class PanelPasswordRecoveryServiceTests private Mock _groupService; private Mock _permissionService; private Mock _vendorService; - private Mock _messageProviderService; + private Mock _customerMessageProviderService; private Mock _signInPolicy; private CustomerSettings _customerSettings; private Customer _customer; @@ -49,7 +49,7 @@ public void Init() _groupService.Setup(x => x.IsRegistered(_customer)).ReturnsAsync(true); _permissionService = new Mock(); _vendorService = new Mock(); - _messageProviderService = new Mock(); + _customerMessageProviderService = new Mock(); _signInPolicy = new Mock(); _signInPolicy.Setup(x => x.PasswordSignIn(It.IsAny(), It.IsAny())) .ReturnsAsync(PasswordSignInDecision.Password); @@ -69,7 +69,7 @@ public void Init() _service = new PanelPasswordRecoveryService(_customerService.Object, _customerManagerService.Object, _historyService.Object, _groupService.Object, _permissionService.Object, _vendorService.Object, - _messageProviderService.Object, translationService.Object, contextAccessor.Object, _customerSettings, + _customerMessageProviderService.Object, translationService.Object, contextAccessor.Object, _customerSettings, _signInPolicy.Object); } @@ -108,7 +108,7 @@ private void GiveToken(DateTime generatedUtc) private void VerifyMessageSent(Times times) { - _messageProviderService.Verify(x => x.SendCustomerPasswordRecoveryMessage( + _customerMessageProviderService.Verify(x => x.SendCustomerPasswordRecoveryMessage( It.IsAny(), It.IsAny(), It.IsAny()), times); } @@ -123,7 +123,7 @@ public async Task SendRecoveryMessage_AdminOfAdminPanel_StoresTokenAndArea_Sends It.Is(t => t.Length == 36), ""), Times.Once); _customerService.Verify(x => x.UpdateUserField(_customer, SystemCustomerFieldNames.PasswordRecoveryArea, "admin", ""), Times.Once); - _messageProviderService.Verify(x => x.SendCustomerPasswordRecoveryMessage(_customer, + _customerMessageProviderService.Verify(x => x.SendCustomerPasswordRecoveryMessage(_customer, It.Is(s => s.Id == "store"), "lang"), Times.Once); } diff --git a/src/Tests/Grand.Web.Store.Tests/Controllers/CustomerControllerTests.cs b/src/Tests/Grand.Web.Store.Tests/Controllers/CustomerControllerTests.cs index c6689bd14e..0a526f29a3 100644 --- a/src/Tests/Grand.Web.Store.Tests/Controllers/CustomerControllerTests.cs +++ b/src/Tests/Grand.Web.Store.Tests/Controllers/CustomerControllerTests.cs @@ -79,7 +79,7 @@ private class TestableCustomerController( ICustomerAttributeService customerAttributeService, IAddressAttributeParser addressAttributeParser, IAddressAttributeService addressAttributeService, - IMessageProviderService messageProviderService, + ICustomerMessageProviderService customerMessageProviderService, IGroupService groupService, IRequestTranslationService translationService, IContextAccessor contextAccessor, @@ -89,7 +89,7 @@ private class TestableCustomerController( : CustomerController(customerService, customerViewModelService, customerManagerService, customerProductService, productReviewService, productReviewViewModelService, productViewModelService, customerAttributeParser, customerAttributeService, addressAttributeParser, addressAttributeService, - messageProviderService, groupService, translationService, contextAccessor, customerSettings, scope, + customerMessageProviderService, groupService, translationService, contextAccessor, customerSettings, scope, customerConfig) { public Task ApplyPostConstraintsPublic(CustomerModel model) => ApplyPostConstraints(model); @@ -110,7 +110,7 @@ private TestableCustomerController BuildStoreController(bool perStoreEnabled = t _customerAttributeServiceMock.Object, new Mock().Object, new Mock().Object, - new Mock().Object, + new Mock().Object, groupService ?? _groupServiceMock.Object, _translationServiceMock.Object, _contextAccessorMock.Object, diff --git a/src/Tests/Grand.Web.Tests/Commands/Customers/PasswordRecoverySendCommandHandlerTests.cs b/src/Tests/Grand.Web.Tests/Commands/Customers/PasswordRecoverySendCommandHandlerTests.cs index a8c721e882..5e6cb12093 100644 --- a/src/Tests/Grand.Web.Tests/Commands/Customers/PasswordRecoverySendCommandHandlerTests.cs +++ b/src/Tests/Grand.Web.Tests/Commands/Customers/PasswordRecoverySendCommandHandlerTests.cs @@ -18,7 +18,7 @@ public class PasswordRecoverySendCommandHandlerTests public async Task ExternalSignInRequired_NoMessage_NoToken() { var customerService = new Mock(); - var messages = new Mock(); + var messages = new Mock(); var policy = new Mock(); var customer = new Customer { Id = "c1" }; policy.Setup(x => x.PasswordSignIn(customer, It.IsAny())).ReturnsAsync(new PasswordSignInDecision(false, new Mock().Object)); @@ -35,7 +35,7 @@ public async Task ExternalSignInRequired_NoMessage_NoToken() [TestMethod] public async Task NullCustomer_NoMessage() { - var messages = new Mock(); + var messages = new Mock(); var handler = new PasswordRecoverySendCommandHandler(new Mock().Object, messages.Object, new Mock().Object); @@ -49,7 +49,7 @@ public async Task NullCustomer_NoMessage() [TestMethod] public async Task PasswordAllowed_MessageSent() { - var messages = new Mock(); + var messages = new Mock(); var policy = new Mock(); policy.Setup(x => x.PasswordSignIn(It.IsAny(), SignInSurface.Storefront)) .ReturnsAsync(PasswordSignInDecision.Password); diff --git a/src/Tests/Grand.Web.Tests/Controllers/AccountControllerExternalRegisterTests.cs b/src/Tests/Grand.Web.Tests/Controllers/AccountControllerExternalRegisterTests.cs index 7f9d5d0f0e..c1e895d28a 100644 --- a/src/Tests/Grand.Web.Tests/Controllers/AccountControllerExternalRegisterTests.cs +++ b/src/Tests/Grand.Web.Tests/Controllers/AccountControllerExternalRegisterTests.cs @@ -32,7 +32,7 @@ public class AccountControllerExternalRegisterTests new("ExternalAuth.OpenIdConnect", "sub-1", "john@example.com", true, "John", "Doe"); private Mock _manager; - private Mock _messages; + private Mock _messages; private Mock _external; private Mock _externalAuth; private CustomerSettings _settings; @@ -43,7 +43,7 @@ public class AccountControllerExternalRegisterTests public void Init() { _manager = new Mock(); - _messages = new Mock(); + _messages = new Mock(); _external = new Mock(); _externalAuth = new Mock(); _settings = new CustomerSettings { UserRegistrationType = UserRegistrationType.Standard }; diff --git a/src/Tests/Grand.Web.Tests/Controllers/AccountControllerProviderOnlyTests.cs b/src/Tests/Grand.Web.Tests/Controllers/AccountControllerProviderOnlyTests.cs index 45fe8eff2f..355552f57f 100644 --- a/src/Tests/Grand.Web.Tests/Controllers/AccountControllerProviderOnlyTests.cs +++ b/src/Tests/Grand.Web.Tests/Controllers/AccountControllerProviderOnlyTests.cs @@ -73,7 +73,7 @@ public void Init() _controller = new AccountController(new Mock().Object, new Mock().Object, context.Object, _customers.Object, _groups.Object, _customerManager.Object, new Mock().Object, - mediator.Object, new Mock().Object, new CaptchaSettings(), + mediator.Object, new Mock().Object, new CaptchaSettings(), _customerSettings = new CustomerSettings { UserRegistrationType = UserRegistrationType.Standard, TwoFactorAuthenticationEnabled = true, AllowUsersToDeleteAccount = true diff --git a/src/Tests/Grand.Web.Tests/Controllers/AccountControllerTwoFactorReturnUrlTests.cs b/src/Tests/Grand.Web.Tests/Controllers/AccountControllerTwoFactorReturnUrlTests.cs index 4330022878..92d1e1a414 100644 --- a/src/Tests/Grand.Web.Tests/Controllers/AccountControllerTwoFactorReturnUrlTests.cs +++ b/src/Tests/Grand.Web.Tests/Controllers/AccountControllerTwoFactorReturnUrlTests.cs @@ -55,7 +55,7 @@ public void Init() var settings = new CustomerSettings { TwoFactorAuthenticationEnabled = true }; _controller = new AccountController(new Mock().Object, new Mock().Object, context.Object, _customers.Object, new Mock().Object, _manager.Object, new Mock().Object, - new Mock().Object, new Mock().Object, new CaptchaSettings(), settings, + new Mock().Object, new Mock().Object, new CaptchaSettings(), settings, new CustomerConfig()); var http = new DefaultHttpContext { Session = new TestSession() }; _controller.ControllerContext = new ControllerContext { HttpContext = http }; diff --git a/src/Tests/Grand.Web.Tests/Controllers/ContentPreviewTests.cs b/src/Tests/Grand.Web.Tests/Controllers/ContentPreviewTests.cs index fc500e5a70..de010d645a 100644 --- a/src/Tests/Grand.Web.Tests/Controllers/ContentPreviewTests.cs +++ b/src/Tests/Grand.Web.Tests/Controllers/ContentPreviewTests.cs @@ -108,7 +108,7 @@ private KnowledgebaseController KnowledgebaseController(KnowledgebaseArticle art return new KnowledgebaseController(new KnowledgebaseSettings { Enabled = true }, knowledgebaseServiceMock.Object, _contextAccessorMock.Object, new Mock().Object, _aclServiceMock.Object, new Mock().Object, - new Mock().Object, new Mock().Object, + new Mock().Object, new Mock().Object, _permissionServiceMock.Object, new CustomerSettings(), new CaptchaSettings(), new LanguageSettings()) { ControllerContext = GetContext(new ServiceCollection() .AddSingleton(new Mock().Object) diff --git a/src/Web/Grand.Web.Admin/Controllers/CustomerController.cs b/src/Web/Grand.Web.Admin/Controllers/CustomerController.cs index 3a72c24f40..75b0c297bc 100644 --- a/src/Web/Grand.Web.Admin/Controllers/CustomerController.cs +++ b/src/Web/Grand.Web.Admin/Controllers/CustomerController.cs @@ -23,7 +23,7 @@ public class CustomerController( Grand.Business.Core.Interfaces.Customers.ICustomerAttributeService customerAttributeService, Grand.Business.Core.Interfaces.Common.Addresses.IAddressAttributeParser addressAttributeParser, Grand.Business.Core.Interfaces.Common.Addresses.IAddressAttributeService addressAttributeService, - Grand.Business.Core.Interfaces.Messages.IMessageProviderService messageProviderService, + Grand.Business.Core.Interfaces.Messages.ICustomerMessageProviderService customerMessageProviderService, Grand.Business.Core.Interfaces.Common.Directory.IGroupService groupService, Grand.Web.Common.Localization.IRequestTranslationService translationService, Grand.Infrastructure.IContextAccessor contextAccessor, @@ -34,5 +34,5 @@ public class CustomerController( : BaseCustomerManagementController(customerService, customerViewModelService, customerManagerService, customerProductService, productReviewService, productReviewViewModelService, productViewModelService, customerAttributeParser, customerAttributeService, addressAttributeParser, addressAttributeService, - messageProviderService, groupService, translationService, contextAccessor, customerSettings, scope, + customerMessageProviderService, groupService, translationService, contextAccessor, customerSettings, scope, permissionService, exportManager); diff --git a/src/Web/Grand.Web.Admin/Controllers/LoginController.cs b/src/Web/Grand.Web.Admin/Controllers/LoginController.cs index ed8a66edd1..6e84c936fa 100644 --- a/src/Web/Grand.Web.Admin/Controllers/LoginController.cs +++ b/src/Web/Grand.Web.Admin/Controllers/LoginController.cs @@ -15,7 +15,7 @@ namespace Grand.Web.Admin.Controllers; [Area(Constants.AreaAdmin)] public class LoginController : BaseLoginController { - public LoginController(CustomerSettings customerSettings, CaptchaSettings captchaSettings, IRequestTranslationService translationService, ICustomerManagerService customerManagerService, ICustomerService customerService, IGrandAuthenticationService authenticationService, IMessageProviderService messageProviderService, IContextAccessor contextAccessor, IMediator mediator) : base(customerSettings, captchaSettings, translationService, customerManagerService, customerService, authenticationService, messageProviderService, contextAccessor, mediator) + public LoginController(CustomerSettings customerSettings, CaptchaSettings captchaSettings, IRequestTranslationService translationService, ICustomerManagerService customerManagerService, ICustomerService customerService, IGrandAuthenticationService authenticationService, ICustomerMessageProviderService customerMessageProviderService, IContextAccessor contextAccessor, IMediator mediator) : base(customerSettings, captchaSettings, translationService, customerManagerService, customerService, authenticationService, customerMessageProviderService, contextAccessor, mediator) { } } diff --git a/src/Web/Grand.Web.AdminShared/Controllers/BaseCustomerController.cs b/src/Web/Grand.Web.AdminShared/Controllers/BaseCustomerController.cs index e805f309ef..b4b88cb752 100644 --- a/src/Web/Grand.Web.AdminShared/Controllers/BaseCustomerController.cs +++ b/src/Web/Grand.Web.AdminShared/Controllers/BaseCustomerController.cs @@ -41,7 +41,7 @@ public abstract class BaseCustomerController( ICustomerAttributeService customerAttributeService, IAddressAttributeParser addressAttributeParser, IAddressAttributeService addressAttributeService, - IMessageProviderService messageProviderService, + ICustomerMessageProviderService customerMessageProviderService, IGroupService groupService, IRequestTranslationService translationService, IContextAccessor contextAccessor, @@ -62,7 +62,7 @@ public abstract class BaseCustomerController( protected ICustomerAttributeService CustomerAttributeService => customerAttributeService; protected IAddressAttributeParser AddressAttributeParser => addressAttributeParser; protected IAddressAttributeService AddressAttributeService => addressAttributeService; - protected IMessageProviderService MessageProviderService => messageProviderService; + protected ICustomerMessageProviderService CustomerMessageProviderService => customerMessageProviderService; protected IGroupService GroupService => groupService; protected IRequestTranslationService TranslationService => translationService; protected IContextAccessor ContextAccessor => contextAccessor; @@ -367,7 +367,7 @@ public async Task SendWelcomeMessage(string id) var (customer, denied) = await LoadAuthorizedCustomer(id); if (customer is null) return denied!; - await messageProviderService.SendCustomerWelcomeMessage(customer, contextAccessor.StoreContext.CurrentStore, + await customerMessageProviderService.SendCustomerWelcomeMessage(customer, contextAccessor.StoreContext.CurrentStore, contextAccessor.WorkContext.WorkingLanguage.Id); Success(translationService.GetResource("Admin.Customers.Customers.SendWelcomeMessage.Success")); return RedirectToAction("Edit", new { id = customer.Id }); @@ -382,7 +382,7 @@ public async Task ReSendActivationMessage(string id) await customerService.UpdateUserField(customer, SystemCustomerFieldNames.AccountActivationToken, Guid.NewGuid().ToString()); - await messageProviderService.SendCustomerEmailValidationMessage(customer, + await customerMessageProviderService.SendCustomerEmailValidationMessage(customer, contextAccessor.StoreContext.CurrentStore, contextAccessor.WorkContext.WorkingLanguage.Id); Success(translationService.GetResource("Admin.Customers.Customers.ReSendActivationMessage.Success")); return RedirectToAction("Edit", new { id = customer.Id }); diff --git a/src/Web/Grand.Web.AdminShared/Controllers/BaseCustomerManagementController.cs b/src/Web/Grand.Web.AdminShared/Controllers/BaseCustomerManagementController.cs index 0a923e3a8b..5e246ec8f7 100644 --- a/src/Web/Grand.Web.AdminShared/Controllers/BaseCustomerManagementController.cs +++ b/src/Web/Grand.Web.AdminShared/Controllers/BaseCustomerManagementController.cs @@ -32,7 +32,7 @@ public abstract class BaseCustomerManagementController( ICustomerAttributeService customerAttributeService, IAddressAttributeParser addressAttributeParser, IAddressAttributeService addressAttributeService, - IMessageProviderService messageProviderService, + ICustomerMessageProviderService customerMessageProviderService, IGroupService groupService, IRequestTranslationService translationService, IContextAccessor contextAccessor, @@ -43,7 +43,7 @@ public abstract class BaseCustomerManagementController( : BaseCustomerController(customerService, customerViewModelService, customerManagerService, customerProductService, productReviewService, productReviewViewModelService, productViewModelService, customerAttributeParser, customerAttributeService, addressAttributeParser, addressAttributeService, - messageProviderService, groupService, translationService, contextAccessor, customerSettings, scope) + customerMessageProviderService, groupService, translationService, contextAccessor, customerSettings, scope) { protected override void CheckTwoFactorEnabledWarning(Customer? existingCustomer, CustomerModel model) { diff --git a/src/Web/Grand.Web.AdminShared/Controllers/BaseLoginController.cs b/src/Web/Grand.Web.AdminShared/Controllers/BaseLoginController.cs index 21333964de..6908e35070 100644 --- a/src/Web/Grand.Web.AdminShared/Controllers/BaseLoginController.cs +++ b/src/Web/Grand.Web.AdminShared/Controllers/BaseLoginController.cs @@ -27,7 +27,7 @@ public abstract class BaseLoginController : BaseController private readonly ICustomerService _customerService; private readonly CustomerSettings _customerSettings; private readonly IMediator _mediator; - private readonly IMessageProviderService _messageProviderService; + private readonly ICustomerMessageProviderService _customerMessageProviderService; private readonly IRequestTranslationService _translationService; private readonly IContextAccessor _contextAccessor; @@ -38,7 +38,7 @@ public BaseLoginController( ICustomerManagerService customerManagerService, ICustomerService customerService, IGrandAuthenticationService authenticationService, - IMessageProviderService messageProviderService, + ICustomerMessageProviderService customerMessageProviderService, IContextAccessor contextAccessor, IMediator mediator) { @@ -48,7 +48,7 @@ public BaseLoginController( _customerManagerService = customerManagerService; _customerService = customerService; _authenticationService = authenticationService; - _messageProviderService = messageProviderService; + _customerMessageProviderService = customerMessageProviderService; _contextAccessor = contextAccessor; _mediator = mediator; } @@ -254,7 +254,7 @@ await twoFactorAuthenticationService.GenerateCodeSetup("", customer, _contextAccessor.StoreContext.CurrentStore, _contextAccessor.WorkContext.WorkingLanguage, _customerSettings.TwoFactorAuthenticationType); if (_customerSettings.TwoFactorAuthenticationType == TwoFactorAuthenticationType.EmailVerification) - await _messageProviderService.SendCustomerEmailTokenValidationMessage(customer, + await _customerMessageProviderService.SendCustomerEmailTokenValidationMessage(customer, _contextAccessor.StoreContext.CurrentStore, _contextAccessor.WorkContext.WorkingLanguage.Id); } diff --git a/src/Web/Grand.Web.AdminShared/Services/CustomerViewModelService.cs b/src/Web/Grand.Web.AdminShared/Services/CustomerViewModelService.cs index ec5ec8d565..37f34542f8 100644 --- a/src/Web/Grand.Web.AdminShared/Services/CustomerViewModelService.cs +++ b/src/Web/Grand.Web.AdminShared/Services/CustomerViewModelService.cs @@ -1161,9 +1161,9 @@ public virtual async Task InsertCustomerNote(string customerId, st if (displayToCustomer) { //email - var messageProviderService = _httpContextAccessor.HttpContext!.RequestServices - .GetRequiredService(); - await messageProviderService.SendNewCustomerNoteMessage(customerNote, + var customerMessageProviderService = _httpContextAccessor.HttpContext!.RequestServices + .GetRequiredService(); + await customerMessageProviderService.SendNewCustomerNoteMessage(customerNote, await _customerService.GetCustomerById(customerId), _contextAccessor.StoreContext.CurrentStore, _contextAccessor.WorkContext.WorkingLanguage.Id); } diff --git a/src/Web/Grand.Web.AdminShared/Services/GiftVoucherViewModelService.cs b/src/Web/Grand.Web.AdminShared/Services/GiftVoucherViewModelService.cs index 0112652f59..4722cca0c6 100644 --- a/src/Web/Grand.Web.AdminShared/Services/GiftVoucherViewModelService.cs +++ b/src/Web/Grand.Web.AdminShared/Services/GiftVoucherViewModelService.cs @@ -22,7 +22,7 @@ public class GiftVoucherViewModelService : IGiftVoucherViewModelService public GiftVoucherViewModelService( IGiftVoucherService giftVoucherService, IOrderService orderService, - IPriceFormatter priceFormatter, IMessageProviderService messageProviderService, + IPriceFormatter priceFormatter, IOrderMessageProviderService orderMessageProviderService, IDateTimeService dateTimeService, ICurrencyService currencyService, LanguageSettings languageSettings, @@ -33,7 +33,7 @@ public GiftVoucherViewModelService( _giftVoucherService = giftVoucherService; _orderService = orderService; _priceFormatter = priceFormatter; - _messageProviderService = messageProviderService; + _orderMessageProviderService = orderMessageProviderService; _dateTimeService = dateTimeService; _currencyService = currencyService; _languageSettings = languageSettings; @@ -163,7 +163,7 @@ public virtual async Task NotifyRecipient(GiftVoucher giftVoucher) languageId = _languageSettings.DefaultAdminLanguageId; } - var queuedEmailId = await _messageProviderService.SendGiftVoucherMessage(giftVoucher, order, languageId); + var queuedEmailId = await _orderMessageProviderService.SendGiftVoucherMessage(giftVoucher, order, languageId); if (queuedEmailId > 0) { giftVoucher.IsRecipientNotified = true; @@ -232,7 +232,7 @@ public virtual async private readonly IGiftVoucherService _giftVoucherService; private readonly IOrderService _orderService; private readonly IPriceFormatter _priceFormatter; - private readonly IMessageProviderService _messageProviderService; + private readonly IOrderMessageProviderService _orderMessageProviderService; private readonly IDateTimeService _dateTimeService; private readonly ICurrencyService _currencyService; private readonly LanguageSettings _languageSettings; diff --git a/src/Web/Grand.Web.AdminShared/Services/MerchandiseReturnViewModelService.cs b/src/Web/Grand.Web.AdminShared/Services/MerchandiseReturnViewModelService.cs index 10c0477b34..251a122b3f 100644 --- a/src/Web/Grand.Web.AdminShared/Services/MerchandiseReturnViewModelService.cs +++ b/src/Web/Grand.Web.AdminShared/Services/MerchandiseReturnViewModelService.cs @@ -27,7 +27,7 @@ public class MerchandiseReturnViewModelService( ICustomerService customerService, IDateTimeService dateTimeService, IRequestTranslationService translationService, - IMessageProviderService messageProviderService, + IMerchandiseReturnMessageProviderService merchandiseReturnMessageProviderService, LanguageSettings languageSettings, IMerchandiseReturnService merchandiseReturnService, IPriceFormatter priceFormatter, @@ -197,7 +197,7 @@ public virtual async Task PrepareAddressModel(AddressModel model, public virtual async Task NotifyCustomer(MerchandiseReturn merchandiseReturn) { var order = await orderService.GetOrderById(merchandiseReturn.OrderId); - await messageProviderService.SendMerchandiseReturnStatusChangedCustomerMessage(merchandiseReturn, order, + await merchandiseReturnMessageProviderService.SendMerchandiseReturnStatusChangedCustomerMessage(merchandiseReturn, order, languageSettings.DefaultAdminLanguageId); } @@ -309,7 +309,7 @@ public virtual async Task InsertMerchandiseReturnNote(MerchandiseReturn merchand //new merchandise return notification if (displayToCustomer) //email - await messageProviderService.SendNewMerchandiseReturnNoteAddedCustomerMessage(merchandiseReturn, + await merchandiseReturnMessageProviderService.SendNewMerchandiseReturnNoteAddedCustomerMessage(merchandiseReturn, merchandiseReturnNote, order); } diff --git a/src/Web/Grand.Web.AdminShared/Services/OrderViewModelService.cs b/src/Web/Grand.Web.AdminShared/Services/OrderViewModelService.cs index dbe5749b78..4e42c1f2ab 100644 --- a/src/Web/Grand.Web.AdminShared/Services/OrderViewModelService.cs +++ b/src/Web/Grand.Web.AdminShared/Services/OrderViewModelService.cs @@ -56,7 +56,7 @@ public class OrderViewModelService : IOrderViewModelService private readonly IPaymentTransactionService _paymentTransactionService; private readonly ICountryService _countryService; private readonly IProductService _productService; - private readonly IMessageProviderService _messageProviderService; + private readonly IOrderMessageProviderService _orderMessageProviderService; private readonly IProductAttributeService _productAttributeService; private readonly IGiftVoucherService _giftVoucherService; private readonly IDownloadService _downloadService; @@ -99,7 +99,7 @@ public OrderViewModelService(IOrderService orderService, IPaymentTransactionService paymentTransactionService, ICountryService countryService, IProductService productService, - IMessageProviderService messageProviderService, + IOrderMessageProviderService orderMessageProviderService, IProductAttributeService productAttributeService, IGiftVoucherService giftVoucherService, IDownloadService downloadService, @@ -137,7 +137,7 @@ public OrderViewModelService(IOrderService orderService, _paymentTransactionService = paymentTransactionService; _countryService = countryService; _productService = productService; - _messageProviderService = messageProviderService; + _orderMessageProviderService = orderMessageProviderService; _productAttributeService = productAttributeService; _giftVoucherService = giftVoucherService; _downloadService = downloadService; @@ -1025,7 +1025,7 @@ public virtual async Task InsertOrderNote(Order order, string downloadId, bool d //new order notification if (displayToCustomer) //email - await _messageProviderService.SendNewOrderNoteAddedCustomerMessage(order, orderNote); + await _orderMessageProviderService.SendNewOrderNoteAddedCustomerMessage(order, orderNote); } public virtual async Task DeleteOrderNote(Order order, string id) diff --git a/src/Web/Grand.Web.AdminShared/Services/PanelPasswordRecoveryService.cs b/src/Web/Grand.Web.AdminShared/Services/PanelPasswordRecoveryService.cs index bddf0f5c1e..f3c432c12d 100644 --- a/src/Web/Grand.Web.AdminShared/Services/PanelPasswordRecoveryService.cs +++ b/src/Web/Grand.Web.AdminShared/Services/PanelPasswordRecoveryService.cs @@ -20,7 +20,7 @@ public class PanelPasswordRecoveryService( IGroupService groupService, IPermissionService permissionService, IVendorService vendorService, - IMessageProviderService messageProviderService, + ICustomerMessageProviderService customerMessageProviderService, IRequestTranslationService translationService, IContextAccessor contextAccessor, CustomerSettings customerSettings, @@ -44,7 +44,7 @@ await customerService.UpdateUserField(customer, SystemCustomerFieldNames.Passwor await customerService.UpdateUserField(customer, SystemCustomerFieldNames.PasswordRecoveryArea, area.ToLowerInvariant()); - await messageProviderService.SendCustomerPasswordRecoveryMessage(customer, + await customerMessageProviderService.SendCustomerPasswordRecoveryMessage(customer, contextAccessor.StoreContext.CurrentStore, contextAccessor.WorkContext.WorkingLanguage.Id); } diff --git a/src/Web/Grand.Web.Store/Controllers/CustomerController.cs b/src/Web/Grand.Web.Store/Controllers/CustomerController.cs index d9577350c8..991a528908 100644 --- a/src/Web/Grand.Web.Store/Controllers/CustomerController.cs +++ b/src/Web/Grand.Web.Store/Controllers/CustomerController.cs @@ -38,7 +38,7 @@ public class CustomerController( Grand.Business.Core.Interfaces.Customers.ICustomerAttributeService customerAttributeService, Grand.Business.Core.Interfaces.Common.Addresses.IAddressAttributeParser addressAttributeParser, Grand.Business.Core.Interfaces.Common.Addresses.IAddressAttributeService addressAttributeService, - Grand.Business.Core.Interfaces.Messages.IMessageProviderService messageProviderService, + Grand.Business.Core.Interfaces.Messages.ICustomerMessageProviderService customerMessageProviderService, IGroupService groupService, Grand.Web.Common.Localization.IRequestTranslationService translationService, IContextAccessor contextAccessor, @@ -48,7 +48,7 @@ public class CustomerController( : BaseCustomerController(customerService, customerViewModelService, customerManagerService, customerProductService, productReviewService, productReviewViewModelService, productViewModelService, customerAttributeParser, customerAttributeService, addressAttributeParser, addressAttributeService, - messageProviderService, groupService, translationService, contextAccessor, customerSettings, scope) + customerMessageProviderService, groupService, translationService, contextAccessor, customerSettings, scope) { //managing customers from the store panel only makes sense when customer identity is scoped per //store (Customer:RegisterCustomersPerStore). When it's off the whole controller is disabled and diff --git a/src/Web/Grand.Web.Store/Controllers/LoginController.cs b/src/Web/Grand.Web.Store/Controllers/LoginController.cs index 667be5ad1b..4cc7b136cd 100644 --- a/src/Web/Grand.Web.Store/Controllers/LoginController.cs +++ b/src/Web/Grand.Web.Store/Controllers/LoginController.cs @@ -15,7 +15,7 @@ namespace Grand.Web.Store.Controllers; [Area(Constants.AreaStore)] public class LoginController : BaseLoginController { - public LoginController(CustomerSettings customerSettings, CaptchaSettings captchaSettings, IRequestTranslationService translationService, ICustomerManagerService customerManagerService, ICustomerService customerService, IGrandAuthenticationService authenticationService, IMessageProviderService messageProviderService, IContextAccessor contextAccessor, IMediator mediator) : base(customerSettings, captchaSettings, translationService, customerManagerService, customerService, authenticationService, messageProviderService, contextAccessor, mediator) + public LoginController(CustomerSettings customerSettings, CaptchaSettings captchaSettings, IRequestTranslationService translationService, ICustomerManagerService customerManagerService, ICustomerService customerService, IGrandAuthenticationService authenticationService, ICustomerMessageProviderService customerMessageProviderService, IContextAccessor contextAccessor, IMediator mediator) : base(customerSettings, captchaSettings, translationService, customerManagerService, customerService, authenticationService, customerMessageProviderService, contextAccessor, mediator) { } } diff --git a/src/Web/Grand.Web.Vendor/Controllers/LoginController.cs b/src/Web/Grand.Web.Vendor/Controllers/LoginController.cs index 4aef6cbdee..18680d3c99 100644 --- a/src/Web/Grand.Web.Vendor/Controllers/LoginController.cs +++ b/src/Web/Grand.Web.Vendor/Controllers/LoginController.cs @@ -15,7 +15,7 @@ namespace Grand.Web.Vendor.Controllers; [Area(Constants.AreaVendor)] public class LoginController : BaseLoginController { - public LoginController(CustomerSettings customerSettings, CaptchaSettings captchaSettings, IRequestTranslationService translationService, ICustomerManagerService customerManagerService, ICustomerService customerService, IGrandAuthenticationService authenticationService, IMessageProviderService messageProviderService, IContextAccessor contextAccessor, IMediator mediator) : base(customerSettings, captchaSettings, translationService, customerManagerService, customerService, authenticationService, messageProviderService, contextAccessor, mediator) + public LoginController(CustomerSettings customerSettings, CaptchaSettings captchaSettings, IRequestTranslationService translationService, ICustomerManagerService customerManagerService, ICustomerService customerService, IGrandAuthenticationService authenticationService, ICustomerMessageProviderService customerMessageProviderService, IContextAccessor contextAccessor, IMediator mediator) : base(customerSettings, captchaSettings, translationService, customerManagerService, customerService, authenticationService, customerMessageProviderService, contextAccessor, mediator) { } } diff --git a/src/Web/Grand.Web/Commands/Handler/Blogs/InsertBlogCommentCommandHandler.cs b/src/Web/Grand.Web/Commands/Handler/Blogs/InsertBlogCommentCommandHandler.cs index b727efdaa8..55c409874e 100644 --- a/src/Web/Grand.Web/Commands/Handler/Blogs/InsertBlogCommentCommandHandler.cs +++ b/src/Web/Grand.Web/Commands/Handler/Blogs/InsertBlogCommentCommandHandler.cs @@ -16,17 +16,17 @@ public class InsertBlogCommentCommandHandler : IRequestHandler Handle(InsertBlogCommentCommand request, Cancella if (!customer.HasContributions) await _customerService.UpdateContributions(customer); //notify a store owner if (_blogSettings.NotifyAboutNewBlogComments) - await _messageProviderService.SendBlogCommentMessage(request.BlogPost, comment, + await _cmsMessageProviderService.SendBlogCommentMessage(request.BlogPost, comment, _languageSettings.DefaultAdminLanguageId, host: _contextAccessor.StoreContext.CurrentHost); return comment; diff --git a/src/Web/Grand.Web/Commands/Handler/Contact/ContactUsSendCommandHandler.cs b/src/Web/Grand.Web/Commands/Handler/Contact/ContactUsSendCommandHandler.cs index e92788fa0e..5d20544a33 100644 --- a/src/Web/Grand.Web/Commands/Handler/Contact/ContactUsSendCommandHandler.cs +++ b/src/Web/Grand.Web/Commands/Handler/Contact/ContactUsSendCommandHandler.cs @@ -15,18 +15,18 @@ public class ContactUsSendCommandHandler : IRequestHandler SendContactUs(ContactUsSendCommand request) var subject = _commonSettings.SubjectFieldOnContactUsForm ? request.Model.Subject : null; var body = FormatText.ConvertText(request.Model.Enquiry); - await _messageProviderService.SendContactUsMessage + await _customerMessageProviderService.SendContactUsMessage (request.Customer, request.Store, request.Language.Id, request.Model.Email.Trim(), request.Model.FullName, subject, body, request.Model.ContactAttributeInfo, request.Model.ContactAttribute, request.IpAddress, diff --git a/src/Web/Grand.Web/Commands/Handler/Customers/CustomerRegisteredCommandHandler.cs b/src/Web/Grand.Web/Commands/Handler/Customers/CustomerRegisteredCommandHandler.cs index f69a4aa29b..25da2864b2 100644 --- a/src/Web/Grand.Web/Commands/Handler/Customers/CustomerRegisteredCommandHandler.cs +++ b/src/Web/Grand.Web/Commands/Handler/Customers/CustomerRegisteredCommandHandler.cs @@ -23,14 +23,14 @@ public class CustomerRegisteredCommandHandler : IRequestHandler Handle(DeleteAccountCommand request, CancellationToken cancellationToken) { //send notification to customer - await _messageProviderService.SendCustomerDeleteStoreOwnerMessage(request.Customer, + await _customerMessageProviderService.SendCustomerDeleteStoreOwnerMessage(request.Customer, _languageSettings.DefaultAdminLanguageId, host: request.Host); //delete emails diff --git a/src/Web/Grand.Web/Commands/Handler/Customers/PasswordRecoverySendCommandHandler.cs b/src/Web/Grand.Web/Commands/Handler/Customers/PasswordRecoverySendCommandHandler.cs index 50a0be2f58..ef95bd26a2 100644 --- a/src/Web/Grand.Web/Commands/Handler/Customers/PasswordRecoverySendCommandHandler.cs +++ b/src/Web/Grand.Web/Commands/Handler/Customers/PasswordRecoverySendCommandHandler.cs @@ -9,18 +9,18 @@ namespace Grand.Web.Commands.Handler.Customers; public class PasswordRecoverySendCommandHandler : IRequestHandler { - private readonly IMessageProviderService _messageProviderService; + private readonly ICustomerMessageProviderService _customerMessageProviderService; private readonly ICustomerService _customerService; private readonly IExternalSignInPolicy _externalSignInPolicy; public PasswordRecoverySendCommandHandler( ICustomerService customerService, - IMessageProviderService messageProviderService, + ICustomerMessageProviderService customerMessageProviderService, IExternalSignInPolicy externalSignInPolicy) { _externalSignInPolicy = externalSignInPolicy; _customerService = customerService; - _messageProviderService = messageProviderService; + _customerMessageProviderService = customerMessageProviderService; } public async Task Handle(PasswordRecoverySendCommand request, CancellationToken cancellationToken) @@ -41,7 +41,7 @@ await _customerService.UpdateUserField(request.Customer, SystemCustomerF null); //send email - await _messageProviderService.SendCustomerPasswordRecoveryMessage(request.Customer, request.Store, + await _customerMessageProviderService.SendCustomerPasswordRecoveryMessage(request.Customer, request.Store, request.Language.Id, host: request.Host); return true; diff --git a/src/Web/Grand.Web/Commands/Handler/News/InsertNewsCommentCommandHandler.cs b/src/Web/Grand.Web/Commands/Handler/News/InsertNewsCommentCommandHandler.cs index 5337d3112a..053f0a2045 100644 --- a/src/Web/Grand.Web/Commands/Handler/News/InsertNewsCommentCommandHandler.cs +++ b/src/Web/Grand.Web/Commands/Handler/News/InsertNewsCommentCommandHandler.cs @@ -13,20 +13,20 @@ public class InsertNewsCommentCommandHandler : IRequestHandler Handle(InsertNewsCommentCommand request, Cancella //notify a store owner; if (_newsSettings.NotifyAboutNewNewsComments) - await _messageProviderService.SendNewsCommentMessage(request.NewsItem, comment, + await _cmsMessageProviderService.SendNewsCommentMessage(request.NewsItem, comment, _languageSettings.DefaultAdminLanguageId, host: _contextAccessor.StoreContext.CurrentHost); return comment; diff --git a/src/Web/Grand.Web/Commands/Handler/Newsletter/SubscribeNewsletterHandler.cs b/src/Web/Grand.Web/Commands/Handler/Newsletter/SubscribeNewsletterHandler.cs index f5926d57c4..a4950acc64 100644 --- a/src/Web/Grand.Web/Commands/Handler/Newsletter/SubscribeNewsletterHandler.cs +++ b/src/Web/Grand.Web/Commands/Handler/Newsletter/SubscribeNewsletterHandler.cs @@ -13,7 +13,7 @@ namespace Grand.Web.Commands.Handler.Newsletter; public class SubscribeNewsletterHandler : IRequestHandler { - private readonly IMessageProviderService _messageProviderService; + private readonly INewsletterMessageProviderService _newsletterMessageProviderService; private readonly INewsletterCategoryService _newsletterCategoryService; private readonly INewsLetterSubscriptionService _newsLetterSubscriptionService; private readonly IRequestTranslationService _translationService; @@ -22,12 +22,12 @@ public class SubscribeNewsletterHandler : IRequestHandler { - private readonly IMessageProviderService _messageProviderService; + private readonly IOrderMessageProviderService _orderMessageProviderService; private readonly IOrderService _orderService; - public InsertOrderNoteCommandHandler(IMessageProviderService messageProviderService, IOrderService orderService) + public InsertOrderNoteCommandHandler(IOrderMessageProviderService orderMessageProviderService, IOrderService orderService) { - _messageProviderService = messageProviderService; + _orderMessageProviderService = orderMessageProviderService; _orderService = orderService; } @@ -28,7 +28,7 @@ public async Task Handle(InsertOrderNoteCommand request, Cancellation await _orderService.InsertOrderNote(orderNote); //email - await _messageProviderService.SendNewOrderNoteAddedCustomerMessage(request.Order, orderNote, + await _orderMessageProviderService.SendNewOrderNoteAddedCustomerMessage(request.Order, orderNote, host: request.Host); return orderNote; diff --git a/src/Web/Grand.Web/Commands/Handler/Orders/MerchandiseReturnSubmitCommandHandler.cs b/src/Web/Grand.Web/Commands/Handler/Orders/MerchandiseReturnSubmitCommandHandler.cs index d0ae78bf22..3ec1ed5bdf 100644 --- a/src/Web/Grand.Web/Commands/Handler/Orders/MerchandiseReturnSubmitCommandHandler.cs +++ b/src/Web/Grand.Web/Commands/Handler/Orders/MerchandiseReturnSubmitCommandHandler.cs @@ -16,7 +16,7 @@ public class MerchandiseReturnSubmitCommandHandler : IRequestHandler Handle( await _merchandiseReturnService.InsertMerchandiseReturn(rr); //notify store owner here (email) - await _messageProviderService.SendNewMerchandiseReturnStoreOwnerMessage(rr, request.Order, + await _merchandiseReturnMessageProviderService.SendNewMerchandiseReturnStoreOwnerMessage(rr, request.Order, _languageSettings.DefaultAdminLanguageId, host: _contextAccessor.StoreContext.CurrentHost); //notify customer - await _messageProviderService.SendNewMerchandiseReturnCustomerMessage(rr, request.Order, + await _merchandiseReturnMessageProviderService.SendNewMerchandiseReturnCustomerMessage(rr, request.Order, request.Order.CustomerLanguageId, host: _contextAccessor.StoreContext.CurrentHost); return rr; diff --git a/src/Web/Grand.Web/Commands/Handler/Products/InsertProductReviewCommandHandler.cs b/src/Web/Grand.Web/Commands/Handler/Products/InsertProductReviewCommandHandler.cs index 2ecce867d0..93d08a793b 100644 --- a/src/Web/Grand.Web/Commands/Handler/Products/InsertProductReviewCommandHandler.cs +++ b/src/Web/Grand.Web/Commands/Handler/Products/InsertProductReviewCommandHandler.cs @@ -17,20 +17,20 @@ public class InsertProductReviewCommandHandler : IRequestHandler Handle(InsertProductReviewCommand request, Canc //notify store owner if (_catalogSettings.NotifyStoreOwnerAboutNewProductReviews) - await _messageProviderService.SendProductReviewMessage(request.Product, productReview, request.Store, + await _catalogMessageProviderService.SendProductReviewMessage(request.Product, productReview, request.Store, _languageSettings.DefaultAdminLanguageId, host: request.Host); return productReview; diff --git a/src/Web/Grand.Web/Commands/Handler/Products/SendProductAskQuestionMessageCommandHandler.cs b/src/Web/Grand.Web/Commands/Handler/Products/SendProductAskQuestionMessageCommandHandler.cs index 51f70bac44..7c68c6524c 100644 --- a/src/Web/Grand.Web/Commands/Handler/Products/SendProductAskQuestionMessageCommandHandler.cs +++ b/src/Web/Grand.Web/Commands/Handler/Products/SendProductAskQuestionMessageCommandHandler.cs @@ -7,16 +7,16 @@ namespace Grand.Web.Commands.Handler.Products; public class SendProductAskQuestionMessageCommandHandler : IRequestHandler { - private readonly IMessageProviderService _messageProviderService; + private readonly ICatalogMessageProviderService _catalogMessageProviderService; - public SendProductAskQuestionMessageCommandHandler(IMessageProviderService messageProviderService) + public SendProductAskQuestionMessageCommandHandler(ICatalogMessageProviderService catalogMessageProviderService) { - _messageProviderService = messageProviderService; + _catalogMessageProviderService = catalogMessageProviderService; } public async Task Handle(SendProductAskQuestionMessageCommand request, CancellationToken cancellationToken) { - await _messageProviderService.SendProductQuestionMessage(request.Customer, request.Store, + await _catalogMessageProviderService.SendProductQuestionMessage(request.Customer, request.Store, request.Language.Id, request.Product, request.Model.Email, request.Model.FullName, request.Model.Phone, FormatText.ConvertText(request.Model.Message), request.RemoteIpAddress, host: request.Host); diff --git a/src/Web/Grand.Web/Commands/Handler/Products/SendProductEmailAFriendMessageCommandHandler.cs b/src/Web/Grand.Web/Commands/Handler/Products/SendProductEmailAFriendMessageCommandHandler.cs index 859c35e073..7259c03dd8 100644 --- a/src/Web/Grand.Web/Commands/Handler/Products/SendProductEmailAFriendMessageCommandHandler.cs +++ b/src/Web/Grand.Web/Commands/Handler/Products/SendProductEmailAFriendMessageCommandHandler.cs @@ -7,16 +7,16 @@ namespace Grand.Web.Commands.Handler.Products; public class SendProductEmailAFriendMessageCommandHandler : IRequestHandler { - private readonly IMessageProviderService _messageProviderService; + private readonly ICatalogMessageProviderService _catalogMessageProviderService; - public SendProductEmailAFriendMessageCommandHandler(IMessageProviderService messageProviderService) + public SendProductEmailAFriendMessageCommandHandler(ICatalogMessageProviderService catalogMessageProviderService) { - _messageProviderService = messageProviderService; + _catalogMessageProviderService = catalogMessageProviderService; } public async Task Handle(SendProductEmailAFriendMessageCommand request, CancellationToken cancellationToken) { - await _messageProviderService.SendProductEmailAFriendMessage(request.Customer, request.Store, + await _catalogMessageProviderService.SendProductEmailAFriendMessage(request.Customer, request.Store, request.Language.Id, request.Product, request.Model.YourEmailAddress, request.Model.FriendEmail, FormatText.ConvertText(request.Model.PersonalMessage), host: request.Host); diff --git a/src/Web/Grand.Web/Commands/Handler/Vendors/ContactVendorCommandHandler.cs b/src/Web/Grand.Web/Commands/Handler/Vendors/ContactVendorCommandHandler.cs index db6590f3b4..b812b435a1 100644 --- a/src/Web/Grand.Web/Commands/Handler/Vendors/ContactVendorCommandHandler.cs +++ b/src/Web/Grand.Web/Commands/Handler/Vendors/ContactVendorCommandHandler.cs @@ -12,14 +12,14 @@ namespace Grand.Web.Commands.Handler.Vendors; public class ContactVendorCommandHandler : IRequestHandler { private readonly CommonSettings _commonSettings; - private readonly IMessageProviderService _messageProviderService; + private readonly IVendorMessageProviderService _vendorMessageProviderService; private readonly IRequestTranslationService _translationService; private readonly IContextAccessor _contextAccessor; - public ContactVendorCommandHandler(IMessageProviderService messageProviderService, IContextAccessor contextAccessor, + public ContactVendorCommandHandler(IVendorMessageProviderService vendorMessageProviderService, IContextAccessor contextAccessor, IRequestTranslationService translationService, CommonSettings commonSettings) { - _messageProviderService = messageProviderService; + _vendorMessageProviderService = vendorMessageProviderService; _contextAccessor = contextAccessor; _translationService = translationService; _commonSettings = commonSettings; @@ -30,7 +30,7 @@ public async Task Handle(ContactVendorSendCommand request, C var subject = _commonSettings.SubjectFieldOnContactUsForm ? request.Model.Subject : null; var body = FormatText.ConvertText(request.Model.Enquiry); - await _messageProviderService.SendContactVendorMessage(_contextAccessor.WorkContext.CurrentCustomer, request.Store, + await _vendorMessageProviderService.SendContactVendorMessage(_contextAccessor.WorkContext.CurrentCustomer, request.Store, request.Vendor, _contextAccessor.WorkContext.WorkingLanguage.Id, request.Model.Email.Trim(), request.Model.FullName, subject, body, request.IpAddress, host: _contextAccessor.StoreContext.CurrentHost); diff --git a/src/Web/Grand.Web/Commands/Handler/Vendors/InsertVendorReviewCommandHandler.cs b/src/Web/Grand.Web/Commands/Handler/Vendors/InsertVendorReviewCommandHandler.cs index 09437083fc..c84e75968d 100644 --- a/src/Web/Grand.Web/Commands/Handler/Vendors/InsertVendorReviewCommandHandler.cs +++ b/src/Web/Grand.Web/Commands/Handler/Vendors/InsertVendorReviewCommandHandler.cs @@ -13,20 +13,20 @@ public class InsertVendorReviewCommandHandler : IRequestHandler Handle(InsertVendorReviewCommand request, Cancel //notify store owner if (_vendorSettings.NotifyVendorAboutNewVendorReviews) - await _messageProviderService.SendVendorReviewMessage(vendorReview, request.Store, + await _vendorMessageProviderService.SendVendorReviewMessage(vendorReview, request.Store, _languageSettings.DefaultAdminLanguageId, host: _contextAccessor.StoreContext.CurrentHost); return vendorReview; diff --git a/src/Web/Grand.Web/Controllers/AccountController.cs b/src/Web/Grand.Web/Controllers/AccountController.cs index cc7321e4ef..9061918f11 100644 --- a/src/Web/Grand.Web/Controllers/AccountController.cs +++ b/src/Web/Grand.Web/Controllers/AccountController.cs @@ -48,7 +48,7 @@ public AccountController( ICustomerManagerService customerManagerService, ICountryService countryService, IMediator mediator, - IMessageProviderService messageProviderService, + ICustomerMessageProviderService customerMessageProviderService, CaptchaSettings captchaSettings, CustomerSettings customerSettings, CustomerConfig customerConfig) @@ -61,7 +61,7 @@ public AccountController( _customerManagerService = customerManagerService; _customerSettings = customerSettings; _countryService = countryService; - _messageProviderService = messageProviderService; + _customerMessageProviderService = customerMessageProviderService; _captchaSettings = captchaSettings; _mediator = mediator; _customerConfig = customerConfig; @@ -173,7 +173,7 @@ public virtual async Task Courses() private readonly ICustomerManagerService _customerManagerService; private readonly ICountryService _countryService; private readonly IMediator _mediator; - private readonly IMessageProviderService _messageProviderService; + private readonly ICustomerMessageProviderService _customerMessageProviderService; private readonly CustomerSettings _customerSettings; private readonly CustomerConfig _customerConfig; private readonly CaptchaSettings _captchaSettings; @@ -629,7 +629,7 @@ await _mediator.Publish(new CustomerRegisteredEvent(_contextAccessor.WorkContext //email validation message await _customerService.UpdateUserField(_contextAccessor.WorkContext.CurrentCustomer, SystemCustomerFieldNames.AccountActivationToken, Guid.NewGuid().ToString()); - await _messageProviderService.SendCustomerEmailValidationMessage( + await _customerMessageProviderService.SendCustomerEmailValidationMessage( _contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore, _contextAccessor.WorkContext.WorkingLanguage.Id, host: _contextAccessor.StoreContext.CurrentHost); @@ -648,7 +648,7 @@ await _messageProviderService.SendCustomerEmailValidationMessage( case UserRegistrationType.Standard: { //send customer welcome message - await _messageProviderService.SendCustomerWelcomeMessage(_contextAccessor.WorkContext.CurrentCustomer, + await _customerMessageProviderService.SendCustomerWelcomeMessage(_contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore, _contextAccessor.WorkContext.WorkingLanguage.Id, host: _contextAccessor.StoreContext.CurrentHost); @@ -796,7 +796,7 @@ public virtual async Task AccountActivation(string token, string await _customerService.UpdateUserField(customer, SystemCustomerFieldNames.AccountActivationToken, ""); //send welcome message - await _messageProviderService.SendCustomerWelcomeMessage(customer, _contextAccessor.StoreContext.CurrentStore, + await _customerMessageProviderService.SendCustomerWelcomeMessage(customer, _contextAccessor.StoreContext.CurrentStore, _contextAccessor.WorkContext.WorkingLanguage.Id, host: _contextAccessor.StoreContext.CurrentHost); var model = new AccountActivationModel { diff --git a/src/Web/Grand.Web/Controllers/KnowledgebaseController.cs b/src/Web/Grand.Web/Controllers/KnowledgebaseController.cs index 6a6a273816..be3256d843 100644 --- a/src/Web/Grand.Web/Controllers/KnowledgebaseController.cs +++ b/src/Web/Grand.Web/Controllers/KnowledgebaseController.cs @@ -35,7 +35,7 @@ public class KnowledgebaseController : BasePublicController private readonly IKnowledgebaseService _knowledgebaseService; private readonly KnowledgebaseSettings _knowledgebaseSettings; private readonly LanguageSettings _languageSettings; - private readonly IMessageProviderService _messageProviderService; + private readonly ICmsMessageProviderService _cmsMessageProviderService; private readonly IRequestPermissionService _permissionService; private readonly IRequestTranslationService _translationService; private readonly IContextAccessor _contextAccessor; @@ -47,7 +47,7 @@ public KnowledgebaseController( ICacheBase cacheBase, IAclService aclService, IRequestTranslationService translationService, - IMessageProviderService messageProviderService, + ICmsMessageProviderService cmsMessageProviderService, IDateTimeService dateTimeService, IRequestPermissionService permissionService, CustomerSettings customerSettings, @@ -62,7 +62,7 @@ public KnowledgebaseController( _translationService = translationService; _captchaSettings = captchaSettings; _languageSettings = languageSettings; - _messageProviderService = messageProviderService; + _cmsMessageProviderService = cmsMessageProviderService; _dateTimeService = dateTimeService; _customerSettings = customerSettings; _permissionService = permissionService; @@ -314,7 +314,7 @@ public virtual async Task ArticleCommentAdd(KnowledgebaseArticleM //notify a store owner if (_knowledgebaseSettings.NotifyAboutNewArticleComments) - await _messageProviderService.SendArticleCommentMessage(article, comment, + await _cmsMessageProviderService.SendArticleCommentMessage(article, comment, _languageSettings.DefaultAdminLanguageId, host: _contextAccessor.StoreContext.CurrentHost); //The text boxes should be cleared after a comment has been posted diff --git a/src/Web/Grand.Web/Controllers/VendorController.cs b/src/Web/Grand.Web/Controllers/VendorController.cs index aa47152ea1..9f11b9f4ff 100644 --- a/src/Web/Grand.Web/Controllers/VendorController.cs +++ b/src/Web/Grand.Web/Controllers/VendorController.cs @@ -33,7 +33,7 @@ public VendorController( IContextAccessor contextAccessor, IRequestTranslationService translationService, ICustomerService customerService, - IMessageProviderService messageProviderService, + IVendorMessageProviderService vendorMessageProviderService, IVendorService vendorService, ISlugService slugService, IPictureService pictureService, @@ -47,7 +47,7 @@ public VendorController( _contextAccessor = contextAccessor; _translationService = translationService; _customerService = customerService; - _messageProviderService = messageProviderService; + _vendorMessageProviderService = vendorMessageProviderService; _vendorService = vendorService; _slugService = slugService; _pictureService = pictureService; @@ -77,7 +77,7 @@ protected virtual async Task UpdatePictureSeoNames(Domain.Vendors.Vendor vendor) private readonly IContextAccessor _contextAccessor; private readonly IRequestTranslationService _translationService; private readonly ICustomerService _customerService; - private readonly IMessageProviderService _messageProviderService; + private readonly IVendorMessageProviderService _vendorMessageProviderService; private readonly IVendorService _vendorService; private readonly ISlugService _slugService; private readonly IPictureService _pictureService; @@ -167,7 +167,7 @@ await _customerService.UpdateCustomerField(_contextAccessor.WorkContext.CurrentC _contextAccessor.WorkContext.CurrentCustomer.VendorId); //notify store owner here (email) - await _messageProviderService.SendNewVendorAccountApplyStoreOwnerMessage(_contextAccessor.WorkContext.CurrentCustomer, + await _vendorMessageProviderService.SendNewVendorAccountApplyStoreOwnerMessage(_contextAccessor.WorkContext.CurrentCustomer, vendor, _contextAccessor.StoreContext.CurrentStore, _languageSettings.DefaultAdminLanguageId, host: _contextAccessor.StoreContext.CurrentHost); diff --git a/src/Web/Grand.Web/Controllers/WishlistController.cs b/src/Web/Grand.Web/Controllers/WishlistController.cs index 22dd2a7e29..5c788a17ce 100644 --- a/src/Web/Grand.Web/Controllers/WishlistController.cs +++ b/src/Web/Grand.Web/Controllers/WishlistController.cs @@ -211,7 +211,7 @@ public virtual async Task DeleteItemFromWishlist(string shoppingC [HttpPost] [DenySystemAccount] public virtual async Task EmailWishlist(WishlistEmailAFriendModel model, - [FromServices] IMessageProviderService messageProviderService, + [FromServices] ICatalogMessageProviderService catalogMessageProviderService, [FromServices] CaptchaSettings captchaSettings) { if (!await _permissionService.Authorize(StandardPermission.EnableWishlist) || @@ -225,7 +225,7 @@ public virtual async Task EmailWishlist(WishlistEmailAFriendModel if (ModelState.IsValid) { //email - await messageProviderService.SendWishlistEmailAFriendMessage(_contextAccessor.WorkContext.CurrentCustomer, + await catalogMessageProviderService.SendWishlistEmailAFriendMessage(_contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore, _contextAccessor.WorkContext.WorkingLanguage.Id, model.YourEmailAddress, model.FriendEmail, FormatText.ConvertText(model.PersonalMessage), diff --git a/src/Web/Grand.Web/Features/Handlers/Customers/GetTwoFactorAuthenticationHandler.cs b/src/Web/Grand.Web/Features/Handlers/Customers/GetTwoFactorAuthenticationHandler.cs index 204c5751a0..8eee97de10 100644 --- a/src/Web/Grand.Web/Features/Handlers/Customers/GetTwoFactorAuthenticationHandler.cs +++ b/src/Web/Grand.Web/Features/Handlers/Customers/GetTwoFactorAuthenticationHandler.cs @@ -11,16 +11,16 @@ public class GetTwoFactorAuthenticationHandler : IRequestHandler { private readonly CustomerSettings _customerSetting; - private readonly IMessageProviderService _messageProviderService; + private readonly ICustomerMessageProviderService _customerMessageProviderService; private readonly ITwoFactorAuthenticationService _twoFactorAuthenticationService; public GetTwoFactorAuthenticationHandler( ITwoFactorAuthenticationService twoFactorAuthenticationService, - IMessageProviderService messageProviderService, + ICustomerMessageProviderService customerMessageProviderService, CustomerSettings customerSetting) { _twoFactorAuthenticationService = twoFactorAuthenticationService; - _messageProviderService = messageProviderService; + _customerMessageProviderService = customerMessageProviderService; _customerSetting = customerSetting; } @@ -36,7 +36,7 @@ public GetTwoFactorAuthenticationHandler( TwoFactorAuthenticationType = _customerSetting.TwoFactorAuthenticationType }; if (_customerSetting.TwoFactorAuthenticationType == TwoFactorAuthenticationType.EmailVerification) - await _messageProviderService.SendCustomerEmailTokenValidationMessage(request.Customer, request.Store, + await _customerMessageProviderService.SendCustomerEmailTokenValidationMessage(request.Customer, request.Store, request.Customer.Id, host: request.Host); return model; From 37e28876613304e2d5a6f4d12c34e610ccc2f219 Mon Sep 17 00:00:00 2001 From: support Date: Thu, 22 Oct 2026 20:45:09 +0200 Subject: [PATCH 2/3] Describe the split message provider services in the agent docs --- .ai/skills/message-notification.md | 50 ++++++++++++++++++------------ .ai/skills/scheduled-task.md | 2 +- AGENTS.md | 2 +- 3 files changed, 33 insertions(+), 21 deletions(-) diff --git a/.ai/skills/message-notification.md b/.ai/skills/message-notification.md index c5ed6194c5..22fb822667 100644 --- a/.ai/skills/message-notification.md +++ b/.ai/skills/message-notification.md @@ -32,18 +32,18 @@ Do not use this skill as the primary review for MongoDB queries or security; com 6. Use DotLiquid syntax: `{{Drop.Property}}` for values, `{% if condition %}...{% endif %}` for conditions, `{% for item in Collection %}...{% endfor %}` for loops. #### Sending Messages -7. Add a typed send method to `IMessageProviderService` in `src/Business/Grand.Business.Core/Interfaces/Messages/IMessageProviderService.cs` when the trigger is a recurring, well-defined business event. Follow the signature pattern: +7. Add a typed send method to the area interface that owns the recipient's flow, `IMessageProviderService` in `src/Business/Grand.Business.Core/Interfaces/Messages/` (Customer, Order, Newsletter, Catalog, MerchandiseReturn, Vendor, Cms), when the trigger is a recurring, well-defined business event. Follow the signature pattern: ```csharp Task SendXxxMessage(TEntity entity, Store store, string languageId); ``` -8. Implement the method in `MessageProviderService` at `src/Business/Grand.Business.Messages/Services/MessageProviderService.cs`. The standard implementation pattern is: - 1. Get `MessageTemplate` by name using `IMessageTemplateService.GetMessageTemplateByName(name, store.Id)`. - 2. Return 0 if the template is not active. - 3. Resolve `EmailAccount` using the template's `EmailAccountId` and `IEmailAccountService`. +8. Implement the method in the matching `MessageProviderService` in `src/Business/Grand.Business.Messages/Services/`. The standard implementation pattern is: + 1. Get the active `MessageTemplate` with `IMessageLookupService.GetMessageTemplate(name, store.Id)`. + 2. Return 0 when it returns null (missing or inactive template). + 3. Resolve `EmailAccount` with `IMessageLookupService.GetEmailAccountOfMessageTemplate`, and the language with `EnsureLanguageIsActive`. 4. Build tokens with `LiquidObjectBuilder` using the appropriate `AddXxxTokens()` methods. 5. Call `await builder.Build()` to get the populated `LiquidObject`. - 6. Call `await SendNotification(messageTemplate, emailAccount, languageId, liquidObject, toEmail, toName)`. -9. Use the generic `SendNotification` overload directly when a one-off or plugin-triggered send does not justify a dedicated typed method. + 6. Call `await _messageProviderService.SendNotification(messageTemplate, emailAccount, languageId, liquidObject, toEmail, toName)`. +9. Use the generic `IMessageProviderService.SendNotification` directly when a one-off or plugin-triggered send does not justify a dedicated typed method. #### DotLiquid Drops and Tokens 10. Add new token properties to an existing `Liquid*` drop class in `src/Business/Grand.Business.Messages/DotLiquidDrops/` when the data already belongs to that drop's entity. Do not add properties that require loading additional entities unless those are already available in the drop constructor. @@ -66,7 +66,7 @@ Do not use this skill as the primary review for MongoDB queries or security; com 2. Prefer the `MessageTokensAddedEvent` handler for plugin-specific tokens rather than modifying core drop classes. 3. Prefer reusing `{{Store.Name}}`, `{{Store.URL}}`, and `{{Store.AdminEmail}}` tokens that are always available via `LiquidStore`. 4. Prefer the `Reference` enum field on `QueuedEmail` (e.g., `Reference.Order`, `Reference.Customer`) to link queued emails to the source entity for audit purposes. -5. Prefer testing `MessageProviderService` with a mock `IMessageTemplateService` that returns a pre-built `MessageTemplate` to verify the queuing call. +5. Prefer testing an `MessageProviderService` built on a real `MessageLookupService` and `MessageProviderService` with a mock `IMessageTemplateService` that returns a pre-built `MessageTemplate` to verify the queuing call. ## Constraints - Never hardcode email addresses in `SendXxx` methods; always resolve them from `EmailAccount` or from the entity being notified. @@ -74,19 +74,30 @@ Do not use this skill as the primary review for MongoDB queries or security; com - Never use tokens that are not backed by a drop property — they will render as empty strings. - Never add rendering logic (formatting, currency, date) to drop property getters unless the same pattern is already used in that drop. - Never store multiple language copies of a template as separate `MessageTemplate` documents; use the `Locales` collection. -- Never call `IMessageProviderService` from inside a Razor view or a controller action directly — trigger sends from handlers or services. +- Never call a message provider service from inside a Razor view or a controller action directly — trigger sends from handlers or services. ## Key Contracts -### IMessageProviderService (`src/Business/Grand.Business.Core/Interfaces/Messages/IMessageProviderService.cs`) -Typed send methods follow the pattern: +### Message provider services (`src/Business/Grand.Business.Core/Interfaces/Messages/`) +Typed send methods live in one interface per area, one method per template: ```csharp -Task SendCustomerRegisteredMessage(Customer customer, Store store, string languageId); -Task SendOrderPlacedCustomerMessage(Order order, Customer customer, Store store, string languageId); -// ... one method per template +// ICustomerMessageProviderService +Task SendCustomerRegisteredMessage(Customer customer, Store store, string languageId, DomainHost host = null); +// IOrderMessageProviderService +Task SendOrderPlacedCustomerMessage(Order order, Customer customer, string languageId, ...); ``` -Generic send: +| Interface | Covers | +|---|---| +| `ICustomerMessageProviderService` | registration, welcome, email validation, password recovery, customer notes, account deletion, contact us | +| `IOrderMessageProviderService` | order placed/paid/completed/cancelled/refunded for customer, store owner and vendor, shipments, order notes, gift vouchers | +| `INewsletterMessageProviderService` | newsletter subscription activation and deactivation | +| `ICatalogMessageProviderService` | email a friend, wishlist, product questions and reviews, low stock, back in stock, auctions and bids | +| `IMerchandiseReturnMessageProviderService` | merchandise return created, status changed, notes | +| `IVendorMessageProviderService` | vendor application, information change, vendor reviews, contact vendor | +| `ICmsMessageProviderService` | blog, knowledgebase article and news comments | + +They share `IMessageLookupService` (store, active template, email account, language) and queue through `IMessageProviderService`, which holds only the generic send: ```csharp Task SendNotification( MessageTemplate messageTemplate, EmailAccount emailAccount, @@ -126,8 +137,9 @@ public class MessageTokensAddedEvent : INotification { | QueuedEmail entity | `src/Core/Grand.Domain/Messages/QueuedEmail.cs` | | EmailAccount entity | `src/Core/Grand.Domain/Messages/EmailAccount.cs` | | MessageTemplateNames constants | `src/Business/Grand.Business.Messages/Services/MessageTemplateNames.cs` | -| IMessageProviderService interface | `src/Business/Grand.Business.Core/Interfaces/Messages/IMessageProviderService.cs` | -| MessageProviderService implementation | `src/Business/Grand.Business.Messages/Services/MessageProviderService.cs` | +| IMessageProviderService (generic `SendNotification`) | `src/Business/Grand.Business.Core/Interfaces/Messages/IMessageProviderService.cs` | +| I<Area>MessageProviderService interfaces, IMessageLookupService | `src/Business/Grand.Business.Core/Interfaces/Messages/` | +| Implementations | `src/Business/Grand.Business.Messages/Services/MessageProviderService.cs`, `MessageLookupService.cs`, `MessageProviderService.cs` | | IMessageTemplateService interface | `src/Business/Grand.Business.Core/Interfaces/Messages/IMessageTemplateService.cs` | | IQueuedEmailService interface | `src/Business/Grand.Business.Core/Interfaces/Messages/IQueuedEmailService.cs` | | IEmailAccountService interface | `src/Business/Grand.Business.Core/Interfaces/Messages/IEmailAccountService.cs` | @@ -191,8 +203,8 @@ Input: Send an email to the customer when an order reaches a custom "Ready for p Output: 1. Add `const string SendOrderReadyForPickupCustomerMessage = "OrderReadyForPickup.CustomerNotification"` to `MessageTemplateNames`. 2. Seed the template in `InstallDataMessageTemplates` with subject `"Your order {{Order.OrderNumber}} is ready"` and a body using `{{Order.*}}` and `{{Store.*}}` tokens. -3. Add `Task SendOrderReadyForPickupCustomerMessage(Order order, Customer customer, Store store, string languageId)` to `IMessageProviderService`. -4. Implement the method in `MessageProviderService`: load template by name, return 0 if inactive, build `LiquidObjectBuilder` with `AddStoreTokens` + `AddOrderTokens` + `AddCustomerTokens`, call `SendNotification` with the customer's email. +3. Add `Task SendOrderReadyForPickupCustomerMessage(Order order, Customer customer, Store store, string languageId)` to `IOrderMessageProviderService`. +4. Implement the method in `OrderMessageProviderService`: load template by name, return 0 if inactive, build `LiquidObjectBuilder` with `AddStoreTokens` + `AddOrderTokens` + `AddCustomerTokens`, call `SendNotification` with the customer's email. 5. Publish `OrderReadyForPickupEvent` from the order status-change service and handle it in a `INotificationHandler` that calls `_messageProviderService.SendOrderReadyForPickupCustomerMessage(...)`. ### Example 2: Plugin Token Extension diff --git a/.ai/skills/scheduled-task.md b/.ai/skills/scheduled-task.md index 84c5342412..f83f61905d 100644 --- a/.ai/skills/scheduled-task.md +++ b/.ai/skills/scheduled-task.md @@ -80,7 +80,7 @@ Do not use this skill as the primary review for MongoDB query safety or security - Never register a task with a DI key that differs from its `ScheduleTaskName` in the database. - Never add a seed entry without also adding a DI registration, and vice versa. - Never introduce locking, semaphores, or static shared state inside a task class. -- Never call `IEmailSender` directly from a task; use `IMessageProviderService` or queue via `IQueuedEmailService` following the message-notification pattern. +- Never call `IEmailSender` directly from a task; use a message provider service or queue via `IQueuedEmailService` following the message-notification pattern. ## Key Contracts diff --git a/AGENTS.md b/AGENTS.md index 8fa4393e86..4814053a9c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -73,7 +73,7 @@ A change that crosses domains loads every skill it touches. | `.ai/skills/admin-area-changes.md` | anything admin-facing that may reach Main Admin, Store Owner or Vendor: shared models, permissions, navigation, validation, scoped data | | `.ai/skills/admin-ui-component-layer.md` | building or converting an admin screen, tab or popup: ``, ``, ``, ``, ``, button hierarchy, Add new placement, known CSS traps | | `.ai/skills/settings-and-localization.md` | settings classes, store-scoped overrides, `ISettingService`, translation resources, `ITranslationService`, `IPluginTranslateResource`, translated entities and admin models | -| `.ai/skills/message-notification.md` | message templates, DotLiquid tokens and drops, `IMessageProviderService`, queued email lifecycle, `LiquidObjectBuilder`, `MessageTokensAddedEvent`, notification handlers | +| `.ai/skills/message-notification.md` | message templates, DotLiquid tokens and drops, message provider services (`IMessageProviderService`, `IMessageProviderService`), queued email lifecycle, `LiquidObjectBuilder`, `MessageTokensAddedEvent`, notification handlers | | `.ai/skills/scheduled-task.md` | `IScheduleTask`, `AddKeyedScoped` registration, `ScheduleTask` seed, distributed locking across instances, error handling, task migrations | | `.ai/skills/permission-navigation.md` | `PermissionSystemName`, `PermissionActionName`, `StandardPermission`, `PermissionProvider`, controller authorization attributes, `AdminSiteMap` entries and their migrations | From 33882d0e33edf694ed8e37be41a2483d7bf8a51e Mon Sep 17 00:00:00 2001 From: support Date: Thu, 22 Oct 2026 22:34:11 +0200 Subject: [PATCH 3/3] Guard message sends against missing customers and vendors - Skip deleted bidders in the auction lost/outbid/store-owner sends and resolve the language per losing bidder instead of reusing the first one - Tolerate a null customer in the back-in-stock send and a missing vendor in the vendor review send - Publish MessageTokensAdded and use the customer's store in the customer-delete store owner notification - Resolve stores through IMessageLookupService.GetStore in the gift voucher and auction-expired sends, dropping IStoreService from the order and catalog services - Point the notification skill and agent at the per-area services and BuildAsync --- .ai/agents/notification-specialist.md | 4 +-- .ai/skills/message-notification.md | 6 ++--- .../Services/CatalogMessageProviderService.cs | 27 ++++++++++--------- .../CustomerMessageProviderService.cs | 4 ++- .../Services/OrderMessageProviderService.cs | 10 ++----- .../Services/VendorMessageProviderService.cs | 2 ++ .../Services/MessageProviderServiceTest.cs | 4 +-- 7 files changed, 29 insertions(+), 28 deletions(-) diff --git a/.ai/agents/notification-specialist.md b/.ai/agents/notification-specialist.md index 6d3bb52d80..2928b2208f 100644 --- a/.ai/agents/notification-specialist.md +++ b/.ai/agents/notification-specialist.md @@ -1,6 +1,6 @@ --- name: Notification Specialist -description: Work with message templates, DotLiquid tokens and drops, IMessageProviderService, queued email lifecycle, LiquidObjectBuilder, and domain event notification handlers. +description: Work with message templates, DotLiquid tokens and drops, the per-area message provider services (IMessageProviderService) and IMessageProviderService, queued email lifecycle, LiquidObjectBuilder, and domain event notification handlers. tools: ["codebase", "search", "edit", "terminal"] --- @@ -10,7 +10,7 @@ which remain the single source of truth. Do not duplicate rules here. --> Read `AGENTS.md` first. Then read `.ai/skills/message-notification.md` for message templates, -DotLiquid tokens and drops, `IMessageProviderService`, queued email lifecycle, +DotLiquid tokens and drops, the per-area `IMessageProviderService` sends and `IMessageProviderService.SendNotification`, queued email lifecycle, `LiquidObjectBuilder`, the `MessageTokensAddedEvent` plugin extension point, and domain event notification handler rules. Run `.ai/checklists/definition-of-done.md` before reporting the change complete. diff --git a/.ai/skills/message-notification.md b/.ai/skills/message-notification.md index 22fb822667..015d0a7530 100644 --- a/.ai/skills/message-notification.md +++ b/.ai/skills/message-notification.md @@ -41,7 +41,7 @@ Do not use this skill as the primary review for MongoDB queries or security; com 2. Return 0 when it returns null (missing or inactive template). 3. Resolve `EmailAccount` with `IMessageLookupService.GetEmailAccountOfMessageTemplate`, and the language with `EnsureLanguageIsActive`. 4. Build tokens with `LiquidObjectBuilder` using the appropriate `AddXxxTokens()` methods. - 5. Call `await builder.Build()` to get the populated `LiquidObject`. + 5. Call `await builder.BuildAsync()` to get the populated `LiquidObject`. 6. Call `await _messageProviderService.SendNotification(messageTemplate, emailAccount, languageId, liquidObject, toEmail, toName)`. 9. Use the generic `IMessageProviderService.SendNotification` directly when a one-off or plugin-triggered send does not justify a dedicated typed method. @@ -116,7 +116,7 @@ var liquidObject = await new LiquidObjectBuilder(_mediator) .AddStoreTokens(store, language, emailAccount) .AddOrderTokens(order, customer, store, host) .AddCustomerTokens(customer, store, host, language) - .Build(); + .BuildAsync(); ``` The drops, the builder and the token commands belong to `Grand.Business.Messages`, not to `Grand.Business.Core`. Other Business projects and plugins compile against Core only, so they never use them directly: a campaign renders through `ICampaignMessageRenderer`, and a plugin adds tokens through `MessageTokensAddedEvent` / `EntityTokensAddedEvent` and `LiquidObject.AdditionalTokens`, which expose drops only as `DotLiquid.Drop`. @@ -205,7 +205,7 @@ Output: 2. Seed the template in `InstallDataMessageTemplates` with subject `"Your order {{Order.OrderNumber}} is ready"` and a body using `{{Order.*}}` and `{{Store.*}}` tokens. 3. Add `Task SendOrderReadyForPickupCustomerMessage(Order order, Customer customer, Store store, string languageId)` to `IOrderMessageProviderService`. 4. Implement the method in `OrderMessageProviderService`: load template by name, return 0 if inactive, build `LiquidObjectBuilder` with `AddStoreTokens` + `AddOrderTokens` + `AddCustomerTokens`, call `SendNotification` with the customer's email. -5. Publish `OrderReadyForPickupEvent` from the order status-change service and handle it in a `INotificationHandler` that calls `_messageProviderService.SendOrderReadyForPickupCustomerMessage(...)`. +5. Publish `OrderReadyForPickupEvent` from the order status-change service and handle it in a `INotificationHandler` that calls `_orderMessageProviderService.SendOrderReadyForPickupCustomerMessage(...)` on the injected `IOrderMessageProviderService`. ### Example 2: Plugin Token Extension Input: A loyalty plugin needs to include a customer's loyalty points balance in existing order emails. diff --git a/src/Business/Grand.Business.Messages/Services/CatalogMessageProviderService.cs b/src/Business/Grand.Business.Messages/Services/CatalogMessageProviderService.cs index 39c214c965..48d57714a9 100644 --- a/src/Business/Grand.Business.Messages/Services/CatalogMessageProviderService.cs +++ b/src/Business/Grand.Business.Messages/Services/CatalogMessageProviderService.cs @@ -1,6 +1,5 @@ using Grand.Business.Core.Commands.Messages.Common; using Grand.Business.Core.Extensions; -using Grand.Business.Core.Interfaces.Common.Stores; using Grand.Business.Core.Interfaces.Messages; using Grand.Business.Core.Queries.Messages; using Grand.Business.Messages.DotLiquidDrops; @@ -22,13 +21,11 @@ public CatalogMessageProviderService( IMessageLookupService messageLookupService, IMessageProviderService messageProviderService, IMediator mediator, - IStoreService storeService, CommonSettings commonSettings) { _messageLookupService = messageLookupService; _messageProviderService = messageProviderService; _mediator = mediator; - _storeService = storeService; _commonSettings = commonSettings; } @@ -39,7 +36,6 @@ public CatalogMessageProviderService( private readonly IMessageLookupService _messageLookupService; private readonly IMessageProviderService _messageProviderService; private readonly IMediator _mediator; - private readonly IStoreService _storeService; private readonly CommonSettings _commonSettings; #endregion @@ -367,7 +363,7 @@ public virtual async Task SendBackInStockMessage(Customer customer, Product await _mediator.MessageTokensAdded(messageTemplate, liquidObject); var toEmail = customer?.Email; - var toName = customer.GetFullName(); + var toName = customer?.GetFullName(); return await _messageProviderService.SendNotification(messageTemplate, emailAccount, languageId, liquidObject, toEmail, toName, @@ -441,8 +437,12 @@ public virtual async Task SendAuctionEndedLostCustomerMessage(Product produ { var builder2 = new LiquidObjectBuilder(_mediator, liquidObject); var customer = await _mediator.Send(new GetCustomerByIdQuery { Id = item.Key }); - if (string.IsNullOrEmpty(languageId)) - languageId = customer.GetUserFieldFromEntity(SystemCustomerFieldNames.LanguageId); + if (customer == null) + continue; + + var customerLanguageId = string.IsNullOrEmpty(languageId) + ? customer.GetUserFieldFromEntity(SystemCustomerFieldNames.LanguageId) + : languageId; builder2.AddCustomerTokens(customer, store, host, language); var liquidObject2 = await builder2.BuildAsync(); @@ -452,7 +452,7 @@ public virtual async Task SendAuctionEndedLostCustomerMessage(Product produ var toEmail = customer.Email; var toName = customer.GetFullName(); await _messageProviderService.SendNotification(messageTemplate, emailAccount, - languageId, liquidObject2, + customerLanguageId, liquidObject2, toEmail, toName, reference: Reference.Customer, objectId: customer.Id); } @@ -520,7 +520,7 @@ public virtual async Task SendAuctionEndedStoreOwnerMessage(Product product { var customer = await _mediator.Send(new GetCustomerByIdQuery { Id = bid.CustomerId }); if (string.IsNullOrEmpty(languageId)) - languageId = customer.GetUserFieldFromEntity(SystemCustomerFieldNames.LanguageId); + languageId = customer?.GetUserFieldFromEntity(SystemCustomerFieldNames.LanguageId); var store = await _messageLookupService.GetStore(bid.StoreId); @@ -532,13 +532,13 @@ public virtual async Task SendAuctionEndedStoreOwnerMessage(Product product emailAccount = await _messageLookupService.GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); builder.AddAuctionTokens(product, bid) - .AddCustomerTokens(customer, store, store.PrimaryHost(), language) .AddStoreTokens(store, language, emailAccount); + builder.AddCustomerTokensIfNotNull(customer, store, store.PrimaryHost(), language); } else { - var store = (await _storeService.GetAllStores()).FirstOrDefault(); - var language = await _messageLookupService.EnsureLanguageIsActive(languageId, store?.Id); + var store = await _messageLookupService.GetStore(""); + var language = await _messageLookupService.EnsureLanguageIsActive(languageId, store.Id); messageTemplate = await _messageLookupService.GetMessageTemplate("AuctionExpired.StoreOwnerNotification", ""); if (messageTemplate == null) return 0; @@ -570,6 +570,9 @@ public virtual async Task SendOutBidCustomerMessage(Product product, string ArgumentNullException.ThrowIfNull(product); var customer = await _mediator.Send(new GetCustomerByIdQuery { Id = bid.CustomerId }); + if (customer == null) + return 0; + if (string.IsNullOrEmpty(languageId)) languageId = customer.GetUserFieldFromEntity(SystemCustomerFieldNames.LanguageId); diff --git a/src/Business/Grand.Business.Messages/Services/CustomerMessageProviderService.cs b/src/Business/Grand.Business.Messages/Services/CustomerMessageProviderService.cs index 5b4d2803e0..51d4160b25 100644 --- a/src/Business/Grand.Business.Messages/Services/CustomerMessageProviderService.cs +++ b/src/Business/Grand.Business.Messages/Services/CustomerMessageProviderService.cs @@ -191,7 +191,7 @@ public virtual async Task SendCustomerDeleteStoreOwnerMessage(Customer cust { ArgumentNullException.ThrowIfNull(customer); - var store = await _messageLookupService.GetStore(""); + var store = await _messageLookupService.GetStore(customer.StoreId); var language = await _messageLookupService.EnsureLanguageIsActive(languageId, store.Id); var messageTemplate = await _messageLookupService.GetMessageTemplate("CustomerDelete.StoreOwnerNotification", store.Id); @@ -207,6 +207,8 @@ public virtual async Task SendCustomerDeleteStoreOwnerMessage(Customer cust .AddCustomerTokens(customer, store, host, language); var liquidObject = await builder.BuildAsync(); + //event notification + await _mediator.MessageTokensAdded(messageTemplate, liquidObject); var toEmail = emailAccount.Email; var toName = emailAccount.DisplayName; diff --git a/src/Business/Grand.Business.Messages/Services/OrderMessageProviderService.cs b/src/Business/Grand.Business.Messages/Services/OrderMessageProviderService.cs index f6d1e6333b..d8d3db74bf 100644 --- a/src/Business/Grand.Business.Messages/Services/OrderMessageProviderService.cs +++ b/src/Business/Grand.Business.Messages/Services/OrderMessageProviderService.cs @@ -1,5 +1,4 @@ using Grand.Business.Core.Extensions; -using Grand.Business.Core.Interfaces.Common.Stores; using Grand.Business.Core.Interfaces.Messages; using Grand.Business.Core.Queries.Messages; using Grand.Business.Messages.DotLiquidDrops; @@ -21,13 +20,11 @@ public class OrderMessageProviderService : IOrderMessageProviderService public OrderMessageProviderService( IMessageLookupService messageLookupService, IMessageProviderService messageProviderService, - IMediator mediator, - IStoreService storeService) + IMediator mediator) { _messageLookupService = messageLookupService; _messageProviderService = messageProviderService; _mediator = mediator; - _storeService = storeService; } #endregion @@ -37,7 +34,6 @@ public OrderMessageProviderService( private readonly IMessageLookupService _messageLookupService; private readonly IMessageProviderService _messageProviderService; private readonly IMediator _mediator; - private readonly IStoreService _storeService; #endregion @@ -492,9 +488,7 @@ public virtual async Task SendGiftVoucherMessage(GiftVoucher giftVoucher, O { ArgumentNullException.ThrowIfNull(giftVoucher); - Store store = null; - if (order != null) store = await _storeService.GetStoreById(order.StoreId); - store ??= (await _storeService.GetAllStores()).FirstOrDefault(); + var store = await _messageLookupService.GetStore(order?.StoreId); var language = await _messageLookupService.EnsureLanguageIsActive(languageId, store?.Id); diff --git a/src/Business/Grand.Business.Messages/Services/VendorMessageProviderService.cs b/src/Business/Grand.Business.Messages/Services/VendorMessageProviderService.cs index 41918c8b1f..2baaf0756f 100644 --- a/src/Business/Grand.Business.Messages/Services/VendorMessageProviderService.cs +++ b/src/Business/Grand.Business.Messages/Services/VendorMessageProviderService.cs @@ -145,6 +145,8 @@ public virtual async Task SendVendorReviewMessage(VendorReview vendorReview var customer = await _mediator.Send(new GetCustomerByIdQuery { Id = vendorReview.CustomerId }); //vendor var vendor = await _mediator.Send(new GetVendorByIdQuery { Id = vendorReview.VendorId }); + if (vendor == null) + return 0; host = store.LinkHost(host); var builder = new LiquidObjectBuilder(_mediator); diff --git a/src/Tests/Grand.Business.Messages.Tests/Services/MessageProviderServiceTest.cs b/src/Tests/Grand.Business.Messages.Tests/Services/MessageProviderServiceTest.cs index a615ec51e0..58e60f68f8 100644 --- a/src/Tests/Grand.Business.Messages.Tests/Services/MessageProviderServiceTest.cs +++ b/src/Tests/Grand.Business.Messages.Tests/Services/MessageProviderServiceTest.cs @@ -89,11 +89,11 @@ public void Init() _customerMessageService = new CustomerMessageProviderService(messageLookupService, _messageService, _mediatorMock.Object, _commonSetting.Object); _catalogMessageService = new CatalogMessageProviderService(messageLookupService, _messageService, - _mediatorMock.Object, _storeServiceMock.Object, _commonSetting.Object); + _mediatorMock.Object, _commonSetting.Object); _vendorMessageService = new VendorMessageProviderService(messageLookupService, _messageService, _mediatorMock.Object, _commonSetting.Object); _orderMessageService = new OrderMessageProviderService(messageLookupService, _messageService, - _mediatorMock.Object, _storeServiceMock.Object); + _mediatorMock.Object); _newsletterMessageService = new NewsletterMessageProviderService(messageLookupService, _messageService, _mediatorMock.Object); _merchandiseReturnMessageService = new MerchandiseReturnMessageProviderService(messageLookupService,