Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .ai/skills/message-notification.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,9 +107,12 @@ Task<int> SendNotification(
IEnumerable<string> attachedDownloads = null,
string replyToEmailAddress = null, string replyToName = null,
string fromEmail = null, string fromName = null, string subject = null,
Reference reference = Reference.None, string objectId = "");
Reference reference = Reference.None, string objectId = "",
string invoiceDownloadId = null);
```

`attachedDownloads` may be shared (a message template's attachment), so deleting an email leaves them alone. `invoiceDownloadId` is owned by that one email: `QueuedEmailService` deletes it with the email, and a requeued email gets its own copy. Deleting a customer deletes the invoice emails of that customer's orders, whatever address they went to.

### LiquidObjectBuilder (`src/Business/Grand.Business.Messages/DotLiquidDrops/LiquidObjectBuilder.cs`)
```csharp
var liquidObject = await new LiquidObjectBuilder(_mediator)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,34 +65,21 @@ await _orderMessageProviderService.SendOrderPlacedStoreOwnerMessage(request.Orde

if (request.NotifyCustomer)
{
string orderPlacedAttachmentFilePath = string.Empty, orderPlacedAttachmentFileName = string.Empty;
var orderPlacedAttachments = new List<string>();
string invoiceDownloadId = null;

try
{
orderPlacedAttachmentFilePath =
_orderSettings.AttachPdfInvoiceToOrderPlacedEmail && !_orderSettings.AttachPdfInvoiceToBinary
? await _pdfService.PrintOrderToPdf(request.Order, request.Order.CustomerLanguageId)
: null;
orderPlacedAttachmentFileName =
_orderSettings.AttachPdfInvoiceToOrderPlacedEmail && !_orderSettings.AttachPdfInvoiceToBinary
? "order.pdf"
: null;
orderPlacedAttachments = _orderSettings.AttachPdfInvoiceToOrderPlacedEmail &&
_orderSettings.AttachPdfInvoiceToBinary
? [
await _pdfService.SaveOrderToBinary(request.Order, request.Order.CustomerLanguageId)
]
: [];
if (_orderSettings.AttachPdfInvoiceToOrderPlacedEmail)
invoiceDownloadId =
await _pdfService.SaveOrderToBinary(request.Order, request.Order.CustomerLanguageId);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error - order placed attachment file {OrderOrderNumber}", request.Order.OrderNumber);
}

await _orderMessageProviderService.SendOrderPlacedCustomerMessage(request.Order, request.Customer,
request.Order.CustomerLanguageId, orderPlacedAttachmentFilePath, orderPlacedAttachmentFileName,
orderPlacedAttachments, request.Host);
request.Order.CustomerLanguageId, invoiceDownloadId, request.Host);
}

if (request.NotifyStoreOwnerAndVendors && request.Order.OrderItems.Any(x => !string.IsNullOrEmpty(x.VendorId)))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@
using Grand.Business.Core.Interfaces.Messages;
using Grand.Domain.Orders;
using Grand.Mediator;
using Microsoft.Extensions.Logging;

namespace Grand.Business.Checkout.Commands.Handlers.Orders;

