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:
@@ -0,0 +1,54 @@
|
||||
using JdeScoping.Core.Interfaces;
|
||||
using JdeScoping.Core.Models;
|
||||
|
||||
namespace JdeScoping.Infrastructure.Auth;
|
||||
|
||||
/// <summary>
|
||||
/// Fake authentication service for development mode.
|
||||
/// Accepts any credentials and returns a predefined user.
|
||||
/// </summary>
|
||||
public sealed class FakeAuthService : IAuthService
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public Task<AuthResult> AuthenticateAsync(
|
||||
string username,
|
||||
string password,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
// Accept any credentials in development mode
|
||||
var user = CreateFakeUser(username);
|
||||
return Task.FromResult(new AuthResult(true, user, null));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<UserInfo?> GetUserInfoAsync(
|
||||
string username,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
var user = CreateFakeUser(username);
|
||||
return Task.FromResult<UserInfo?>(user);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<bool> IsInGroupAsync(
|
||||
string username,
|
||||
string groupName,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
// Always return true in development mode
|
||||
return Task.FromResult(true);
|
||||
}
|
||||
|
||||
private static UserInfo CreateFakeUser(string username)
|
||||
{
|
||||
return new UserInfo
|
||||
{
|
||||
Dn = $"CN={username},OU=Users,DC=example,DC=com",
|
||||
Username = username.ToLowerInvariant(),
|
||||
FirstName = "Dev",
|
||||
LastName = "User",
|
||||
EmailAddress = $"{username}@example.com",
|
||||
Title = "Developer"
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,242 @@
|
||||
using System.DirectoryServices.Protocols;
|
||||
using System.Net;
|
||||
using JdeScoping.Core.Interfaces;
|
||||
using JdeScoping.Core.Models;
|
||||
using JdeScoping.Core.Options;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace JdeScoping.Infrastructure.Auth;
|
||||
|
||||
/// <summary>
|
||||
/// LDAP-based authentication service implementation using System.DirectoryServices.Protocols
|
||||
/// </summary>
|
||||
public sealed class LdapAuthService : IAuthService
|
||||
{
|
||||
private const string LdapLookupFormat = "(sAMAccountName={0})";
|
||||
|
||||
private readonly LdapOptions _options;
|
||||
private readonly AuthOptions _authOptions;
|
||||
private readonly ILogger<LdapAuthService> _logger;
|
||||
|
||||
public LdapAuthService(
|
||||
IOptions<LdapOptions> options,
|
||||
IOptions<AuthOptions> authOptions,
|
||||
ILogger<LdapAuthService> logger)
|
||||
{
|
||||
_options = options.Value;
|
||||
_authOptions = authOptions.Value;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<AuthResult> AuthenticateAsync(
|
||||
string username,
|
||||
string password,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(username) || string.IsNullOrWhiteSpace(password))
|
||||
{
|
||||
return new AuthResult(false, null, "Username and password are required");
|
||||
}
|
||||
|
||||
// Check if user is in admin bypass list
|
||||
var isAdminBypass = _authOptions.AdminBypassUsers
|
||||
.Any(u => string.Equals(u, username, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
// Try each configured LDAP server
|
||||
string? lastError = null;
|
||||
foreach (var serverUrl in _options.ServerUrls)
|
||||
{
|
||||
try
|
||||
{
|
||||
ct.ThrowIfCancellationRequested();
|
||||
|
||||
// Attempt authentication
|
||||
if (!await TryBindAsync(serverUrl, username, password, ct))
|
||||
{
|
||||
return new AuthResult(false, null, "Incorrect username or password");
|
||||
}
|
||||
|
||||
// Verify group membership (unless admin bypass)
|
||||
if (!isAdminBypass && !string.IsNullOrEmpty(_options.GroupDn))
|
||||
{
|
||||
if (!await IsInGroupInternalAsync(serverUrl, username, password, _options.GroupDn, ct))
|
||||
{
|
||||
return new AuthResult(false, null, "User is not a member of the required security group");
|
||||
}
|
||||
}
|
||||
|
||||
// Lookup user info
|
||||
var userInfo = await LookupUserAsync(serverUrl, username, password, ct);
|
||||
if (userInfo is null)
|
||||
{
|
||||
return new AuthResult(false, null, "Failed to retrieve user information");
|
||||
}
|
||||
|
||||
_logger.LogInformation("User {Username} authenticated successfully via {Server}", username, serverUrl);
|
||||
return new AuthResult(true, userInfo, null);
|
||||
}
|
||||
catch (LdapException ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "LDAP authentication failed for server {Server}", serverUrl);
|
||||
lastError = ex.Message;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Unexpected error during LDAP authentication for server {Server}", serverUrl);
|
||||
lastError = ex.Message;
|
||||
}
|
||||
}
|
||||
|
||||
return new AuthResult(false, null, lastError ?? "Unable to connect to directory server");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<UserInfo?> GetUserInfoAsync(string username, CancellationToken ct = default)
|
||||
{
|
||||
// Not implemented for LDAP - user info is only available during authentication
|
||||
throw new NotSupportedException("GetUserInfoAsync requires password for LDAP lookup");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<bool> IsInGroupAsync(
|
||||
string username,
|
||||
string groupName,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
// This method requires stored credentials or service account - not supported
|
||||
// Group membership is checked during authentication when credentials are available
|
||||
throw new NotSupportedException("IsInGroupAsync requires password for LDAP lookup. Use AuthenticateAsync instead.");
|
||||
}
|
||||
|
||||
private async Task<bool> TryBindAsync(
|
||||
string serverUrl,
|
||||
string username,
|
||||
string password,
|
||||
CancellationToken ct)
|
||||
{
|
||||
using var connection = CreateConnection(serverUrl);
|
||||
var credential = new NetworkCredential(username, password);
|
||||
connection.Credential = credential;
|
||||
connection.AuthType = AuthType.Negotiate;
|
||||
|
||||
try
|
||||
{
|
||||
await Task.Run(() => connection.Bind(), ct);
|
||||
return true;
|
||||
}
|
||||
catch (LdapException ex) when (ex.ErrorCode == 49) // Invalid credentials
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<bool> IsInGroupInternalAsync(
|
||||
string serverUrl,
|
||||
string username,
|
||||
string password,
|
||||
string groupDn,
|
||||
CancellationToken ct)
|
||||
{
|
||||
using var connection = CreateConnection(serverUrl);
|
||||
connection.Credential = new NetworkCredential(username, password);
|
||||
connection.AuthType = AuthType.Negotiate;
|
||||
await Task.Run(() => connection.Bind(), ct);
|
||||
|
||||
var searchRequest = new SearchRequest(
|
||||
_options.SearchBase,
|
||||
string.Format(LdapLookupFormat, EscapeLdapSearchFilter(username)),
|
||||
SearchScope.Subtree,
|
||||
"memberOf");
|
||||
|
||||
var response = (SearchResponse)await Task.Run(
|
||||
() => connection.SendRequest(searchRequest), ct);
|
||||
|
||||
foreach (SearchResultEntry entry in response.Entries)
|
||||
{
|
||||
var memberOf = entry.Attributes["memberOf"];
|
||||
if (memberOf != null)
|
||||
{
|
||||
foreach (var group in memberOf.GetValues(typeof(string)))
|
||||
{
|
||||
if (string.Equals((string)group, groupDn, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private async Task<UserInfo?> LookupUserAsync(
|
||||
string serverUrl,
|
||||
string username,
|
||||
string password,
|
||||
CancellationToken ct)
|
||||
{
|
||||
using var connection = CreateConnection(serverUrl);
|
||||
connection.Credential = new NetworkCredential(username, password);
|
||||
connection.AuthType = AuthType.Negotiate;
|
||||
await Task.Run(() => connection.Bind(), ct);
|
||||
|
||||
var searchRequest = new SearchRequest(
|
||||
_options.SearchBase,
|
||||
string.Format(LdapLookupFormat, EscapeLdapSearchFilter(username)),
|
||||
SearchScope.Subtree,
|
||||
"distinguishedName", "givenName", "sn", "mail", "title");
|
||||
|
||||
var response = (SearchResponse)await Task.Run(
|
||||
() => connection.SendRequest(searchRequest), ct);
|
||||
|
||||
if (response.Entries.Count == 0) return null;
|
||||
|
||||
var entry = response.Entries[0];
|
||||
return new UserInfo
|
||||
{
|
||||
Dn = GetAttribute(entry, "distinguishedName"),
|
||||
Username = username.ToLowerInvariant(),
|
||||
FirstName = GetAttribute(entry, "givenName"),
|
||||
LastName = GetAttribute(entry, "sn"),
|
||||
EmailAddress = GetAttribute(entry, "mail"),
|
||||
Title = GetAttribute(entry, "title")
|
||||
};
|
||||
}
|
||||
|
||||
private LdapConnection CreateConnection(string serverUrl)
|
||||
{
|
||||
var connection = new LdapConnection(serverUrl);
|
||||
connection.SessionOptions.ProtocolVersion = 3;
|
||||
connection.SessionOptions.SecureSocketLayer = false;
|
||||
connection.Timeout = TimeSpan.FromSeconds(_options.ConnectionTimeoutSeconds);
|
||||
return connection;
|
||||
}
|
||||
|
||||
private static string GetAttribute(SearchResultEntry entry, string name)
|
||||
{
|
||||
var attr = entry.Attributes[name];
|
||||
return attr?.Count > 0 ? (string)attr[0] : string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Escapes special characters in LDAP search filter values
|
||||
/// </summary>
|
||||
private static string EscapeLdapSearchFilter(string value)
|
||||
{
|
||||
if (string.IsNullOrEmpty(value))
|
||||
return value;
|
||||
|
||||
return value
|
||||
.Replace("\\", "\\5c")
|
||||
.Replace("*", "\\2a")
|
||||
.Replace("(", "\\28")
|
||||
.Replace(")", "\\29")
|
||||
.Replace("\0", "\\00");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
using JdeScoping.Core.Interfaces;
|
||||
using JdeScoping.Core.Options;
|
||||
using JdeScoping.Infrastructure.Auth;
|
||||
using JdeScoping.Infrastructure.Sources.Cms;
|
||||
using JdeScoping.Infrastructure.Sources.Jde;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
||||
namespace Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for registering infrastructure services.
|
||||
/// </summary>
|
||||
public static class InfrastructureDependencyInjection
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds infrastructure services (data sources, auth) to the service collection.
|
||||
/// </summary>
|
||||
/// <param name="services">The service collection.</param>
|
||||
/// <param name="configuration">The configuration.</param>
|
||||
/// <returns>The service collection for chaining.</returns>
|
||||
public static IServiceCollection AddInfrastructure(
|
||||
this IServiceCollection services,
|
||||
IConfiguration configuration)
|
||||
{
|
||||
// Bind configuration
|
||||
services.Configure<DataSourceOptions>(
|
||||
configuration.GetSection(DataSourceOptions.SectionName));
|
||||
services.Configure<AuthOptions>(
|
||||
configuration.GetSection(AuthOptions.SectionName));
|
||||
services.Configure<LdapOptions>(
|
||||
configuration.GetSection(LdapOptions.SectionName));
|
||||
|
||||
// Register data sources based on configuration
|
||||
var dataSourceOptions = configuration
|
||||
.GetSection(DataSourceOptions.SectionName)
|
||||
.Get<DataSourceOptions>();
|
||||
|
||||
if (dataSourceOptions?.UseFileDataSource == true)
|
||||
{
|
||||
services.AddScoped<IJdeDataSource, JdeFileDataSource>();
|
||||
services.AddScoped<ICmsDataSource, CmsFileDataSource>();
|
||||
}
|
||||
else
|
||||
{
|
||||
services.AddScoped<IJdeDataSource, JdeOracleDataSource>();
|
||||
services.AddScoped<ICmsDataSource, CmsOracleDataSource>();
|
||||
}
|
||||
|
||||
// Register auth service based on configuration
|
||||
var authOptions = configuration
|
||||
.GetSection(AuthOptions.SectionName)
|
||||
.Get<AuthOptions>();
|
||||
|
||||
if (authOptions?.UseFakeAuth == true)
|
||||
{
|
||||
services.AddScoped<IAuthService, FakeAuthService>();
|
||||
}
|
||||
else
|
||||
{
|
||||
services.AddScoped<IAuthService, LdapAuthService>();
|
||||
}
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Dapper" Version="2.1.66" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Options" Version="10.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="10.0.1" />
|
||||
<PackageReference Include="Oracle.ManagedDataAccess.Core" Version="23.26.0" />
|
||||
<PackageReference Include="System.DirectoryServices.Protocols" Version="10.0.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\JdeScoping.Core\JdeScoping.Core.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,43 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text.Json;
|
||||
using JdeScoping.Core.Interfaces;
|
||||
using JdeScoping.Core.Models;
|
||||
using JdeScoping.Core.Models.Quality;
|
||||
using JdeScoping.Core.Options;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace JdeScoping.Infrastructure.Sources.Cms;
|
||||
|
||||
/// <summary>
|
||||
/// File-based CMS data source for development/testing.
|
||||
/// </summary>
|
||||
public class CmsFileDataSource : ICmsDataSource
|
||||
{
|
||||
private readonly string _dataDirectory;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CmsFileDataSource"/> class.
|
||||
/// </summary>
|
||||
public CmsFileDataSource(IOptions<DataSourceOptions> options)
|
||||
{
|
||||
_dataDirectory = options.Value.FileDirectory;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async IAsyncEnumerable<MisData> GetMisDataAsync(
|
||||
DateTime? minimumDt = null,
|
||||
[EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||
{
|
||||
var filePath = Path.Combine(_dataDirectory, "misdata.json");
|
||||
if (!File.Exists(filePath))
|
||||
yield break;
|
||||
|
||||
var json = await File.ReadAllTextAsync(filePath, cancellationToken);
|
||||
var items = JsonSerializer.Deserialize<List<MisData>>(json) ?? [];
|
||||
|
||||
foreach (var item in items.Where(m => !minimumDt.HasValue || (m.ReleaseDate.HasValue && m.ReleaseDate.Value >= minimumDt)))
|
||||
{
|
||||
yield return item;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
using Dapper;
|
||||
using JdeScoping.Core.Interfaces;
|
||||
using JdeScoping.Core.Models;
|
||||
using JdeScoping.Core.Models.Quality;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Oracle.ManagedDataAccess.Client;
|
||||
|
||||
namespace JdeScoping.Infrastructure.Sources.Cms;
|
||||
|
||||
/// <summary>
|
||||
/// Oracle-based CMS data source for production use.
|
||||
/// </summary>
|
||||
public class CmsOracleDataSource : ICmsDataSource
|
||||
{
|
||||
private readonly string _connectionString;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CmsOracleDataSource"/> class.
|
||||
/// </summary>
|
||||
public CmsOracleDataSource(IConfiguration configuration)
|
||||
{
|
||||
_connectionString = configuration.GetConnectionString("CMS")
|
||||
?? throw new InvalidOperationException("CMS connection string not configured");
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async IAsyncEnumerable<MisData> GetMisDataAsync(
|
||||
DateTime? minimumDt = null,
|
||||
[EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||
{
|
||||
await using var connection = new OracleConnection(_connectionString);
|
||||
await connection.OpenAsync(cancellationToken);
|
||||
|
||||
// TODO: Implement actual CMS query for MIS data
|
||||
var sql = minimumDt.HasValue
|
||||
? "SELECT * FROM MISDATA WHERE RELEASE_DATE >= :MinDate"
|
||||
: "SELECT * FROM MISDATA";
|
||||
|
||||
var results = await connection.QueryAsync<MisData>(sql, new { MinDate = minimumDt });
|
||||
foreach (var item in results)
|
||||
{
|
||||
yield return item;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text.Json;
|
||||
using JdeScoping.Core.Interfaces;
|
||||
using JdeScoping.Core.Models;
|
||||
using JdeScoping.Core.Models.Inventory;
|
||||
using JdeScoping.Core.Models.Organization;
|
||||
using JdeScoping.Core.Models.WorkOrders;
|
||||
using JdeScoping.Core.Options;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace JdeScoping.Infrastructure.Sources.Jde;
|
||||
|
||||
/// <summary>
|
||||
/// File-based JDE data source for development/testing.
|
||||
/// </summary>
|
||||
public class JdeFileDataSource : IJdeDataSource
|
||||
{
|
||||
private readonly string _dataDirectory;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="JdeFileDataSource"/> class.
|
||||
/// </summary>
|
||||
public JdeFileDataSource(IOptions<DataSourceOptions> options)
|
||||
{
|
||||
_dataDirectory = options.Value.FileDirectory;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async IAsyncEnumerable<WorkOrder> GetWorkOrdersAsync(
|
||||
DateTime? minimumDt = null,
|
||||
[EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||
{
|
||||
var items = await LoadFromFileAsync<WorkOrder>("workorders.json", cancellationToken);
|
||||
foreach (var item in items.Where(wo => !minimumDt.HasValue || (wo.LastUpdateDt.HasValue && wo.LastUpdateDt.Value >= minimumDt)))
|
||||
{
|
||||
yield return item;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async IAsyncEnumerable<LotUsage> GetLotUsagesAsync(
|
||||
DateTime? minimumDt = null,
|
||||
[EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||
{
|
||||
var items = await LoadFromFileAsync<LotUsage>("lotusages.json", cancellationToken);
|
||||
foreach (var item in items.Where(lu => !minimumDt.HasValue || (lu.LastUpdateDt.HasValue && lu.LastUpdateDt.Value >= minimumDt)))
|
||||
{
|
||||
yield return item;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async IAsyncEnumerable<Lot> GetLotsAsync(
|
||||
DateTime? minimumDt = null,
|
||||
[EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||
{
|
||||
var items = await LoadFromFileAsync<Lot>("lots.json", cancellationToken);
|
||||
foreach (var item in items.Where(l => !minimumDt.HasValue || (l.LastUpdateDt.HasValue && l.LastUpdateDt.Value >= minimumDt)))
|
||||
{
|
||||
yield return item;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async IAsyncEnumerable<Item> GetItemsAsync(
|
||||
DateTime? minimumDt = null,
|
||||
[EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||
{
|
||||
var items = await LoadFromFileAsync<Item>("items.json", cancellationToken);
|
||||
foreach (var item in items.Where(i => !minimumDt.HasValue || (i.LastUpdateDt.HasValue && i.LastUpdateDt.Value >= minimumDt)))
|
||||
{
|
||||
yield return item;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async IAsyncEnumerable<WorkCenter> GetWorkCentersAsync(
|
||||
DateTime? minimumDt = null,
|
||||
[EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||
{
|
||||
var items = await LoadFromFileAsync<WorkCenter>("workcenters.json", cancellationToken);
|
||||
foreach (var item in items.Where(wc => !minimumDt.HasValue || (wc.LastUpdateDt.HasValue && wc.LastUpdateDt.Value >= minimumDt)))
|
||||
{
|
||||
yield return item;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async IAsyncEnumerable<ProfitCenter> GetProfitCentersAsync(
|
||||
DateTime? minimumDt = null,
|
||||
[EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||
{
|
||||
var items = await LoadFromFileAsync<ProfitCenter>("profitcenters.json", cancellationToken);
|
||||
foreach (var item in items.Where(pc => !minimumDt.HasValue || (pc.LastUpdateDt.HasValue && pc.LastUpdateDt.Value >= minimumDt)))
|
||||
{
|
||||
yield return item;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async IAsyncEnumerable<JdeUser> GetUsersAsync(
|
||||
DateTime? minimumDt = null,
|
||||
[EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||
{
|
||||
var items = await LoadFromFileAsync<JdeUser>("users.json", cancellationToken);
|
||||
foreach (var item in items.Where(u => !minimumDt.HasValue || (u.LastUpdateDt.HasValue && u.LastUpdateDt.Value >= minimumDt)))
|
||||
{
|
||||
yield return item;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async IAsyncEnumerable<Branch> GetBranchesAsync(
|
||||
DateTime? minimumDt = null,
|
||||
[EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||
{
|
||||
var items = await LoadFromFileAsync<Branch>("branches.json", cancellationToken);
|
||||
foreach (var item in items)
|
||||
{
|
||||
yield return item;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<List<T>> LoadFromFileAsync<T>(string fileName, CancellationToken cancellationToken)
|
||||
{
|
||||
var filePath = Path.Combine(_dataDirectory, fileName);
|
||||
if (!File.Exists(filePath))
|
||||
return [];
|
||||
|
||||
var json = await File.ReadAllTextAsync(filePath, cancellationToken);
|
||||
return JsonSerializer.Deserialize<List<T>>(json) ?? [];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
using Dapper;
|
||||
using JdeScoping.Core.Interfaces;
|
||||
using JdeScoping.Core.Models;
|
||||
using JdeScoping.Core.Models.Inventory;
|
||||
using JdeScoping.Core.Models.Organization;
|
||||
using JdeScoping.Core.Models.WorkOrders;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Oracle.ManagedDataAccess.Client;
|
||||
|
||||
namespace JdeScoping.Infrastructure.Sources.Jde;
|
||||
|
||||
/// <summary>
|
||||
/// Oracle-based JDE data source for production use.
|
||||
/// </summary>
|
||||
public class JdeOracleDataSource : IJdeDataSource
|
||||
{
|
||||
private readonly string _connectionString;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="JdeOracleDataSource"/> class.
|
||||
/// </summary>
|
||||
public JdeOracleDataSource(IConfiguration configuration)
|
||||
{
|
||||
_connectionString = configuration.GetConnectionString("JDE")
|
||||
?? throw new InvalidOperationException("JDE connection string not configured");
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async IAsyncEnumerable<WorkOrder> GetWorkOrdersAsync(
|
||||
DateTime? minimumDt = null,
|
||||
[EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||
{
|
||||
await using var connection = new OracleConnection(_connectionString);
|
||||
await connection.OpenAsync(cancellationToken);
|
||||
|
||||
// TODO: Implement actual JDE query with proper column mapping
|
||||
var sql = minimumDt.HasValue
|
||||
? "SELECT * FROM F4801 WHERE UPMJ >= :MinDate"
|
||||
: "SELECT * FROM F4801";
|
||||
|
||||
var results = await connection.QueryAsync<WorkOrder>(sql, new { MinDate = minimumDt });
|
||||
foreach (var item in results)
|
||||
{
|
||||
yield return item;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async IAsyncEnumerable<LotUsage> GetLotUsagesAsync(
|
||||
DateTime? minimumDt = null,
|
||||
[EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||
{
|
||||
await using var connection = new OracleConnection(_connectionString);
|
||||
await connection.OpenAsync(cancellationToken);
|
||||
|
||||
// TODO: Implement actual JDE query
|
||||
var sql = minimumDt.HasValue
|
||||
? "SELECT * FROM F4111 WHERE UPMJ >= :MinDate"
|
||||
: "SELECT * FROM F4111";
|
||||
|
||||
var results = await connection.QueryAsync<LotUsage>(sql, new { MinDate = minimumDt });
|
||||
foreach (var item in results)
|
||||
{
|
||||
yield return item;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async IAsyncEnumerable<Lot> GetLotsAsync(
|
||||
DateTime? minimumDt = null,
|
||||
[EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||
{
|
||||
await using var connection = new OracleConnection(_connectionString);
|
||||
await connection.OpenAsync(cancellationToken);
|
||||
|
||||
var sql = minimumDt.HasValue
|
||||
? "SELECT * FROM F4108 WHERE UPMJ >= :MinDate"
|
||||
: "SELECT * FROM F4108";
|
||||
|
||||
var results = await connection.QueryAsync<Lot>(sql, new { MinDate = minimumDt });
|
||||
foreach (var item in results)
|
||||
{
|
||||
yield return item;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async IAsyncEnumerable<Item> GetItemsAsync(
|
||||
DateTime? minimumDt = null,
|
||||
[EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||
{
|
||||
await using var connection = new OracleConnection(_connectionString);
|
||||
await connection.OpenAsync(cancellationToken);
|
||||
|
||||
var sql = minimumDt.HasValue
|
||||
? "SELECT * FROM F4101 WHERE UPMJ >= :MinDate"
|
||||
: "SELECT * FROM F4101";
|
||||
|
||||
var results = await connection.QueryAsync<Item>(sql, new { MinDate = minimumDt });
|
||||
foreach (var item in results)
|
||||
{
|
||||
yield return item;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async IAsyncEnumerable<WorkCenter> GetWorkCentersAsync(
|
||||
DateTime? minimumDt = null,
|
||||
[EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||
{
|
||||
await using var connection = new OracleConnection(_connectionString);
|
||||
await connection.OpenAsync(cancellationToken);
|
||||
|
||||
var sql = minimumDt.HasValue
|
||||
? "SELECT * FROM F30006 WHERE UPMJ >= :MinDate"
|
||||
: "SELECT * FROM F30006";
|
||||
|
||||
var results = await connection.QueryAsync<WorkCenter>(sql, new { MinDate = minimumDt });
|
||||
foreach (var item in results)
|
||||
{
|
||||
yield return item;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async IAsyncEnumerable<ProfitCenter> GetProfitCentersAsync(
|
||||
DateTime? minimumDt = null,
|
||||
[EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||
{
|
||||
await using var connection = new OracleConnection(_connectionString);
|
||||
await connection.OpenAsync(cancellationToken);
|
||||
|
||||
// TODO: Implement actual query for profit centers
|
||||
var sql = "SELECT * FROM F0006";
|
||||
|
||||
var results = await connection.QueryAsync<ProfitCenter>(sql);
|
||||
foreach (var item in results)
|
||||
{
|
||||
yield return item;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async IAsyncEnumerable<JdeUser> GetUsersAsync(
|
||||
DateTime? minimumDt = null,
|
||||
[EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||
{
|
||||
await using var connection = new OracleConnection(_connectionString);
|
||||
await connection.OpenAsync(cancellationToken);
|
||||
|
||||
var sql = minimumDt.HasValue
|
||||
? "SELECT * FROM F0092 WHERE UPMJ >= :MinDate"
|
||||
: "SELECT * FROM F0092";
|
||||
|
||||
var results = await connection.QueryAsync<JdeUser>(sql, new { MinDate = minimumDt });
|
||||
foreach (var item in results)
|
||||
{
|
||||
yield return item;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async IAsyncEnumerable<Branch> GetBranchesAsync(
|
||||
DateTime? minimumDt = null,
|
||||
[EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||
{
|
||||
await using var connection = new OracleConnection(_connectionString);
|
||||
await connection.OpenAsync(cancellationToken);
|
||||
|
||||
// TODO: Implement actual query for branches
|
||||
var sql = "SELECT * FROM F0101";
|
||||
|
||||
var results = await connection.QueryAsync<Branch>(sql);
|
||||
foreach (var item in results)
|
||||
{
|
||||
yield return item;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user