529 lines
21 KiB
C#
529 lines
21 KiB
C#
using Microsoft.EntityFrameworkCore;
|
|
using Prefab.Catalog.Domain.Entities;
|
|
using Prefab.Catalog.Domain.Services;
|
|
using Prefab.Shared.Catalog.Products;
|
|
using Prefab.Catalog.Api.Data;
|
|
using Prefab.Handler;
|
|
using Shouldly;
|
|
|
|
namespace Prefab.Tests.Unit.Modules.Catalog.Domain.Services;
|
|
|
|
[Trait(TraitName.Category, TraitCategory.Unit)]
|
|
public sealed class PricingServiceShould : IDisposable
|
|
{
|
|
private readonly AppDb _writeDb;
|
|
private readonly AppDbReadOnly _readDb;
|
|
private readonly PricingService _service;
|
|
private readonly Product _model;
|
|
private readonly OptionDefinition _colorOption;
|
|
private readonly OptionValue _ivoryOptionValue;
|
|
private readonly OptionDefinition _leadLengthOption;
|
|
|
|
public PricingServiceShould()
|
|
{
|
|
var databaseName = Guid.NewGuid().ToString();
|
|
var handlerAccessor = new TestHandlerContextAccessor();
|
|
|
|
var writeOptions = new DbContextOptionsBuilder<AppDb>()
|
|
.UseInMemoryDatabase(databaseName)
|
|
.Options;
|
|
var readOptions = new DbContextOptionsBuilder<AppDbReadOnly>()
|
|
.UseInMemoryDatabase(databaseName)
|
|
.Options;
|
|
|
|
_writeDb = new AppDb(writeOptions, handlerAccessor);
|
|
_readDb = new AppDbReadOnly(readOptions, handlerAccessor);
|
|
|
|
(_model, _colorOption, _ivoryOptionValue, _leadLengthOption) = SeedCatalogAsync().GetAwaiter().GetResult();
|
|
_service = new PricingService(_readDb);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task EmitExpectedPriceForChoiceOption()
|
|
{
|
|
var request = new QuotePrice.Request(
|
|
_model.Id,
|
|
null,
|
|
new List<QuotePrice.Selection>
|
|
{
|
|
new(_colorOption.Id, _ivoryOptionValue.Id, null)
|
|
});
|
|
|
|
var response = await _service.QuoteAsync(request, TestContext.Current.CancellationToken);
|
|
|
|
response.UnitPrice.ShouldBe(13.49m);
|
|
response.Breakdown.ShouldContain(b => b.Option == _colorOption.Name && b.Delta == 0.50m);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task EmitExpectedPriceForNumericOption()
|
|
{
|
|
var request = new QuotePrice.Request(
|
|
_model.Id,
|
|
null,
|
|
new List<QuotePrice.Selection>
|
|
{
|
|
new(_leadLengthOption.Id, null, 18m)
|
|
});
|
|
|
|
var response = await _service.QuoteAsync(request, TestContext.Current.CancellationToken);
|
|
|
|
response.UnitPrice.ShouldBe(25.59m);
|
|
response.Breakdown.ShouldContain(b => b.Option == _leadLengthOption.Name && b.Delta == 12.60m);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ApplyTierPricingWhenThresholdReached()
|
|
{
|
|
var request = new QuotePrice.Request(
|
|
_model.Id,
|
|
null,
|
|
new List<QuotePrice.Selection>
|
|
{
|
|
new(_leadLengthOption.Id, null, 40m)
|
|
});
|
|
|
|
var response = await _service.QuoteAsync(request, TestContext.Current.CancellationToken);
|
|
|
|
response.UnitPrice.ShouldBe(41.99m);
|
|
response.Breakdown.ShouldContain(b => b.Option == _leadLengthOption.Name && b.Delta == 29.00m);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task RespectTierBoundaryAtTwentySixFeet()
|
|
{
|
|
var belowThreshold = new QuotePrice.Request(
|
|
_model.Id,
|
|
null,
|
|
new List<QuotePrice.Selection>
|
|
{
|
|
new(_leadLengthOption.Id, null, 25m)
|
|
});
|
|
|
|
var aboveThreshold = new QuotePrice.Request(
|
|
_model.Id,
|
|
null,
|
|
new List<QuotePrice.Selection>
|
|
{
|
|
new(_leadLengthOption.Id, null, 26m)
|
|
});
|
|
|
|
var below = await _service.QuoteAsync(belowThreshold, TestContext.Current.CancellationToken);
|
|
var above = await _service.QuoteAsync(aboveThreshold, TestContext.Current.CancellationToken);
|
|
|
|
below.UnitPrice.ShouldBe(30.49m); // base 12.99 + 25 * 0.70
|
|
above.UnitPrice.ShouldBe(33.59m); // base 12.99 + (26 * 0.60) + 5
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ApplyPercentOptionsWithoutCompounding()
|
|
{
|
|
var checker = new StubUniqueChecker();
|
|
var model = await Product.CreateModel($"Percent Model {Guid.NewGuid():N}", $"percent-model-{Guid.NewGuid():N}", null, checker, TestContext.Current.CancellationToken);
|
|
model.SetBasePrice(100m);
|
|
|
|
var markupA = await OptionDefinition.CreateChoice(model, "markup_a", "Markup A", checker, isVariantAxis: false, percentScope: null, cancellationToken: TestContext.Current.CancellationToken);
|
|
var tenPercent = markupA.AddValue("ten_percent", "Ten Percent", 10m, PriceDeltaKind.Percent);
|
|
|
|
var markupB = await OptionDefinition.CreateChoice(model, "markup_b", "Markup B", checker, isVariantAxis: false, percentScope: null, cancellationToken: TestContext.Current.CancellationToken);
|
|
var twentyPercent = markupB.AddValue("twenty_percent", "Twenty Percent", 20m, PriceDeltaKind.Percent);
|
|
|
|
_writeDb.Products.Add(model);
|
|
await _writeDb.SaveChangesAsync(TestContext.Current.CancellationToken);
|
|
|
|
var request = new QuotePrice.Request(
|
|
model.Id,
|
|
null,
|
|
new List<QuotePrice.Selection>
|
|
{
|
|
new(markupA.Id, tenPercent.Id, null),
|
|
new(markupB.Id, twentyPercent.Id, null)
|
|
});
|
|
|
|
var response = await _service.QuoteAsync(request, TestContext.Current.CancellationToken);
|
|
|
|
response.UnitPrice.ShouldBe(130m);
|
|
response.Breakdown.ShouldContain(b => b.Option == markupA.Name && b.Delta == 10m);
|
|
response.Breakdown.ShouldContain(b => b.Option == markupB.Name && b.Delta == 20m);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ApplyNumericOnlyPercentAdjustments()
|
|
{
|
|
var (model, lengthOption) = await CreateModelWithLeadLengthAsync();
|
|
|
|
var checker = new StubUniqueChecker();
|
|
var wireSize = await OptionDefinition.CreateChoice(
|
|
model,
|
|
"wire_size",
|
|
"Wire Size",
|
|
checker,
|
|
isVariantAxis: false,
|
|
percentScope: PercentScope.NumericOnly,
|
|
cancellationToken: TestContext.Current.CancellationToken);
|
|
var fifteenPercent = wireSize.AddValue("fifteen_percent", "15%", 15m, PriceDeltaKind.Percent);
|
|
|
|
_writeDb.Products.Add(model);
|
|
await _writeDb.SaveChangesAsync(TestContext.Current.CancellationToken);
|
|
|
|
var request = new QuotePrice.Request(
|
|
model.Id,
|
|
null,
|
|
new List<QuotePrice.Selection>
|
|
{
|
|
new(lengthOption.Id, null, 40m),
|
|
new(wireSize.Id, fifteenPercent.Id, null)
|
|
});
|
|
|
|
var response = await _service.QuoteAsync(request, TestContext.Current.CancellationToken);
|
|
|
|
response.UnitPrice.ShouldBe(46.34m);
|
|
response.Breakdown.ShouldContain(b => b.Option == wireSize.Name && b.Delta == 4.35m);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ApplyBasePlusNumericPercentAdjustments()
|
|
{
|
|
var (model, lengthOption) = await CreateModelWithLeadLengthAsync();
|
|
|
|
var checker = new StubUniqueChecker();
|
|
var wireSize = await OptionDefinition.CreateChoice(
|
|
model,
|
|
"wire_size_plus",
|
|
"Wire Size",
|
|
checker,
|
|
isVariantAxis: false,
|
|
percentScope: PercentScope.BasePlusNumeric,
|
|
cancellationToken: TestContext.Current.CancellationToken);
|
|
var fifteenPercent = wireSize.AddValue("fifteen_percent_plus", "15%", 15m, PriceDeltaKind.Percent);
|
|
|
|
_writeDb.Products.Add(model);
|
|
await _writeDb.SaveChangesAsync(TestContext.Current.CancellationToken);
|
|
|
|
var request = new QuotePrice.Request(
|
|
model.Id,
|
|
null,
|
|
new List<QuotePrice.Selection>
|
|
{
|
|
new(lengthOption.Id, null, 40m),
|
|
new(wireSize.Id, fifteenPercent.Id, null)
|
|
});
|
|
|
|
var response = await _service.QuoteAsync(request, TestContext.Current.CancellationToken);
|
|
|
|
response.UnitPrice.ShouldBe(48.29m);
|
|
response.Breakdown.ShouldContain(b => b.Option == wireSize.Name && Math.Abs(b.Delta - 6.30m) < 0.01m);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task QuoteCeilingTBarBoxAssemblyScenario()
|
|
{
|
|
var checker = new StubUniqueChecker();
|
|
var product = await Product.CreateModel(
|
|
$"ceiling-tbar-{Guid.NewGuid():N}",
|
|
$"ceiling-tbar-{Guid.NewGuid():N}",
|
|
null,
|
|
checker,
|
|
TestContext.Current.CancellationToken);
|
|
product.SetBasePrice(39.00m);
|
|
|
|
var blankCover = await OptionDefinition.CreateChoice(product, "blank_cover", "Blank Cover", checker, cancellationToken: TestContext.Current.CancellationToken);
|
|
var blankYes = blankCover.AddValue("yes", "Yes", 1.25m, PriceDeltaKind.Absolute);
|
|
|
|
var groundTail = await OptionDefinition.CreateChoice(product, "ground_tail", "Ground Tail", checker, cancellationToken: TestContext.Current.CancellationToken);
|
|
var groundYes = groundTail.AddValue("yes", "Yes", 0.75m, PriceDeltaKind.Absolute);
|
|
|
|
var boxColor = await OptionDefinition.CreateChoice(product, "box_color", "Box Color", checker, cancellationToken: TestContext.Current.CancellationToken);
|
|
var redColor = boxColor.AddValue("red", "Red", 0.50m, PriceDeltaKind.Absolute);
|
|
|
|
var bracketType = await OptionDefinition.CreateChoice(product, "bracket_type", "Bracket Type", checker, cancellationToken: TestContext.Current.CancellationToken);
|
|
var adjustableBracket = bracketType.AddValue("adjustable", "Adjustable", 2.00m, PriceDeltaKind.Absolute);
|
|
|
|
var dedicatedDropwire = await OptionDefinition.CreateChoice(product, "dedicated_dropwire", "Dedicated Dropwire", checker, cancellationToken: TestContext.Current.CancellationToken);
|
|
var dropwireYes = dedicatedDropwire.AddValue("yes", "Yes", 3.00m, PriceDeltaKind.Absolute);
|
|
|
|
var flexWhip = await OptionDefinition.CreateNumber(
|
|
product,
|
|
"flex_whip_length",
|
|
"Flex Whip Length",
|
|
"ft",
|
|
1m,
|
|
100m,
|
|
1m,
|
|
0.85m,
|
|
checker,
|
|
cancellationToken: TestContext.Current.CancellationToken);
|
|
|
|
_writeDb.Products.Add(product);
|
|
await _writeDb.SaveChangesAsync(TestContext.Current.CancellationToken);
|
|
|
|
var request = new QuotePrice.Request(
|
|
product.Id,
|
|
null,
|
|
new List<QuotePrice.Selection>
|
|
{
|
|
new(blankCover.Id, blankYes.Id, null),
|
|
new(groundTail.Id, groundYes.Id, null),
|
|
new(boxColor.Id, redColor.Id, null),
|
|
new(bracketType.Id, adjustableBracket.Id, null),
|
|
new(dedicatedDropwire.Id, dropwireYes.Id, null),
|
|
new(flexWhip.Id, null, 10m)
|
|
});
|
|
|
|
var response = await _service.QuoteAsync(request, TestContext.Current.CancellationToken);
|
|
|
|
response.UnitPrice.ShouldBe(55.00m);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task QuoteFanHangerAssemblyScenario()
|
|
{
|
|
var checker = new StubUniqueChecker();
|
|
var product = await Product.CreateModel(
|
|
$"fan-hanger-{Guid.NewGuid():N}",
|
|
$"fan-hanger-{Guid.NewGuid():N}",
|
|
null,
|
|
checker,
|
|
TestContext.Current.CancellationToken);
|
|
product.SetBasePrice(42.00m);
|
|
|
|
var groundTail = await OptionDefinition.CreateChoice(product, "ground_tail", "Ground Tail", checker, cancellationToken: TestContext.Current.CancellationToken);
|
|
var groundYes = groundTail.AddValue("yes", "Yes", 0.75m, PriceDeltaKind.Absolute);
|
|
|
|
var blankCover = await OptionDefinition.CreateChoice(product, "blank_cover", "Blank Cover", checker, cancellationToken: TestContext.Current.CancellationToken);
|
|
var blankYes = blankCover.AddValue("yes", "Yes", 1.25m, PriceDeltaKind.Absolute);
|
|
|
|
var dedicatedDropwire = await OptionDefinition.CreateChoice(product, "dedicated_dropwire", "Dedicated Dropwire", checker, cancellationToken: TestContext.Current.CancellationToken);
|
|
var dropwireYes = dedicatedDropwire.AddValue("yes", "Yes", 3.00m, PriceDeltaKind.Absolute);
|
|
|
|
_writeDb.Products.Add(product);
|
|
await _writeDb.SaveChangesAsync(TestContext.Current.CancellationToken);
|
|
|
|
var request = new QuotePrice.Request(
|
|
product.Id,
|
|
null,
|
|
new List<QuotePrice.Selection>
|
|
{
|
|
new(groundTail.Id, groundYes.Id, null),
|
|
new(blankCover.Id, blankYes.Id, null),
|
|
new(dedicatedDropwire.Id, dropwireYes.Id, null)
|
|
});
|
|
|
|
var response = await _service.QuoteAsync(request, TestContext.Current.CancellationToken);
|
|
|
|
response.UnitPrice.ShouldBe(47.00m);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task QuoteRaisedDeviceAssemblyScenario()
|
|
{
|
|
var checker = new StubUniqueChecker();
|
|
var product = await Product.CreateModel(
|
|
$"raised-device-{Guid.NewGuid():N}",
|
|
$"raised-device-{Guid.NewGuid():N}",
|
|
null,
|
|
checker,
|
|
TestContext.Current.CancellationToken);
|
|
product.SetBasePrice(29.00m);
|
|
|
|
var deviceColor = await OptionDefinition.CreateChoice(product, "device_color", "Device Color", checker, cancellationToken: TestContext.Current.CancellationToken);
|
|
var ivory = deviceColor.AddValue("ivory", "Ivory", 0.50m, PriceDeltaKind.Absolute);
|
|
|
|
var grade = await OptionDefinition.CreateChoice(product, "grade", "Grade", checker, cancellationToken: TestContext.Current.CancellationToken);
|
|
var spec = grade.AddValue("spec", "Spec", 1.00m, PriceDeltaKind.Absolute);
|
|
|
|
var boxSize = await OptionDefinition.CreateChoice(product, "box_size", "Box Size", checker, cancellationToken: TestContext.Current.CancellationToken);
|
|
var largeBox = boxSize.AddValue("4_11_16", "4-11/16\" Square", 0.75m, PriceDeltaKind.Absolute);
|
|
|
|
var leadLength = await OptionDefinition.CreateNumber(
|
|
product,
|
|
"lead_length",
|
|
"Lead Length",
|
|
"ft",
|
|
1m,
|
|
100m,
|
|
1m,
|
|
0.70m,
|
|
checker,
|
|
cancellationToken: TestContext.Current.CancellationToken);
|
|
leadLength.AddTier(26m, null, 0.60m, 5.00m);
|
|
|
|
var wireSize = await OptionDefinition.CreateChoice(product, "wire_size", "Wire Size", checker, percentScope: PercentScope.NumericOnly, cancellationToken: TestContext.Current.CancellationToken);
|
|
_ = wireSize.AddValue("awg14", "AWG 14", 0m, PriceDeltaKind.Percent);
|
|
var awg12 = wireSize.AddValue("awg12", "AWG 12", 15m, PriceDeltaKind.Percent);
|
|
|
|
var wagos = await OptionDefinition.CreateChoice(product, "wagos", "Wago Connectors", checker, cancellationToken: TestContext.Current.CancellationToken);
|
|
var wagosYes = wagos.AddValue("yes", "Yes", 2.00m, PriceDeltaKind.Absolute);
|
|
|
|
_writeDb.Products.Add(product);
|
|
await _writeDb.SaveChangesAsync(TestContext.Current.CancellationToken);
|
|
|
|
var request = new QuotePrice.Request(
|
|
product.Id,
|
|
null,
|
|
new List<QuotePrice.Selection>
|
|
{
|
|
new(deviceColor.Id, ivory.Id, null),
|
|
new(grade.Id, spec.Id, null),
|
|
new(boxSize.Id, largeBox.Id, null),
|
|
new(leadLength.Id, null, 30m),
|
|
new(wireSize.Id, awg12.Id, null),
|
|
new(wagos.Id, wagosYes.Id, null)
|
|
});
|
|
|
|
var response = await _service.QuoteAsync(request, TestContext.Current.CancellationToken);
|
|
|
|
response.UnitPrice.ShouldBe(59.70m);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task QuotePigtailedDeviceScenario()
|
|
{
|
|
var checker = new StubUniqueChecker();
|
|
var product = await Product.CreateModel(
|
|
$"pigtailed-device-{Guid.NewGuid():N}",
|
|
$"pigtailed-device-{Guid.NewGuid():N}",
|
|
null,
|
|
checker,
|
|
TestContext.Current.CancellationToken);
|
|
product.SetBasePrice(24.00m);
|
|
|
|
var deviceType = await OptionDefinition.CreateChoice(product, "device_type", "Device Type", checker, cancellationToken: TestContext.Current.CancellationToken);
|
|
var gfci15 = deviceType.AddValue("gfci15", "GFCI 15A", 0m, PriceDeltaKind.Absolute);
|
|
|
|
var deviceColor = await OptionDefinition.CreateChoice(product, "device_color", "Device Color", checker, cancellationToken: TestContext.Current.CancellationToken);
|
|
var ivory = deviceColor.AddValue("ivory", "Ivory", 0.50m, PriceDeltaKind.Absolute);
|
|
|
|
var grade = await OptionDefinition.CreateChoice(product, "grade", "Grade", checker, cancellationToken: TestContext.Current.CancellationToken);
|
|
var spec = grade.AddValue("spec", "Spec", 1.00m, PriceDeltaKind.Absolute);
|
|
|
|
var leadLength = await OptionDefinition.CreateNumber(
|
|
product,
|
|
"lead_length",
|
|
"Lead Length",
|
|
"ft",
|
|
1m,
|
|
100m,
|
|
1m,
|
|
0.55m,
|
|
checker,
|
|
cancellationToken: TestContext.Current.CancellationToken);
|
|
leadLength.AddTier(50m, null, 0.45m, 4.00m);
|
|
|
|
var wireSize = await OptionDefinition.CreateChoice(product, "wire_size", "Wire Size", checker, percentScope: PercentScope.NumericOnly, cancellationToken: TestContext.Current.CancellationToken);
|
|
_ = wireSize.AddValue("awg14", "AWG 14", 0m, PriceDeltaKind.Percent);
|
|
var awg12 = wireSize.AddValue("awg12", "AWG 12", 15m, PriceDeltaKind.Percent);
|
|
|
|
var wagos = await OptionDefinition.CreateChoice(product, "wagos", "Wago Connectors", checker, cancellationToken: TestContext.Current.CancellationToken);
|
|
var wagosYes = wagos.AddValue("yes", "Yes", 2.00m, PriceDeltaKind.Absolute);
|
|
|
|
_writeDb.Products.Add(product);
|
|
await _writeDb.SaveChangesAsync(TestContext.Current.CancellationToken);
|
|
|
|
var request = new QuotePrice.Request(
|
|
product.Id,
|
|
null,
|
|
new List<QuotePrice.Selection>
|
|
{
|
|
new(deviceType.Id, gfci15.Id, null),
|
|
new(deviceColor.Id, ivory.Id, null),
|
|
new(grade.Id, spec.Id, null),
|
|
new(leadLength.Id, null, 60m),
|
|
new(wireSize.Id, awg12.Id, null),
|
|
new(wagos.Id, wagosYes.Id, null)
|
|
});
|
|
|
|
var response = await _service.QuoteAsync(request, TestContext.Current.CancellationToken);
|
|
|
|
response.UnitPrice.ShouldBe(63.15m);
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
_writeDb.Dispose();
|
|
_readDb.Dispose();
|
|
GC.SuppressFinalize(this);
|
|
}
|
|
|
|
private async Task<(Product Model, OptionDefinition ColorOption, OptionValue IvoryValue, OptionDefinition LeadLengthOption)> SeedCatalogAsync()
|
|
{
|
|
var checker = new StubUniqueChecker();
|
|
var model = await Product.CreateModel("Prefab ATR Box Light", "prefab-atr-box-light", null, checker, TestContext.Current.CancellationToken);
|
|
model.SetBasePrice(12.99m);
|
|
|
|
var colorOption = await OptionDefinition.CreateChoice(model, "color", "Color", checker, isVariantAxis: false, cancellationToken: TestContext.Current.CancellationToken);
|
|
_ = colorOption.AddValue("white", "White", 0m, PriceDeltaKind.Absolute);
|
|
var ivoryValue = colorOption.AddValue("ivory", "Ivory", 0.50m, PriceDeltaKind.Absolute);
|
|
|
|
var leadLengthOption = await OptionDefinition.CreateNumber(
|
|
model,
|
|
"lead_length",
|
|
"Lead Length",
|
|
"ft",
|
|
1m,
|
|
100m,
|
|
1m,
|
|
0.70m,
|
|
uniqueChecker: checker,
|
|
cancellationToken: TestContext.Current.CancellationToken);
|
|
leadLengthOption.AddTier(26m, null, 0.60m, 5m);
|
|
|
|
_writeDb.Products.Add(model);
|
|
await _writeDb.SaveChangesAsync(TestContext.Current.CancellationToken);
|
|
|
|
return (model, colorOption, ivoryValue, leadLengthOption);
|
|
}
|
|
|
|
private async Task<(Product Model, OptionDefinition LeadLengthOption)> CreateModelWithLeadLengthAsync()
|
|
{
|
|
var checker = new StubUniqueChecker();
|
|
var model = await Product.CreateModel($"Lead Model {Guid.NewGuid():N}", $"lead-model-{Guid.NewGuid():N}", null, checker, TestContext.Current.CancellationToken);
|
|
model.SetBasePrice(12.99m);
|
|
|
|
var leadLengthOption = await OptionDefinition.CreateNumber(
|
|
model,
|
|
$"lead_length_{Guid.NewGuid():N}",
|
|
"Lead Length",
|
|
"ft",
|
|
1m,
|
|
100m,
|
|
1m,
|
|
0.70m,
|
|
uniqueChecker: checker,
|
|
cancellationToken: TestContext.Current.CancellationToken);
|
|
leadLengthOption.AddTier(26m, null, 0.60m, 5m);
|
|
|
|
return (model, leadLengthOption);
|
|
}
|
|
|
|
private sealed class TestHandlerContextAccessor : IHandlerContextAccessor
|
|
{
|
|
public HandlerContext? Current => null;
|
|
|
|
public IDisposable Set(HandlerContext handlerContext) => new Noop();
|
|
|
|
private sealed class Noop : IDisposable
|
|
{
|
|
public void Dispose()
|
|
{
|
|
}
|
|
}
|
|
}
|
|
|
|
private sealed class StubUniqueChecker : IUniqueChecker
|
|
{
|
|
public Task<bool> CategoryNameIsUnique(string name, CancellationToken cancellationToken = default) => Task.FromResult(true);
|
|
|
|
public Task<bool> ProductModelNameIsUnique(string name, CancellationToken cancellationToken = default) => Task.FromResult(true);
|
|
|
|
public Task<bool> ProductSlugIsUnique(string? slug, CancellationToken cancellationToken = default) => Task.FromResult(true);
|
|
|
|
public Task<bool> ProductSkuIsUnique(string sku, CancellationToken cancellationToken = default) => Task.FromResult(true);
|
|
|
|
public Task<bool> OptionCodeIsUniqueForProduct(Guid productId, string code, CancellationToken cancellationToken = default) => Task.FromResult(true);
|
|
}
|
|
}
|
|
|
|
|
|
|