public class OrderStatusCommandHandler : IRequestHandler<SetOrderStatusCommand, bool>
{
private readonly ICustomerService _customerService;
private readonly ILogger<OrderStatusCommandHandler> _logger;
private readonly LoyaltyPointsSettings _loyaltyPointsSettings;
private readonly IMediator _mediator;
private readonly IOrderMessageProviderService _orderMessageProviderService;
Expand All @@ -27,7 +29,8 @@ public OrderStatusCommandHandler(
IVendorService vendorService,
IMediator mediator,
OrderSettings orderSettings,
LoyaltyPointsSettings loyaltyPointsSettings)
LoyaltyPointsSettings loyaltyPointsSettings,
ILogger<OrderStatusCommandHandler> logger)
{
_orderService = orderService;
_customerService = customerService;
Expand All @@ -37,6 +40,7 @@ public OrderStatusCommandHandler(
_mediator = mediator;
_orderSettings = orderSettings;
_loyaltyPointsSettings = loyaltyPointsSettings;
_logger = logger;
}

public async Task<bool> Handle(SetOrderStatusCommand request, CancellationToken cancellationToken)
Expand Down Expand Up @@ -66,23 +70,21 @@ await _orderService.InsertOrderNote(new OrderNote {
{
//notification

var orderCompletedAttachmentFilePath = _orderSettings.AttachPdfInvoiceToOrderCompletedEmail
? await _pdfService.PrintOrderToPdf(request.Order, "")
: null;
var orderCompletedAttachmentFileName =
_orderSettings.AttachPdfInvoiceToOrderCompletedEmail ? "order.pdf" : null;
string invoiceDownloadId = null;

var orderCompletedAttachments = _orderSettings.AttachPdfInvoiceToOrderCompletedEmail &&
_orderSettings.AttachPdfInvoiceToBinary
? [
await _pdfService.SaveOrderToBinary(request.Order, "")
]
: new List<string>();
try
{
if (_orderSettings.AttachPdfInvoiceToOrderCompletedEmail)
invoiceDownloadId = await _pdfService.SaveOrderToBinary(request.Order, "");
}
catch (Exception ex)
{
_logger.LogError(ex, "Error - order completed attachment file {OrderOrderNumber}", request.Order.OrderNumber);
}
Comment on lines +80 to +83

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not changed. This catch is a deliberate best-effort boundary: PDF composition and file storage fail with many unrelated exception types, and none of them may stop the surrounding work. The exception is logged, and the same pattern is already used in OrderNotificationCommandHandler and across the business layer.


await _orderMessageProviderService
.SendOrderCompletedCustomerMessage(request.Order, customer, request.Order.CustomerLanguageId,
orderCompletedAttachmentFilePath,
orderCompletedAttachmentFileName, orderCompletedAttachments);
invoiceDownloadId);
}

if (prevOrderStatus != (int)OrderStatusSystem.Cancelled &&
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,15 @@
using Grand.Domain.Localization;
using Grand.Domain.Orders;
using Grand.Mediator;
using Microsoft.Extensions.Logging;

namespace Grand.Business.Checkout.Commands.Handlers.Orders;

public class ProcessOrderPaidCommandHandler : IRequestHandler<ProcessOrderPaidCommand, bool>
{
private readonly ICustomerService _customerService;
private readonly LanguageSettings _languageSettings;
private readonly ILogger<ProcessOrderPaidCommandHandler> _logger;
private readonly IMediator _mediator;
private readonly IOrderMessageProviderService _orderMessageProviderService;
private readonly OrderSettings _orderSettings;
Expand All @@ -25,14 +27,16 @@ public ProcessOrderPaidCommandHandler(
IMediator mediator,
IPdfService pdfService,
OrderSettings orderSettings,
LanguageSettings languageSettings)
LanguageSettings languageSettings,
ILogger<ProcessOrderPaidCommandHandler> logger)
{
_orderMessageProviderService = orderMessageProviderService;
_customerService = customerService;
_mediator = mediator;
_pdfService = pdfService;
_orderSettings = orderSettings;
_languageSettings = languageSettings;
_logger = logger;
}

public async Task<bool> Handle(ProcessOrderPaidCommand request, CancellationToken cancellationToken)
Expand Down Expand Up @@ -61,24 +65,20 @@ protected virtual async Task ProcessOrderPaid(Order order)
{
var customer = await _customerService.GetCustomerById(order.CustomerId);

var orderPaidAttachmentFilePath =
_orderSettings.AttachPdfInvoiceToOrderPaidEmail && !_orderSettings.AttachPdfInvoiceToBinary
? await _pdfService.PrintOrderToPdf(order, "")
: null;
var orderPaidAttachmentFileName =
_orderSettings.AttachPdfInvoiceToOrderPaidEmail && !_orderSettings.AttachPdfInvoiceToBinary
? "order.pdf"
: null;
string invoiceDownloadId = null;

var orderPaidAttachments =
_orderSettings.AttachPdfInvoiceToOrderPaidEmail && _orderSettings.AttachPdfInvoiceToBinary
? [
await _pdfService.SaveOrderToBinary(order, "")
]
: new List<string>();
try
{
if (_orderSettings.AttachPdfInvoiceToOrderPaidEmail)
invoiceDownloadId = await _pdfService.SaveOrderToBinary(order, "");
}
catch (Exception ex)
{
_logger.LogError(ex, "Error - order paid attachment file {OrderOrderNumber}", order.OrderNumber);
}
Comment on lines +75 to +78

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not changed. This catch is a deliberate best-effort boundary: PDF composition and file storage fail with many unrelated exception types, and none of them may stop the surrounding work. The exception is logged, and the same pattern is already used in OrderNotificationCommandHandler and across the business layer.


await _orderMessageProviderService.SendOrderPaidCustomerMessage(order, customer, order.CustomerLanguageId,
orderPaidAttachmentFilePath, orderPaidAttachmentFileName, orderPaidAttachments);
invoiceDownloadId);

await _orderMessageProviderService.SendOrderPaidStoreOwnerMessage(order, customer,
_languageSettings.DefaultAdminLanguageId);
Expand Down
22 changes: 0 additions & 22 deletions src/Business/Grand.Business.Common/Services/Pdf/PdfService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,7 @@
using Grand.Domain.Tax;
using Grand.Mediator;
using Grand.SharedKernel.Extensions;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Logging;
using Path = System.IO.Path;

namespace Grand.Business.Common.Services.Pdf;

Expand All @@ -44,13 +42,11 @@ public class PdfService : IPdfService
private readonly IStoreFilesContext _storeFilesContext;
private readonly IStoreService _storeService;
private readonly ITranslationService _translationService;
private readonly IWebHostEnvironment _webHostEnvironment;

public PdfService(
IRepository<Download> downloadRepository,
ITranslationService translationService,
IStoreFilesContext storeFilesContext,
IWebHostEnvironment webHostEnvironment,
IProductService productService,
ICountryService countryService,
IStoreService storeService,
Expand All @@ -65,7 +61,6 @@ public PdfService(
_downloadRepository = downloadRepository;
_translationService = translationService;
_storeFilesContext = storeFilesContext;
_webHostEnvironment = webHostEnvironment;
_productService = productService;
_countryService = countryService;
_storeService = storeService;
Expand Down Expand Up @@ -97,23 +92,6 @@ public async Task PrintOrdersToPdf(Stream stream, IList<Order> orders, string la
await stream.WriteAsync(pdf.Build(), cancellationToken);
}

public async Task<string> PrintOrderToPdf(Order order, string languageId, string vendorId = "")
{
ArgumentNullException.ThrowIfNull(order);

var fileName = $"order_{order.OrderGuid}_{CommonHelper.GenerateRandomDigitCode(4)}.pdf";

var dir = Path.Combine(_webHostEnvironment.WebRootPath, "assets/files/exportimport");
ArgumentNullException.ThrowIfNull(dir);

if (!System.IO.Directory.Exists(dir)) System.IO.Directory.CreateDirectory(dir);

var filePath = Path.Combine(dir, fileName);
await using var fileStream = new FileStream(filePath, FileMode.Create);
await PrintOrdersToPdf(fileStream, [order], languageId, vendorId);
return filePath;
}

public async Task PrintPackagingSlipsToPdf(Stream stream, IList<Shipment> shipments, string languageId = "",
CancellationToken cancellationToken = default)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,6 @@ namespace Grand.Business.Core.Interfaces.Common.Pdf;
/// </summary>
public interface IPdfService
{
/// <summary>
/// Print an order to PDF
/// </summary>
/// <param name="order">Order</param>
/// <param name="languageId">Language identifier; 0 to use a language used when placing an order</param>
/// <param name="vendorId">
/// Vendor identifier to limit products; 0 to to print all products. If specified, then totals won't
/// be printed
/// </param>
/// <returns>A path of generated file</returns>
Task<string> PrintOrderToPdf(Order order, string languageId, string vendorId = "");

/// <summary>
/// Save an order PDF
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ public interface IMessageProviderService
/// <param name="subject">Subject. If specified, then it overrides subject of a message template</param>
/// <param name="reference">Reference object</param>
/// <param name="objectId">Reference object ident</param>
/// <param name="invoiceDownloadId">Download of the invoice; the queued email owns it and deletes it with itself</param>
/// <returns>Queued email identifier</returns>
Task<int> SendNotification(MessageTemplate messageTemplate,
EmailAccount emailAccount, string languageId, LiquidObject liquidObject,
Expand All @@ -32,5 +33,5 @@ Task<int> SendNotification(MessageTemplate messageTemplate,
IEnumerable<string> attachedDownloads = null,
string replyToEmailAddress = null, string replyToName = null,
string fromEmail = null, string fromName = null, string subject = null,
Reference reference = Reference.None, string objectId = "");
Reference reference = Reference.None, string objectId = "", string invoiceDownloadId = null);
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,15 +50,12 @@ Task<int> SendOrderPlacedStoreOwnerMessage(Order order, Customer customer, strin
/// <param name="order">Order instance</param>
/// <param name="customer">Customer instance</param>
/// <param name="languageId">Message language identifier</param>
/// <param name="attachmentFilePath">Attachment file path</param>
/// <param name="attachmentFileName">
/// Attachment file name. If specified, then this file name will be sent to a recipient.
/// Otherwise, "AttachmentFilePath" name will be used.
/// <param name="invoiceDownloadId">
/// Download of the invoice to attach. The queued email takes ownership of it; it is deleted when no email is queued
/// </param>
/// <param name="attachments">Attachments ident</param>
/// <returns>Queued email identifier</returns>
Task<int> SendOrderPaidCustomerMessage(Order order, Customer customer, string languageId,
string attachmentFilePath = null, string attachmentFileName = null, IEnumerable<string> attachments = null);
string invoiceDownloadId = null);

/// <summary>
/// Sends an order paid notification to a vendor
Expand All @@ -75,19 +72,15 @@ Task<int> SendOrderPaidCustomerMessage(Order order, Customer customer, string la
/// <param name="order">Order instance</param>
/// <param name="customer">Customer</param>
/// <param name="languageId">Message language identifier</param>
/// <param name="attachmentFilePath">Attachment file path</param>
/// <param name="attachmentFileName">
/// Attachment file name. If specified, then this file name will be sent to a recipient.
/// Otherwise, "AttachmentFilePath" name will be used.
/// <param name="invoiceDownloadId">
/// Download of the invoice to attach. The queued email takes ownership of it; it is deleted when no email is queued
/// </param>
/// <param name="attachments">Attachments ident</param>
/// <returns>Queued email identifier</returns>
/// <param name="host">
/// Host of the storefront request; links use it when it is one of the store's domains, else the primary domain
/// </param>
Task<int> SendOrderPlacedCustomerMessage(Order order, Customer customer, string languageId,
string attachmentFilePath = null, string attachmentFileName = null, IEnumerable<string> attachments = null,
DomainHost host = null);
string invoiceDownloadId = null, DomainHost host = null);

/// <summary>
/// Sends a shipment sent notification to a customer
Expand All @@ -111,15 +104,12 @@ Task<int> SendOrderPlacedCustomerMessage(Order order, Customer customer, string
/// <param name="order">Order instance</param>
/// <param name="customer">Customer instance</param>
/// <param name="languageId">Message language identifier</param>
/// <param name="attachmentFilePath">Attachment file path</param>
/// <param name="attachmentFileName">
/// Attachment file name. If specified, then this file name will be sent to a recipient.
/// Otherwise, "AttachmentFilePath" name will be used.
/// <param name="invoiceDownloadId">
/// Download of the invoice to attach. The queued email takes ownership of it; it is deleted when no email is queued
/// </param>
/// <param name="attachments">Attachments ident</param>
/// <returns>Queued email identifier</returns>
Task<int> SendOrderCompletedCustomerMessage(Order order, Customer customer, string languageId,
string attachmentFilePath = null, string attachmentFileName = null, IEnumerable<string> attachments = null);
string invoiceDownloadId = null);

/// <summary>
/// Sends an order cancelled notification to a customer
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,30 @@ public interface IQueuedEmailService
Task UpdateQueuedEmail(QueuedEmail queuedEmail);

/// <summary>
/// Deleted a queued email
/// Deleted a queued email and its invoice
/// </summary>
/// <param name="queuedEmail">Queued email</param>
Task DeleteQueuedEmail(QueuedEmail queuedEmail);

/// <summary>
/// Deleted a customer emails
/// Copies the invoice of a queued email, so that an email created from it owns a download of its own
/// </summary>
/// <param name="invoiceDownloadId">Download identifier of the invoice</param>
/// <returns>Download identifier of the copy; null when there is no invoice to copy</returns>
Task<string> CopyInvoiceDownload(string invoiceDownloadId);

/// <summary>
/// Deleted a customer emails and their invoices
/// </summary>
/// <param name="email">email</param>
Task DeleteCustomerEmail(string email);

/// <summary>
/// Deleted the invoice emails of orders and their invoices
/// </summary>
/// <param name="orderIds">Order identifiers</param>
Task DeleteOrderInvoiceEmails(IEnumerable<string> orderIds);

/// <summary>
/// Gets a queued email by identifier
/// </summary>
Expand Down Expand Up @@ -67,7 +80,7 @@ Task<IPagedList<QueuedEmail>> SearchEmails(string fromEmail,
int pageIndex = 0, int pageSize = int.MaxValue);

/// <summary>
/// Delete all queued emails
/// Delete all queued emails and their invoices
/// </summary>
Task DeleteAllEmails();
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,10 @@ public interface IDownloadService
/// </summary>
/// <param name="download">Download</param>
Task DeleteDownload(Download download);

/// <summary>
/// Deletes a download by identifier, without loading its file. Does nothing when it no longer exists
/// </summary>
/// <param name="downloadId">Download identifier</param>
Task DeleteDownloadById(string downloadId);
}
Loading
Loading