Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#1090: Notification to the consumer when the product is back in stock #1091

Merged
merged 1 commit into from
Mar 10, 2024
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,31 @@
<div class="out-of-stock">
<span class="label label-danger">@Localizer["Out of stock"]</span>
</div>

<div class="back-in-stock-subscribe">
<form action="api/stocks/back-in-stock">
<div>
@if (SignInManager.IsSignedIn(User))
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
@if (SignInManager.IsSignedIn(User))
@if (User.Identity.IsAuthenticated)

{
<p>Subscribe and we'll notify you when the product is back in stock.</p>
<div class="d-flex">
<input type="hidden" name="productId" value="@Model.Id" />
<input type="hidden" class="form-control mr-2" name="customerEmail" value="@User.Claims.First(c => c.Type == "email").Value"/>
<button type="button" class="btn btn-primary btn-back-in-stock-subscribe" id="subscribeBackInStock">Subscribe</button>
</div>
}
else
{
<p>Leave your email and we'll notify you when the product is in stock</p>
<div class="d-flex">
<input type="hidden" name="productId" value="@Model.Id" />
<input type="text" class="form-control mr-2" name="customerEmail" />
<button type="button" class="btn btn-primary btn-back-in-stock-subscribe" id="subscribeBackInStock">Send</button>
</div>
}
</div>
</form>
</div>
}
<div class="add-to-cart">
<form class="inline">
Expand Down
18 changes: 18 additions & 0 deletions src/Modules/SimplCommerce.Module.Catalog/wwwroot/product-detail.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,4 +71,22 @@ $(document).ready(function () {
});
}
});

