Init
This commit is contained in:
997
Prefab.Catalog/Data/Seeder.cs
Normal file
997
Prefab.Catalog/Data/Seeder.cs
Normal file
@@ -0,0 +1,997 @@
|
||||
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);
|
||||
}
|
||||
Reference in New Issue
Block a user