Add Columbia catalog mapper, shared upsert, and sync service
Phase 2: the mapping and sync core. - ColumbiaCatalogMapper (pure/static, unit-tested): maps an API product to a PowderCatalogItem. Derives manufacturer (PPG/KP Pigments/Columbia) from taxonomy+SKU; flags additives into the Powder Additives category; takes base price from the top-level price with variant fallback; captures variation / tiered pricing as JSON; parses the free-text cure schedule into all curves (three degree glyphs, @/at, multi-curve in order, partial-cure -> none) with the first as the primary temp/time; strips HTML descriptions; joins color groups; normalizes chemistry; flags clear-coat powders. - PowderCatalogUpsertService (IPowderCatalogUpsertService): single upsert path matching on (VendorName, SKU). Copies only feed-sourced fields and leaves enrichment fields (specific gravity, coverage, transfer efficiency, finish) untouched so syncs never wipe lazily-enriched TDS/AI data. - ColumbiaCatalogSyncService (IColumbiaCatalogSyncService): pulls the full catalog, maps + de-dupes, upserts, then reconciles discontinuations ONLY on a complete pull (a partial pull throws and aborts before the sweep). Reactivates reappearing items; records last-synced/last-result platform settings. - 25 mapper unit tests covering the cure parser, manufacturer derivation, simple/variable pricing, chemistry, color, and HTML cases from real records. Full suite green (261 passed). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
namespace PowderCoating.Application.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// Orchestrates a full Columbia Coatings catalog sync: pull every product, map and upsert it, then
|
||||
/// (only on a complete pull) reconcile discontinuations. Used by both the scheduled background job
|
||||
/// and the manual "Sync now" admin action.
|
||||
/// </summary>
|
||||
public interface IColumbiaCatalogSyncService
|
||||
{
|
||||
/// <summary>
|
||||
/// Runs one full sync. Assumes the caller has already decided it should run (enabled / due).
|
||||
/// Returns a result describing the outcome; never throws for an expected failure (not
|
||||
/// configured, partial pull, HTTP error) — those are reported on the result instead.
|
||||
/// </summary>
|
||||
Task<ColumbiaSyncResult> RunSyncAsync(CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
/// <summary>Outcome of a Columbia catalog sync run.</summary>
|
||||
public class ColumbiaSyncResult
|
||||
{
|
||||
public bool Success { get; set; }
|
||||
public string? ErrorMessage { get; set; }
|
||||
|
||||
public int TotalFetched { get; set; }
|
||||
public int Inserted { get; set; }
|
||||
public int Updated { get; set; }
|
||||
public int Unchanged { get; set; }
|
||||
public int Skipped { get; set; }
|
||||
public int Discontinued { get; set; }
|
||||
public int Reactivated { get; set; }
|
||||
|
||||
public DateTime StartedAt { get; set; }
|
||||
public TimeSpan Duration { get; set; }
|
||||
|
||||
/// <summary>One-line summary suitable for storing in the last-result platform setting / UI.</summary>
|
||||
public string Summary =>
|
||||
Success
|
||||
? $"{TotalFetched} fetched: {Inserted} new, {Updated} updated, {Unchanged} unchanged, " +
|
||||
$"{Discontinued} discontinued, {Reactivated} reactivated ({Duration.TotalSeconds:F0}s)"
|
||||
: $"Failed: {ErrorMessage}";
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
using PowderCoating.Core.Entities;
|
||||
|
||||
namespace PowderCoating.Application.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// Shared upsert for the platform powder catalog: matches incoming records to existing rows by
|
||||
/// (VendorName, SKU), inserts new ones, and updates changed ones in place. Used by BOTH the manual
|
||||
/// JSON file import and the Columbia API sync so there is a single upsert path, only the mapping
|
||||
/// differs. Does NOT handle discontinuation — that is a sync-specific concern.
|
||||
/// </summary>
|
||||
public interface IPowderCatalogUpsertService
|
||||
{
|
||||
/// <summary>
|
||||
/// Applies <paramref name="incoming"/> mapped catalog items. Only fields sourced from the feed
|
||||
/// are copied on update; enrichment fields (specific gravity, coverage, transfer efficiency,
|
||||
/// finish) are preserved so they are not wiped by a feed that never carries them. Changed and
|
||||
/// inserted rows get <paramref name="runTimestamp"/> stamped on LastSyncedAt/UpdatedAt.
|
||||
/// </summary>
|
||||
Task<PowderCatalogUpsertResult> UpsertAsync(
|
||||
IReadOnlyList<PowderCatalogItem> incoming,
|
||||
DateTime runTimestamp,
|
||||
CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
/// <summary>Counts from an upsert run.</summary>
|
||||
public class PowderCatalogUpsertResult
|
||||
{
|
||||
public int Inserted { get; set; }
|
||||
public int Updated { get; set; }
|
||||
public int Unchanged { get; set; }
|
||||
public int Skipped { get; set; }
|
||||
}
|
||||
Reference in New Issue
Block a user