Skip to content

Commit 7b02a40

Browse files
B2B purchasing roles, order limits and order approval for company accounts (#995)
1 parent edd71d0 commit 7b02a40

116 files changed

Lines changed: 7086 additions & 73 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎src/Business/Grand.Business.Checkout/Commands/Handlers/Orders/CheckOrderStatusCommandHandler.cs‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@ public async Task<bool> Handle(CheckOrderStatusCommand request, CancellationToke
2727
{
2828
ArgumentNullException.ThrowIfNull(request.Order);
2929

30+
//an order waiting for an approval stays as it is until it is decided
31+
if (request.Order.OrderStatusId == (int)OrderStatusSystem.AwaitingApproval)
32+
return true;
33+
3034
if (request.Order.PaymentStatusId == PaymentStatus.Paid && !request.Order.PaidDateUtc.HasValue)
3135
{
3236
//ensure that paid date is set

‎src/Business/Grand.Business.Checkout/Commands/Handlers/Orders/OrderNotificationCommandHandler.cs‎

Lines changed: 35 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -43,55 +43,59 @@ public async Task Handle(OrderNotificationCommand request, CancellationToken can
4343

4444
try
4545
{
46-
if (request.OriginalCustomerIfImpersonated != null)
46+
if (request.OriginalCustomerIfImpersonated != null && request.AddOrderPlacedNote)
4747
//this order is placed by a store administrator impersonating a customer
4848
await _orderService.InsertOrderNote(new OrderNote {
4949
Note =
5050
$"Order placed by a store owner ('{request.OriginalCustomerIfImpersonated.Email}'. ID = {request.OriginalCustomerIfImpersonated.Id}) impersonating the customer.",
5151
DisplayToCustomer = false,
5252
OrderId = request.Order.Id
5353
});
54-
else
54+
else if (request.AddOrderPlacedNote)
5555
await _orderService.InsertOrderNote(new OrderNote {
5656
Note = "Order placed",
5757
DisplayToCustomer = false,
5858
OrderId = request.Order.Id
5959
});
6060

6161
//send email notifications
62-
await _orderMessageProviderService.SendOrderPlacedStoreOwnerMessage(request.Order, request.Customer,
63-
_languageSettings.DefaultAdminLanguageId, request.Host);
62+
if (request.NotifyStoreOwnerAndVendors)
63+
await _orderMessageProviderService.SendOrderPlacedStoreOwnerMessage(request.Order, request.Customer,
64+
_languageSettings.DefaultAdminLanguageId, request.Host);
6465

65-
string orderPlacedAttachmentFilePath = string.Empty, orderPlacedAttachmentFileName = string.Empty;
66-
var orderPlacedAttachments = new List<string>();
67-
68-
try
69-
{
70-
orderPlacedAttachmentFilePath =
71-
_orderSettings.AttachPdfInvoiceToOrderPlacedEmail && !_orderSettings.AttachPdfInvoiceToBinary
72-
? await _pdfService.PrintOrderToPdf(request.Order, request.Order.CustomerLanguageId)
73-
: null;
74-
orderPlacedAttachmentFileName =
75-
_orderSettings.AttachPdfInvoiceToOrderPlacedEmail && !_orderSettings.AttachPdfInvoiceToBinary
76-
? "order.pdf"
77-
: null;
78-
orderPlacedAttachments = _orderSettings.AttachPdfInvoiceToOrderPlacedEmail &&
79-
_orderSettings.AttachPdfInvoiceToBinary
80-
? [
81-
await _pdfService.SaveOrderToBinary(request.Order, request.Order.CustomerLanguageId)
82-
]
83-
: [];
84-
}
85-
catch (Exception ex)
66+
if (request.NotifyCustomer)
8667
{
87-
_logger.LogError(ex, "Error - order placed attachment file {OrderOrderNumber}", request.Order.OrderNumber);
88-
}
68+
string orderPlacedAttachmentFilePath = string.Empty, orderPlacedAttachmentFileName = string.Empty;
69+
var orderPlacedAttachments = new List<string>();
70+
71+
try
72+
{
73+
orderPlacedAttachmentFilePath =
74+
_orderSettings.AttachPdfInvoiceToOrderPlacedEmail && !_orderSettings.AttachPdfInvoiceToBinary
75+
? await _pdfService.PrintOrderToPdf(request.Order, request.Order.CustomerLanguageId)
76+
: null;
77+
orderPlacedAttachmentFileName =
78+
_orderSettings.AttachPdfInvoiceToOrderPlacedEmail && !_orderSettings.AttachPdfInvoiceToBinary
79+
? "order.pdf"
80+
: null;
81+
orderPlacedAttachments = _orderSettings.AttachPdfInvoiceToOrderPlacedEmail &&
82+
_orderSettings.AttachPdfInvoiceToBinary
83+
? [
84+
await _pdfService.SaveOrderToBinary(request.Order, request.Order.CustomerLanguageId)
85+
]
86+
: [];
87+
}
88+
catch (Exception ex)
89+
{
90+
_logger.LogError(ex, "Error - order placed attachment file {OrderOrderNumber}", request.Order.OrderNumber);
91+
}
8992

90-
await _orderMessageProviderService.SendOrderPlacedCustomerMessage(request.Order, request.Customer,
91-
request.Order.CustomerLanguageId, orderPlacedAttachmentFilePath, orderPlacedAttachmentFileName,
92-
orderPlacedAttachments, request.Host);
93+
await _orderMessageProviderService.SendOrderPlacedCustomerMessage(request.Order, request.Customer,
94+
request.Order.CustomerLanguageId, orderPlacedAttachmentFilePath, orderPlacedAttachmentFileName,
95+
orderPlacedAttachments, request.Host);
96+
}
9397

94-
if (request.Order.OrderItems.Any(x => !string.IsNullOrEmpty(x.VendorId)))
98+
if (request.NotifyStoreOwnerAndVendors && request.Order.OrderItems.Any(x => !string.IsNullOrEmpty(x.VendorId)))
9599
{
96100
var vendors = await _mediator.Send(new GetVendorsInOrderQuery { Order = request.Order });
97101
foreach (var vendor in vendors)

‎src/Business/Grand.Business.Checkout/Commands/Handlers/Orders/PlaceOrderCommandHandler.cs‎

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ public class PlaceOrderCommandHandler : IRequestHandler<PlaceOrderCommand, Place
4747
private readonly ILanguageService _languageService;
4848
private readonly ILogger<PlaceOrderCommandHandler> _logger;
4949
private readonly IMediator _mediator;
50+
private readonly IOrderApprovalService _orderApprovalService;
51+
private readonly ITranslationService _translationService;
5052
private readonly ICatalogMessageProviderService _catalogMessageProviderService;
5153
private readonly IOrderService _orderService;
5254
private readonly OrderSettings _orderSettings;
@@ -100,6 +102,8 @@ public PlaceOrderCommandHandler(
100102
IAuctionService auctionService,
101103
ICountryService countryService,
102104
IShoppingCartValidator shoppingCartValidator,
105+
IOrderApprovalService orderApprovalService,
106+
ITranslationService translationService,
103107
ShippingSettings shippingSettings,
104108
ShoppingCartSettings shoppingCartSettings,
105109
PaymentSettings paymentSettings,
@@ -136,6 +140,8 @@ public PlaceOrderCommandHandler(
136140
_auctionService = auctionService;
137141
_countryService = countryService;
138142
_shoppingCartValidator = shoppingCartValidator;
143+
_orderApprovalService = orderApprovalService;
144+
_translationService = translationService;
139145
_shippingSettings = shippingSettings;
140146
_shoppingCartSettings = shoppingCartSettings;
141147
_paymentSettings = paymentSettings;
@@ -163,6 +169,16 @@ public async Task<PlaceOrderResult> Handle(PlaceOrderCommand command, Cancellati
163169
if (result.Errors.Any())
164170
return result;
165171

172+
//an order over the customer's limit is placed awaiting an approval, with an allowed payment method only
173+
var approval = await _orderApprovalService.RequiresApproval(details.Cart, details.Context);
174+
details.ApprovalRequired = approval.Required;
175+
if (details.ApprovalRequired && !_orderApprovalService.IsPaymentMethodAllowed(details.PaymentMethodSystemName))
176+
{
177+
result.AddError(_translationService.GetResource("Checkout.OrderApproval.PaymentMethodNotAllowed",
178+
command.Language.Id));
179+
return result;
180+
}
181+
166182
#region Payment workflow
167183

168184
var processPayment = await PrepareProcessPayment(details);
@@ -194,7 +210,8 @@ await _mediator.Send(new OrderNotificationCommand {
194210
Order = result.PlacedOrder,
195211
Customer = details.Customer,
196212
OriginalCustomerIfImpersonated = command.OriginalCustomerIfImpersonated,
197-
Host = command.Host
213+
Host = command.Host,
214+
NotifyStoreOwnerAndVendors = !details.ApprovalRequired
198215
}, cancellationToken);
199216

200217
//check order status
@@ -203,7 +220,8 @@ await _mediator.Send(new OrderNotificationCommand {
203220
//raise event
204221
await _mediator.Publish(new OrderPlacedEvent(result.PlacedOrder), cancellationToken);
205222

206-
if (result.PlacedOrder.PaymentStatusId == PaymentStatus.Paid)
223+
//an order awaiting approval is processed as paid once it is approved (store owner and vendors hear of it then)
224+
if (!details.ApprovalRequired && result.PlacedOrder.PaymentStatusId == PaymentStatus.Paid)
207225
await _mediator.Send(new ProcessOrderPaidCommand { Order = result.PlacedOrder }, cancellationToken);
208226

209227
#endregion
@@ -929,7 +947,9 @@ protected virtual Order PrepareOrderHeader(PaymentTransaction paymentTransaction
929947
CurrencyRate = details.CurrencyRate,
930948
Rate = details.Currency.Rate,
931949
AffiliateId = details.AffiliateId,
932-
OrderStatusId = (int)OrderStatusSystem.Pending,
950+
OrderStatusId = details.ApprovalRequired
951+
? (int)OrderStatusSystem.AwaitingApproval
952+
: (int)OrderStatusSystem.Pending,
933953
PaymentMethodSystemName = paymentTransaction.PaymentMethodSystemName,
934954
PaymentOptionAttribute =
935955
details.Customer.GetUserFieldFromEntity<string>(SystemCustomerFieldNames.PaymentOptionAttribute,

‎src/Business/Grand.Business.Checkout/Commands/Handlers/Orders/ProcessOrderPaidCommandHandler.cs‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,10 @@ protected virtual async Task ProcessOrderPaid(Order order)
4949
{
5050
ArgumentNullException.ThrowIfNull(order);
5151

52+
//an order waiting for an approval is processed as paid once it is approved, not before
53+
if (order.OrderStatusId == (int)OrderStatusSystem.AwaitingApproval)
54+
return;
55+
5256
//raise event
5357
await _mediator.Publish(new OrderPaidEvent(order));
5458

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
using Grand.Business.Core.Events.Checkout.Orders;
2+
using Grand.Business.Core.Interfaces.Checkout.Orders;
3+
using Grand.Domain.Orders;
4+
using Grand.Mediator;
5+
using Microsoft.Extensions.Logging;
6+
7+
namespace Grand.Business.Checkout.Events.Orders;
8+
9+
/// <summary>
10+
/// Closes the approval request of an order cancelled before anyone decided it
11+
/// </summary>
12+
public class OrderCancelledOrderApprovalHandler : INotificationHandler<OrderCancelledEvent>
13+
{
14+
private readonly ILogger<OrderCancelledOrderApprovalHandler> _logger;
15+
private readonly IOrderApprovalService _orderApprovalService;
16+
17+
public OrderCancelledOrderApprovalHandler(IOrderApprovalService orderApprovalService,
18+
ILogger<OrderCancelledOrderApprovalHandler> logger)
19+
{
20+
_orderApprovalService = orderApprovalService;
21+
_logger = logger;
22+
}
23+
24+
public async Task Handle(OrderCancelledEvent notification, CancellationToken cancellationToken)
25+
{
26+
var order = notification.Order;
27+
try
28+
{
29+
// a decision or a withdrawal cancels the order first and then saves its own status over this one
30+
var approval = await _orderApprovalService.GetByOrderId(order.Id);
31+
if (approval is not { Status: OrderApprovalStatus.Pending })
32+
return;
33+
34+
approval.Status = OrderApprovalStatus.Cancelled;
35+
approval.DecidedOnUtc = DateTime.UtcNow;
36+
await _orderApprovalService.Update(approval);
37+
}
38+
catch (Exception ex)
39+
{
40+
// the order is already cancelled: publishing must complete whatever happens here
41+
_logger.LogError(ex,
42+
"Order {OrderNumber} ({OrderId}) was cancelled but its approval request could not be closed",
43+
order.OrderNumber, order.Id);
44+
}
45+
}
46+
}

0 commit comments

Comments
 (0)