Initial commit: JDE Scoping Tool migration project

Set up repository with legacy .NET Framework 4.8 source (OLD/),
new .NET 10 Blazor solution (NEW/), OpenSpec specifications,
documentation, and project configuration.
This commit is contained in:
Joseph Doherty
2026-01-02 07:43:29 -05:00
commit 26ff8d9b4f
1761 changed files with 596509 additions and 0 deletions
@@ -0,0 +1,78 @@
namespace JdeScoping.DataSync.Configuration;
/// <summary>
/// Configuration for a single data source table sync.
/// </summary>
public class DataSourceConfig
{
/// <summary>
/// Target table name in SQL Server cache.
/// </summary>
public required string TableName { get; set; }
/// <summary>
/// Source system: "JDE" or "CMS".
/// </summary>
public required string SourceSystem { get; set; }
/// <summary>
/// Source data identifier (e.g., "WORKORDER", "LOTUSAGE").
/// </summary>
public string SourceData { get; set; } = string.Empty;
/// <summary>
/// Name of IDataFetcher implementation type (without generic suffix).
/// </summary>
public required string FetcherTypeName { get; set; }
/// <summary>
/// Optional IPostProcessor implementation type name.
/// </summary>
public string? PostProcessorTypeName { get; set; }
/// <summary>
/// Whether this data source is enabled for sync.
/// </summary>
public bool IsEnabled { get; set; } = true;
/// <summary>
/// Mass sync schedule configuration.
/// </summary>
public ScheduleConfig MassConfig { get; set; } = new();
/// <summary>
/// Daily incremental sync configuration.
/// </summary>
public ScheduleConfig DailyConfig { get; set; } = new();
/// <summary>
/// Hourly incremental sync configuration.
/// </summary>
public ScheduleConfig HourlyConfig { get; set; } = new();
}
/// <summary>
/// Schedule configuration for a sync type (Mass/Daily/Hourly).
/// </summary>
public class ScheduleConfig
{
/// <summary>
/// Whether this schedule is enabled.
/// </summary>
public bool Enabled { get; set; } = true;
/// <summary>
/// Interval in minutes between syncs.
/// </summary>
public int IntervalMinutes { get; set; }
/// <summary>
/// Whether to truncate the table before syncing (mass updates only).
/// </summary>
public bool PrepurgeData { get; set; } = false;
/// <summary>
/// Whether to rebuild indexes after syncing (mass updates only).
/// </summary>
public bool ReIndexData { get; set; } = false;
}
@@ -0,0 +1,65 @@
using System.ComponentModel.DataAnnotations;
namespace JdeScoping.DataSync.Configuration;
/// <summary>
/// Configuration options for the data synchronization service.
/// </summary>
public class DataSyncOptions
{
/// <summary>
/// Configuration section name in appsettings.json.
/// </summary>
public const string SectionName = "DataSync";
/// <summary>
/// Time between schedule checks (default: 1 minute).
/// </summary>
public TimeSpan CheckInterval { get; set; } = TimeSpan.FromMinutes(1);
/// <summary>
/// Maximum parallel sync operations (default: 8).
/// </summary>
[Range(1, 32)]
public int MaxDegreeOfParallelism { get; set; } = 8;
/// <summary>
/// Records per batch for streaming (default: 1,000,000).
/// </summary>
[Range(1000, 10_000_000)]
public int BatchSize { get; set; } = 1_000_000;
/// <summary>
/// Rows per bulk copy batch (default: 10,000).
/// </summary>
[Range(100, 100_000)]
public int BulkCopyBatchSize { get; set; } = 10_000;
/// <summary>
/// Multiplier for lookback window (default: 3).
/// </summary>
[Range(1, 10)]
public int LookbackMultiplier { get; set; } = 3;
/// <summary>
/// Days to retain DataUpdate history (default: 30).
/// </summary>
[Range(1, 365)]
public int PurgeRetentionDays { get; set; } = 30;
/// <summary>
/// Timeout in seconds for sync operations (default: 3600 = 1 hour).
/// </summary>
[Range(60, 86400)]
public int SyncTimeoutSeconds { get; set; } = 3600;
/// <summary>
/// Whether the data sync service is enabled (default: true).
/// </summary>
public bool Enabled { get; set; } = true;
/// <summary>
/// Per-table data source configurations.
/// </summary>
public List<DataSourceConfig> DataSources { get; set; } = [];
}
@@ -0,0 +1,24 @@
using System.Linq.Expressions;
using JdeScoping.Core.Models.Organization;
using JdeScoping.DataSync.Contracts;
namespace JdeScoping.DataSync.Configuration.MergeConfigurations;
/// <summary>
/// Merge configuration for Branch entities.
/// </summary>
public sealed class BranchMergeConfiguration : IMergeConfiguration<Branch>
{
public string TableName => "Branch";
public Expression<Func<Branch, object>> MatchOn =>
x => x.Code;
public Expression<Func<Branch, object>>? UpdateColumns =>
x => x.Description;
public Expression<Func<Branch, Branch, bool>>? UpdateWhen =>
(src, tgt) => src.LastUpdateDt > tgt.LastUpdateDt;
public Expression<Func<Branch, object>>? InsertColumns => null;
}
@@ -0,0 +1,30 @@
using System.Linq.Expressions;
using JdeScoping.Core.Models.Inventory;
using JdeScoping.DataSync.Contracts;
namespace JdeScoping.DataSync.Configuration.MergeConfigurations;
/// <summary>
/// Merge configuration for Item entities.
/// </summary>
public sealed class ItemMergeConfiguration : IMergeConfiguration<Item>
{
public string TableName => "Item";
public Expression<Func<Item, object>> MatchOn =>
x => x.ShortItemNumber;
public Expression<Func<Item, object>>? UpdateColumns =>
x => new
{
x.ItemNumber,
x.Description,
x.PlanningFamily,
x.StockingType
};
public Expression<Func<Item, Item, bool>>? UpdateWhen =>
(src, tgt) => src.LastUpdateDt > tgt.LastUpdateDt;
public Expression<Func<Item, object>>? InsertColumns => null;
}
@@ -0,0 +1,24 @@
using System.Linq.Expressions;
using JdeScoping.Core.Models.Organization;
using JdeScoping.DataSync.Contracts;
namespace JdeScoping.DataSync.Configuration.MergeConfigurations;
/// <summary>
/// Merge configuration for JdeUser entities.
/// </summary>
public sealed class JdeUserMergeConfiguration : IMergeConfiguration<JdeUser>
{
public string TableName => "JdeUser";
public Expression<Func<JdeUser, object>> MatchOn =>
x => x.AddressNumber;
public Expression<Func<JdeUser, object>>? UpdateColumns =>
x => new { x.UserId, x.FullName };
public Expression<Func<JdeUser, JdeUser, bool>>? UpdateWhen =>
(src, tgt) => src.LastUpdateDt > tgt.LastUpdateDt;
public Expression<Func<JdeUser, object>>? InsertColumns => null;
}
@@ -0,0 +1,33 @@
using System.Linq.Expressions;
using JdeScoping.Core.Models.Inventory;
using JdeScoping.DataSync.Contracts;
namespace JdeScoping.DataSync.Configuration.MergeConfigurations;
/// <summary>
/// Merge configuration for Lot entities.
/// </summary>
public sealed class LotMergeConfiguration : IMergeConfiguration<Lot>
{
public string TableName => "Lot";
public Expression<Func<Lot, object>> MatchOn =>
x => new { x.LotNumber, x.BranchCode };
public Expression<Func<Lot, object>>? UpdateColumns =>
x => new
{
x.ShortItemNumber,
x.ItemNumber,
x.SupplierCode,
x.StatusCode,
x.Memo1,
x.Memo2,
x.Memo3
};
public Expression<Func<Lot, Lot, bool>>? UpdateWhen =>
(src, tgt) => src.LastUpdateDt > tgt.LastUpdateDt;
public Expression<Func<Lot, object>>? InsertColumns => null;
}
@@ -0,0 +1,31 @@
using System.Linq.Expressions;
using JdeScoping.Core.Models.Inventory;
using JdeScoping.DataSync.Contracts;
namespace JdeScoping.DataSync.Configuration.MergeConfigurations;
/// <summary>
/// Merge configuration for LotUsage entities.
/// </summary>
public sealed class LotUsageMergeConfiguration : IMergeConfiguration<LotUsage>
{
public string TableName => "LotUsage";
public Expression<Func<LotUsage, object>> MatchOn =>
x => x.UniqueId;
public Expression<Func<LotUsage, object>>? UpdateColumns =>
x => new
{
x.WorkOrderNumber,
x.LotNumber,
x.BranchCode,
x.ShortItemNumber,
x.Quantity
};
public Expression<Func<LotUsage, LotUsage, bool>>? UpdateWhen =>
(src, tgt) => src.LastUpdateDt > tgt.LastUpdateDt;
public Expression<Func<LotUsage, object>>? InsertColumns => null;
}
@@ -0,0 +1,34 @@
using System.Linq.Expressions;
using JdeScoping.Core.Models.Quality;
using JdeScoping.DataSync.Contracts;
namespace JdeScoping.DataSync.Configuration.MergeConfigurations;
/// <summary>
/// Merge configuration for MisData entities.
/// </summary>
public sealed class MisDataMergeConfiguration : IMergeConfiguration<MisData>
{
public string TableName => "MisData";
public Expression<Func<MisData, object>> MatchOn =>
x => new { x.ItemNumber, x.BranchCode, x.SequenceNumber, x.MisNumber, x.CharNumber };
public Expression<Func<MisData, object>>? UpdateColumns =>
x => new
{
x.RevId,
x.TestDescription,
x.SamplingType,
x.SamplingValue,
x.ToolsGauges,
x.WorkInstructions,
x.Status,
x.ReleaseDate
};
// MisData doesn't have LastUpdateDt, so always update on match
public Expression<Func<MisData, MisData, bool>>? UpdateWhen => null;
public Expression<Func<MisData, object>>? InsertColumns => null;
}
@@ -0,0 +1,24 @@
using System.Linq.Expressions;
using JdeScoping.Core.Models.Organization;
using JdeScoping.DataSync.Contracts;
namespace JdeScoping.DataSync.Configuration.MergeConfigurations;
/// <summary>
/// Merge configuration for ProfitCenter entities.
/// </summary>
public sealed class ProfitCenterMergeConfiguration : IMergeConfiguration<ProfitCenter>
{
public string TableName => "ProfitCenter";
public Expression<Func<ProfitCenter, object>> MatchOn =>
x => x.Code;
public Expression<Func<ProfitCenter, object>>? UpdateColumns =>
x => x.Description;
public Expression<Func<ProfitCenter, ProfitCenter, bool>>? UpdateWhen =>
(src, tgt) => src.LastUpdateDt > tgt.LastUpdateDt;
public Expression<Func<ProfitCenter, object>>? InsertColumns => null;
}
@@ -0,0 +1,24 @@
using System.Linq.Expressions;
using JdeScoping.Core.Models.Organization;
using JdeScoping.DataSync.Contracts;
namespace JdeScoping.DataSync.Configuration.MergeConfigurations;
/// <summary>
/// Merge configuration for WorkCenter entities.
/// </summary>
public sealed class WorkCenterMergeConfiguration : IMergeConfiguration<WorkCenter>
{
public string TableName => "WorkCenter";
public Expression<Func<WorkCenter, object>> MatchOn =>
x => x.Code;
public Expression<Func<WorkCenter, object>>? UpdateColumns =>
x => x.Description;
public Expression<Func<WorkCenter, WorkCenter, bool>>? UpdateWhen =>
(src, tgt) => src.LastUpdateDt > tgt.LastUpdateDt;
public Expression<Func<WorkCenter, object>>? InsertColumns => null;
}
@@ -0,0 +1,38 @@
using System.Linq.Expressions;
using JdeScoping.Core.Models.WorkOrders;
using JdeScoping.DataSync.Contracts;
namespace JdeScoping.DataSync.Configuration.MergeConfigurations;
/// <summary>
/// Merge configuration for WorkOrder entities.
/// </summary>
public sealed class WorkOrderMergeConfiguration : IMergeConfiguration<WorkOrder>
{
public string TableName => "WorkOrder";
public Expression<Func<WorkOrder, object>> MatchOn =>
x => new { x.WorkOrderNumber, x.BranchCode };
public Expression<Func<WorkOrder, object>>? UpdateColumns =>
x => new
{
x.LotNumber,
x.ItemNumber,
x.ShortItemNumber,
x.ParentWorkOrderNumber,
x.OrderQuantity,
x.HeldQuantity,
x.ShippedQuantity,
x.StatusCode,
x.StatusCodeUpdateDt,
x.IssueDate,
x.StartDate,
x.RoutingType
};
public Expression<Func<WorkOrder, WorkOrder, bool>>? UpdateWhen =>
(src, tgt) => src.LastUpdateDt > tgt.LastUpdateDt;
public Expression<Func<WorkOrder, object>>? InsertColumns => null; // All columns
}