9.3 The low-stock check
Staff hear about low stock through whatever the shop uses to reach them. Shopfront logs it for now, behind an interface, so a real channel replaces one class later.
Staff/IStaffNotifier.cs:
namespace Shopfront.Staff;
public interface IStaffNotifier
{
Task Notify(string message, CancellationToken cancellationToken);
}Staff/LoggingStaffNotifier.cs:
namespace Shopfront.Staff;
// Writes each message to the log. A real shop sends it to a channel staff read.
public class LoggingStaffNotifier(ILogger<LoggingStaffNotifier> logger) : IStaffNotifier
{
public Task Notify(string message, CancellationToken cancellationToken)
{
logger.LogWarning("For staff: {Message}", message);
return Task.CompletedTask;
}
}The check's input carries the one setting it has: how few is too few. Being stored with the manifest, it can be changed without a release.
Trains/LowStockCheck/LowStockCheckInput.cs:
using Trax.Effect.Models.Manifest;
namespace Shopfront.Trains.LowStockCheck;
public record LowStockCheckInput : IManifestProperties
{
public int Threshold { get; init; } = 3;
}Trains/LowStockCheck/LowStockCheckOutput.cs:
namespace Shopfront.Trains.LowStockCheck;
public record LowStockCheckOutput(List<string> LowSkus);Trains/LowStockCheck/ILowStockCheckTrain.cs:
using Trax.Effect.Services.ServiceTrain;
namespace Shopfront.Trains.LowStockCheck;
public interface ILowStockCheckTrain : IServiceTrain<LowStockCheckInput, LowStockCheckOutput>;Trains/LowStockCheck/Junctions/FindLowStockJunction.cs:
using Microsoft.EntityFrameworkCore;
using Shopfront.Data;
using Trax.Effect.Services.EffectJunction;
namespace Shopfront.Trains.LowStockCheck.Junctions;
internal class FindLowStockJunction(IShopfrontDbContext db)
: EffectJunction<LowStockCheckInput, LowStockCheckOutput>
{
public override async Task<LowStockCheckOutput> Run(LowStockCheckInput input) =>
new(
await db
.Products.Where(p => p.Stock <= input.Threshold)
.OrderBy(p => p.Sku)
.Select(p => p.Sku)
.ToListAsync(CancellationToken)
);
}Trains/LowStockCheck/Junctions/NotifyStaffJunction.cs:
using Shopfront.Staff;
using Trax.Core.Functional;
using Trax.Effect.Services.EffectJunction;
namespace Shopfront.Trains.LowStockCheck.Junctions;
internal class NotifyStaffJunction(IStaffNotifier staff) : EffectJunction<LowStockCheckOutput, Unit>
{
public override async Task<Unit> Run(LowStockCheckOutput low)
{
if (low.LowSkus.Count > 0)
await staff.Notify($"Low on {string.Join(", ", low.LowSkus)}.", CancellationToken);
return Unit.Default;
}
}Trains/LowStockCheck/LowStockCheckTrain.cs:
using Shopfront.Trains.LowStockCheck.Junctions;
using Trax.Core.Functional;
using Trax.Effect.Services.ServiceTrain;
namespace Shopfront.Trains.LowStockCheck;
public class LowStockCheckTrain
: ServiceTrain<LowStockCheckInput, LowStockCheckOutput>,
ILowStockCheckTrain
{
protected override Task<Either<Exception, LowStockCheckOutput>> Junctions() =>
Chain<FindLowStockJunction>().Chain<NotifyStaffJunction>().Resolve();
}NotifyStaffJunction returns Unit, so Memory still holds the LowStockCheckOutput the first
junction made when the chain resolves, and that is the train's output.
The notifier is registered with the rest of the shop.
Extensions/ShopfrontServiceCollectionExtensions.cs:
using Microsoft.EntityFrameworkCore;
using Shopfront.Auth;
using Shopfront.Data;
using Shopfront.Failures;
using Shopfront.Fraud;
using Shopfront.Inventory;
using Shopfront.Payments;
using Shopfront.Staff;
using Trax.Core.Decisions;
using Trax.Effect.Services.FailureClassifier;
namespace Shopfront.Extensions;
public static class ShopfrontServiceCollectionExtensions
{
// Everything Shopfront's trains need, apart from Trax itself.
public static IServiceCollection AddShopfront(
this IServiceCollection services,
string connectionString
) =>
services
// One context per scope, built for that scope's caller: a pool would share it.
.AddDbContextFactory<ShopfrontDbContext>(
options => options.UseNpgsql(connectionString),
ServiceLifetime.Scoped
)
.AddScoped<IShopfrontDbContext>(sp =>
sp.GetRequiredService<IDbContextFactory<ShopfrontDbContext>>().CreateDbContext()
)
.AddScoped<IShopfrontCaller, TraxShopfrontCaller>()
.AddScoped<Catalogue>()
.AddSingleton<IPaymentProvider, FakePaymentProvider>()
.AddSingleton<IFailureClassifier, ShopfrontFailureClassifier>()
.AddSingleton<IDecider>(FraudRules.Live())
.AddSingleton<ICandidateFraudDecider, StricterFraudRules>()
.AddSingleton<IStaffNotifier, LoggingStaffNotifier>();
}SDK Reference
AddScheduler | Schedule | Scheduling helpers | Exclusions | Scheduling options | Dependent trains
Next: 9.4 Schedule them