$("#subscribeBackInStock").on('click', function (e) {
e.preventDefault();
var $form = $(this).closest("form"),
productId = $(this).closest("form").find('input[name=productId]').val(),
customerEmail = $(this).closest("form").find('input[name=customerEmail]').val();

var that = this;
$.post($form.attr('action'), $form.serializeArray())
.done(function (result) {
$(that).closest('.back-in-stock-subscribe').html('<b>Thank you. We\'ll notify you.</b>');
})
.fail(function (result) {
if (result.status === 409) {
$(that).closest('.back-in-stock-subscribe').html('<b>You\'ve already subscribed.</b>');
}
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ namespace SimplCommerce.Module.Core.Extensions
{
public interface IWorkContext
{
string GetCurrentHostName();

Task<User> GetCurrentUser();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@ public class WorkContext : IWorkContext
_configuration = configuration;
}

public string GetCurrentHostName()
{
return _httpContext.Request.Host.Value;
}

Comment on lines +36 to +40
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
public string GetCurrentHostName()
{
return _httpContext.Request.Host.Value;
}
public string GetCurrentHostName() => _httpContext.Request.Host.Value;

public async Task<User> GetCurrentUser()
{
if (_currentUser != null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,21 @@ public class StockApiController : Controller
{
private readonly IRepository<Stock> _stockRepository;
private readonly IStockService _stockService;
private readonly IStockSubscriptionService _stockSubscriptionService;
private readonly IWorkContext _workContext;
private readonly IRepository<Warehouse> _warehouseRepository;
private readonly IRepository<StockHistory> _stockHistoryRepository;
private readonly IRepository<BackInStockSubscription> _backInStockSubscriptionRepository;

public StockApiController(IRepository<Stock> stockRepository, IStockService stockService, IWorkContext workContext, IRepository<Warehouse> warehouseRepository, IRepository<StockHistory> stockHistoryRepository)
public StockApiController(IRepository<Stock> stockRepository, IStockService stockService, IWorkContext workContext, IRepository<Warehouse> warehouseRepository, IRepository<StockHistory> stockHistoryRepository, IRepository<BackInStockSubscription> backInStockSubscriptionRepository, IStockSubscriptionService stockSubscriptionService)
{
_stockRepository = stockRepository;
_stockService = stockService;
_workContext = workContext;
_warehouseRepository = warehouseRepository;
_stockHistoryRepository = stockHistoryRepository;
_backInStockSubscriptionRepository = backInStockSubscriptionRepository;
_stockSubscriptionService = stockSubscriptionService;
}

[HttpPost("grid")]
Expand Down Expand Up @@ -135,5 +139,21 @@ public async Task<IActionResult> GetStockHistory(int warehouseId, int productId)

return Ok(stockHistory);
}

[AllowAnonymous]
[HttpPost("back-in-stock")]
public async Task<IActionResult> BackInStockSubscribe(long productId, string customerEmail)
{
if (await _backInStockSubscriptionRepository.Query()
.Where(o => o.ProductId == productId && o.CustomerEmail == customerEmail)
.AnyAsync())
Comment on lines +147 to +149
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
if (await _backInStockSubscriptionRepository.Query()
.Where(o => o.ProductId == productId && o.CustomerEmail == customerEmail)
.AnyAsync())
if (await _backInStockSubscriptionRepository.Query()
.AnyAsync(o => o.ProductId == productId && o.CustomerEmail == customerEmail))

{
return Conflict();
}

await _stockSubscriptionService.BackInStockSubscribeAsync(productId, customerEmail);

return Ok();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
@using SimplCommerce.Module.Catalog.Models
@using SimplCommerce.Module.Core.Extensions
@using SimplCommerce.Module.Core.Services

@inject IWorkContext _workContext
@inject IMediaService _mediaService

@{
Layout = null;

var hostName = _workContext.GetCurrentHostName();
var thumbnailUrl = _mediaService.GetThumbnailUrl(Model.ThumbnailImage);
}

@model SimplCommerce.Module.Catalog.Models.Product

<div style="border: 1px solid #ebebeb; width: 50%; padding: 16px 24px; margin: 0 auto;">
Hi, <b>@Model.Name</b> in now available.
Get in now before it out of stock again.
<br />
<br />
<div>
<img src="https://@hostName@thumbnailUrl" />
<h2>@Model.Name</h2>
<h4 style="color:red;">$1000</h4>
</div>
<a href="https://@hostName/@Model.Slug">
View product
</a>
</div>
14 changes: 14 additions & 0 deletions src/Modules/SimplCommerce.Module.Inventory/Event/BackInStock.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using MediatR;

namespace SimplCommerce.Module.Inventory.Event
{
public class BackInStock : INotification
Copy link
Member

Choose a reason for hiding this comment

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

ProductBackInStock might more meaningful

{
public long ProductId { get; set; }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using MediatR;
using SimplCommerce.Module.Inventory.Services;

namespace SimplCommerce.Module.Inventory.Event
{
public class BackInStockSendEmailHandler : INotificationHandler<BackInStock>
{
public readonly IStockSubscriptionService _stockSubscriptionService;

public BackInStockSendEmailHandler(IStockSubscriptionService stockSubscriptionService)
{
_stockSubscriptionService = stockSubscriptionService;
}
Comment on lines +16 to +19
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
public BackInStockSendEmailHandler(IStockSubscriptionService stockSubscriptionService)
{
_stockSubscriptionService = stockSubscriptionService;
}
public BackInStockSendEmailHandler(IStockSubscriptionService stockSubscriptionService)
=> _stockSubscriptionService = stockSubscriptionService;


public async Task Handle(BackInStock notification, CancellationToken cancellationToken)
{
await _stockSubscriptionService.BackInStockSendNotificationsAsync(notification.ProductId);
}
Comment on lines +21 to +24
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
public async Task Handle(BackInStock notification, CancellationToken cancellationToken)
{
await _stockSubscriptionService.BackInStockSendNotificationsAsync(notification.ProductId);
}
public async Task Handle(BackInStock notification, CancellationToken cancellationToken)
=> await _stockSubscriptionService.BackInStockSendNotificationsAsync(notification.ProductId);

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using SimplCommerce.Infrastructure.Models;

namespace SimplCommerce.Module.Inventory.Models
{
public class BackInStockSubscription : EntityBase
{
public long ProductId { get; set; }
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
public long ProductId { get; set; }
public long ProductId { get; set; }

public string CustomerEmail { get; set; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@
using SimplCommerce.Infrastructure.Modules;
using SimplCommerce.Module.Inventory.Services;
using SimplCommerce.Infrastructure;
using MediatR;
using SimplCommerce.Module.Catalog.Events;
using SimplCommerce.Module.Core.Events;
using SimplCommerce.Module.Inventory.Event;

namespace SimplCommerce.Module.Inventory
{
Expand All @@ -12,6 +16,9 @@ public class ModuleInitializer : IModuleInitializer
public void ConfigureServices(IServiceCollection serviceCollection)
{
serviceCollection.AddTransient<IStockService, StockService>();
serviceCollection.AddTransient<IStockSubscriptionService, StockSubscriptionService>();
serviceCollection.AddTransient<INotificationHandler<BackInStock>, BackInStockSendEmailHandler>();

Copy link
Member

Choose a reason for hiding this comment

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

Suggested change


GlobalConfiguration.RegisterAngularModule("simplAdmin.inventory");
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace SimplCommerce.Module.Inventory.Services
{
public interface IStockSubscriptionService
{
Task BackInStockSubscribeAsync(long productId, string customerEmail);

Task BackInStockSendNotificationsAsync(long productId);
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using MediatR;
using Microsoft.EntityFrameworkCore;
using SimplCommerce.Infrastructure.Data;
using SimplCommerce.Module.Catalog.Models;
using SimplCommerce.Module.Inventory.Event;
using SimplCommerce.Module.Inventory.Models;

namespace SimplCommerce.Module.Inventory.Services
Expand All @@ -13,12 +15,14 @@ public class StockService : IStockService
private readonly IRepository<Stock> _stockRepository;
private readonly IRepository<Product> _productRepository;
private readonly IRepository<StockHistory> _stockHistoryRepository;
private readonly IMediator _mediator;

public StockService(IRepository<Stock> stockRepository, IRepository<Product> productRepository, IRepository<StockHistory> stockHistoryRepository)
public StockService(IRepository<Stock> stockRepository, IRepository<Product> productRepository, IRepository<StockHistory> stockHistoryRepository, IMediator mediator)
{
_stockRepository = stockRepository;
_productRepository = productRepository;
_stockHistoryRepository = stockHistoryRepository;
_mediator = mediator;
}

public async Task AddAllProduct(Warehouse warehouse)
Expand All @@ -44,6 +48,8 @@ public async Task UpdateStock(StockUpdateRequest stockUpdateRequest)
var product = await _productRepository.Query().FirstOrDefaultAsync(x => x.Id == stockUpdateRequest.ProductId);
var stock = await _stockRepository.Query().FirstOrDefaultAsync(x => x.ProductId == stockUpdateRequest.ProductId && x.WarehouseId == stockUpdateRequest.WarehouseId);

var prevStockQuantity = product.StockQuantity;

Copy link
Member

Choose a reason for hiding this comment

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

Suggested change

stock.Quantity = stock.Quantity + stockUpdateRequest.AdjustedQuantity;
product.StockQuantity = product.StockQuantity + stockUpdateRequest.AdjustedQuantity;
var stockHistory = new StockHistory
Expand All @@ -58,6 +64,11 @@ public async Task UpdateStock(StockUpdateRequest stockUpdateRequest)

_stockHistoryRepository.Add(stockHistory);
await _stockHistoryRepository.SaveChangesAsync();

if (prevStockQuantity <= 0 && product.StockQuantity > 0)
{
await _mediator.Publish(new BackInStock { ProductId = product.Id });
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.EntityFrameworkCore;
using SimplCommerce.Infrastructure.Data;
using SimplCommerce.Infrastructure.Web;
using SimplCommerce.Module.Catalog.Models;
using SimplCommerce.Module.Core.Models;
using SimplCommerce.Module.Core.Services;
using SimplCommerce.Module.Inventory.Models;

namespace SimplCommerce.Module.Inventory.Services
{
public class StockSubscriptionService : IStockSubscriptionService
{
private readonly IRepository<BackInStockSubscription> _backInStockSubscriptionRepository;
private readonly IRepository<Product> _productRepository;
private readonly IEmailSender _emailSender;
private readonly IRazorViewRenderer _viewRender;

public StockSubscriptionService(IRepository<BackInStockSubscription> backInStockSubscriptionRepository, IEmailSender emailSender, IRazorViewRenderer viewRender, IRepository<Product> productRepository)
{
_backInStockSubscriptionRepository = backInStockSubscriptionRepository;
_emailSender = emailSender;
_viewRender = viewRender;
_productRepository = productRepository;
}

public async Task BackInStockSendNotificationsAsync(long productId)
{
var subscriptions = await _backInStockSubscriptionRepository
.Query()
.Where(o => o.ProductId == productId)
.ToListAsync();

var product = await _productRepository
.Query()
.Where(o => o.Id == productId)
.Include(o => o.ThumbnailImage)
.FirstOrDefaultAsync();

var emailBody = await _viewRender.RenderViewToStringAsync("/Areas/Inventory/Views/EmailTemplates/BackInStockEmail.cshtml", product);
var emailSubject = $"Back in stock";

foreach (var subscription in subscriptions)
{
await _emailSender.SendEmailAsync(subscription.CustomerEmail, emailSubject, emailBody, true);

_backInStockSubscriptionRepository.Remove(subscription);
}

await _backInStockSubscriptionRepository.SaveChangesAsync();
}

public async Task BackInStockSubscribeAsync(long productId, string customerEmail)
{
var subscription = new BackInStockSubscription
{
ProductId = productId,
CustomerEmail = customerEmail
};

_backInStockSubscriptionRepository.Add(subscription);
await _backInStockSubscriptionRepository.SaveChangesAsync();
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
await _backInStockSubscriptionRepository.SaveChangesAsync();
await _backInStockSubscriptionRepository.SaveChangesAsync();

}
}
}