Add Columbia Coatings catalog integration schema fields

Phase 1a of the Columbia Coatings API integration. Adds the persisted
fields the sync/mapper will need, ahead of the client and sync service:

PowderCatalogItem:
- Category: our product category (e.g. "Powder Additives" for gram-sold
  pigments) derived from vendor taxonomy at import, not stored raw.
- Source: provenance (e.g. "Columbia Coatings API"), kept separate from
  VendorName (= derived manufacturer) so a distributor's right-to-delete
  can purge by feed regardless of manufacturer.
- ChemistryType: resin chemistry (Polyester/TGIC/Epoxy/...), distinct
  from Finish.
- MilThickness: recommended film build as vendor free text.
- CureScheduleText: raw cure schedule verbatim (formats vary widely).
- CureCurvesJson: all parsed cure curves, so alternate low-temp curves
  are preserved for heat-sensitive substrates, not just the primary.
- FormulationChanges: vendor reformulation log; a signal cure specs may
  have changed.

InventoryItem:
- PowderCatalogItemId: loose link to the catalog row (matches the
  QuoteItemCoat pattern) so inventory detail can show manufacturer-level
  status (e.g. discontinued/cannot reorder) and future change flags.
  Nulled, never cascaded, when source catalog data is purged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-17 10:37:56 -04:00
