998 lines
49 KiB
C#
998 lines
49 KiB
C#
using System.Globalization;
|
|
using System.Text.Json;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using Microsoft.Extensions.Logging;
|
|
using Prefab.Catalog.Data.SeedSupport;
|
|
using Prefab.Catalog.Domain.Entities;
|
|
using Prefab.Catalog.Domain.Services;
|
|
using Prefab.Data.Entities;
|
|
using Prefab.Data.Seeder;
|
|
|
|
namespace Prefab.Catalog.Data;
|
|
|
|
public class Seeder : PrefabDbSeeder<Seeder, IModuleDb>
|
|
{
|
|
private static readonly CategorySeed[] Categories =
|
|
{
|
|
new("ceiling-supports", "Ceiling Supports", 1, true),
|
|
new("boxes-and-covers", "Boxes & Covers", 2, true),
|
|
new("prefab-assemblies", "Prefabricated Assemblies", 3, true),
|
|
new("rod-strut-hardware", "Rod & Strut Hardware", 4, false)
|
|
};
|
|
|
|
private const string LoadGuidanceKey = "catalog.product.safety.load_guidance";
|
|
private const string RequireGroundTailKey = "catalog.product.safety.require_ground_tail";
|
|
private const string CatalogDocsKey = "catalog.product.docs";
|
|
private const string FaceStyleAttributeName = "face_style";
|
|
private const string FaceStyleValue = "Decora";
|
|
private const string BoxSizeAttributeName = "box_size_in";
|
|
private const string BoxDepthAttributeName = "box_depth_in";
|
|
private const string BoxCapacityAttributeName = "box_capacity";
|
|
private const string BoxConstructionAttributeName = "box_construction";
|
|
private const string BoxSideKnockoutsAttributeName = "box_side_knockouts";
|
|
private const string BoxBottomKnockoutsAttributeName = "box_bottom_knockouts";
|
|
private const string BoxMaterialAttributeName = "box_material";
|
|
private const string AreaClassificationAttributeName = "area_classification";
|
|
private const string RodLengthAttributeName = "rod_length_in";
|
|
private const string RodDiameterAttributeName = "rod_diameter_in";
|
|
private const string RodFinishAttributeName = "rod_finish";
|
|
private const string RodMaterialAttributeName = "rod_material";
|
|
private const string BoxesCoversDocTitle = "Eaton 4\" Steel Square Overview";
|
|
private const string BoxesCoversDocUrl = "https://www.eaton.com/us/en-us/catalog/crouse-hinds/4-inch-square-steel-boxes.html";
|
|
private const string RodStrutDocTitle = "Eaton Channel Nuts, U-Bolts, ATR & Hardware";
|
|
private const string RodStrutDocUrl = "https://www.eaton.com/us/en-us/catalog/crouse-hinds/channel-nuts-u-bolts-threaded-rod.html";
|
|
private static readonly JsonSerializerOptions JsonOptions = new()
|
|
{
|
|
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
|
WriteIndented = false
|
|
};
|
|
|
|
protected override Task ApplyViews(IModuleDb db, IServiceProvider serviceProvider, CancellationToken cancellationToken) =>
|
|
Task.CompletedTask;
|
|
|
|
protected override async Task SeedData(IModuleDb db, IServiceProvider serviceProvider, CancellationToken cancellationToken)
|
|
{
|
|
var logger = serviceProvider.GetRequiredService<ILogger<Seeder>>();
|
|
var checker = serviceProvider.GetRequiredService<IUniqueChecker>();
|
|
|
|
logger.LogInformation("Seeding catalog MVP products and categories.");
|
|
|
|
var categories = await EnsureCategoriesAsync(db, checker, logger, cancellationToken);
|
|
|
|
var ceilingSupports = categories["ceiling-supports"];
|
|
var boxesCovers = categories["boxes-and-covers"];
|
|
var prefabAssemblies = categories["prefab-assemblies"];
|
|
var rodStrutHardware = categories["rod-strut-hardware"];
|
|
|
|
var faceStyleDefinition = await EnsureAttributeDefinitionAsync(db, FaceStyleAttributeName, AttributeDataType.Text, cancellationToken);
|
|
|
|
await EnsureCeilingTBarAssemblyAsync(db, checker, ceilingSupports, faceStyleDefinition, cancellationToken);
|
|
await EnsureFanHangerAssemblyAsync(db, checker, ceilingSupports, cancellationToken);
|
|
await EnsureFourInchSteelSquareAsync(db, checker, boxesCovers, cancellationToken);
|
|
await EnsureAllThreadedRodAsync(db, checker, rodStrutHardware, cancellationToken);
|
|
await EnsureRaisedDeviceAssemblyAsync(db, checker, prefabAssemblies, faceStyleDefinition, cancellationToken);
|
|
await EnsurePigtailedDeviceAsync(db, checker, prefabAssemblies, faceStyleDefinition, cancellationToken);
|
|
|
|
await db.SaveChangesAsync(cancellationToken);
|
|
|
|
logger.LogInformation("Catalog MVP seeding complete.");
|
|
}
|
|
|
|
private static async Task<Dictionary<string, Category>> EnsureCategoriesAsync(
|
|
IModuleDb db,
|
|
IUniqueChecker checker,
|
|
ILogger logger,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
var categories = new Dictionary<string, Category>(StringComparer.OrdinalIgnoreCase);
|
|
|
|
foreach (var seed in Categories)
|
|
{
|
|
var existing = await db.Categories
|
|
.FirstOrDefaultAsync(c => c.Slug == seed.Slug, cancellationToken);
|
|
|
|
if (existing is null)
|
|
{
|
|
var category = await Category.Create(seed.Name, null, checker, cancellationToken);
|
|
category.ConfigureMetadata(seed.Slug, seed.DisplayOrder, seed.IsFeatured, null, null);
|
|
db.Categories.Add(category);
|
|
categories[seed.Slug] = category;
|
|
logger.LogInformation("Created category {Slug}.", seed.Slug);
|
|
continue;
|
|
}
|
|
|
|
if (!string.Equals(existing.Name, seed.Name, StringComparison.Ordinal))
|
|
{
|
|
await existing.Rename(seed.Name, checker, cancellationToken);
|
|
}
|
|
|
|
existing.ConfigureMetadata(seed.Slug, seed.DisplayOrder, seed.IsFeatured, existing.HeroImageUrl, existing.Icon);
|
|
categories[seed.Slug] = existing;
|
|
}
|
|
|
|
return categories;
|
|
}
|
|
|
|
private static async Task EnsureCeilingTBarAssemblyAsync(
|
|
IModuleDb db,
|
|
IUniqueChecker checker,
|
|
Category category,
|
|
AttributeDefinition faceStyleDefinition,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
const string slug = "ceiling-tbar-box-assembly";
|
|
var product = await LoadModelAsync(db, slug, cancellationToken);
|
|
|
|
if (product is null)
|
|
{
|
|
product = await Product.CreateModel(
|
|
name: "Ceiling T-Bar Box Assembly",
|
|
slug,
|
|
description: "Factory-assembled T-Bar box light assembly with flexible bracket options.",
|
|
checker,
|
|
cancellationToken);
|
|
|
|
db.Products.Add(product);
|
|
}
|
|
else if (!string.Equals(product.Name, "Ceiling T-Bar Box Assembly", StringComparison.Ordinal))
|
|
{
|
|
await product.Rename("Ceiling T-Bar Box Assembly", checker, cancellationToken);
|
|
}
|
|
|
|
product.SetBasePrice(39.00m);
|
|
product.ChangeDescription("Factory-assembled T-Bar box light assembly with flexible bracket options.");
|
|
product.AssignToCategory(category.Id, true);
|
|
|
|
var boxShape = await EnsureChoiceOptionAsync(product, checker, "box_shape", "Box Shape", isVariantAxis: true, cancellationToken: cancellationToken);
|
|
var octagon = EnsureChoiceValue(boxShape, "octagon", "Octagon", 0m, PriceDeltaKind.Absolute);
|
|
var square = EnsureChoiceValue(boxShape, "square", "Square", 0m, PriceDeltaKind.Absolute);
|
|
|
|
var boxDepth = await EnsureChoiceOptionAsync(product, checker, "box_depth", "Box Depth", isVariantAxis: true, cancellationToken: cancellationToken);
|
|
var depth15 = EnsureChoiceValue(boxDepth, "1_5", "1.5 in", 0m, PriceDeltaKind.Absolute);
|
|
var depth2125 = EnsureChoiceValue(boxDepth, "2_125", "2.125 in", 0m, PriceDeltaKind.Absolute);
|
|
|
|
var blankCover = await EnsureChoiceOptionAsync(product, checker, "blank_cover", "Blank Cover", cancellationToken: cancellationToken);
|
|
EnsureChoiceValue(blankCover, "no", "No", 0m, PriceDeltaKind.Absolute);
|
|
EnsureChoiceValue(blankCover, "yes", "Yes", 1.25m, PriceDeltaKind.Absolute);
|
|
|
|
var circuitMarking = await EnsureChoiceOptionAsync(product, checker, "circuit_marking", "Circuit Marking", cancellationToken: cancellationToken);
|
|
EnsureChoiceValue(circuitMarking, "none", "None", 0m, PriceDeltaKind.Absolute);
|
|
EnsureChoiceValue(circuitMarking, "laser", "Laser", 0m, PriceDeltaKind.Absolute);
|
|
EnsureChoiceValue(circuitMarking, "stamped", "Stamped", 0m, PriceDeltaKind.Absolute);
|
|
|
|
var groundTail = await EnsureChoiceOptionAsync(product, checker, "ground_tail", "Ground Tail", cancellationToken: cancellationToken);
|
|
EnsureChoiceValue(groundTail, "no", "No", 0m, PriceDeltaKind.Absolute);
|
|
EnsureChoiceValue(groundTail, "yes", "Yes", 0.75m, PriceDeltaKind.Absolute);
|
|
|
|
var boxColor = await EnsureChoiceOptionAsync(product, checker, "box_color", "Box Color", cancellationToken: cancellationToken);
|
|
EnsureChoiceValue(boxColor, "white", "White", 0m, PriceDeltaKind.Absolute);
|
|
EnsureChoiceValue(boxColor, "red", "Red", 0.50m, PriceDeltaKind.Absolute);
|
|
|
|
var bracketType = await EnsureChoiceOptionAsync(product, checker, "bracket_type", "Bracket Type", cancellationToken: cancellationToken);
|
|
var fixedBracket = EnsureChoiceValue(bracketType, "fixed_24in", "Fixed 24\"", 0m, PriceDeltaKind.Absolute);
|
|
var adjustableBracket = EnsureChoiceValue(bracketType, "adjustable", "Adjustable", 2.00m, PriceDeltaKind.Absolute);
|
|
|
|
var dedicatedDropwire = await EnsureChoiceOptionAsync(product, checker, "dedicated_dropwire", "Dedicated Dropwire", cancellationToken: cancellationToken);
|
|
EnsureChoiceValue(dedicatedDropwire, "no", "No", 0m, PriceDeltaKind.Absolute);
|
|
var dedicatedYes = EnsureChoiceValue(dedicatedDropwire, "yes", "Yes", 3.00m, PriceDeltaKind.Absolute);
|
|
|
|
var flexWhip = await EnsureNumberOptionAsync(
|
|
product,
|
|
checker,
|
|
"flex_whip_length",
|
|
"Flex Whip Length",
|
|
unit: "ft",
|
|
min: 1m,
|
|
max: 100m,
|
|
step: 1m,
|
|
pricePerUnit: 0.85m,
|
|
cancellationToken);
|
|
|
|
AddTierIfMissing(flexWhip, 26m, null, 0.60m, 5.00m);
|
|
|
|
AddRuleIfMissing(
|
|
product,
|
|
OptionRuleTargetKind.OptionDefinition,
|
|
boxColor.Id,
|
|
RuleEffect.Show,
|
|
() => OptionRuleSetBuilder
|
|
.ForDefinition(product, boxColor, RuleEffect.Show, RuleMode.All)
|
|
.WhenEquals(boxShape, square)
|
|
.Build());
|
|
|
|
AddRuleIfMissing(
|
|
product,
|
|
OptionRuleTargetKind.OptionDefinition,
|
|
dedicatedDropwire.Id,
|
|
RuleEffect.Show,
|
|
() => OptionRuleSetBuilder
|
|
.ForDefinition(product, dedicatedDropwire, RuleEffect.Show, RuleMode.All)
|
|
.WhenEquals(bracketType, adjustableBracket)
|
|
.Build());
|
|
|
|
await UpsertGenericAttributeAsync(db, product, LoadGuidanceKey, "Adjustable bracket assemblies require an independent support.", cancellationToken);
|
|
|
|
await EnsureVariantAsync(product, checker, "TBA-OCT-15", $"Octagon / {depth15.Label}", 39.00m, cancellationToken, (boxShape, octagon), (boxDepth, depth15));
|
|
await EnsureVariantAsync(product, checker, "TBA-OCT-2125", $"Octagon / {depth2125.Label}", 39.00m, cancellationToken, (boxShape, octagon), (boxDepth, depth2125));
|
|
await EnsureVariantAsync(product, checker, "TBA-SQR-15", $"Square / {depth15.Label}", 39.00m, cancellationToken, (boxShape, square), (boxDepth, depth15));
|
|
await EnsureVariantAsync(product, checker, "TBA-SQR-2125", $"Square / {depth2125.Label}", 39.00m, cancellationToken, (boxShape, square), (boxDepth, depth2125));
|
|
}
|
|
|
|
private static async Task EnsureFanHangerAssemblyAsync(
|
|
IModuleDb db,
|
|
IUniqueChecker checker,
|
|
Category category,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
const string slug = "fan-hanger-assembly";
|
|
var product = await LoadModelAsync(db, slug, cancellationToken);
|
|
|
|
if (product is null)
|
|
{
|
|
product = await Product.CreateModel(
|
|
name: "Fan Hanger Assembly",
|
|
slug,
|
|
description: "Pre-assembled fan hanger with KwikWire suspension kits.",
|
|
checker,
|
|
cancellationToken);
|
|
db.Products.Add(product);
|
|
}
|
|
else if (!string.Equals(product.Name, "Fan Hanger Assembly", StringComparison.Ordinal))
|
|
{
|
|
await product.Rename("Fan Hanger Assembly", checker, cancellationToken);
|
|
}
|
|
|
|
product.SetBasePrice(42.00m);
|
|
product.ChangeDescription("Pre-assembled fan hanger with KwikWire suspension kits.");
|
|
product.AssignToCategory(category.Id, true);
|
|
|
|
var suspensionKit = await EnsureChoiceOptionAsync(product, checker, "suspension_kit", "Suspension Kit", isVariantAxis: true, cancellationToken: cancellationToken);
|
|
var kit10 = EnsureChoiceValue(suspensionKit, "kwikwire_10ft", "KwikWire 10ft", 0m, PriceDeltaKind.Absolute);
|
|
var kit15 = EnsureChoiceValue(suspensionKit, "kwikwire_15ft", "KwikWire 15ft", 0m, PriceDeltaKind.Absolute);
|
|
|
|
var boxDepth = await EnsureChoiceOptionAsync(product, checker, "box_depth", "Box Depth", isVariantAxis: true, cancellationToken: cancellationToken);
|
|
var depth15 = EnsureChoiceValue(boxDepth, "1_5", "1.5 in", 0m, PriceDeltaKind.Absolute);
|
|
var depth2125 = EnsureChoiceValue(boxDepth, "2_125", "2.125 in", 0m, PriceDeltaKind.Absolute);
|
|
|
|
var groundTail = await EnsureChoiceOptionAsync(product, checker, "ground_tail", "Ground Tail", cancellationToken: cancellationToken);
|
|
EnsureChoiceValue(groundTail, "no", "No", 0m, PriceDeltaKind.Absolute);
|
|
EnsureChoiceValue(groundTail, "yes", "Yes", 0.75m, PriceDeltaKind.Absolute);
|
|
|
|
var blankCover = await EnsureChoiceOptionAsync(product, checker, "blank_cover", "Blank Cover", cancellationToken: cancellationToken);
|
|
EnsureChoiceValue(blankCover, "no", "No", 0m, PriceDeltaKind.Absolute);
|
|
EnsureChoiceValue(blankCover, "yes", "Yes", 1.25m, PriceDeltaKind.Absolute);
|
|
|
|
var dedicatedDropwire = await EnsureChoiceOptionAsync(product, checker, "dedicated_dropwire", "Dedicated Dropwire", cancellationToken: cancellationToken);
|
|
EnsureChoiceValue(dedicatedDropwire, "no", "No", 0m, PriceDeltaKind.Absolute);
|
|
EnsureChoiceValue(dedicatedDropwire, "yes", "Yes", 3.00m, PriceDeltaKind.Absolute);
|
|
|
|
AddRuleIfMissing(
|
|
product,
|
|
OptionRuleTargetKind.OptionDefinition,
|
|
dedicatedDropwire.Id,
|
|
RuleEffect.Show,
|
|
() => OptionRuleSetBuilder
|
|
.ForDefinition(product, dedicatedDropwire, RuleEffect.Show, RuleMode.All)
|
|
.WhenNotIn(suspensionKit, kit10, kit15)
|
|
.Build());
|
|
|
|
await UpsertGenericAttributeAsync(db, product, RequireGroundTailKey, bool.TrueString.ToLowerInvariant(), cancellationToken);
|
|
|
|
await EnsureVariantAsync(product, checker, "FH-KW10-15", $"KwikWire 10ft / {depth15.Label}", 42.00m, cancellationToken, (suspensionKit, kit10), (boxDepth, depth15));
|
|
await EnsureVariantAsync(product, checker, "FH-KW10-2125", $"KwikWire 10ft / {depth2125.Label}", 42.00m, cancellationToken, (suspensionKit, kit10), (boxDepth, depth2125));
|
|
await EnsureVariantAsync(product, checker, "FH-KW15-15", $"KwikWire 15ft / {depth15.Label}", 42.00m, cancellationToken, (suspensionKit, kit15), (boxDepth, depth15));
|
|
await EnsureVariantAsync(product, checker, "FH-KW15-2125", $"KwikWire 15ft / {depth2125.Label}", 42.00m, cancellationToken, (suspensionKit, kit15), (boxDepth, depth2125));
|
|
}
|
|
|
|
private static async Task EnsureFourInchSteelSquareAsync(
|
|
IModuleDb db,
|
|
IUniqueChecker checker,
|
|
Category category,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
const string slug = "four-inch-steel-square";
|
|
var product = await LoadModelAsync(db, slug, cancellationToken);
|
|
|
|
if (product is null)
|
|
{
|
|
product = await Product.CreateModel(
|
|
name: "4\" Square Boxes & Rings (Steel)",
|
|
slug,
|
|
description: "Steel 4\" square drawn boxes and raised rings for commercial rough-in work.",
|
|
checker,
|
|
cancellationToken);
|
|
db.Products.Add(product);
|
|
}
|
|
else if (!string.Equals(product.Name, "4\" Square Boxes & Rings (Steel)", StringComparison.Ordinal))
|
|
{
|
|
await product.Rename("4\" Square Boxes & Rings (Steel)", checker, cancellationToken);
|
|
}
|
|
|
|
product.SetBasePrice(0m);
|
|
product.ChangeDescription("Steel 4\" square drawn boxes and raised rings for commercial rough-in work.");
|
|
product.AssignToCategory(category.Id, true);
|
|
|
|
await EnsureVariantAsync(product, checker, "TP412PF", "4\" Square Outlet Box - 1.5\" Depth (Drawn) with Pigtail", 4.79m, cancellationToken);
|
|
await EnsureVariantAsync(product, checker, "TP428", "4\" Square Extension Ring - 1.5\" Depth", 3.59m, cancellationToken);
|
|
await EnsureVariantAsync(product, checker, "TP833", "4\" Square Extension Ring - 1.5\" Depth, Air-Plenum Rated", 5.29m, cancellationToken);
|
|
await EnsureVariantAsync(product, checker, "TP480", "4\" Square Mud Ring - 1-Device, Flat", 2.29m, cancellationToken);
|
|
|
|
var boxSizeDefinition = await EnsureAttributeDefinitionAsync(db, BoxSizeAttributeName, AttributeDataType.Number, cancellationToken, unit: "in");
|
|
var boxDepthDefinition = await EnsureAttributeDefinitionAsync(db, BoxDepthAttributeName, AttributeDataType.Number, cancellationToken, unit: "in");
|
|
var boxCapacityDefinition = await EnsureAttributeDefinitionAsync(db, BoxCapacityAttributeName, AttributeDataType.Text, cancellationToken);
|
|
var boxConstructionDefinition = await EnsureAttributeDefinitionAsync(db, BoxConstructionAttributeName, AttributeDataType.Enum, cancellationToken);
|
|
var boxSideKnockoutsDefinition = await EnsureAttributeDefinitionAsync(db, BoxSideKnockoutsAttributeName, AttributeDataType.Text, cancellationToken);
|
|
var boxBottomKnockoutsDefinition = await EnsureAttributeDefinitionAsync(db, BoxBottomKnockoutsAttributeName, AttributeDataType.Text, cancellationToken);
|
|
var boxMaterialDefinition = await EnsureAttributeDefinitionAsync(db, BoxMaterialAttributeName, AttributeDataType.Enum, cancellationToken);
|
|
var areaClassificationDefinition = await EnsureAttributeDefinitionAsync(db, AreaClassificationAttributeName, AttributeDataType.Enum, cancellationToken);
|
|
|
|
await UpsertProductDocsAsync(
|
|
db,
|
|
product,
|
|
new[] { (BoxesCoversDocTitle, BoxesCoversDocUrl) },
|
|
cancellationToken);
|
|
|
|
var variants = product.Variants
|
|
.Where(variant => variant.DeletedOn == null && !string.IsNullOrWhiteSpace(variant.Sku))
|
|
.ToDictionary(variant => variant.Sku!, StringComparer.OrdinalIgnoreCase);
|
|
|
|
if (variants.TryGetValue("TP412PF", out var tp412pf))
|
|
{
|
|
tp412pf.UpsertSpec(boxSizeDefinition.Id, FormatDecimal(4m), 4m, boxSizeDefinition.Unit, null);
|
|
tp412pf.UpsertSpec(boxDepthDefinition.Id, FormatDecimal(1.5m), 1.5m, boxDepthDefinition.Unit, null);
|
|
tp412pf.UpsertSpec(boxCapacityDefinition.Id, "22 cu in", null, null, null);
|
|
tp412pf.UpsertSpec(boxConstructionDefinition.Id, "Drawn", null, null, "drawn");
|
|
tp412pf.UpsertSpec(boxSideKnockoutsDefinition.Id, "(8) 3/4 in", null, null, null);
|
|
tp412pf.UpsertSpec(boxBottomKnockoutsDefinition.Id, "(3) 1/2 in, (2) 3/4 in", null, null, null);
|
|
tp412pf.UpsertSpec(boxMaterialDefinition.Id, "Steel", null, null, "steel");
|
|
}
|
|
|
|
if (variants.TryGetValue("TP428", out var tp428))
|
|
{
|
|
tp428.UpsertSpec(boxSizeDefinition.Id, FormatDecimal(4m), 4m, boxSizeDefinition.Unit, null);
|
|
tp428.UpsertSpec(boxDepthDefinition.Id, FormatDecimal(1.5m), 1.5m, boxDepthDefinition.Unit, null);
|
|
tp428.UpsertSpec(boxCapacityDefinition.Id, "22 cu in", null, null, null);
|
|
tp428.UpsertSpec(boxConstructionDefinition.Id, "Drawn", null, null, "drawn");
|
|
tp428.UpsertSpec(boxSideKnockoutsDefinition.Id, "(8) 3/4 in", null, null, null);
|
|
tp428.UpsertSpec(boxBottomKnockoutsDefinition.Id, "(3) 1/2 in, (2) 3/4 in", null, null, null);
|
|
tp428.UpsertSpec(boxMaterialDefinition.Id, "Steel", null, null, "steel");
|
|
}
|
|
|
|
if (variants.TryGetValue("TP833", out var tp833))
|
|
{
|
|
tp833.UpsertSpec(boxSizeDefinition.Id, FormatDecimal(4m), 4m, boxSizeDefinition.Unit, null);
|
|
tp833.UpsertSpec(boxDepthDefinition.Id, FormatDecimal(1.5m), 1.5m, boxDepthDefinition.Unit, null);
|
|
tp833.UpsertSpec(boxCapacityDefinition.Id, "30.3 cu in", null, null, null);
|
|
tp833.UpsertSpec(boxConstructionDefinition.Id, "Drawn", null, null, "drawn");
|
|
tp833.UpsertSpec(boxSideKnockoutsDefinition.Id, "No knockouts", null, null, null);
|
|
tp833.UpsertSpec(boxBottomKnockoutsDefinition.Id, "No knockouts", null, null, null);
|
|
tp833.UpsertSpec(boxMaterialDefinition.Id, "Steel", null, null, "steel");
|
|
tp833.UpsertSpec(areaClassificationDefinition.Id, "Air Plenum", null, null, "air-plenum");
|
|
}
|
|
|
|
if (variants.TryGetValue("TP480", out var tp480))
|
|
{
|
|
tp480.UpsertSpec(boxSizeDefinition.Id, FormatDecimal(4m), 4m, boxSizeDefinition.Unit, null);
|
|
tp480.UpsertSpec(boxDepthDefinition.Id, FormatDecimal(1.5m), 1.5m, boxDepthDefinition.Unit, null);
|
|
tp480.UpsertSpec(boxCapacityDefinition.Id, "N/A", null, null, null);
|
|
tp480.UpsertSpec(boxConstructionDefinition.Id, "Drawn", null, null, "drawn");
|
|
tp480.UpsertSpec(boxSideKnockoutsDefinition.Id, "No knockouts", null, null, null);
|
|
tp480.UpsertSpec(boxBottomKnockoutsDefinition.Id, "No knockouts", null, null, null);
|
|
tp480.UpsertSpec(boxMaterialDefinition.Id, "Steel", null, null, "steel");
|
|
tp480.UpsertSpec(areaClassificationDefinition.Id, "Air Plenum", null, null, "air-plenum");
|
|
}
|
|
}
|
|
|
|
private static async Task EnsureAllThreadedRodAsync(
|
|
IModuleDb db,
|
|
IUniqueChecker checker,
|
|
Category category,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
const string slug = "all-threaded-rod";
|
|
var product = await LoadModelAsync(db, slug, cancellationToken);
|
|
|
|
if (product is null)
|
|
{
|
|
product = await Product.CreateModel(
|
|
name: "Threaded Rod (All-Thread)",
|
|
slug,
|
|
description: "All-thread rod family with stocked diameters, lengths, and finishes for trapeze hangers and hardware kits.",
|
|
checker,
|
|
cancellationToken);
|
|
db.Products.Add(product);
|
|
}
|
|
else if (!string.Equals(product.Name, "Threaded Rod (All-Thread)", StringComparison.Ordinal))
|
|
{
|
|
await product.Rename("Threaded Rod (All-Thread)", checker, cancellationToken);
|
|
}
|
|
|
|
product.SetBasePrice(0m);
|
|
product.ChangeDescription("All-thread rod family with stocked diameters, lengths, finishes, and optional hardware packs.");
|
|
product.AssignToCategory(category.Id, true);
|
|
|
|
await UpsertGenericAttributeAsync(db, product, LoadGuidanceKey, "Verify load tables; size and finish affect ratings.", cancellationToken);
|
|
await UpsertProductDocsAsync(
|
|
db,
|
|
product,
|
|
new[] { (RodStrutDocTitle, RodStrutDocUrl) },
|
|
cancellationToken);
|
|
|
|
var lengthDefinition = await EnsureAttributeDefinitionAsync(db, RodLengthAttributeName, AttributeDataType.Number, cancellationToken, unit: "in");
|
|
var diameterDefinition = await EnsureAttributeDefinitionAsync(db, RodDiameterAttributeName, AttributeDataType.Number, cancellationToken, unit: "in");
|
|
var finishDefinition = await EnsureAttributeDefinitionAsync(db, RodFinishAttributeName, AttributeDataType.Enum, cancellationToken);
|
|
var materialDefinition = await EnsureAttributeDefinitionAsync(db, RodMaterialAttributeName, AttributeDataType.Enum, cancellationToken);
|
|
|
|
var diameterOption = await EnsureChoiceOptionAsync(product, checker, "diameter", "Diameter", isVariantAxis: true, cancellationToken: cancellationToken);
|
|
var diameterQuarter = EnsureChoiceValue(diameterOption, "1_4", "1/4\"", 0m, PriceDeltaKind.Absolute);
|
|
var diameterThreeEighths = EnsureChoiceValue(diameterOption, "3_8", "3/8\"", 0m, PriceDeltaKind.Absolute);
|
|
var diameterHalf = EnsureChoiceValue(diameterOption, "1_2", "1/2\"", 0m, PriceDeltaKind.Absolute);
|
|
|
|
var lengthOption = await EnsureChoiceOptionAsync(product, checker, "length", "Length", isVariantAxis: true, cancellationToken: cancellationToken);
|
|
var length36 = EnsureChoiceValue(lengthOption, "36_in", "36\" (3 ft)", 0m, PriceDeltaKind.Absolute);
|
|
var length72 = EnsureChoiceValue(lengthOption, "72_in", "72\" (6 ft)", 0m, PriceDeltaKind.Absolute);
|
|
var length120 = EnsureChoiceValue(lengthOption, "120_in", "120\" (10 ft)", 0m, PriceDeltaKind.Absolute);
|
|
var length144 = EnsureChoiceValue(lengthOption, "144_in", "144\" (12 ft)", 0m, PriceDeltaKind.Absolute);
|
|
|
|
var finishOption = await EnsureChoiceOptionAsync(product, checker, "finish", "Finish", isVariantAxis: true, cancellationToken: cancellationToken);
|
|
var finishZinc = EnsureChoiceValue(finishOption, "zinc", "Zinc-Plated", 0m, PriceDeltaKind.Absolute);
|
|
var finishSs304 = EnsureChoiceValue(finishOption, "ss304", "Stainless 304", 0m, PriceDeltaKind.Absolute);
|
|
|
|
var hardwarePackA = await EnsureChoiceOptionAsync(product, checker, "hardware_pack_a", "Hardware Pack A", cancellationToken: cancellationToken);
|
|
var packAYes = EnsureChoiceValue(hardwarePackA, "yes", "Yes", 12.00m, PriceDeltaKind.Absolute);
|
|
var packANo = EnsureChoiceValue(hardwarePackA, "no", "No", 0m, PriceDeltaKind.Absolute);
|
|
|
|
var packARodCoupler = await EnsureChoiceOptionAsync(product, checker, "pack_a_rod_coupler", "Pack A Rod Coupler", cancellationToken: cancellationToken);
|
|
EnsureChoiceValue(packARodCoupler, "none", "None", 0m, PriceDeltaKind.Absolute);
|
|
EnsureChoiceValue(packARodCoupler, "1_4", "1/4\"", 0m, PriceDeltaKind.Absolute);
|
|
EnsureChoiceValue(packARodCoupler, "3_8", "3/8\"", 0m, PriceDeltaKind.Absolute);
|
|
EnsureChoiceValue(packARodCoupler, "1_2", "1/2\"", 0m, PriceDeltaKind.Absolute);
|
|
|
|
var packANuts = await EnsureChoiceOptionAsync(product, checker, "pack_a_nuts", "Pack A Nuts", cancellationToken: cancellationToken);
|
|
EnsureChoiceValue(packANuts, "hex", "Hex", 0m, PriceDeltaKind.Absolute);
|
|
EnsureChoiceValue(packANuts, "jam", "Jam", 0m, PriceDeltaKind.Absolute);
|
|
|
|
var packAWashers = await EnsureChoiceOptionAsync(product, checker, "pack_a_washers", "Pack A Washers", cancellationToken: cancellationToken);
|
|
EnsureChoiceValue(packAWashers, "flat", "Flat", 0m, PriceDeltaKind.Absolute);
|
|
EnsureChoiceValue(packAWashers, "lock", "Lock", 0m, PriceDeltaKind.Absolute);
|
|
|
|
var packABeamClamp = await EnsureChoiceOptionAsync(product, checker, "pack_a_beam_clamp", "Pack A Beam Clamp", cancellationToken: cancellationToken);
|
|
EnsureChoiceValue(packABeamClamp, "bc1", "BC-1", 0m, PriceDeltaKind.Absolute);
|
|
EnsureChoiceValue(packABeamClamp, "bc2", "BC-2", 0m, PriceDeltaKind.Absolute);
|
|
|
|
var packASammy = await EnsureChoiceOptionAsync(product, checker, "pack_a_sammy", "Pack A Sammy", cancellationToken: cancellationToken);
|
|
EnsureChoiceValue(packASammy, "wood", "Wood", 0m, PriceDeltaKind.Absolute);
|
|
EnsureChoiceValue(packASammy, "steel", "Steel", 0m, PriceDeltaKind.Absolute);
|
|
|
|
var hardwarePackB = await EnsureChoiceOptionAsync(product, checker, "hardware_pack_b", "Hardware Pack B", cancellationToken: cancellationToken);
|
|
var packBYes = EnsureChoiceValue(hardwarePackB, "yes", "Yes", 12.00m, PriceDeltaKind.Absolute);
|
|
var packBNo = EnsureChoiceValue(hardwarePackB, "no", "No", 0m, PriceDeltaKind.Absolute);
|
|
|
|
_ = packANo;
|
|
_ = packBNo;
|
|
_ = length72;
|
|
|
|
var packBRodCoupler = await EnsureChoiceOptionAsync(product, checker, "pack_b_rod_coupler", "Pack B Rod Coupler", cancellationToken: cancellationToken);
|
|
EnsureChoiceValue(packBRodCoupler, "none", "None", 0m, PriceDeltaKind.Absolute);
|
|
EnsureChoiceValue(packBRodCoupler, "1_4", "1/4\"", 0m, PriceDeltaKind.Absolute);
|
|
EnsureChoiceValue(packBRodCoupler, "3_8", "3/8\"", 0m, PriceDeltaKind.Absolute);
|
|
EnsureChoiceValue(packBRodCoupler, "1_2", "1/2\"", 0m, PriceDeltaKind.Absolute);
|
|
|
|
var packBNuts = await EnsureChoiceOptionAsync(product, checker, "pack_b_nuts", "Pack B Nuts", cancellationToken: cancellationToken);
|
|
EnsureChoiceValue(packBNuts, "hex", "Hex", 0m, PriceDeltaKind.Absolute);
|
|
EnsureChoiceValue(packBNuts, "jam", "Jam", 0m, PriceDeltaKind.Absolute);
|
|
|
|
var packBWashers = await EnsureChoiceOptionAsync(product, checker, "pack_b_washers", "Pack B Washers", cancellationToken: cancellationToken);
|
|
EnsureChoiceValue(packBWashers, "flat", "Flat", 0m, PriceDeltaKind.Absolute);
|
|
EnsureChoiceValue(packBWashers, "lock", "Lock", 0m, PriceDeltaKind.Absolute);
|
|
|
|
var packBBeamClamp = await EnsureChoiceOptionAsync(product, checker, "pack_b_beam_clamp", "Pack B Beam Clamp", cancellationToken: cancellationToken);
|
|
EnsureChoiceValue(packBBeamClamp, "bc1", "BC-1", 0m, PriceDeltaKind.Absolute);
|
|
EnsureChoiceValue(packBBeamClamp, "bc2", "BC-2", 0m, PriceDeltaKind.Absolute);
|
|
|
|
var packBSammy = await EnsureChoiceOptionAsync(product, checker, "pack_b_sammy", "Pack B Sammy", cancellationToken: cancellationToken);
|
|
EnsureChoiceValue(packBSammy, "wood", "Wood", 0m, PriceDeltaKind.Absolute);
|
|
EnsureChoiceValue(packBSammy, "steel", "Steel", 0m, PriceDeltaKind.Absolute);
|
|
|
|
foreach (var option in new[] { packARodCoupler, packANuts, packAWashers, packABeamClamp, packASammy })
|
|
{
|
|
AddRuleIfMissing(
|
|
product,
|
|
OptionRuleTargetKind.OptionDefinition,
|
|
option.Id,
|
|
RuleEffect.Show,
|
|
() => OptionRuleSetBuilder
|
|
.ForDefinition(product, option, RuleEffect.Show, RuleMode.All)
|
|
.WhenEquals(hardwarePackA, packAYes)
|
|
.Build());
|
|
}
|
|
|
|
foreach (var option in new[] { packBRodCoupler, packBNuts, packBWashers, packBBeamClamp, packBSammy })
|
|
{
|
|
AddRuleIfMissing(
|
|
product,
|
|
OptionRuleTargetKind.OptionDefinition,
|
|
option.Id,
|
|
RuleEffect.Show,
|
|
() => OptionRuleSetBuilder
|
|
.ForDefinition(product, option, RuleEffect.Show, RuleMode.All)
|
|
.WhenEquals(hardwarePackB, packBYes)
|
|
.Build());
|
|
}
|
|
|
|
await EnsureVariantAsync(
|
|
product,
|
|
checker,
|
|
"ATR-025-36-SS",
|
|
"All-Thread Rod - 1/4\" x 36\", Stainless 304",
|
|
8.50m,
|
|
cancellationToken,
|
|
(diameterOption, diameterQuarter),
|
|
(lengthOption, length36),
|
|
(finishOption, finishSs304));
|
|
|
|
await EnsureVariantAsync(
|
|
product,
|
|
checker,
|
|
"ATR-038-144-SS",
|
|
"All-Thread Rod - 3/8\" x 144\", Stainless 304",
|
|
34.00m,
|
|
cancellationToken,
|
|
(diameterOption, diameterThreeEighths),
|
|
(lengthOption, length144),
|
|
(finishOption, finishSs304));
|
|
|
|
await EnsureVariantAsync(
|
|
product,
|
|
checker,
|
|
"ATR-050-120-ZN",
|
|
"All-Thread Rod - 1/2\" x 120\", Zinc-Plated",
|
|
22.00m,
|
|
cancellationToken,
|
|
(diameterOption, diameterHalf),
|
|
(lengthOption, length120),
|
|
(finishOption, finishZinc));
|
|
|
|
var variants = product.Variants
|
|
.Where(variant => variant.DeletedOn == null && !string.IsNullOrWhiteSpace(variant.Sku))
|
|
.ToDictionary(variant => variant.Sku!, StringComparer.OrdinalIgnoreCase);
|
|
|
|
if (variants.TryGetValue("ATR-025-36-SS", out var atr02536))
|
|
{
|
|
atr02536.UpsertSpec(diameterDefinition.Id, FormatDecimal(0.25m), 0.25m, diameterDefinition.Unit, null);
|
|
atr02536.UpsertSpec(lengthDefinition.Id, FormatDecimal(36m), 36m, lengthDefinition.Unit, null);
|
|
atr02536.UpsertSpec(finishDefinition.Id, "Stainless 304", null, null, "ss304");
|
|
atr02536.UpsertSpec(materialDefinition.Id, "Stainless 304", null, null, "stainless_304");
|
|
}
|
|
|
|
if (variants.TryGetValue("ATR-038-144-SS", out var atr038144))
|
|
{
|
|
atr038144.UpsertSpec(diameterDefinition.Id, FormatDecimal(0.375m), 0.375m, diameterDefinition.Unit, null);
|
|
atr038144.UpsertSpec(lengthDefinition.Id, FormatDecimal(144m), 144m, lengthDefinition.Unit, null);
|
|
atr038144.UpsertSpec(finishDefinition.Id, "Stainless 304", null, null, "ss304");
|
|
atr038144.UpsertSpec(materialDefinition.Id, "Stainless 304", null, null, "stainless_304");
|
|
}
|
|
|
|
if (variants.TryGetValue("ATR-050-120-ZN", out var atr050120))
|
|
{
|
|
atr050120.UpsertSpec(diameterDefinition.Id, FormatDecimal(0.5m), 0.5m, diameterDefinition.Unit, null);
|
|
atr050120.UpsertSpec(lengthDefinition.Id, FormatDecimal(120m), 120m, lengthDefinition.Unit, null);
|
|
atr050120.UpsertSpec(finishDefinition.Id, "Zinc-Plated", null, null, "zinc");
|
|
}
|
|
}
|
|
|
|
private static async Task EnsureRaisedDeviceAssemblyAsync(
|
|
IModuleDb db,
|
|
IUniqueChecker checker,
|
|
Category category,
|
|
AttributeDefinition faceStyleDefinition,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
const string slug = "raised-device-assembly";
|
|
var product = await LoadModelAsync(db, slug, cancellationToken);
|
|
|
|
if (product is null)
|
|
{
|
|
product = await Product.CreateModel(
|
|
name: "Raised Device Assembly",
|
|
slug,
|
|
description: "Raised device assembly with configurable device, lead, and wiring options.",
|
|
checker,
|
|
cancellationToken);
|
|
db.Products.Add(product);
|
|
}
|
|
else if (!string.Equals(product.Name, "Raised Device Assembly", StringComparison.Ordinal))
|
|
{
|
|
await product.Rename("Raised Device Assembly", checker, cancellationToken);
|
|
}
|
|
|
|
product.SetBasePrice(29.00m);
|
|
product.ChangeDescription("Raised device assembly with configurable device, lead, and wiring options.");
|
|
product.AssignToCategory(category.Id, true);
|
|
|
|
var deviceType = await EnsureChoiceOptionAsync(product, checker, "device_type", "Device Type", cancellationToken: cancellationToken);
|
|
var duplex15 = EnsureChoiceValue(deviceType, "duplex15", "Duplex 15A", 0m, PriceDeltaKind.Absolute);
|
|
var duplex20 = EnsureChoiceValue(deviceType, "duplex20", "Duplex 20A", 0m, PriceDeltaKind.Absolute);
|
|
var decora15 = EnsureChoiceValue(deviceType, "decora15", "Decora 15A", 0m, PriceDeltaKind.Absolute);
|
|
var gfci15 = EnsureChoiceValue(deviceType, "gfci15", "GFCI 15A", 0m, PriceDeltaKind.Absolute);
|
|
var gfci20 = EnsureChoiceValue(deviceType, "gfci20", "GFCI 20A", 0m, PriceDeltaKind.Absolute);
|
|
|
|
var deviceColor = await EnsureChoiceOptionAsync(product, checker, "device_color", "Device Color", cancellationToken: cancellationToken);
|
|
EnsureChoiceValue(deviceColor, "white", "White", 0m, PriceDeltaKind.Absolute);
|
|
EnsureChoiceValue(deviceColor, "ivory", "Ivory", 0.50m, PriceDeltaKind.Absolute);
|
|
EnsureChoiceValue(deviceColor, "gray", "Gray", 0.50m, PriceDeltaKind.Absolute);
|
|
|
|
var grade = await EnsureChoiceOptionAsync(product, checker, "grade", "Grade", cancellationToken: cancellationToken);
|
|
EnsureChoiceValue(grade, "res", "Residential", 0m, PriceDeltaKind.Absolute);
|
|
EnsureChoiceValue(grade, "spec", "Spec", 1.00m, PriceDeltaKind.Absolute);
|
|
EnsureChoiceValue(grade, "xhd", "X-Heavy Duty", 2.00m, PriceDeltaKind.Absolute);
|
|
|
|
var boxSize = await EnsureChoiceOptionAsync(product, checker, "box_size", "Box Size", cancellationToken: cancellationToken);
|
|
EnsureChoiceValue(boxSize, "4sq", "4\" Square", 0m, PriceDeltaKind.Absolute);
|
|
EnsureChoiceValue(boxSize, "4_11_16", "4-11/16\" Square", 0.75m, PriceDeltaKind.Absolute);
|
|
|
|
var leadLength = await EnsureNumberOptionAsync(
|
|
product,
|
|
checker,
|
|
"lead_length",
|
|
"Lead Length",
|
|
unit: "ft",
|
|
min: 1m,
|
|
max: 100m,
|
|
step: 1m,
|
|
pricePerUnit: 0.70m,
|
|
cancellationToken);
|
|
AddTierIfMissing(leadLength, 26m, null, 0.60m, 5.00m);
|
|
|
|
var wireSize = await EnsureChoiceOptionAsync(product, checker, "wire_size", "Wire Size", cancellationToken: cancellationToken);
|
|
var awg14Raised = EnsureChoiceValue(wireSize, "awg14", "AWG 14", 0m, PriceDeltaKind.Percent);
|
|
_ = EnsureChoiceValue(wireSize, "awg12", "AWG 12", 15m, PriceDeltaKind.Percent);
|
|
wireSize.SetPercentScope(PercentScope.NumericOnly);
|
|
|
|
var wagos = await EnsureChoiceOptionAsync(product, checker, "wagos", "Wago Connectors", cancellationToken: cancellationToken);
|
|
EnsureChoiceValue(wagos, "no", "No", 0m, PriceDeltaKind.Absolute);
|
|
EnsureChoiceValue(wagos, "yes", "Yes", 2.00m, PriceDeltaKind.Absolute);
|
|
|
|
AddRuleIfMissing(
|
|
product,
|
|
OptionRuleTargetKind.OptionValue,
|
|
awg14Raised.Id,
|
|
RuleEffect.Show,
|
|
() => OptionRuleSetBuilder
|
|
.ForValue(product, awg14Raised, RuleEffect.Show, RuleMode.All)
|
|
.WhenNotIn(deviceType, duplex20, gfci20)
|
|
.Build());
|
|
|
|
product.UpsertSpec(faceStyleDefinition.Id, FaceStyleValue, null, null, null);
|
|
}
|
|
|
|
private static async Task EnsurePigtailedDeviceAsync(
|
|
IModuleDb db,
|
|
IUniqueChecker checker,
|
|
Category category,
|
|
AttributeDefinition faceStyleDefinition,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
const string slug = "pigtailed-device";
|
|
var product = await LoadModelAsync(db, slug, cancellationToken);
|
|
|
|
if (product is null)
|
|
{
|
|
product = await Product.CreateModel(
|
|
name: "Pigtailed Device",
|
|
slug,
|
|
description: "Pigtailed device assembly with configurable wiring.",
|
|
checker,
|
|
cancellationToken);
|
|
db.Products.Add(product);
|
|
}
|
|
else if (!string.Equals(product.Name, "Pigtailed Device", StringComparison.Ordinal))
|
|
{
|
|
await product.Rename("Pigtailed Device", checker, cancellationToken);
|
|
}
|
|
|
|
product.SetBasePrice(24.00m);
|
|
product.ChangeDescription("Pigtailed device assembly with configurable wiring.");
|
|
product.AssignToCategory(category.Id, true);
|
|
|
|
var deviceType = await EnsureChoiceOptionAsync(product, checker, "device_type", "Device Type", cancellationToken: cancellationToken);
|
|
EnsureChoiceValue(deviceType, "duplex15", "Duplex 15A", 0m, PriceDeltaKind.Absolute);
|
|
EnsureChoiceValue(deviceType, "decora15", "Decora 15A", 0m, PriceDeltaKind.Absolute);
|
|
var gfci15 = EnsureChoiceValue(deviceType, "gfci15", "GFCI 15A", 0m, PriceDeltaKind.Absolute);
|
|
|
|
var deviceColor = await EnsureChoiceOptionAsync(product, checker, "device_color", "Device Color", cancellationToken: cancellationToken);
|
|
EnsureChoiceValue(deviceColor, "white", "White", 0m, PriceDeltaKind.Absolute);
|
|
EnsureChoiceValue(deviceColor, "ivory", "Ivory", 0.50m, PriceDeltaKind.Absolute);
|
|
|
|
var grade = await EnsureChoiceOptionAsync(product, checker, "grade", "Grade", cancellationToken: cancellationToken);
|
|
EnsureChoiceValue(grade, "res", "Residential", 0m, PriceDeltaKind.Absolute);
|
|
EnsureChoiceValue(grade, "spec", "Spec", 1.00m, PriceDeltaKind.Absolute);
|
|
|
|
var leadLength = await EnsureNumberOptionAsync(
|
|
product,
|
|
checker,
|
|
"lead_length",
|
|
"Lead Length",
|
|
unit: "ft",
|
|
min: 1m,
|
|
max: 100m,
|
|
step: 1m,
|
|
pricePerUnit: 0.55m,
|
|
cancellationToken);
|
|
AddTierIfMissing(leadLength, 50m, null, 0.45m, 4.00m);
|
|
|
|
var wireSize = await EnsureChoiceOptionAsync(product, checker, "wire_size", "Wire Size", cancellationToken: cancellationToken);
|
|
var awg14Pigtailed = EnsureChoiceValue(wireSize, "awg14", "AWG 14", 0m, PriceDeltaKind.Percent);
|
|
_ = EnsureChoiceValue(wireSize, "awg12", "AWG 12", 15m, PriceDeltaKind.Percent);
|
|
wireSize.SetPercentScope(PercentScope.NumericOnly);
|
|
|
|
var wagos = await EnsureChoiceOptionAsync(product, checker, "wagos", "Wago Connectors", cancellationToken: cancellationToken);
|
|
EnsureChoiceValue(wagos, "no", "No", 0m, PriceDeltaKind.Absolute);
|
|
EnsureChoiceValue(wagos, "yes", "Yes", 2.00m, PriceDeltaKind.Absolute);
|
|
|
|
product.UpsertSpec(faceStyleDefinition.Id, FaceStyleValue, null, null, null);
|
|
}
|
|
|
|
private static async Task<Product?> LoadModelAsync(IModuleDb db, string slug, CancellationToken cancellationToken) =>
|
|
await db.Products
|
|
.Where(p => p.DeletedOn == null && p.Kind == ProductKind.Model && p.Slug == slug)
|
|
.Include(p => p.Options).ThenInclude(o => o.Values)
|
|
.Include(p => p.Options).ThenInclude(o => o.Tiers)
|
|
.Include(p => p.RuleSets).ThenInclude(r => r.Conditions)
|
|
.Include(p => p.Variants).ThenInclude(v => v.AxisValues)
|
|
.Include(p => p.Attributes)
|
|
.Include(p => p.Categories)
|
|
.FirstOrDefaultAsync(cancellationToken);
|
|
|
|
private static async Task<OptionDefinition> EnsureChoiceOptionAsync(
|
|
Product product,
|
|
IUniqueChecker checker,
|
|
string code,
|
|
string name,
|
|
bool isVariantAxis = false,
|
|
PercentScope? percentScope = null,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
var existing = product.Options.FirstOrDefault(o =>
|
|
o.DeletedOn == null && string.Equals(o.Code, code, StringComparison.OrdinalIgnoreCase));
|
|
|
|
if (existing is not null)
|
|
{
|
|
if (percentScope.HasValue)
|
|
{
|
|
existing.SetPercentScope(percentScope.Value);
|
|
}
|
|
|
|
return existing;
|
|
}
|
|
|
|
return await OptionDefinition.CreateChoice(product, code, name, checker, isVariantAxis, percentScope, cancellationToken);
|
|
}
|
|
|
|
private static async Task<OptionDefinition> EnsureNumberOptionAsync(
|
|
Product product,
|
|
IUniqueChecker checker,
|
|
string code,
|
|
string name,
|
|
string unit,
|
|
decimal? min,
|
|
decimal? max,
|
|
decimal? step,
|
|
decimal? pricePerUnit,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
var existing = product.Options.FirstOrDefault(o =>
|
|
o.DeletedOn == null && string.Equals(o.Code, code, StringComparison.OrdinalIgnoreCase));
|
|
|
|
if (existing is not null)
|
|
{
|
|
return existing;
|
|
}
|
|
|
|
return await OptionDefinition.CreateNumber(
|
|
product,
|
|
code,
|
|
name,
|
|
unit,
|
|
min,
|
|
max,
|
|
step,
|
|
pricePerUnit,
|
|
checker,
|
|
cancellationToken: cancellationToken);
|
|
}
|
|
|
|
private static OptionValue EnsureChoiceValue(
|
|
OptionDefinition option,
|
|
string code,
|
|
string label,
|
|
decimal priceDelta,
|
|
PriceDeltaKind deltaKind)
|
|
{
|
|
var existing = option.Values.FirstOrDefault(v =>
|
|
v.DeletedOn == null && string.Equals(v.Code, code, StringComparison.OrdinalIgnoreCase));
|
|
|
|
if (existing is not null)
|
|
{
|
|
option.ChangeValue(existing.Id, label, priceDelta, deltaKind);
|
|
return existing;
|
|
}
|
|
|
|
return option.AddValue(code, label, priceDelta, deltaKind);
|
|
}
|
|
|
|
private static void AddTierIfMissing(
|
|
OptionDefinition option,
|
|
decimal fromInclusive,
|
|
decimal? toInclusive,
|
|
decimal unitRate,
|
|
decimal? flatDelta)
|
|
{
|
|
var existing = option.Tiers.FirstOrDefault(tier =>
|
|
tier.DeletedOn == null &&
|
|
tier.FromInclusive == fromInclusive &&
|
|
Nullable.Equals(tier.ToInclusive, toInclusive));
|
|
|
|
if (existing is not null)
|
|
{
|
|
option.ChangeTier(existing.Id, fromInclusive, toInclusive, unitRate, flatDelta);
|
|
return;
|
|
}
|
|
|
|
option.AddTier(fromInclusive, toInclusive, unitRate, flatDelta);
|
|
}
|
|
|
|
private static async Task EnsureVariantAsync(
|
|
Product model,
|
|
IUniqueChecker checker,
|
|
string sku,
|
|
string variantName,
|
|
decimal price,
|
|
CancellationToken cancellationToken,
|
|
params (OptionDefinition Definition, OptionValue Value)[] axisSelections)
|
|
{
|
|
var existing = model.Variants
|
|
.FirstOrDefault(v => v.DeletedOn == null && string.Equals(v.Sku, sku, StringComparison.OrdinalIgnoreCase));
|
|
|
|
Product variant;
|
|
if (existing is null)
|
|
{
|
|
variant = await Product.CreateVariant(
|
|
model.Id,
|
|
sku,
|
|
variantName,
|
|
price,
|
|
checker,
|
|
cancellationToken);
|
|
|
|
model.AttachVariant(variant);
|
|
}
|
|
else
|
|
{
|
|
variant = existing;
|
|
}
|
|
variant.SetBasePrice(price);
|
|
|
|
foreach (var (definition, value) in axisSelections)
|
|
{
|
|
var axisValue = variant.AxisValues
|
|
.FirstOrDefault(av => av.OptionDefinitionId == definition.Id);
|
|
|
|
if (axisValue is null)
|
|
{
|
|
axisValue = new VariantAxisValue
|
|
{
|
|
ProductVariantId = variant.Id,
|
|
ProductVariant = variant,
|
|
OptionDefinitionId = definition.Id,
|
|
OptionDefinition = definition,
|
|
OptionValueId = value.Id,
|
|
OptionValue = value
|
|
};
|
|
variant.AxisValues.Add(axisValue);
|
|
}
|
|
else
|
|
{
|
|
axisValue.OptionValueId = value.Id;
|
|
axisValue.OptionValue = value;
|
|
}
|
|
}
|
|
}
|
|
|
|
private static void AddRuleIfMissing(
|
|
Product product,
|
|
OptionRuleTargetKind targetKind,
|
|
Guid targetId,
|
|
RuleEffect effect,
|
|
Func<OptionRuleSet> factory)
|
|
{
|
|
var exists = product.RuleSets.Any(rule =>
|
|
rule.DeletedOn == null &&
|
|
rule.TargetKind == targetKind &&
|
|
rule.TargetId == targetId &&
|
|
rule.Effect == effect);
|
|
|
|
if (!exists)
|
|
{
|
|
factory();
|
|
}
|
|
}
|
|
|
|
private static string FormatDecimal(decimal value) =>
|
|
value.ToString("0.##", CultureInfo.InvariantCulture);
|
|
|
|
private static Task UpsertProductDocsAsync(
|
|
IModuleDb db,
|
|
Product product,
|
|
IEnumerable<(string Title, string Url)> docs,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
var payload = JsonSerializer.Serialize(
|
|
docs.Select(doc => new { title = doc.Title, url = doc.Url }),
|
|
JsonOptions);
|
|
|
|
return UpsertGenericAttributeAsync(db, product, CatalogDocsKey, payload, cancellationToken);
|
|
}
|
|
|
|
private static async Task UpsertGenericAttributeAsync(
|
|
IModuleDb db,
|
|
Product product,
|
|
string key,
|
|
string value,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
var keyGroup = typeof(Product).FullName ?? "Product";
|
|
|
|
var existing = await db.GenericAttributes
|
|
.FirstOrDefaultAsync(attr =>
|
|
attr.EntityId == product.Id &&
|
|
attr.KeyGroup == keyGroup &&
|
|
attr.Key == key &&
|
|
attr.DeletedOn == null,
|
|
cancellationToken);
|
|
|
|
if (existing is null)
|
|
{
|
|
var attribute = new GenericAttribute
|
|
{
|
|
EntityId = product.Id,
|
|
KeyGroup = keyGroup,
|
|
Key = key,
|
|
Type = typeof(string).FullName ?? "System.String",
|
|
Value = value
|
|
};
|
|
|
|
db.GenericAttributes.Add(attribute);
|
|
return;
|
|
}
|
|
|
|
if (!string.Equals(existing.Value, value, StringComparison.Ordinal))
|
|
{
|
|
existing.Value = value;
|
|
}
|
|
}
|
|
|
|
private static async Task<AttributeDefinition> EnsureAttributeDefinitionAsync(
|
|
IModuleDb db,
|
|
string name,
|
|
AttributeDataType dataType,
|
|
CancellationToken cancellationToken,
|
|
string? unit = null)
|
|
{
|
|
var existing = await db.AttributeDefinitions
|
|
.FirstOrDefaultAsync(def => def.DeletedOn == null && def.Name == name, cancellationToken);
|
|
|
|
if (existing is not null)
|
|
{
|
|
return existing;
|
|
}
|
|
|
|
var definition = AttributeDefinition.Create(name, dataType, unit);
|
|
db.AttributeDefinitions.Add(definition);
|
|
return definition;
|
|
}
|
|
|
|
private sealed record CategorySeed(string Slug, string Name, int DisplayOrder, bool IsFeatured);
|
|
}
|