parent 0498decfb0
commit c98f9faf63
5 changed files with 11596 additions and 3 deletions
@@ -31,6 +31,15 @@ public class InventoryItem : BaseEntity
public string? SdsUrl { get; set; } // Safety Data Sheet URL (from powder catalog or manual entry) public string? SdsUrl { get; set; } // Safety Data Sheet URL (from powder catalog or manual entry)
public string? TdsUrl { get; set; } // Technical Data Sheet URL (from powder catalog or manual entry) public string? TdsUrl { get; set; } // Technical Data Sheet URL (from powder catalog or manual entry)
/// <summary>
/// Optional link to the platform powder catalog record this item was created from.
/// Populated when an item is added via the catalog lookup, or back-filled by Manufacturer+SKU.
/// Lets the inventory detail screen surface manufacturer-level status (e.g. "discontinued by
/// manufacturer — cannot reorder") and future price/reformulation change flags. Nulled — not
/// cascaded — if the source catalog data is purged (the shop's own stock record must survive).
/// </summary>
public int? PowderCatalogItemId { get; set; }
// Sample Panel Tracking (coating category items only) // Sample Panel Tracking (coating category items only)
public bool HasSamplePanel { get; set; } = false; public bool HasSamplePanel { get; set; } = false;
@@ -40,9 +40,30 @@ public class PowderCatalogItem
/// <summary>Cure hold time at cure temperature, in minutes.</summary> /// <summary>Cure hold time at cure temperature, in minutes.</summary>
public int? CureTimeMinutes { get; set; } public int? CureTimeMinutes { get; set; }
/// <summary>
/// Raw cure schedule text exactly as supplied by the vendor — e.g. "10 minutes @ 400°F".
/// Preserved verbatim because vendor formats vary wildly and some carry application notes
/// that don't reduce to a single temp/time pair (partial cures, clear-coat steps).
/// </summary>
public string? CureScheduleText { get; set; }
/// <summary>
/// All parsed cure curves as JSON — e.g. [{"tempF":400,"minutes":10},{"tempF":350,"minutes":20}].
/// Many powders list alternate lower-temperature curves; these matter for heat-sensitive
/// substrates that cannot take the standard 400°F cure, so we keep every curve, not just the
/// primary one in <see cref="CureTemperatureF"/>/<see cref="CureTimeMinutes"/>.
/// </summary>
public string? CureCurvesJson { get; set; }
/// <summary>Finish type — e.g. Gloss, Matte, Satin, Metallic, Texture.</summary> /// <summary>Finish type — e.g. Gloss, Matte, Satin, Metallic, Texture.</summary>
public string? Finish { get; set; } public string? Finish { get; set; }
/// <summary>Resin chemistry — e.g. "Polyester", "TGIC", "Epoxy", "Hybrid". Distinct from <see cref="Finish"/>.</summary>
public string? ChemistryType { get; set; }
/// <summary>Recommended film build (mil thickness) as free text from the vendor — e.g. "2.0-3.0 Mils".</summary>
public string? MilThickness { get; set; }
/// <summary>Comma-separated color family tags — e.g. "Blue,Purple".</summary> /// <summary>Comma-separated color family tags — e.g. "Blue,Purple".</summary>
public string? ColorFamilies { get; set; } public string? ColorFamilies { get; set; }
@@ -60,6 +81,29 @@ public class PowderCatalogItem
// ── Catalog management ──────────────────────────────────────────────── // ── Catalog management ────────────────────────────────────────────────
/// <summary>
/// Our internal product category — e.g. "Powder Additives" for pigments/additives that are
/// sold by weight in grams and mixed into clear rather than sprayed as a standalone powder.
/// Null/empty for standard powders. Derived at import from the vendor's taxonomy, NOT stored
/// from their raw category list.
/// </summary>
public string? Category { get; set; }
/// <summary>
/// Provenance of this record — e.g. "Columbia Coatings API". Kept SEPARATE from
/// <see cref="VendorName"/> (which holds the derived manufacturer) so we can honor a
/// distributor's right-to-delete by purging every record that came from their feed,
/// regardless of which manufacturer made the product.
/// </summary>
public string? Source { get; set; }
/// <summary>
/// Reformulation history as supplied by the vendor — e.g. "Formulation Change: 05/22/26".
/// Not a reliable modified-date (free text, reformulations only) but a useful signal that a
/// product's formula — and therefore its cure specs — may have changed.
/// </summary>
public string? FormulationChanges { get; set; }
/// <summary>True when the vendor has discontinued this product. Kept for historical lookups.</summary> /// <summary>True when the vendor has discontinued this product. Kept for historical lookups.</summary>
public bool IsDiscontinued { get; set; } = false; public bool IsDiscontinued { get; set; } = false;
@@ -0,0 +1,141 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace PowderCoating.Infrastructure.Migrations
{
/// <inheritdoc />
public partial class AddColumbiaCatalogIntegrationFields : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "Category",
table: "PowderCatalogItems",
type: "nvarchar(max)",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "ChemistryType",
table: "PowderCatalogItems",
type: "nvarchar(max)",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "CureCurvesJson",
table: "PowderCatalogItems",
type: "nvarchar(max)",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "CureScheduleText",
table: "PowderCatalogItems",
type: "nvarchar(max)",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "FormulationChanges",
table: "PowderCatalogItems",
type: "nvarchar(max)",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "MilThickness",
table: "PowderCatalogItems",
type: "nvarchar(max)",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "Source",
table: "PowderCatalogItems",
type: "nvarchar(max)",
nullable: true);
migrationBuilder.AddColumn<int>(
name: "PowderCatalogItemId",
table: "InventoryItems",
type: "int",
nullable: true);
migrationBuilder.UpdateData(
table: "PricingTiers",
keyColumn: "Id",
keyValue: 1,
column: "CreatedAt",
value: new DateTime(2026, 6, 17, 14, 21, 27, 126, DateTimeKind.Utc).AddTicks(6644));
migrationBuilder.UpdateData(
table: "PricingTiers",
keyColumn: "Id",
keyValue: 2,
column: "CreatedAt",
value: new DateTime(2026, 6, 17, 14, 21, 27, 126, DateTimeKind.Utc).AddTicks(6651));
migrationBuilder.UpdateData(
table: "PricingTiers",
keyColumn: "Id",
keyValue: 3,
column: "CreatedAt",
value: new DateTime(2026, 6, 17, 14, 21, 27, 126, DateTimeKind.Utc).AddTicks(6652));
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "Category",
table: "PowderCatalogItems");
migrationBuilder.DropColumn(
name: "ChemistryType",
table: "PowderCatalogItems");
migrationBuilder.DropColumn(
name: "CureCurvesJson",
table: "PowderCatalogItems");
migrationBuilder.DropColumn(
name: "CureScheduleText",
table: "PowderCatalogItems");
migrationBuilder.DropColumn(
name: "FormulationChanges",
table: "PowderCatalogItems");
migrationBuilder.DropColumn(
name: "MilThickness",
table: "PowderCatalogItems");
migrationBuilder.DropColumn(
name: "Source",
table: "PowderCatalogItems");
migrationBuilder.DropColumn(
name: "PowderCatalogItemId",
table: "InventoryItems");
migrationBuilder.UpdateData(
table: "PricingTiers",
keyColumn: "Id",
keyValue: 1,
column: "CreatedAt",
value: new DateTime(2026, 6, 14, 1, 21, 46, 131, DateTimeKind.Utc).AddTicks(4191));
migrationBuilder.UpdateData(
table: "PricingTiers",
keyColumn: "Id",
keyValue: 2,
column: "CreatedAt",
value: new DateTime(2026, 6, 14, 1, 21, 46, 131, DateTimeKind.Utc).AddTicks(4196));
migrationBuilder.UpdateData(
table: "PricingTiers",
keyColumn: "Id",
keyValue: 3,
column: "CreatedAt",
value: new DateTime(2026, 6, 14, 1, 21, 46, 131, DateTimeKind.Utc).AddTicks(4197));
}
}
}
@@ -4173,6 +4173,9 @@ namespace PowderCoating.Infrastructure.Migrations
b.Property<string>("Notes") b.Property<string>("Notes")
.HasColumnType("nvarchar(max)"); .HasColumnType("nvarchar(max)");
b.Property<int?>("PowderCatalogItemId")
.HasColumnType("int");
b.Property<int?>("PrimaryVendorId") b.Property<int?>("PrimaryVendorId")
.HasColumnType("int"); .HasColumnType("int");
@@ -6936,6 +6939,12 @@ namespace PowderCoating.Infrastructure.Migrations
b.Property<string>("ApplicationGuideUrl") b.Property<string>("ApplicationGuideUrl")
.HasColumnType("nvarchar(max)"); .HasColumnType("nvarchar(max)");
b.Property<string>("Category")
.HasColumnType("nvarchar(max)");
b.Property<string>("ChemistryType")
.HasColumnType("nvarchar(max)");
b.Property<string>("ColorFamilies") b.Property<string>("ColorFamilies")
.HasColumnType("nvarchar(max)"); .HasColumnType("nvarchar(max)");
@@ -6949,6 +6958,12 @@ namespace PowderCoating.Infrastructure.Migrations
b.Property<DateTime>("CreatedAt") b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2"); .HasColumnType("datetime2");
b.Property<string>("CureCurvesJson")
.HasColumnType("nvarchar(max)");
b.Property<string>("CureScheduleText")
.HasColumnType("nvarchar(max)");
b.Property<decimal?>("CureTemperatureF") b.Property<decimal?>("CureTemperatureF")
.HasColumnType("decimal(18,2)"); .HasColumnType("decimal(18,2)");
@@ -6961,6 +6976,9 @@ namespace PowderCoating.Infrastructure.Migrations
b.Property<string>("Finish") b.Property<string>("Finish")
.HasColumnType("nvarchar(max)"); .HasColumnType("nvarchar(max)");
b.Property<string>("FormulationChanges")
.HasColumnType("nvarchar(max)");
b.Property<string>("ImageUrl") b.Property<string>("ImageUrl")
.HasColumnType("nvarchar(max)"); .HasColumnType("nvarchar(max)");
@@ -6973,6 +6991,9 @@ namespace PowderCoating.Infrastructure.Migrations
b.Property<DateTime?>("LastSyncedAt") b.Property<DateTime?>("LastSyncedAt")
.HasColumnType("datetime2"); .HasColumnType("datetime2");
b.Property<string>("MilThickness")
.HasColumnType("nvarchar(max)");
b.Property<string>("PriceTiersJson") b.Property<string>("PriceTiersJson")
.HasColumnType("nvarchar(max)"); .HasColumnType("nvarchar(max)");
@@ -6989,6 +7010,9 @@ namespace PowderCoating.Infrastructure.Migrations
.IsRequired() .IsRequired()
.HasColumnType("nvarchar(450)"); .HasColumnType("nvarchar(450)");
b.Property<string>("Source")
.HasColumnType("nvarchar(max)");
b.Property<decimal?>("SpecificGravity") b.Property<decimal?>("SpecificGravity")
.HasColumnType("decimal(18,2)"); .HasColumnType("decimal(18,2)");
@@ -7210,7 +7234,7 @@ namespace PowderCoating.Infrastructure.Migrations
{ {
Id = 1, Id = 1,
CompanyId = 0, CompanyId = 0,
CreatedAt = new DateTime(2026, 6, 14, 1, 21, 46, 131, DateTimeKind.Utc).AddTicks(4191), CreatedAt = new DateTime(2026, 6, 17, 14, 21, 27, 126, DateTimeKind.Utc).AddTicks(6644),
Description = "Standard pricing for regular customers", Description = "Standard pricing for regular customers",
DiscountPercent = 0m, DiscountPercent = 0m,
IsActive = true, IsActive = true,
@@ -7221,7 +7245,7 @@ namespace PowderCoating.Infrastructure.Migrations
{ {
Id = 2, Id = 2,
CompanyId = 0, CompanyId = 0,
CreatedAt = new DateTime(2026, 6, 14, 1, 21, 46, 131, DateTimeKind.Utc).AddTicks(4196), CreatedAt = new DateTime(2026, 6, 17, 14, 21, 27, 126, DateTimeKind.Utc).AddTicks(6651),
Description = "5% discount for preferred customers", Description = "5% discount for preferred customers",
DiscountPercent = 5m, DiscountPercent = 5m,
IsActive = true, IsActive = true,
@@ -7232,7 +7256,7 @@ namespace PowderCoating.Infrastructure.Migrations
{ {
Id = 3, Id = 3,
CompanyId = 0, CompanyId = 0,
CreatedAt = new DateTime(2026, 6, 14, 1, 21, 46, 131, DateTimeKind.Utc).AddTicks(4197), CreatedAt = new DateTime(2026, 6, 17, 14, 21, 27, 126, DateTimeKind.Utc).AddTicks(6652),
Description = "10% discount for premium customers", Description = "10% discount for premium customers",
DiscountPercent = 10m, DiscountPercent = 10m,
IsActive = true, IsActive = true,