refactor: remove unused CMS/JDE repositories and data sources

Remove legacy JDE and CMS direct-access code that is no longer used:
- Delete ICmsDataSource, IJdeDataSource interfaces
- Delete ISearchProcessor, IUpdateProcessor interfaces
- Delete IJdeRepository and ICmsRepository (all partials)
- Delete JdeRepository and CmsRepository implementations
- Delete JdeQueries and CmsQueries
- Delete JdeFileDataSource, JdeOracleDataSource
- Delete CmsFileDataSource, CmsOracleDataSource
- Remove unused methods from LotFinderRepository interfaces
- Delete associated unit tests (CmsRepositoryTests, JdeRepositoryTests)

All data sync now uses ETL pipelines via DataSync project.
This commit is contained in:
Joseph Doherty
2026-01-07 05:04:49 -05:00
parent 6952f686fa
commit 1618b6664d
52 changed files with 1497 additions and 3779 deletions
+21 -21
View File
@@ -39,33 +39,33 @@ This document describes all data synchronization imports from the legacy JDE Sco
| # | Import Name | Source | Dest Table | Mass | Daily | Hourly | Filter | Cache File | Notes |
|---|-------------|--------|------------|------|-------|--------|--------|------------|-------|
| 1 | WorkOrder | JDE | WorkOrder_Curr | Yes | Yes | Yes | Yes | `workorder_curr.json.zstd` | |
| 2 | LotUsage | JDE | LotUsage_Curr | Yes | Yes | Yes | Yes | `lotusage_curr.json.zstd` | |
| 3 | Item | JDE | Item | Yes | Yes | Yes | Yes | `item.json.zstd` | |
| 4 | Lot | JDE | Lot | Yes | Yes | Yes | Yes | `lot.json.zstd` | |
| 5 | WorkOrderTime | JDE | WorkOrderTime_Curr | Yes | Yes | Yes | Yes | `workordertime_curr.json.zstd` | |
| 6 | WorkOrderComponent | JDE | WorkOrderComponent_Curr | Yes | Yes | Yes | Yes | `workordercomponent_curr.json.zstd` | |
| 7 | WorkOrderStep | JDE | WorkOrderStep_Curr | Yes | Yes | Yes | Yes | `workorderstep_curr.json.zstd` | |
| 8 | WorkOrderRouting | JDE | WorkOrderRouting | Yes | Yes | Yes | Yes | `workorderrouting.json.zstd` | |
| 9 | Branch | JDE | Branch | Yes | Yes | Yes | Yes | `branch.json.zstd` | typeCode='BP' |
| 10 | ProfitCenter | JDE | ProfitCenter | Yes | Yes | Yes | Yes | `profitcenter.json.zstd` | typeCode='I3' |
| 11 | WorkCenter | JDE | WorkCenter | Yes | Yes | Yes | Yes | `workcenter.json.zstd` | typeCode='WC' |
| 1 | WorkOrder | JDE | WorkOrder_Curr | Yes | Yes | Yes | Yes | `workorder_curr.pb.zstd` | |
| 2 | LotUsage | JDE | LotUsage_Curr | Yes | Yes | Yes | Yes | `lotusage_curr.pb.zstd` | |
| 3 | Item | JDE | Item | Yes | Yes | Yes | Yes | `item.pb.zstd` | |
| 4 | Lot | JDE | Lot | Yes | Yes | Yes | Yes | `lot.pb.zstd` | |
| 5 | WorkOrderTime | JDE | WorkOrderTime_Curr | Yes | Yes | Yes | Yes | `workordertime_curr.pb.zstd` | |
| 6 | WorkOrderComponent | JDE | WorkOrderComponent_Curr | Yes | Yes | Yes | Yes | `workordercomponent_curr.pb.zstd` | |
| 7 | WorkOrderStep | JDE | WorkOrderStep_Curr | Yes | Yes | Yes | Yes | `workorderstep_curr.pb.zstd` | |
| 8 | WorkOrderRouting | JDE | WorkOrderRouting | Yes | Yes | Yes | Yes | `workorderrouting.pb.zstd` | |
| 9 | Branch | JDE | Branch | Yes | Yes | Yes | Yes | `branch.pb.zstd` | typeCode='BP' |
| 10 | ProfitCenter | JDE | ProfitCenter | Yes | Yes | Yes | Yes | `profitcenter.pb.zstd` | typeCode='I3' |
| 11 | WorkCenter | JDE | WorkCenter | Yes | Yes | Yes | Yes | `workcenter.pb.zstd` | typeCode='WC' |
| 12 | StatusCode | JDE | StatusCode | Yes | Yes | Yes | Yes | *(none)* | GIW connection |
| 13 | JdeUser | JDE | JdeUser | Yes | Yes | Yes | No | `jdeuser.json.zstd` | Same query both |
| 14 | OrgHierarchy | JDE | OrgHierarchy | Yes | Yes | Yes | Yes | `orghierarchy.json.zstd` | |
| 15 | RouteMaster | JDE | RouteMaster | Yes | Yes | Yes | Yes | `routemaster.json.zstd` | |
| 16 | FunctionCode | JDE | FunctionCode | Yes | Yes | Yes | No | `functioncode.json.zstd` | Always full reload |
| 17 | MisData | CMS | MisData | Yes | Yes | No | Yes | `misdata.json.zstd` | Hourly disabled |
| 13 | JdeUser | JDE | JdeUser | Yes | Yes | Yes | No | `jdeuser.pb.zstd` | Same query both |
| 14 | OrgHierarchy | JDE | OrgHierarchy | Yes | Yes | Yes | Yes | `orghierarchy.pb.zstd` | |
| 15 | RouteMaster | JDE | RouteMaster | Yes | Yes | Yes | Yes | `routemaster.pb.zstd` | |
| 16 | FunctionCode | JDE | FunctionCode | Yes | Yes | Yes | No | `functioncode.pb.zstd` | Always full reload |
| 17 | MisData | CMS | MisData | Yes | Yes | No | Yes | `misdata.pb.zstd` | Hourly disabled |
### Archive Syncs (5) - ALL DISABLED
| # | Import Name | Source | Dest Table | Mass | Daily | Hourly | Filter | Cache File | Notes |
|---|-------------|--------|------------|------|-------|--------|--------|------------|-------|
| 18 | WorkOrder_Archive | JDE | WorkOrder_Hist | No | No | No | No | `workorder_hist.json.zstd` | DISABLED |
| 19 | LotUsage_Archive | JDE | LotUsage_Hist | No | No | No | No | `lotusage_hist.json.zstd` | DISABLED |
| 20 | WorkOrderTime_Archive | JDE | WorkOrderTime_Hist | No | No | No | No | `workordertime_hist.json.zstd` | DISABLED |
| 21 | WorkOrderComponent_Archive | JDE | WorkOrderComponent_Hist | No | No | No | No | `workordercomponent_hist.json.zstd` | DISABLED |
| 22 | WorkOrderStep_Archive | JDE | WorkOrderStep_Hist | No | No | No | No | `workorderstep_hist.json.zstd` | DISABLED |
| 18 | WorkOrder_Archive | JDE | WorkOrder_Hist | No | No | No | No | `workorder_hist.pb.zstd` | DISABLED |
| 19 | LotUsage_Archive | JDE | LotUsage_Hist | No | No | No | No | `lotusage_hist.pb.zstd` | DISABLED |
| 20 | WorkOrderTime_Archive | JDE | WorkOrderTime_Hist | No | No | No | No | `workordertime_hist.pb.zstd` | DISABLED |
| 21 | WorkOrderComponent_Archive | JDE | WorkOrderComponent_Hist | No | No | No | No | `workordercomponent_hist.pb.zstd` | DISABLED |
| 22 | WorkOrderStep_Archive | JDE | WorkOrderStep_Hist | No | No | No | No | `workorderstep_hist.pb.zstd` | DISABLED |
**Cache File Location:** `CACHED_DB_FILES/`
@@ -1,17 +0,0 @@
using JdeScoping.Core.Models;
using JdeScoping.Core.Models.Quality;
namespace JdeScoping.Core.Interfaces;
/// <summary>
/// Interface for fetching data from CMS source system.
/// </summary>
public interface ICmsDataSource
{
/// <summary>
/// Gets MIS data from CMS as an async stream.
/// </summary>
/// <param name="minimumDt">Minimum update timestamp for incremental fetch. Null for full fetch.</param>
/// <param name="cancellationToken">Cancellation token.</param>
IAsyncEnumerable<MisData> GetMisDataAsync(DateTime? minimumDt = null, CancellationToken cancellationToken = default);
}
@@ -1,68 +0,0 @@
using JdeScoping.Core.Models;
using JdeScoping.Core.Models.Inventory;
using JdeScoping.Core.Models.Organization;
using JdeScoping.Core.Models.WorkOrders;
namespace JdeScoping.Core.Interfaces;
/// <summary>
/// Interface for fetching data from JDE (JD Edwards) source system.
/// </summary>
public interface IJdeDataSource
{
/// <summary>
/// Gets work orders from JDE as an async stream.
/// </summary>
/// <param name="minimumDt">Minimum update timestamp for incremental fetch. Null for full fetch.</param>
/// <param name="cancellationToken">Cancellation token.</param>
IAsyncEnumerable<WorkOrder> GetWorkOrdersAsync(DateTime? minimumDt = null, CancellationToken cancellationToken = default);
/// <summary>
/// Gets lot usage records from JDE as an async stream.
/// </summary>
/// <param name="minimumDt">Minimum update timestamp for incremental fetch. Null for full fetch.</param>
/// <param name="cancellationToken">Cancellation token.</param>
IAsyncEnumerable<LotUsage> GetLotUsagesAsync(DateTime? minimumDt = null, CancellationToken cancellationToken = default);
/// <summary>
/// Gets lots from JDE as an async stream.
/// </summary>
/// <param name="minimumDt">Minimum update timestamp for incremental fetch. Null for full fetch.</param>
/// <param name="cancellationToken">Cancellation token.</param>
IAsyncEnumerable<Lot> GetLotsAsync(DateTime? minimumDt = null, CancellationToken cancellationToken = default);
/// <summary>
/// Gets items from JDE as an async stream.
/// </summary>
/// <param name="minimumDt">Minimum update timestamp for incremental fetch. Null for full fetch.</param>
/// <param name="cancellationToken">Cancellation token.</param>
IAsyncEnumerable<Item> GetItemsAsync(DateTime? minimumDt = null, CancellationToken cancellationToken = default);
/// <summary>
/// Gets work centers from JDE as an async stream.
/// </summary>
/// <param name="minimumDt">Minimum update timestamp for incremental fetch. Null for full fetch.</param>
/// <param name="cancellationToken">Cancellation token.</param>
IAsyncEnumerable<WorkCenter> GetWorkCentersAsync(DateTime? minimumDt = null, CancellationToken cancellationToken = default);
/// <summary>
/// Gets profit centers from JDE as an async stream.
/// </summary>
/// <param name="minimumDt">Minimum update timestamp for incremental fetch. Null for full fetch.</param>
/// <param name="cancellationToken">Cancellation token.</param>
IAsyncEnumerable<ProfitCenter> GetProfitCentersAsync(DateTime? minimumDt = null, CancellationToken cancellationToken = default);
/// <summary>
/// Gets JDE users as an async stream.
/// </summary>
/// <param name="minimumDt">Minimum update timestamp for incremental fetch. Null for full fetch.</param>
/// <param name="cancellationToken">Cancellation token.</param>
IAsyncEnumerable<JdeUser> GetUsersAsync(DateTime? minimumDt = null, CancellationToken cancellationToken = default);
/// <summary>
/// Gets branches from JDE as an async stream.
/// </summary>
/// <param name="minimumDt">Minimum update timestamp for incremental fetch. Null for full fetch.</param>
/// <param name="cancellationToken">Cancellation token.</param>
IAsyncEnumerable<Branch> GetBranchesAsync(DateTime? minimumDt = null, CancellationToken cancellationToken = default);
}
@@ -13,44 +13,4 @@ public partial interface ILotFinderRepository
/// <param name="ct">Cancellation token.</param>
/// <returns>Latest data updates.</returns>
Task<List<DataUpdate>> GetLastDataUpdatesAsync(CancellationToken ct = default);
/// <summary>
/// Gets table schema specification for dynamic SQL generation.
/// </summary>
/// <param name="tableName">Table name.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>Table specification with columns and primary key.</returns>
Task<TableSpec> GetTableSpecAsync(string tableName, CancellationToken ct = default);
/// <summary>
/// Rebuilds all indices on a table with fillfactor of 95.
/// Table name is validated against whitelist for SQL injection prevention.
/// </summary>
/// <param name="tableName">Table name (must be in whitelist).</param>
/// <param name="ct">Cancellation token.</param>
/// <exception cref="ArgumentException">Thrown if table name is not in whitelist.</exception>
Task RebuildIndicesAsync(string tableName, CancellationToken ct = default);
/// <summary>
/// Post-processes imported MIS data to set obsolete dates.
/// </summary>
/// <param name="ct">Cancellation token.</param>
Task PostProcessMisDataAsync(CancellationToken ct = default);
/// <summary>
/// Performs bulk insert of records into a table.
/// </summary>
/// <typeparam name="T">Record type.</typeparam>
/// <param name="tableName">Target table name.</param>
/// <param name="records">Records to insert.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>Number of records inserted.</returns>
Task<int> BulkInsertAsync<T>(string tableName, IEnumerable<T> records, CancellationToken ct = default);
/// <summary>
/// Truncates a table, removing all records.
/// </summary>
/// <param name="tableName">Table name.</param>
/// <param name="ct">Cancellation token.</param>
Task TruncateTableAsync(string tableName, CancellationToken ct = default);
}
@@ -43,14 +43,6 @@ public partial interface ILotFinderRepository
/// <returns>Top 25 matching work centers.</returns>
Task<List<WorkCenter>> SearchWorkCentersAsync(string filter, CancellationToken ct = default);
/// <summary>
/// Looks up work centers by codes.
/// </summary>
/// <param name="codes">Work center codes to match.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>Matching work centers.</returns>
Task<List<WorkCenter>> LookupWorkCentersAsync(List<string> codes, CancellationToken ct = default);
/// <summary>
/// Searches profit centers by code or description.
/// </summary>
@@ -59,14 +51,6 @@ public partial interface ILotFinderRepository
/// <returns>Top 25 matching profit centers.</returns>
Task<List<ProfitCenter>> SearchProfitCentersAsync(string filter, CancellationToken ct = default);
/// <summary>
/// Looks up profit centers by codes.
/// </summary>
/// <param name="codes">Profit center codes to match.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>Matching profit centers.</returns>
Task<List<ProfitCenter>> LookupProfitCentersAsync(List<string> codes, CancellationToken ct = default);
/// <summary>
/// Searches users by user ID, full name, or address number.
/// </summary>
@@ -75,14 +59,6 @@ public partial interface ILotFinderRepository
/// <returns>Top 25 matching users.</returns>
Task<List<JdeUser>> SearchUsersAsync(string filter, CancellationToken ct = default);
/// <summary>
/// Looks up users by user IDs or address numbers.
/// </summary>
/// <param name="userIds">User IDs or address numbers to match.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>Matching users.</returns>
Task<List<JdeUser>> LookupUsersAsync(List<string> userIds, CancellationToken ct = default);
/// <summary>
/// Looks up lots by lot number and item number.
/// </summary>
@@ -1,4 +1,3 @@
using JdeScoping.Core.Models.Enums;
using JdeScoping.Core.Models.Search;
namespace JdeScoping.Core.Interfaces;
@@ -46,20 +45,4 @@ public partial interface ILotFinderRepository
/// <param name="ct">Cancellation token.</param>
/// <returns>Generated search ID.</returns>
Task<int> SubmitSearchAsync(Search search, CancellationToken ct = default);
/// <summary>
/// Updates the status of a search.
/// </summary>
/// <param name="id">Search ID.</param>
/// <param name="status">New status.</param>
/// <param name="ct">Cancellation token.</param>
Task UpdateSearchStatusAsync(int id, SearchStatus status, CancellationToken ct = default);
/// <summary>
/// Stores the Excel results for a completed search.
/// </summary>
/// <param name="id">Search ID.</param>
/// <param name="results">Excel file bytes.</param>
/// <param name="ct">Cancellation token.</param>
Task UpdateSearchResultsAsync(int id, byte[] results, CancellationToken ct = default);
}
@@ -1,17 +0,0 @@
namespace JdeScoping.Core.Interfaces;
/// <summary>
/// Processor for executing user search requests.
/// </summary>
public interface ISearchProcessor
{
/// <summary>
/// Processes a queued search request.
/// </summary>
Task ProcessSearchAsync(int searchId, CancellationToken cancellationToken = default);
/// <summary>
/// Gets the next pending search to process.
/// </summary>
Task<int?> GetNextPendingSearchAsync(CancellationToken cancellationToken = default);
}
@@ -1,22 +0,0 @@
namespace JdeScoping.Core.Interfaces;
/// <summary>
/// Processor for data synchronization from JDE/CMS to local cache.
/// </summary>
public interface IUpdateProcessor
{
/// <summary>
/// Runs a mass (full) data refresh.
/// </summary>
Task RunMassRefreshAsync(CancellationToken cancellationToken = default);
/// <summary>
/// Runs a daily incremental data refresh.
/// </summary>
Task RunDailyRefreshAsync(CancellationToken cancellationToken = default);
/// <summary>
/// Runs an hourly data refresh.
/// </summary>
Task RunHourlyRefreshAsync(CancellationToken cancellationToken = default);
}
@@ -38,8 +38,6 @@ public static class DataAccessDependencyInjection
// Register repositories as scoped (per-request lifetime)
services.AddScoped<ILotFinderRepository, LotFinderRepository>();
services.AddScoped<IJdeRepository, JdeRepository>();
services.AddScoped<ICmsRepository, CmsRepository>();
// Register SqlKata compiler (singleton, thread-safe)
services.AddSingleton<SqlServerCompiler>();
@@ -74,8 +72,6 @@ public static class DataAccessDependencyInjection
// Register repositories as scoped (per-request lifetime)
services.AddScoped<ILotFinderRepository, LotFinderRepository>();
services.AddScoped<IJdeRepository, JdeRepository>();
services.AddScoped<ICmsRepository, CmsRepository>();
return services;
}
@@ -1,19 +0,0 @@
using JdeScoping.Core.Models;
using JdeScoping.Core.Models.Quality;
namespace JdeScoping.DataAccess.Interfaces;
/// <summary>
/// Repository for accessing CMS Oracle database.
/// </summary>
public interface ICmsRepository
{
/// <summary>
/// Gets Manufacturing Information System (MIS) data from CMS database.
/// Uses MisDataTimeoutSeconds timeout due to complex 10-table JOIN.
/// </summary>
/// <param name="lastUpdateDt">Optional cutoff for incremental sync.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>Streaming MIS data records.</returns>
IAsyncEnumerable<MisData> GetMisDataAsync(DateTime? lastUpdateDt = null, CancellationToken ct = default);
}
@@ -1,43 +0,0 @@
using JdeScoping.Core.Models.Inventory;
namespace JdeScoping.DataAccess.Interfaces;
/// <summary>
/// Inventory (lots) operations for JDE Oracle repository.
/// </summary>
public partial interface IJdeRepository
{
/// <summary>
/// Gets lot master data from production schema, optionally filtered by last update.
/// </summary>
/// <param name="lastUpdateDt">Optional cutoff for incremental sync.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>Streaming lots.</returns>
IAsyncEnumerable<Lot> GetLotsAsync(DateTime? lastUpdateDt = null, CancellationToken ct = default);
/// <summary>
/// Gets lot usage (cardex) transactions from production schema, optionally filtered by last update.
/// Uses special LotUsageTimeoutSeconds timeout due to large dataset.
/// </summary>
/// <param name="lastUpdateDt">Optional cutoff for incremental sync.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>Streaming lot usages.</returns>
IAsyncEnumerable<LotUsage> GetLotUsagesAsync(DateTime? lastUpdateDt = null, CancellationToken ct = default);
/// <summary>
/// Gets lot usage transactions from archive schema.
/// </summary>
/// <param name="lastUpdateDt">Optional cutoff for incremental sync.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>Streaming archived lot usages.</returns>
IAsyncEnumerable<LotUsage> GetLotUsagesArchiveAsync(DateTime? lastUpdateDt = null, CancellationToken ct = default);
/// <summary>
/// Gets lot location tracking from JDE Stage view.
/// Uses JDE Stage connection.
/// </summary>
/// <param name="lastUpdateDt">Optional cutoff for incremental sync.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>Streaming lot locations.</returns>
IAsyncEnumerable<LotLocation> GetLotLocationsAsync(DateTime? lastUpdateDt = null, CancellationToken ct = default);
}
@@ -1,86 +0,0 @@
using JdeScoping.Core.Models;
using JdeScoping.Core.Models.Inventory;
using JdeScoping.Core.Models.Lookup;
using JdeScoping.Core.Models.Organization;
namespace JdeScoping.DataAccess.Interfaces;
/// <summary>
/// Reference data operations for JDE Oracle repository.
/// </summary>
public partial interface IJdeRepository
{
/// <summary>
/// Gets item master data from production schema, optionally filtered by last update.
/// </summary>
/// <param name="lastUpdateDt">Optional cutoff for incremental sync.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>Streaming items.</returns>
IAsyncEnumerable<Item> GetItemsAsync(DateTime? lastUpdateDt = null, CancellationToken ct = default);
/// <summary>
/// Gets user/operator data from production schema.
/// Note: Incremental filtering not supported for users (full sync always).
/// </summary>
/// <param name="lastUpdateDt">Ignored (full sync always).</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>Streaming users.</returns>
IAsyncEnumerable<JdeUser> GetUsersAsync(DateTime? lastUpdateDt = null, CancellationToken ct = default);
/// <summary>
/// Gets branch business units from production schema (type code 'BP').
/// </summary>
/// <param name="lastUpdateDt">Optional cutoff for incremental sync.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>Streaming branches.</returns>
IAsyncEnumerable<Branch> GetBranchesAsync(DateTime? lastUpdateDt = null, CancellationToken ct = default);
/// <summary>
/// Gets profit center business units from production schema (type code 'I3').
/// </summary>
/// <param name="lastUpdateDt">Optional cutoff for incremental sync.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>Streaming profit centers.</returns>
IAsyncEnumerable<ProfitCenter> GetProfitCentersAsync(DateTime? lastUpdateDt = null, CancellationToken ct = default);
/// <summary>
/// Gets work center business units from production schema (type code 'WC').
/// </summary>
/// <param name="lastUpdateDt">Optional cutoff for incremental sync.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>Streaming work centers.</returns>
IAsyncEnumerable<WorkCenter> GetWorkCentersAsync(DateTime? lastUpdateDt = null, CancellationToken ct = default);
/// <summary>
/// Gets status codes from JDE Stage view.
/// Uses JDE Stage connection.
/// </summary>
/// <param name="lastUpdateDt">Optional cutoff for incremental sync.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>Streaming status codes.</returns>
IAsyncEnumerable<StatusCode> GetStatusCodesAsync(DateTime? lastUpdateDt = null, CancellationToken ct = default);
/// <summary>
/// Gets function codes from production schema.
/// Note: Does not support incremental filtering (full sync always).
/// </summary>
/// <param name="ct">Cancellation token.</param>
/// <returns>Streaming function codes.</returns>
IAsyncEnumerable<FunctionCode> GetFunctionCodesAsync(CancellationToken ct = default);
/// <summary>
/// Gets organization hierarchy (work center to profit center mapping) from production schema.
/// </summary>
/// <param name="lastUpdateDt">Optional cutoff for incremental sync.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>Streaming org hierarchy records.</returns>
IAsyncEnumerable<OrgHierarchy> GetOrgHierarchyAsync(DateTime? lastUpdateDt = null, CancellationToken ct = default);
/// <summary>
/// Gets item routing master data from production schema.
/// </summary>
/// <param name="lastUpdateDt">Optional cutoff for incremental sync.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>Streaming route masters.</returns>
IAsyncEnumerable<RouteMaster> GetRouteMastersAsync(DateTime? lastUpdateDt = null, CancellationToken ct = default);
}
@@ -1,81 +0,0 @@
using JdeScoping.Core.Models.WorkOrders;
namespace JdeScoping.DataAccess.Interfaces;
/// <summary>
/// Work order operations for JDE Oracle repository.
/// </summary>
public partial interface IJdeRepository
{
/// <summary>
/// Gets work orders from production schema, optionally filtered by last update.
/// </summary>
/// <param name="lastUpdateDt">Optional cutoff for incremental sync.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>Streaming work orders.</returns>
IAsyncEnumerable<WorkOrder> GetWorkOrdersAsync(DateTime? lastUpdateDt = null, CancellationToken ct = default);
/// <summary>
/// Gets work orders from archive schema.
/// </summary>
/// <param name="lastUpdateDt">Optional cutoff for incremental sync.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>Streaming archived work orders.</returns>
IAsyncEnumerable<WorkOrder> GetWorkOrdersArchiveAsync(DateTime? lastUpdateDt = null, CancellationToken ct = default);
/// <summary>
/// Gets work order steps from production schema, optionally filtered by last update.
/// </summary>
/// <param name="lastUpdateDt">Optional cutoff for incremental sync.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>Streaming work order steps.</returns>
IAsyncEnumerable<WorkOrderStep> GetWorkOrderStepsAsync(DateTime? lastUpdateDt = null, CancellationToken ct = default);
/// <summary>
/// Gets work order steps from archive schema.
/// </summary>
/// <param name="lastUpdateDt">Optional cutoff for incremental sync.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>Streaming archived work order steps.</returns>
IAsyncEnumerable<WorkOrderStep> GetWorkOrderStepsArchiveAsync(DateTime? lastUpdateDt = null, CancellationToken ct = default);
/// <summary>
/// Gets work order time transactions from production schema, optionally filtered by last update.
/// </summary>
/// <param name="lastUpdateDt">Optional cutoff for incremental sync.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>Streaming work order times.</returns>
IAsyncEnumerable<WorkOrderTime> GetWorkOrderTimesAsync(DateTime? lastUpdateDt = null, CancellationToken ct = default);
/// <summary>
/// Gets work order time transactions from archive schema.
/// </summary>
/// <param name="lastUpdateDt">Optional cutoff for incremental sync.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>Streaming archived work order times.</returns>
IAsyncEnumerable<WorkOrderTime> GetWorkOrderTimesArchiveAsync(DateTime? lastUpdateDt = null, CancellationToken ct = default);
/// <summary>
/// Gets work order routing transactions from production schema, optionally filtered by last update.
/// </summary>
/// <param name="lastUpdateDt">Optional cutoff for incremental sync.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>Streaming work order routings.</returns>
IAsyncEnumerable<WorkOrderRouting> GetWorkOrderRoutingsAsync(DateTime? lastUpdateDt = null, CancellationToken ct = default);
/// <summary>
/// Gets work order component usage from production schema, optionally filtered by last update.
/// </summary>
/// <param name="lastUpdateDt">Optional cutoff for incremental sync.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>Streaming work order components.</returns>
IAsyncEnumerable<WorkOrderComponent> GetWorkOrderComponentsAsync(DateTime? lastUpdateDt = null, CancellationToken ct = default);
/// <summary>
/// Gets work order component usage from archive schema.
/// </summary>
/// <param name="lastUpdateDt">Optional cutoff for incremental sync.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>Streaming archived work order components.</returns>
IAsyncEnumerable<WorkOrderComponent> GetWorkOrderComponentsArchiveAsync(DateTime? lastUpdateDt = null, CancellationToken ct = default);
}
@@ -1,9 +0,0 @@
namespace JdeScoping.DataAccess.Interfaces;
/// <summary>
/// Repository for accessing JDE Oracle database.
/// All methods return IAsyncEnumerable for memory-efficient streaming.
/// </summary>
public partial interface IJdeRepository
{
}
@@ -25,11 +25,6 @@ public class DataAccessOptions
/// </summary>
public int MisDataTimeoutSeconds { get; set; } = 60000;
/// <summary>
/// Timeout for index rebuild operations in seconds.
/// </summary>
public int RebuildIndexTimeoutSeconds { get; set; } = 600;
/// <summary>
/// JDE production schema name (e.g., PRODDTA).
/// </summary>
@@ -1,45 +0,0 @@
namespace JdeScoping.DataAccess.Queries;
/// <summary>
/// SQL query constants for the CMS Oracle database.
/// </summary>
public static class CmsQueries
{
/// <summary>
/// Gets Manufacturing Information System (MIS) data from CMS database.
/// Complex 10-table JOIN through CMS schema (INFODBA).
/// </summary>
public const string SqlGetMisData = @"
SELECT DISTINCT
mis.P_PART_NUMBER AS ItemNumber,
mis.P_OPERATION_NUMBER AS SequenceNumber,
item.PITEM_ID AS MISNumber,
itemrev.PITEM_REVISION_ID AS RevID,
TRIM(mis.P_SITE) AS BranchCode,
zim_test_details.P_SEQ_NUMBER AS CharNumber,
zim_test_details.P_TEST_DESC AS TestDescription,
zim_test_details.P_SAMPL_TYPE AS SamplingType,
zim_test_details.P_SAMPL_VALUE AS SamplingValue,
zim_test_details.P_TOOLS AS ToolsGauges,
zim_test_details.P_WORK_INTR AS WorkInstructions,
Status.PNAME AS Status,
Status.PDATE_RELEASED AS ReleaseDate
FROM INFODBA.PITEM item
INNER JOIN INFODBA.PITEMREVISION itemrev ON (item.PUID = itemrev.RITEMS_TAGU)
INNER JOIN INFODBA.PRELEASE_STATUS_LIST listing ON (itemrev.PUID = listing.PUID)
INNER JOIN INFODBA.PRELEASESTATUS Status ON (listing.PVALU_0 = Status.PUID)
INNER JOIN INFODBA.PIMANRELATION imanrel ON (itemrev.PUID = imanrel.RPRIMARY_OBJECTU)
INNER JOIN INFODBA.PFORM form ON (imanrel.RSECONDARY_OBJECTU = form.PUID)
INNER JOIN INFODBA.PZIMMERMISDETAILS zim_mis ON (form.RDATA_FILEU = zim_mis.PUID)
INNER JOIN INFODBA.P_TEST_DETAILS test_details ON (zim_mis.PUID = test_details.PUID)
INNER JOIN INFODBA.P_PART_ASSOCIATION ppa ON (ppa.PUID = test_details.PUID)
INNER JOIN INFODBA.PMISDATAOBJECT mis ON (mis.PUID = ppa.PVALU_0)
INNER JOIN INFODBA.PZIMTESTDETAILS zim_test_details ON (test_details.PVALU_0 = zim_test_details.PUID)
WHERE Status.PNAME IN ('Current', 'BackLevel')";
/// <summary>
/// Gets MIS data updated since specified date from CMS database.
/// </summary>
public const string SqlGetMisDataFiltered = SqlGetMisData + @"
AND Status.PDATE_RELEASED >= :lastUpdateDT";
}
@@ -1,113 +0,0 @@
namespace JdeScoping.DataAccess.Queries;
/// <summary>
/// Inventory related SQL queries (Lots, Lot Usages, Lot Locations, Items)
/// </summary>
public static partial class JdeQueries
{
/// <summary>
/// Gets all lots from production schema.
/// </summary>
public const string SqlGetLots = @"
SELECT TRIM(lot.IOLOTN) AS LotNumber,
TRIM(lot.IOMCU) AS BranchCode,
lot.IOITM AS ShortItemNumber,
TRIM(lot.IOLITM) AS ItemNumber,
lot.IOVEND AS SupplierCode,
lot.IOLOTS AS StatusCode,
TRIM(lot.IOLOT1) AS Memo1,
TRIM(lot.IOLOT2) AS Memo2,
TRIM(lot.IOLOT3) AS Memo3,
lot.IOUPMJ AS LastUpdateDate,
lot.IOTDAY AS LastUpdateTime
FROM {ProductionSchema}.F4108 lot
WHERE TRIM(lot.IOLOTN) IS NOT NULL AND
TRIM(lot.IOMCU) IS NOT NULL";
/// <summary>
/// Gets lots updated since specified date from production schema.
/// </summary>
public const string SqlGetLotsFiltered = SqlGetLots + @"
AND (lot.IOUPMJ > :dateUpdated OR
(lot.IOUPMJ = :dateUpdated AND lot.IOTDAY >= :timeUpdated))";
/// <summary>
/// Gets all lot usages (cardex) from production schema.
/// </summary>
public const string SqlGetLotUsages = @"
SELECT lu.ILUKID AS UniqueId,
lu.ILDOCO AS WorkOrderNumber,
TRIM(lu.ILLOTN) AS LotNumber,
TRIM(lu.ILMCU) AS BranchCode,
lu.ILITM AS ShortItemNumber,
lu.ILTRQT AS Quantity,
lu.ILTRDJ AS LastUpdateDate,
lu.ILTDAY AS LastUpdateTime
FROM {ProductionSchema}.F4111 lu
WHERE lu.ILDCT = 'IM' AND
TRIM(lu.ILLOTN) IS NOT NULL";
/// <summary>
/// Gets lot usages updated since specified date from production schema.
/// </summary>
public const string SqlGetLotUsagesFiltered = SqlGetLotUsages + @"
AND (lu.ILTRDJ > :dateUpdated OR
(lu.ILTRDJ = :dateUpdated AND lu.ILTDAY >= :timeUpdated))";
/// <summary>
/// Gets all lot usages from archive schema.
/// </summary>
public const string SqlGetLotUsagesArchive = @"
SELECT lu.ILUKID AS UniqueId,
lu.ILDOCO AS WorkOrderNumber,
TRIM(lu.ILLOTN) AS LotNumber,
TRIM(lu.ILMCU) AS BranchCode,
lu.ILITM AS ShortItemNumber,
lu.ILTRQT AS Quantity,
lu.ILTRDJ AS LastUpdateDate,
lu.ILTDAY AS LastUpdateTime
FROM {ArchiveSchema}.F4111 lu
WHERE lu.ILDCT = 'IM' AND
TRIM(lu.ILLOTN) IS NOT NULL";
/// <summary>
/// Gets all lot locations from production schema.
/// </summary>
public const string SqlGetLotLocations = @"
SELECT TRIM(il.LILOTN) AS LotNumber,
il.LIITM AS ShortItemNumber,
TRIM(il.LIMCU) AS BranchCode,
COALESCE(TRIM(il.LILOCN), ' ') AS Location,
il.LIUPMJ AS LastUpdateDate,
il.LITDAY AS LastUpdateTime
FROM {ProductionSchema}.F41021 il
WHERE TRIM(il.LILOTN) IS NOT NULL";
/// <summary>
/// Gets lot locations updated since specified date from production schema.
/// </summary>
public const string SqlGetLotLocationsFiltered = SqlGetLotLocations + @"
AND (il.LIUPMJ > :dateUpdated OR
(il.LIUPMJ = :dateUpdated AND il.LITDAY >= :timeUpdated))";
/// <summary>
/// Gets all items from production schema.
/// </summary>
public const string SqlGetItems = @"
SELECT pn.IMITM AS ShortItemNumber,
TRIM(pn.IMLITM) AS ItemNumber,
TRIM(pn.IMDSC1) AS Description,
TRIM(pn.IMPRP4) AS PlanningFamily,
TRIM(pn.IMSTKT) AS StockingType,
pn.IMUPMJ AS LastUpdateDate,
pn.IMTDAY AS LastUpdateTime
FROM {ProductionSchema}.F4101 pn
WHERE TRIM(pn.IMLITM) IS NOT NULL";
/// <summary>
/// Gets items updated since specified date from production schema.
/// </summary>
public const string SqlGetItemsFiltered = SqlGetItems + @"
AND (pn.IMUPMJ > :dateUpdated OR
(pn.IMUPMJ = :dateUpdated AND pn.IMTDAY >= :timeUpdated))";
}
@@ -1,50 +0,0 @@
namespace JdeScoping.DataAccess.Queries;
/// <summary>
/// Lookup related SQL queries (Status Codes, Function Codes)
/// </summary>
public static partial class JdeQueries
{
/// <summary>
/// Gets all work order status codes from production schema.
/// Status codes are stored in UDC table F0005 with SY='00' and RT='SS'.
/// </summary>
public const string SqlGetStatusCodes = @"
SELECT TRIM(sc.DRKY) AS Code,
TRIM(sc.DRDL01) AS Description,
sc.DRUPMJ AS LastUpdateDate,
sc.DRUPMT AS LastUpdateTime
FROM {ProductionSchema}.F0005 sc
WHERE TRIM(sc.DRSY) = '00' AND
sc.DRRT = 'SS' AND
TRIM(sc.DRKY) IS NOT NULL";
/// <summary>
/// Gets status codes updated since specified date from production schema.
/// </summary>
public const string SqlGetStatusCodesFiltered = SqlGetStatusCodes + @"
AND (sc.DRUPMJ > :dateUpdated OR
(sc.DRUPMJ = :dateUpdated AND sc.DRUPMT >= :timeUpdated))";
/// <summary>
/// Gets all function codes from production schema.
/// Function codes are stored in F00192 (MES codes table).
/// Uses LISTAGG to concatenate multiple descriptions for same code.
/// </summary>
public const string SqlGetFunctionCodes = @"
SELECT Code,
TRIM(LISTAGG(Description, ' ') WITHIN GROUP(ORDER BY Description) ||
CASE WHEN MAX(total_lengthb) > 4000 THEN '...' ELSE '' END) AS Description,
SYSDATE AS LastUpdateDt
FROM (
SELECT TRIM(fc.CFKY) AS Code,
TRIM(ASCIISTR(fc.CFDS80)) AS Description,
SUM(LENGTHB(TRIM(fc.CFDS80))+1) OVER(PARTITION BY TRIM(fc.CFKY) ORDER BY TRIM(fc.CFDS80)) - 1 cumul_lengthb,
SUM(LENGTHB(TRIM(fc.CFDS80))+1) OVER(PARTITION BY TRIM(fc.CFKY)) - 1 total_lengthb,
COUNT(*) OVER(PARTITION BY TRIM(fc.CFKY)) num_values
FROM {ProductionSchema}.F00192 fc
WHERE TRIM(fc.CFKY) IS NOT NULL
)
WHERE total_lengthb <= 4000 OR cumul_lengthb <= 4000 - length('...')
GROUP BY Code";
}
@@ -1,95 +0,0 @@
namespace JdeScoping.DataAccess.Queries;
/// <summary>
/// Organization related SQL queries (Users, Business Units, Org Hierarchy, Route Masters)
/// </summary>
public static partial class JdeQueries
{
/// <summary>
/// Gets all JDE users from production schema.
/// Uses CTE to get unique users with most recent address book record.
/// </summary>
public const string SqlGetUsers = @"
WITH USER_CTE AS (
SELECT ab.ABAN8 AS AddressNumber,
TRIM(pro.ULUSER) AS UserId,
TRIM(ab.ABALPH) AS FullName,
ab.ABUPMJ AS LastUpdateDate,
ab.ABUPMT AS LastUpdateTime,
ROW_NUMBER() OVER (PARTITION BY ab.ABAN8 ORDER BY ab.ABUPMJ DESC, ab.ABUPMT DESC) RN
FROM {ProductionSchema}.F0101 ab
LEFT OUTER JOIN {ProductionSchema}.F0092 pro ON (ab.ABAN8 = pro.ULAN8)
WHERE ab.ABATE = 'Y'
)
SELECT AddressNumber,
UserId,
FullName,
LastUpdateDate,
LastUpdateTime
FROM USER_CTE
WHERE RN = 1";
/// <summary>
/// Gets all business units of specified type from production schema.
/// Type codes: 'WC' = Work Center, 'PC' = Profit Center, 'BR' = Branch
/// </summary>
public const string SqlGetBusinessUnits = @"
SELECT TRIM(wc.MCMCU) AS Code,
TRIM(wc.MCDL01) AS Description,
wc.MCUPMJ AS LastUpdateDate,
wc.MCUPMT AS LastUpdateTime
FROM {ProductionSchema}.F0006 wc
WHERE wc.MCSTYL = :typeCode";
/// <summary>
/// Gets business units of specified type updated since specified date from production schema.
/// </summary>
public const string SqlGetBusinessUnitsFiltered = SqlGetBusinessUnits + @"
AND (wc.MCUPMJ > :dateUpdated OR
(wc.MCUPMJ = :dateUpdated AND wc.MCUPMT >= :timeUpdated))";
/// <summary>
/// Gets all organization hierarchy records from production schema.
/// Maps work centers to profit centers and branches.
/// </summary>
public const string SqlGetOrgHierarchy = @"
SELECT TRIM(oh.IWMCUW) AS ProfitCenterCode,
TRIM(oh.IWMCU) AS WorkCenterCode,
TRIM(oh.IWMMCU) AS BranchCode,
oh.IWUPMJ AS LastUpdateDate,
oh.IWTDAY AS LastUpdateTime
FROM {ProductionSchema}.F30006 oh
WHERE TRIM(oh.IWMCU) IS NOT NULL AND
TRIM(oh.IWMMCU) IS NOT NULL";
/// <summary>
/// Gets org hierarchy records updated since specified date from production schema.
/// </summary>
public const string SqlGetOrgHierarchyFiltered = SqlGetOrgHierarchy + @"
AND (oh.IWUPMJ > :dateUpdated OR
(oh.IWUPMJ = :dateUpdated AND oh.IWTDAY >= :timeUpdated))";
/// <summary>
/// Gets all route masters from production schema.
/// </summary>
public const string SqlGetRouteMasters = @"
SELECT TRIM(rm.IRMMCU) AS BranchCode,
TRIM(rm.IRKITL) AS ItemNumber,
TRIM(rm.IRTRT) AS RoutingType,
rm.IROPSQ / 10.0 AS SequenceNumber,
TRIM(rm.IRURRF) AS FunctionCode,
TRIM(rm.IRMCU) AS WorkCenterCode,
rm.IREFFF AS StartDateDate,
rm.IREFFT AS EndDateDate,
rm.IRUPMJ AS LastUpdateDate,
rm.IRTDAY AS LastUpdateTime
FROM {ProductionSchema}.F3003 rm
WHERE TRIM(rm.IRKITL) IS NOT NULL";
/// <summary>
/// Gets route masters updated since specified date from production schema.
/// </summary>
public const string SqlGetRouteMastersFiltered = SqlGetRouteMasters + @"
AND (rm.IRUPMJ > :dateUpdated OR
(rm.IRUPMJ = :dateUpdated AND rm.IRTDAY >= :timeUpdated))";
}
@@ -1,234 +0,0 @@
namespace JdeScoping.DataAccess.Queries;
/// <summary>
/// Work order related SQL queries (Work Orders, Steps, Times, Routings, Components)
/// </summary>
public static partial class JdeQueries
{
/// <summary>
/// Gets all work orders from production schema.
/// </summary>
public const string SqlGetWorkorders = @"
SELECT wo.WADOCO AS WorkOrderNumber,
TRIM(wo.WAMMCU) AS BranchCode,
TRIM(wo.WALOTN) AS LotNumber,
TRIM(wo.WALITM) AS ItemNumber,
wo.WAITM AS ShortItemNumber,
TRIM(wo.WAPARS) AS ParentWorkOrderNumber,
wo.WAUORG / 100.0 AS OrderQuantity,
wo.WASOBK / 100.0 AS HeldQuantity,
wo.WASOQS / 100.0 AS ShippedQuantity,
TRIM(wo.WASRST) AS StatusCode,
CASE wo.WADCG WHEN 0 THEN TO_DATE('1900-01-01', 'YYYY-MM-DD')
ELSE TO_DATE(wo.WADCG+1900000,'YYYYDDD') END AS StatusCodeUpdateDT,
CASE wo.WATRDJ WHEN 0 THEN TO_DATE('1900-01-01', 'YYYY-MM-DD')
ELSE TO_DATE(wo.WATRDJ+1900000,'YYYYDDD') END AS IssueDate,
CASE wo.WASTRT WHEN 0 THEN TO_DATE('1900-01-01', 'YYYY-MM-DD')
ELSE TO_DATE(wo.WASTRT+1900000,'YYYYDDD') END AS StartDate,
TRIM(wo.WATRT) AS RoutingType,
wo.WAUPMJ AS LastUpdateDate,
wo.WATDAY AS LastUpdateTime
FROM {ProductionSchema}.F4801 wo";
/// <summary>
/// Gets work orders updated since specified date from production schema.
/// </summary>
public const string SqlGetWorkordersFiltered = SqlGetWorkorders + @"
WHERE (wo.WAUPMJ > :dateUpdated OR
(wo.WAUPMJ = :dateUpdated AND wo.WATDAY >= :timeUpdated))";
/// <summary>
/// Gets all work orders from archive schema.
/// </summary>
public const string SqlGetWorkordersArchive = @"
SELECT wo.WADOCO AS WorkOrderNumber,
TRIM(wo.WAMMCU) AS BranchCode,
TRIM(wo.WALOTN) AS LotNumber,
TRIM(wo.WALITM) AS ItemNumber,
wo.WAITM AS ShortItemNumber,
TRIM(wo.WAPARS) AS ParentWorkOrderNumber,
wo.WAUORG / 100.0 AS OrderQuantity,
wo.WASOBK / 100.0 AS HeldQuantity,
wo.WASOQS / 100.0 AS ShippedQuantity,
TRIM(wo.WASRST) AS StatusCode,
CASE wo.WADCG WHEN 0 THEN TO_DATE('1900-01-01', 'YYYY-MM-DD')
ELSE TO_DATE(wo.WADCG+1900000,'YYYYDDD') END AS StatusCodeUpdateDT,
CASE wo.WATRDJ WHEN 0 THEN TO_DATE('1900-01-01', 'YYYY-MM-DD')
ELSE TO_DATE(wo.WATRDJ+1900000,'YYYYDDD') END AS IssueDate,
CASE wo.WASTRT WHEN 0 THEN TO_DATE('1900-01-01', 'YYYY-MM-DD')
ELSE TO_DATE(wo.WASTRT+1900000,'YYYYDDD') END AS StartDate,
TRIM(wo.WATRT) AS RoutingType,
wo.WAUPMJ AS LastUpdateDate,
wo.WATDAY AS LastUpdateTime
FROM {ArchiveSchema}.F4801 wo";
/// <summary>
/// Gets work order steps from production schema.
/// </summary>
public const string SqlGetWorkorderSteps = @"
SELECT wos.WLDOCO AS WorkOrderNumber,
wos.WLOPSQ/10 AS StepNumber,
TRIM(wos.WLMCU) AS WorkCenterCode,
TRIM(wos.WLMMCU) AS BranchCode,
TRIM(wos.WLDSC1) AS StepDescription,
TRIM(mes.CFDS80) AS FunctionOperationDescription,
wos.WLOPSC AS StepTypeCode,
CASE wos.WLSTRT WHEN 0 THEN NULL
ELSE TO_DATE(wos.WLSTRT+1900000,'YYYYDDD') END AS StartDT,
CASE wos.WLSTRX WHEN 0 THEN NULL
ELSE TO_DATE(wos.WLSTRX+1900000,'YYYYDDD') END AS EndDT,
TRIM(wos.WLURRF) AS FunctionCode,
wos.WLSOCN / 100.0 AS ScrappedQuantity,
wos.WLUPMJ AS LastUpdateDate,
wos.WLTDAY AS LastUpdateTime
FROM {ProductionSchema}.F3112 wos
LEFT OUTER JOIN {ProductionSchema}.F00192 mes ON (wos.WLURRF = mes.CFKY)
WHERE TRIM(wos.WLMCU) IS NOT NULL AND
TRIM(wos.WLMMCU) IS NOT NULL";
/// <summary>
/// Gets work order steps updated since specified date from production schema.
/// </summary>
public const string SqlGetWorkorderStepsFiltered = SqlGetWorkorderSteps + @"
AND (wos.WLUPMJ > :dateUpdated OR
(wos.WLUPMJ = :dateUpdated AND wos.WLTDAY >= :timeUpdated))";
/// <summary>
/// Gets work order steps from archive schema.
/// </summary>
public const string SqlGetWorkorderStepsArchive = @"
SELECT wos.WLDOCO AS WorkOrderNumber,
wos.WLOPSQ/10 AS StepNumber,
TRIM(wos.WLMCU) AS WorkCenterCode,
TRIM(wos.WLMMCU) AS BranchCode,
TRIM(wos.WLDSC1) AS StepDescription,
TRIM(mes.CFDS80) AS FunctionOperationDescription,
wos.WLOPSC AS StepTypeCode,
CASE wos.WLSTRT WHEN 0 THEN NULL
ELSE TO_DATE(wos.WLSTRT+1900000,'YYYYDDD') END AS StartDT,
CASE wos.WLSTRX WHEN 0 THEN NULL
ELSE TO_DATE(wos.WLSTRX+1900000,'YYYYDDD') END AS EndDT,
TRIM(wos.WLURRF) AS FunctionCode,
wos.WLSOCN / 100.0 AS ScrappedQuantity,
wos.WLUPMJ AS LastUpdateDate,
wos.WLTDAY AS LastUpdateTime
FROM {ArchiveSchema}.F3112 wos
LEFT OUTER JOIN {ProductionSchema}.F00192 mes ON (wos.WLURRF = mes.CFKY)
WHERE TRIM(wos.WLMCU) IS NOT NULL AND
TRIM(wos.WLMMCU) IS NOT NULL";
/// <summary>
/// Gets work order time transactions from production schema.
/// </summary>
public const string SqlGetWorkorderTimes = @"
SELECT wot.WTUKID AS UniqueID,
wot.WTDOCO AS WorkOrderNumber,
wot.WTOPSQ/10 AS StepNumber,
TRIM(wot.WTMCU) AS WorkCenterCode,
TRIM(wot.WTMMCU) AS BranchCode,
wot.WTAN8 AS AddressNumber,
CASE wot.WTDGL WHEN 0 THEN NULL
ELSE TO_DATE(wot.WTDGL+1900000,'YYYYDDD') END AS GlDate,
wot.WTUPMJ AS LastUpdateDate,
wot.WTTDAY AS LastUpdateTime
FROM {ProductionSchema}.F31122 wot
WHERE TRIM(wot.WTMCU) IS NOT NULL AND
TRIM(wot.WTMMCU) IS NOT NULL";
/// <summary>
/// Gets work order times updated since specified date from production schema.
/// </summary>
public const string SqlGetWorkorderTimesFiltered = SqlGetWorkorderTimes + @"
AND (wot.WTUPMJ > :dateUpdated OR
(wot.WTUPMJ = :dateUpdated AND wot.WTTDAY >= :timeUpdated))";
/// <summary>
/// Gets work order time transactions from archive schema.
/// </summary>
public const string SqlGetWorkorderTimesArchive = @"
SELECT wot.WTUKID AS UniqueID,
wot.WTDOCO AS WorkOrderNumber,
wot.WTOPSQ/10 AS StepNumber,
TRIM(wot.WTMCU) AS WorkCenterCode,
TRIM(wot.WTMMCU) AS BranchCode,
wot.WTAN8 AS AddressNumber,
CASE wot.WTDGL WHEN 0 THEN NULL
ELSE TO_DATE(wot.WTDGL+1900000,'YYYYDDD') END AS GlDate,
wot.WTUPMJ AS LastUpdateDate,
wot.WTTDAY AS LastUpdateTime
FROM {ArchiveSchema}.F31122 wot
WHERE TRIM(wot.WTMCU) IS NOT NULL AND
TRIM(wot.WTMMCU) IS NOT NULL";
/// <summary>
/// Gets work order routing transactions from production schema.
/// </summary>
public const string SqlGetWorkorderRoutings = @"
SELECT TRIM(woz.SZEDUS) AS UserID,
TRIM(woz.SZEDBT) AS BatchNumber,
TRIM(woz.SZEDTN) AS TransactionNumber,
woz.SZEDLN AS LineNumber,
woz.SZOPSQ / 10.0 AS StepNumber,
TRIM(woz.SZMCU) AS WorkCenterCode,
woz.SZDOCO AS WorkOrderNumber,
TRIM(woz.SZTRT) AS RoutingType,
TRIM(woz.SZMMCU) AS BranchCode,
TRIM(woz.SZDSC1) AS StepDescription,
TRIM(woz.SZURRF) AS FunctionCode,
woz.SZTRDJ AS TransactionDate_Date,
woz.SZUPMJ AS LastUpdateDate,
woz.SZTDAY AS LastUpdateTime
FROM {ProductionSchema}.F3112Z1 woz
WHERE woz.SZTYTN = 'JDERTG' AND
woz.SZDRIN = '2' AND
woz.SZTNAC = '02' AND
woz.SZPID = 'ER31410' AND
TRIM(woz.SZEDUS) IS NOT NULL AND
TRIM(woz.SZEDBT) IS NOT NULL AND
TRIM(woz.SZEDTN) IS NOT NULL AND
TRIM(woz.SZMCU) IS NOT NULL";
/// <summary>
/// Gets work order routings updated since specified date from production schema.
/// </summary>
public const string SqlGetWorkorderRoutingsFiltered = SqlGetWorkorderRoutings + @"
AND (woz.SZUPMJ > :dateUpdated OR
(woz.SZUPMJ = :dateUpdated AND woz.SZTDAY >= :timeUpdated))";
/// <summary>
/// Gets work order component usage from production schema.
/// </summary>
public const string SqlGetWorkorderComponents = @"
SELECT woc.WMUKID AS UniqueID,
woc.WMDOCO AS WorkOrderNumber,
TRIM(woc.WMLOTN) AS LotNumber,
TRIM(woc.WMCMCU) AS BranchCode,
woc.WMCPIT AS ShortItemNumber,
woc.WMTRQT / 100.0 AS Quantity,
woc.WMUPMJ AS LastUpdateDate,
woc.WMTDAY AS LastUpdateTime
FROM {ProductionSchema}.F3111 woc
WHERE TRIM(woc.WMLOTN) IS NOT NULL";
/// <summary>
/// Gets work order components updated since specified date from production schema.
/// </summary>
public const string SqlGetWorkorderComponentsFiltered = SqlGetWorkorderComponents + @"
AND (woc.WMUPMJ > :dateUpdated OR
(woc.WMUPMJ = :dateUpdated AND woc.WMTDAY >= :timeUpdated))";
/// <summary>
/// Gets work order component usage from archive schema.
/// </summary>
public const string SqlGetWorkorderComponentsArchive = @"
SELECT woc.WMUKID AS UniqueID,
woc.WMDOCO AS WorkOrderNumber,
TRIM(woc.WMLOTN) AS LotNumber,
TRIM(woc.WMCMCU) AS BranchCode,
woc.WMCPIT AS ShortItemNumber,
woc.WMTRQT / 100.0 AS Quantity,
woc.WMUPMJ AS LastUpdateDate,
woc.WMTDAY AS LastUpdateTime
FROM {ArchiveSchema}.F3111 woc
WHERE TRIM(woc.WMLOTN) IS NOT NULL";
}
@@ -1,9 +0,0 @@
namespace JdeScoping.DataAccess.Queries;
/// <summary>
/// SQL query constants for the JDE Oracle database.
/// Schema placeholders ({ProductionSchema}, {ArchiveSchema}, {StageSchema}) are replaced at runtime.
/// </summary>
public static partial class JdeQueries
{
}
@@ -24,88 +24,4 @@ public static partial class LotFinderQueries
cte.NumberRecords
FROM DU_CTE cte
WHERE cte.RN = 1";
/// <summary>
/// Gets column metadata for a table.
/// </summary>
public const string SqlGetTableColumns = @"
SELECT c.name AS Name,
CASE t2.name
WHEN 'varchar' THEN 'VARCHAR(' + CAST(c.max_length AS VARCHAR(10)) + ')'
WHEN 'decimal' THEN 'DECIMAL(' + CAST(c.precision AS VARCHAR(4)) + ',' + CAST(c.scale AS VARCHAR(4)) + ')'
ELSE UPPER(t2.name)
END AS Definition
FROM sys.columns c
INNER JOIN sys.types AS t2 ON (c.system_type_id = t2.system_type_id)
INNER JOIN sys.tables t ON (c.object_id = t.object_id)
WHERE t.name = @name
ORDER BY c.column_id";
/// <summary>
/// Gets primary key columns for a table.
/// </summary>
public const string SqlGetTablePrimaryKey = @"
SELECT COLUMN_NAME AS Name
FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE
WHERE OBJECTPROPERTY(OBJECT_ID(CONSTRAINT_SCHEMA + '.' + QUOTENAME(CONSTRAINT_NAME)), 'IsPrimaryKey') = 1 AND
TABLE_NAME = @name
ORDER BY ORDINAL_POSITION";
/// <summary>
/// Rebuilds all indices on a table. Use string.Format to inject table name.
/// </summary>
public const string SqlRebuildIndices = "ALTER INDEX ALL ON {0} REBUILD WITH (FILLFACTOR = 95);";
/// <summary>
/// Post-processing script to set MIS data obsoletion dates.
/// </summary>
public const string SqlPostprocessMisData = @"
SET ANSI_WARNINGS OFF;
WITH cte AS (
SELECT md.MisNumber, md.RevID, md.Status, MIN(md.ReleaseDate) Released
FROM dbo.MisData AS md
GROUP BY md.MisNumber, md.RevID, md.Status
)
UPDATE dbo.MisData
SET ObsoleteDate = bl.Released
FROM cte bl
WHERE MisData.MisNumber = bl.MisNumber AND
MisData.RevID = bl.RevID AND
MisData.Status = 'Current' AND
bl.Status = 'BackLevel';
WITH cte AS (
SELECT md.MisNumber, md.RevID, md.Status, MIN(md.ReleaseDate) Released
FROM dbo.MisData AS md
GROUP BY md.MisNumber, md.RevID, md.Status
)
UPDATE dbo.MisData
SET ObsoleteDate = (SELECT TOP 1 nl.Released
FROM cte nl
WHERE MisData.MisNumber = nl.MisNumber AND
MisData.RevID < nl.RevID AND
MisData.Status = nl.Status
ORDER BY nl.RevID)
WHERE ObsoleteDate IS NULL;
ALTER INDEX [PK_MisData] ON [dbo].[MisData] REBUILD;";
/// <summary>
/// Inserts a new data update tracking record.
/// </summary>
public const string SqlInsertDataUpdate = @"
INSERT INTO dbo.DataUpdate (SourceSystem, SourceData, TableName, StartDT, UpdateType)
VALUES (@sourceSystem, @sourceData, @tableName, @startDT, @updateType);
SELECT CAST(SCOPE_IDENTITY() AS BIGINT);";
/// <summary>
/// Completes a data update tracking record.
/// </summary>
public const string SqlCompleteDataUpdate = @"
UPDATE dbo.DataUpdate
SET EndDT = @endDT,
WasSuccessful = @wasSuccessful,
NumberRecords = @numberRecords
WHERE ID = @id";
}
@@ -52,17 +52,6 @@ public static partial class LotFinderQueries
wc.Description LIKE '%' + @filter + '%'
ORDER BY wc.Code";
/// <summary>
/// Looks up work centers by codes using STRING_SPLIT.
/// </summary>
public const string SqlLookupWorkCenters = @"
SELECT wc.Code,
wc.Description,
wc.LastUpdateDT
FROM dbo.WorkCenter AS wc
WHERE wc.Code IN (SELECT LTRIM(RTRIM(value)) FROM STRING_SPLIT(@workCenterCodes, ','))
ORDER BY wc.Code";
/// <summary>
/// Searches profit centers by code or description.
/// </summary>
@@ -76,17 +65,6 @@ public static partial class LotFinderQueries
pc.Description LIKE '%' + @filter + '%'
ORDER BY pc.Code";
/// <summary>
/// Looks up profit centers by codes using STRING_SPLIT.
/// </summary>
public const string SqlLookupProfitCenters = @"
SELECT pc.Code,
pc.Description,
pc.LastUpdateDT
FROM dbo.ProfitCenter AS pc
WHERE pc.Code IN (SELECT LTRIM(RTRIM(value)) FROM STRING_SPLIT(@profitCenterCodes, ','))
ORDER BY pc.Code";
/// <summary>
/// Searches users by user ID, full name, or address number.
/// </summary>
@@ -102,19 +80,6 @@ public static partial class LotFinderQueries
CAST(u.AddressNumber AS VARCHAR(10)) LIKE '%' + @filter + '%'
ORDER BY u.UserID, u.FullName";
/// <summary>
/// Looks up users by user IDs or address numbers using STRING_SPLIT.
/// </summary>
public const string SqlLookupUsers = @"
SELECT u.AddressNumber,
u.UserID,
u.FullName,
u.LastUpdateDT
FROM dbo.JdeUser AS u
WHERE u.UserID IN (SELECT LTRIM(RTRIM(value)) FROM STRING_SPLIT(@userIds, ','))
OR CAST(u.AddressNumber AS VARCHAR(20)) IN (SELECT LTRIM(RTRIM(value)) FROM STRING_SPLIT(@userIds, ','))
ORDER BY u.UserID";
/// <summary>
/// Looks up lots by lot number and item number using OPENJSON.
/// </summary>
@@ -55,22 +55,4 @@ public static partial class LotFinderQueries
SELECT s.Results
FROM dbo.Search AS s
WHERE s.ID = @id";
/// <summary>
/// Updates search status.
/// </summary>
public const string SqlUpdateSearchStatus = @"
UPDATE dbo.Search
SET Status = @status,
StartDT = CASE WHEN @status = 2 THEN GETUTCDATE() ELSE StartDT END,
EndDT = CASE WHEN @status >= 3 THEN GETUTCDATE() ELSE EndDT END
WHERE ID = @id";
/// <summary>
/// Updates search results.
/// </summary>
public const string SqlUpdateSearchResults = @"
UPDATE dbo.Search
SET Results = @results
WHERE ID = @id";
}
@@ -1,83 +0,0 @@
using System.Runtime.CompilerServices;
using Dapper;
using JdeScoping.Core.Models;
using JdeScoping.Core.Models.Quality;
using JdeScoping.DataAccess.Options;
using JdeScoping.DataAccess.Interfaces;
using JdeScoping.DataAccess.Queries;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Oracle.ManagedDataAccess.Client;
namespace JdeScoping.DataAccess.Repositories;
/// <summary>
/// Repository implementation for the CMS Oracle database.
/// </summary>
public class CmsRepository : ICmsRepository
{
private readonly IDbConnectionFactory _connectionFactory;
private readonly ILogger<CmsRepository> _logger;
private readonly IOptions<DataAccessOptions> _options;
private const string RepositoryName = "CmsRepository";
/// <summary>
/// Initializes a new instance of the <see cref="CmsRepository"/> class.
/// </summary>
public CmsRepository(
IDbConnectionFactory connectionFactory,
ILogger<CmsRepository> logger,
IOptions<DataAccessOptions> options)
{
_connectionFactory = connectionFactory ?? throw new ArgumentNullException(nameof(connectionFactory));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_options = options ?? throw new ArgumentNullException(nameof(options));
}
/// <inheritdoc/>
public async IAsyncEnumerable<MisData> GetMisDataAsync(
DateTime? lastUpdateDt = null,
[EnumeratorCancellation] CancellationToken ct = default)
{
var sql = lastUpdateDt.HasValue
? CmsQueries.SqlGetMisDataFiltered
: CmsQueries.SqlGetMisData;
var parameters = lastUpdateDt.HasValue
? new { lastUpdateDT = lastUpdateDt.Value }
: null;
OracleConnection? connection = null;
try
{
connection = await _connectionFactory.CreateCmsConnectionAsync(ct);
// Use Query with buffered: false for streaming
var results = connection.Query<MisData>(
sql,
parameters,
commandTimeout: _options.Value.MisDataTimeoutSeconds,
buffered: false);
foreach (var item in results)
{
ct.ThrowIfCancellationRequested();
// Convert ReleaseDate to local time if present
if (item.ReleaseDate.HasValue)
{
item.ReleaseDate = item.ReleaseDate.Value.ToLocalTime();
}
yield return item;
}
}
finally
{
if (connection != null)
{
await connection.DisposeAsync();
}
}
}
}
@@ -1,94 +0,0 @@
using System.Runtime.CompilerServices;
using JdeScoping.Core.Helpers;
using JdeScoping.Core.Models.Inventory;
using JdeScoping.DataAccess.Queries;
namespace JdeScoping.DataAccess.Repositories;
/// <summary>
/// Inventory (lots) operations for JDE Oracle repository.
/// </summary>
public partial class JdeRepository
{
/// <inheritdoc/>
public async IAsyncEnumerable<Lot> GetLotsAsync(
DateTime? lastUpdateDt = null,
[EnumeratorCancellation] CancellationToken ct = default)
{
var sql = ApplySchemaReplacements(
lastUpdateDt.HasValue
? JdeQueries.SqlGetLotsFiltered
: JdeQueries.SqlGetLots);
var parameters = lastUpdateDt.HasValue
? new { dateUpdated = lastUpdateDt.Value.ToJdeDate(), timeUpdated = lastUpdateDt.Value.ToJdeTime() }
: null;
await foreach (var item in StreamQueryAsync<Lot>(
sql, parameters, nameof(GetLotsAsync), "SQL_GET_LOTS", ct))
{
yield return item;
}
}
/// <inheritdoc/>
public async IAsyncEnumerable<LotUsage> GetLotUsagesAsync(
DateTime? lastUpdateDt = null,
[EnumeratorCancellation] CancellationToken ct = default)
{
var sql = ApplySchemaReplacements(
lastUpdateDt.HasValue
? JdeQueries.SqlGetLotUsagesFiltered
: JdeQueries.SqlGetLotUsages);
var parameters = lastUpdateDt.HasValue
? new { dateUpdated = lastUpdateDt.Value.ToJdeDate(), timeUpdated = lastUpdateDt.Value.ToJdeTime() }
: null;
// Use special lot usage timeout due to large dataset
await foreach (var item in StreamQueryAsync<LotUsage>(
sql, parameters, nameof(GetLotUsagesAsync), "SQL_GET_LOT_USAGES", ct,
_options.Value.LotUsageTimeoutSeconds))
{
yield return item;
}
}
/// <inheritdoc/>
public async IAsyncEnumerable<LotUsage> GetLotUsagesArchiveAsync(
DateTime? lastUpdateDt = null,
[EnumeratorCancellation] CancellationToken ct = default)
{
var sql = ApplySchemaReplacements(JdeQueries.SqlGetLotUsagesArchive);
// Use special lot usage timeout due to large dataset
await foreach (var item in StreamQueryAsync<LotUsage>(
sql, null, nameof(GetLotUsagesArchiveAsync), "SQL_GET_LOT_USAGES_ARCHIVE", ct,
_options.Value.LotUsageTimeoutSeconds))
{
yield return item;
}
}
/// <inheritdoc/>
public async IAsyncEnumerable<LotLocation> GetLotLocationsAsync(
DateTime? lastUpdateDt = null,
[EnumeratorCancellation] CancellationToken ct = default)
{
var sql = ApplySchemaReplacements(
lastUpdateDt.HasValue
? JdeQueries.SqlGetLotLocationsFiltered
: JdeQueries.SqlGetLotLocations);
var parameters = lastUpdateDt.HasValue
? new { lastUpdateDT = lastUpdateDt.Value }
: null;
// Use JDE Stage connection for lot locations
await foreach (var item in StreamQueryFromStageAsync<LotLocation>(
sql, parameters, nameof(GetLotLocationsAsync), "SQL_GET_LOT_LOCATIONS", ct))
{
yield return item;
}
}
}
@@ -1,191 +0,0 @@
using System.Runtime.CompilerServices;
using JdeScoping.Core.Helpers;
using JdeScoping.Core.Models;
using JdeScoping.Core.Models.Inventory;
using JdeScoping.Core.Models.Lookup;
using JdeScoping.Core.Models.Organization;
using JdeScoping.DataAccess.Queries;
namespace JdeScoping.DataAccess.Repositories;
/// <summary>
/// Reference data operations for JDE Oracle repository.
/// </summary>
public partial class JdeRepository
{
/// <inheritdoc/>
public async IAsyncEnumerable<Item> GetItemsAsync(
DateTime? lastUpdateDt = null,
[EnumeratorCancellation] CancellationToken ct = default)
{
var sql = ApplySchemaReplacements(
lastUpdateDt.HasValue
? JdeQueries.SqlGetItemsFiltered
: JdeQueries.SqlGetItems);
var parameters = lastUpdateDt.HasValue
? new { dateUpdated = lastUpdateDt.Value.ToJdeDate(), timeUpdated = lastUpdateDt.Value.ToJdeTime() }
: null;
await foreach (var item in StreamQueryAsync<Item>(
sql, parameters, nameof(GetItemsAsync), "SQL_GET_ITEMS", ct))
{
yield return item;
}
}
/// <inheritdoc/>
public async IAsyncEnumerable<JdeUser> GetUsersAsync(
DateTime? lastUpdateDt = null,
[EnumeratorCancellation] CancellationToken ct = default)
{
// Users always do full sync (incremental not supported)
var sql = ApplySchemaReplacements(JdeQueries.SqlGetUsers);
await foreach (var item in StreamQueryAsync<JdeUser>(
sql, null, nameof(GetUsersAsync), "SQL_GET_USERS", ct))
{
yield return item;
}
}
/// <inheritdoc/>
public async IAsyncEnumerable<Branch> GetBranchesAsync(
DateTime? lastUpdateDt = null,
[EnumeratorCancellation] CancellationToken ct = default)
{
var sql = ApplySchemaReplacements(
lastUpdateDt.HasValue
? JdeQueries.SqlGetBusinessUnitsFiltered
: JdeQueries.SqlGetBusinessUnits);
var parameters = lastUpdateDt.HasValue
? new { typeCode = "BP", dateUpdated = lastUpdateDt.Value.ToJdeDate(), timeUpdated = lastUpdateDt.Value.ToJdeTime() }
: new { typeCode = "BP", dateUpdated = 0, timeUpdated = 0 };
await foreach (var item in StreamQueryAsync<Branch>(
sql, parameters, nameof(GetBranchesAsync), "SQL_GET_BUSINESS_UNITS", ct))
{
yield return item;
}
}
/// <inheritdoc/>
public async IAsyncEnumerable<ProfitCenter> GetProfitCentersAsync(
DateTime? lastUpdateDt = null,
[EnumeratorCancellation] CancellationToken ct = default)
{
var sql = ApplySchemaReplacements(
lastUpdateDt.HasValue
? JdeQueries.SqlGetBusinessUnitsFiltered
: JdeQueries.SqlGetBusinessUnits);
var parameters = lastUpdateDt.HasValue
? new { typeCode = "I3", dateUpdated = lastUpdateDt.Value.ToJdeDate(), timeUpdated = lastUpdateDt.Value.ToJdeTime() }
: new { typeCode = "I3", dateUpdated = 0, timeUpdated = 0 };
await foreach (var item in StreamQueryAsync<ProfitCenter>(
sql, parameters, nameof(GetProfitCentersAsync), "SQL_GET_BUSINESS_UNITS", ct))
{
yield return item;
}
}
/// <inheritdoc/>
public async IAsyncEnumerable<WorkCenter> GetWorkCentersAsync(
DateTime? lastUpdateDt = null,
[EnumeratorCancellation] CancellationToken ct = default)
{
var sql = ApplySchemaReplacements(
lastUpdateDt.HasValue
? JdeQueries.SqlGetBusinessUnitsFiltered
: JdeQueries.SqlGetBusinessUnits);
var parameters = lastUpdateDt.HasValue
? new { typeCode = "WC", dateUpdated = lastUpdateDt.Value.ToJdeDate(), timeUpdated = lastUpdateDt.Value.ToJdeTime() }
: new { typeCode = "WC", dateUpdated = 0, timeUpdated = 0 };
await foreach (var item in StreamQueryAsync<WorkCenter>(
sql, parameters, nameof(GetWorkCentersAsync), "SQL_GET_BUSINESS_UNITS", ct))
{
yield return item;
}
}
/// <inheritdoc/>
public async IAsyncEnumerable<StatusCode> GetStatusCodesAsync(
DateTime? lastUpdateDt = null,
[EnumeratorCancellation] CancellationToken ct = default)
{
var sql = ApplySchemaReplacements(
lastUpdateDt.HasValue
? JdeQueries.SqlGetStatusCodesFiltered
: JdeQueries.SqlGetStatusCodes);
var parameters = lastUpdateDt.HasValue
? new { lastUpdateDT = lastUpdateDt.Value }
: null;
// Use JDE Stage connection for status codes
await foreach (var item in StreamQueryFromStageAsync<StatusCode>(
sql, parameters, nameof(GetStatusCodesAsync), "SQL_GET_STATUS_CODES", ct))
{
yield return item;
}
}
/// <inheritdoc/>
public async IAsyncEnumerable<FunctionCode> GetFunctionCodesAsync(
[EnumeratorCancellation] CancellationToken ct = default)
{
var sql = ApplySchemaReplacements(JdeQueries.SqlGetFunctionCodes);
await foreach (var item in StreamQueryAsync<FunctionCode>(
sql, null, nameof(GetFunctionCodesAsync), "SQL_GET_FUNCTION_CODES", ct))
{
yield return item;
}
}
/// <inheritdoc/>
public async IAsyncEnumerable<OrgHierarchy> GetOrgHierarchyAsync(
DateTime? lastUpdateDt = null,
[EnumeratorCancellation] CancellationToken ct = default)
{
var sql = ApplySchemaReplacements(
lastUpdateDt.HasValue
? JdeQueries.SqlGetOrgHierarchyFiltered
: JdeQueries.SqlGetOrgHierarchy);
var parameters = lastUpdateDt.HasValue
? new { dateUpdated = lastUpdateDt.Value.ToJdeDate(), timeUpdated = lastUpdateDt.Value.ToJdeTime() }
: null;
await foreach (var item in StreamQueryAsync<OrgHierarchy>(
sql, parameters, nameof(GetOrgHierarchyAsync), "SQL_GET_ORG_HIERARCHY", ct))
{
yield return item;
}
}
/// <inheritdoc/>
public async IAsyncEnumerable<RouteMaster> GetRouteMastersAsync(
DateTime? lastUpdateDt = null,
[EnumeratorCancellation] CancellationToken ct = default)
{
var sql = ApplySchemaReplacements(
lastUpdateDt.HasValue
? JdeQueries.SqlGetRouteMastersFiltered
: JdeQueries.SqlGetRouteMasters);
var parameters = lastUpdateDt.HasValue
? new { dateUpdated = lastUpdateDt.Value.ToJdeDate(), timeUpdated = lastUpdateDt.Value.ToJdeTime() }
: null;
await foreach (var item in StreamQueryAsync<RouteMaster>(
sql, parameters, nameof(GetRouteMastersAsync), "SQL_GET_ROUTE_MASTERS", ct))
{
yield return item;
}
}
}
@@ -1,173 +0,0 @@
using System.Runtime.CompilerServices;
using JdeScoping.Core.Helpers;
using JdeScoping.Core.Models.WorkOrders;
using JdeScoping.DataAccess.Queries;
namespace JdeScoping.DataAccess.Repositories;
/// <summary>
/// Work order operations for JDE Oracle repository.
/// </summary>
public partial class JdeRepository
{
/// <inheritdoc/>
public async IAsyncEnumerable<WorkOrder> GetWorkOrdersAsync(
DateTime? lastUpdateDt = null,
[EnumeratorCancellation] CancellationToken ct = default)
{
var sql = ApplySchemaReplacements(
lastUpdateDt.HasValue
? JdeQueries.SqlGetWorkordersFiltered
: JdeQueries.SqlGetWorkorders);
var parameters = lastUpdateDt.HasValue
? new { dateUpdated = lastUpdateDt.Value.ToJdeDate(), timeUpdated = lastUpdateDt.Value.ToJdeTime() }
: null;
await foreach (var item in StreamQueryAsync<WorkOrder>(
sql, parameters, nameof(GetWorkOrdersAsync), "SQL_GET_WORKORDERS", ct))
{
yield return item;
}
}
/// <inheritdoc/>
public async IAsyncEnumerable<WorkOrder> GetWorkOrdersArchiveAsync(
DateTime? lastUpdateDt = null,
[EnumeratorCancellation] CancellationToken ct = default)
{
var sql = ApplySchemaReplacements(JdeQueries.SqlGetWorkordersArchive);
await foreach (var item in StreamQueryAsync<WorkOrder>(
sql, null, nameof(GetWorkOrdersArchiveAsync), "SQL_GET_WORKORDERS_ARCHIVE", ct))
{
yield return item;
}
}
/// <inheritdoc/>
public async IAsyncEnumerable<WorkOrderStep> GetWorkOrderStepsAsync(
DateTime? lastUpdateDt = null,
[EnumeratorCancellation] CancellationToken ct = default)
{
var sql = ApplySchemaReplacements(
lastUpdateDt.HasValue
? JdeQueries.SqlGetWorkorderStepsFiltered
: JdeQueries.SqlGetWorkorderSteps);
var parameters = lastUpdateDt.HasValue
? new { dateUpdated = lastUpdateDt.Value.ToJdeDate(), timeUpdated = lastUpdateDt.Value.ToJdeTime() }
: null;
await foreach (var item in StreamQueryAsync<WorkOrderStep>(
sql, parameters, nameof(GetWorkOrderStepsAsync), "SQL_GET_WORKORDER_STEPS", ct))
{
yield return item;
}
}
/// <inheritdoc/>
public async IAsyncEnumerable<WorkOrderStep> GetWorkOrderStepsArchiveAsync(
DateTime? lastUpdateDt = null,
[EnumeratorCancellation] CancellationToken ct = default)
{
var sql = ApplySchemaReplacements(JdeQueries.SqlGetWorkorderStepsArchive);
await foreach (var item in StreamQueryAsync<WorkOrderStep>(
sql, null, nameof(GetWorkOrderStepsArchiveAsync), "SQL_GET_WORKORDER_STEPS_ARCHIVE", ct))
{
yield return item;
}
}
/// <inheritdoc/>
public async IAsyncEnumerable<WorkOrderTime> GetWorkOrderTimesAsync(
DateTime? lastUpdateDt = null,
[EnumeratorCancellation] CancellationToken ct = default)
{
var sql = ApplySchemaReplacements(
lastUpdateDt.HasValue
? JdeQueries.SqlGetWorkorderTimesFiltered
: JdeQueries.SqlGetWorkorderTimes);
var parameters = lastUpdateDt.HasValue
? new { dateUpdated = lastUpdateDt.Value.ToJdeDate(), timeUpdated = lastUpdateDt.Value.ToJdeTime() }
: null;
await foreach (var item in StreamQueryAsync<WorkOrderTime>(
sql, parameters, nameof(GetWorkOrderTimesAsync), "SQL_GET_WORKORDER_TIMES", ct))
{
yield return item;
}
}
/// <inheritdoc/>
public async IAsyncEnumerable<WorkOrderTime> GetWorkOrderTimesArchiveAsync(
DateTime? lastUpdateDt = null,
[EnumeratorCancellation] CancellationToken ct = default)
{
var sql = ApplySchemaReplacements(JdeQueries.SqlGetWorkorderTimesArchive);
await foreach (var item in StreamQueryAsync<WorkOrderTime>(
sql, null, nameof(GetWorkOrderTimesArchiveAsync), "SQL_GET_WORKORDER_TIMES_ARCHIVE", ct))
{
yield return item;
}
}
/// <inheritdoc/>
public async IAsyncEnumerable<WorkOrderRouting> GetWorkOrderRoutingsAsync(
DateTime? lastUpdateDt = null,
[EnumeratorCancellation] CancellationToken ct = default)
{
var sql = ApplySchemaReplacements(
lastUpdateDt.HasValue
? JdeQueries.SqlGetWorkorderRoutingsFiltered
: JdeQueries.SqlGetWorkorderRoutings);
var parameters = lastUpdateDt.HasValue
? new { dateUpdated = lastUpdateDt.Value.ToJdeDate(), timeUpdated = lastUpdateDt.Value.ToJdeTime() }
: null;
await foreach (var item in StreamQueryAsync<WorkOrderRouting>(
sql, parameters, nameof(GetWorkOrderRoutingsAsync), "SQL_GET_WORKORDER_ROUTINGS", ct))
{
yield return item;
}
}
/// <inheritdoc/>
public async IAsyncEnumerable<WorkOrderComponent> GetWorkOrderComponentsAsync(
DateTime? lastUpdateDt = null,
[EnumeratorCancellation] CancellationToken ct = default)
{
var sql = ApplySchemaReplacements(
lastUpdateDt.HasValue
? JdeQueries.SqlGetWorkorderComponentsFiltered
: JdeQueries.SqlGetWorkorderComponents);
var parameters = lastUpdateDt.HasValue
? new { dateUpdated = lastUpdateDt.Value.ToJdeDate(), timeUpdated = lastUpdateDt.Value.ToJdeTime() }
: null;
await foreach (var item in StreamQueryAsync<WorkOrderComponent>(
sql, parameters, nameof(GetWorkOrderComponentsAsync), "SQL_GET_WORKORDER_COMPONENTS", ct))
{
yield return item;
}
}
/// <inheritdoc/>
public async IAsyncEnumerable<WorkOrderComponent> GetWorkOrderComponentsArchiveAsync(
DateTime? lastUpdateDt = null,
[EnumeratorCancellation] CancellationToken ct = default)
{
var sql = ApplySchemaReplacements(JdeQueries.SqlGetWorkorderComponentsArchive);
await foreach (var item in StreamQueryAsync<WorkOrderComponent>(
sql, null, nameof(GetWorkOrderComponentsArchiveAsync), "SQL_GET_WORKORDER_COMPONENTS_ARCHIVE", ct))
{
yield return item;
}
}
}
@@ -1,113 +0,0 @@
using System.Runtime.CompilerServices;
using Dapper;
using JdeScoping.DataAccess.Options;
using JdeScoping.DataAccess.Interfaces;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Oracle.ManagedDataAccess.Client;
namespace JdeScoping.DataAccess.Repositories;
/// <summary>
/// Repository implementation for the JDE Oracle database.
/// </summary>
public partial class JdeRepository : IJdeRepository
{
private readonly IDbConnectionFactory _connectionFactory;
private readonly ILogger<JdeRepository> _logger;
private readonly IOptions<DataAccessOptions> _options;
private const string RepositoryName = "JdeRepository";
/// <summary>
/// Initializes a new instance of the <see cref="JdeRepository"/> class.
/// </summary>
public JdeRepository(
IDbConnectionFactory connectionFactory,
ILogger<JdeRepository> logger,
IOptions<DataAccessOptions> options)
{
_connectionFactory = connectionFactory ?? throw new ArgumentNullException(nameof(connectionFactory));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_options = options ?? throw new ArgumentNullException(nameof(options));
}
private string ApplySchemaReplacements(string sql)
{
return sql
.Replace("{ProductionSchema}", _options.Value.ProductionSchema)
.Replace("{ArchiveSchema}", _options.Value.ArchiveSchema)
.Replace("{StageSchema}", _options.Value.StageSchema);
}
private async IAsyncEnumerable<T> StreamQueryAsync<T>(
string sql,
object? parameters,
string operation,
string queryName,
[EnumeratorCancellation] CancellationToken ct,
int? timeoutSeconds = null)
{
OracleConnection? connection = null;
try
{
connection = await _connectionFactory.CreateJdeConnectionAsync(ct);
var timeout = timeoutSeconds ?? _options.Value.DefaultTimeoutSeconds;
// Use Query with buffered: false for streaming
var results = connection.Query<T>(
sql,
parameters,
commandTimeout: timeout,
buffered: false);
foreach (var item in results)
{
ct.ThrowIfCancellationRequested();
yield return item;
}
}
finally
{
if (connection != null)
{
await connection.DisposeAsync();
}
}
}
private async IAsyncEnumerable<T> StreamQueryFromStageAsync<T>(
string sql,
object? parameters,
string operation,
string queryName,
[EnumeratorCancellation] CancellationToken ct,
int? timeoutSeconds = null)
{
OracleConnection? connection = null;
try
{
connection = await _connectionFactory.CreateJdeStageConnectionAsync(ct);
var timeout = timeoutSeconds ?? _options.Value.DefaultTimeoutSeconds;
// Use Query with buffered: false for streaming
var results = connection.Query<T>(
sql,
parameters,
commandTimeout: timeout,
buffered: false);
foreach (var item in results)
{
ct.ThrowIfCancellationRequested();
yield return item;
}
}
finally
{
if (connection != null)
{
await connection.DisposeAsync();
}
}
}
}
@@ -1,8 +1,6 @@
using System.Data;
using Dapper;
using JdeScoping.Core.Models.Infrastructure;
using JdeScoping.DataAccess.Queries;
using Microsoft.Data.SqlClient;
namespace JdeScoping.DataAccess.Repositories;
@@ -33,166 +31,4 @@ public partial class LotFinderRepository
throw;
}
}
/// <inheritdoc/>
public async Task<TableSpec> GetTableSpecAsync(string tableName, CancellationToken ct = default)
{
const string operation = nameof(GetTableSpecAsync);
try
{
var tableSpec = new TableSpec(tableName);
await using var connection = await _connectionFactory.CreateLotFinderConnectionAsync(ct);
// Load columns
var columns = await connection.QueryAsync<ColumnSpec>(
LotFinderQueries.SqlGetTableColumns,
new { name = tableName },
commandTimeout: _options.Value.DefaultTimeoutSeconds);
tableSpec.Columns.AddRange(columns);
// Load primary key
var pkColumns = await connection.QueryAsync<string>(
LotFinderQueries.SqlGetTablePrimaryKey,
new { name = tableName },
commandTimeout: _options.Value.DefaultTimeoutSeconds);
foreach (var columnName in pkColumns)
{
var column = tableSpec.GetColumn(columnName);
if (column != null)
{
tableSpec.PrimaryKey.Add(column);
}
}
return tableSpec;
}
catch (OperationCanceledException)
{
throw;
}
catch (Exception ex)
{
LogAndThrow(ex, operation, "SQL_GET_TABLE_COLUMNS");
throw;
}
}
/// <inheritdoc/>
public async Task RebuildIndicesAsync(string tableName, CancellationToken ct = default)
{
const string operation = nameof(RebuildIndicesAsync);
// Validate table name against whitelist (SQL injection prevention)
if (!ValidTableNames.Contains(tableName))
{
throw new ArgumentException($"Invalid table name: {tableName}", nameof(tableName));
}
try
{
await using var connection = await _connectionFactory.CreateLotFinderConnectionAsync(ct);
var sql = $"ALTER INDEX ALL ON [{tableName}] REBUILD WITH (FILLFACTOR = 95)";
await connection.ExecuteAsync(sql, commandTimeout: _options.Value.RebuildIndexTimeoutSeconds);
}
catch (OperationCanceledException)
{
throw;
}
catch (Exception ex)
{
LogAndThrow(ex, operation, "SQL_REBUILD_INDICES");
throw;
}
}
/// <inheritdoc/>
public async Task PostProcessMisDataAsync(CancellationToken ct = default)
{
const string operation = nameof(PostProcessMisDataAsync);
try
{
await using var connection = await _connectionFactory.CreateLotFinderConnectionAsync(ct);
await connection.ExecuteAsync(
LotFinderQueries.SqlPostprocessMisData,
commandTimeout: _options.Value.DefaultTimeoutSeconds);
}
catch (OperationCanceledException)
{
throw;
}
catch (Exception ex)
{
LogAndThrow(ex, operation, "SQL_POSTPROCESS_MISDATA");
throw;
}
}
/// <inheritdoc/>
public async Task<int> BulkInsertAsync<T>(string tableName, IEnumerable<T> records, CancellationToken ct = default)
{
const string operation = nameof(BulkInsertAsync);
// Validate table name against whitelist
if (!ValidTableNames.Contains(tableName))
{
throw new ArgumentException($"Invalid table name: {tableName}", nameof(tableName));
}
try
{
await using var connection = await _connectionFactory.CreateLotFinderConnectionAsync(ct);
// Use SqlBulkCopy for efficient bulk insert
using var bulkCopy = new SqlBulkCopy(connection)
{
DestinationTableName = $"dbo.[{tableName}]",
BulkCopyTimeout = _options.Value.DefaultTimeoutSeconds
};
// Convert records to DataTable
var recordList = records.ToList();
var dataTable = ToDataTable(recordList);
await bulkCopy.WriteToServerAsync(dataTable, ct);
return recordList.Count;
}
catch (OperationCanceledException)
{
throw;
}
catch (Exception ex)
{
LogAndThrow(ex, operation, "BulkInsert");
throw;
}
}
/// <inheritdoc/>
public async Task TruncateTableAsync(string tableName, CancellationToken ct = default)
{
const string operation = nameof(TruncateTableAsync);
// Validate table name against whitelist
if (!ValidTableNames.Contains(tableName))
{
throw new ArgumentException($"Invalid table name: {tableName}", nameof(tableName));
}
try
{
await using var connection = await _connectionFactory.CreateLotFinderConnectionAsync(ct);
var sql = $"TRUNCATE TABLE dbo.[{tableName}]";
await connection.ExecuteAsync(sql, commandTimeout: _options.Value.DefaultTimeoutSeconds);
}
catch (OperationCanceledException)
{
throw;
}
catch (Exception ex)
{
LogAndThrow(ex, operation, "TruncateTable");
throw;
}
}
}
@@ -113,32 +113,6 @@ public partial class LotFinderRepository
}
}
/// <inheritdoc/>
public async Task<List<WorkCenter>> LookupWorkCentersAsync(List<string> codes, CancellationToken ct = default)
{
const string operation = nameof(LookupWorkCentersAsync);
try
{
var workCenterCodesCsv = string.Join(",", codes);
await using var connection = await _connectionFactory.CreateLotFinderConnectionAsync(ct);
var result = await connection.QueryAsync<WorkCenter>(
LotFinderQueries.SqlLookupWorkCenters,
new { workCenterCodes = workCenterCodesCsv },
commandTimeout: _options.Value.DefaultTimeoutSeconds);
return result.ToList();
}
catch (OperationCanceledException)
{
throw;
}
catch (Exception ex)
{
LogAndThrow(ex, operation, "SQL_LOOKUP_WORK_CENTERS");
throw;
}
}
/// <inheritdoc/>
public async Task<List<ProfitCenter>> SearchProfitCentersAsync(string filter, CancellationToken ct = default)
{
@@ -163,32 +137,6 @@ public partial class LotFinderRepository
}
}
/// <inheritdoc/>
public async Task<List<ProfitCenter>> LookupProfitCentersAsync(List<string> codes, CancellationToken ct = default)
{
const string operation = nameof(LookupProfitCentersAsync);
try
{
var profitCenterCodesCsv = string.Join(",", codes);
await using var connection = await _connectionFactory.CreateLotFinderConnectionAsync(ct);
var result = await connection.QueryAsync<ProfitCenter>(
LotFinderQueries.SqlLookupProfitCenters,
new { profitCenterCodes = profitCenterCodesCsv },
commandTimeout: _options.Value.DefaultTimeoutSeconds);
return result.ToList();
}
catch (OperationCanceledException)
{
throw;
}
catch (Exception ex)
{
LogAndThrow(ex, operation, "SQL_LOOKUP_PROFIT_CENTERS");
throw;
}
}
/// <inheritdoc/>
public async Task<List<JdeUser>> SearchUsersAsync(string filter, CancellationToken ct = default)
{
@@ -213,32 +161,6 @@ public partial class LotFinderRepository
}
}
/// <inheritdoc/>
public async Task<List<JdeUser>> LookupUsersAsync(List<string> userIds, CancellationToken ct = default)
{
const string operation = nameof(LookupUsersAsync);
try
{
var userIdsCsv = string.Join(",", userIds);
await using var connection = await _connectionFactory.CreateLotFinderConnectionAsync(ct);
var result = await connection.QueryAsync<JdeUser>(
LotFinderQueries.SqlLookupUsers,
new { userIds = userIdsCsv },
commandTimeout: _options.Value.DefaultTimeoutSeconds);
return result.ToList();
}
catch (OperationCanceledException)
{
throw;
}
catch (Exception ex)
{
LogAndThrow(ex, operation, "SQL_LOOKUP_USERS");
throw;
}
}
/// <inheritdoc/>
public async Task<List<Lot>> LookupLotsAsync(List<LotViewModel> lots, CancellationToken ct = default)
{
@@ -152,49 +152,4 @@ public partial class LotFinderRepository
}
}
/// <inheritdoc/>
public async Task UpdateSearchStatusAsync(int id, SearchStatus status, CancellationToken ct = default)
{
const string operation = nameof(UpdateSearchStatusAsync);
try
{
await using var connection = await _connectionFactory.CreateLotFinderConnectionAsync(ct);
await connection.ExecuteAsync(
LotFinderQueries.SqlUpdateSearchStatus,
new { id, status = (int)status },
commandTimeout: _options.Value.DefaultTimeoutSeconds);
}
catch (OperationCanceledException)
{
throw;
}
catch (Exception ex)
{
LogAndThrow(ex, operation, "SQL_UPDATE_SEARCH_STATUS");
throw;
}
}
/// <inheritdoc/>
public async Task UpdateSearchResultsAsync(int id, byte[] results, CancellationToken ct = default)
{
const string operation = nameof(UpdateSearchResultsAsync);
try
{
await using var connection = await _connectionFactory.CreateLotFinderConnectionAsync(ct);
await connection.ExecuteAsync(
LotFinderQueries.SqlUpdateSearchResults,
new { id, results },
commandTimeout: _options.Value.DefaultTimeoutSeconds);
}
catch (OperationCanceledException)
{
throw;
}
catch (Exception ex)
{
LogAndThrow(ex, operation, "SQL_UPDATE_SEARCH_RESULTS");
throw;
}
}
}
@@ -1,4 +1,3 @@
using System.Data;
using JdeScoping.Core.Interfaces;
using JdeScoping.DataAccess.Options;
using JdeScoping.DataAccess.Exceptions;
@@ -19,22 +18,6 @@ public partial class LotFinderRepository : ILotFinderRepository
private readonly IOptions<DataAccessOptions> _options;
private const string RepositoryName = "LotFinderRepository";
/// <summary>
/// Valid table names for index rebuild operations (SQL injection whitelist).
/// </summary>
private static readonly HashSet<string> ValidTableNames = new(StringComparer.OrdinalIgnoreCase)
{
"Branch", "DataUpdate", "FunctionCode", "Item", "JdeUser",
"Lot", "LotLocation", "LotUsage_Curr", "LotUsage_Hist",
"MisData", "OrgHierarchy", "ProfitCenter", "RouteMaster",
"Search", "StatusCode", "WorkCenter",
"WorkOrder_Curr", "WorkOrder_Hist",
"WorkOrderComponent_Curr", "WorkOrderComponent_Hist",
"WorkOrderRouting",
"WorkOrderStep_Curr", "WorkOrderStep_Hist",
"WorkOrderTime_Curr", "WorkOrderTime_Hist"
};
/// <summary>
/// Initializes a new instance of the <see cref="LotFinderRepository"/> class.
/// </summary>
@@ -83,41 +66,4 @@ public partial class LotFinderRepository : ILotFinderRepository
// SQL Server timeout error number: -2
return ex.Number == -2;
}
private static DataTable ToDataTable<T>(List<T> items)
{
var dataTable = new DataTable();
var properties = typeof(T).GetProperties()
.Where(p => p.CanRead && IsSupportedType(p.PropertyType))
.ToArray();
foreach (var prop in properties)
{
var type = Nullable.GetUnderlyingType(prop.PropertyType) ?? prop.PropertyType;
dataTable.Columns.Add(prop.Name, type);
}
foreach (var item in items)
{
var row = dataTable.NewRow();
foreach (var prop in properties)
{
var value = prop.GetValue(item);
row[prop.Name] = value ?? DBNull.Value;
}
dataTable.Rows.Add(row);
}
return dataTable;
}
private static bool IsSupportedType(Type type)
{
var underlyingType = Nullable.GetUnderlyingType(type) ?? type;
return underlyingType.IsPrimitive
|| underlyingType == typeof(string)
|| underlyingType == typeof(DateTime)
|| underlyingType == typeof(decimal)
|| underlyingType == typeof(Guid);
}
}
-8
View File
@@ -1,9 +1,7 @@
using JdeScoping.Api;
using JdeScoping.Core.Interfaces;
using JdeScoping.DataAccess.Options;
using JdeScoping.DataSync.Options;
using JdeScoping.ExcelIO.Options;
using JdeScoping.Infrastructure.Options;
using JdeScoping.Database;
using Microsoft.Extensions.Options;
@@ -72,11 +70,5 @@ static void ValidateServices(IServiceProvider services)
_ = provider.GetRequiredService<IOptions<DataSyncOptions>>();
_ = provider.GetRequiredService<IOptions<ExcelExportOptions>>();
_ = provider.GetRequiredService<IOptions<SearchProcessingOptions>>();
_ = provider.GetRequiredService<IOptions<DataSourceOptions>>();
// Validate data source services
_ = provider.GetRequiredService<IJdeDataSource>();
_ = provider.GetRequiredService<ICmsDataSource>();
Console.WriteLine("Service validation completed successfully.");
}
@@ -3,8 +3,6 @@ using JdeScoping.Core.Options;
using JdeScoping.Infrastructure.Auth;
using JdeScoping.Infrastructure.Options;
using JdeScoping.Infrastructure.Security;
using JdeScoping.Infrastructure.Sources.Cms;
using JdeScoping.Infrastructure.Sources.Jde;
using Microsoft.Extensions.Configuration;
namespace Microsoft.Extensions.DependencyInjection;
@@ -25,27 +23,9 @@ public static class InfrastructureDependencyInjection
IConfiguration configuration)
{
// Bind configuration
services.Configure<DataSourceOptions>(
configuration.GetSection(DataSourceOptions.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 ldapOptions = configuration
.GetSection(LdapOptions.SectionName)
@@ -7,13 +7,11 @@
</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>
@@ -1,22 +0,0 @@
namespace JdeScoping.Infrastructure.Options;
/// <summary>
/// Configuration options for data source selection (Oracle vs file-based).
/// </summary>
public class DataSourceOptions
{
/// <summary>
/// Configuration section name in appsettings.json.
/// </summary>
public const string SectionName = "DataSource";
/// <summary>
/// Use file-based data sources instead of Oracle for development.
/// </summary>
public bool UseFileDataSource { get; set; } = false;
/// <summary>
/// Directory containing JSON data files for file-based data source.
/// </summary>
public string FileDirectory { get; set; } = "DevData";
}
@@ -1,43 +0,0 @@
using System.Runtime.CompilerServices;
using System.Text.Json;
using JdeScoping.Core.Interfaces;
using JdeScoping.Core.Models;
using JdeScoping.Core.Models.Quality;
using JdeScoping.Infrastructure.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;
}
}
}
@@ -1,46 +0,0 @@
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;
}
}
}
@@ -1,133 +0,0 @@
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.Infrastructure.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) ?? [];
}
}
@@ -1,180 +0,0 @@
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;
}
}
}
@@ -1,228 +0,0 @@
using JdeScoping.DataAccess.Options;
using JdeScoping.DataAccess.Exceptions;
using JdeScoping.DataAccess.Interfaces;
using JdeScoping.DataAccess.Repositories;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using NSubstitute;
using NSubstitute.ExceptionExtensions;
using Shouldly;
using Xunit;
namespace JdeScoping.DataAccess.Tests;
/// <summary>
/// Unit tests for CmsRepository.
/// </summary>
public class CmsRepositoryTests
{
private readonly IDbConnectionFactory _connectionFactory;
private readonly ILogger<CmsRepository> _logger;
private readonly IOptions<DataAccessOptions> _options;
public CmsRepositoryTests()
{
_connectionFactory = Substitute.For<IDbConnectionFactory>();
_logger = Substitute.For<ILogger<CmsRepository>>();
_options = Microsoft.Extensions.Options.Options.Create(new DataAccessOptions
{
DefaultTimeoutSeconds = 30,
MisDataTimeoutSeconds = 60000
});
}
#region Constructor Tests
[Fact]
public void Constructor_NullConnectionFactory_ThrowsArgumentNullException()
{
// Act & Assert
Should.Throw<ArgumentNullException>(
() => new CmsRepository(null!, _logger, _options))
.ParamName.ShouldBe("connectionFactory");
}
[Fact]
public void Constructor_NullLogger_ThrowsArgumentNullException()
{
// Act & Assert
Should.Throw<ArgumentNullException>(
() => new CmsRepository(_connectionFactory, null!, _options))
.ParamName.ShouldBe("logger");
}
[Fact]
public void Constructor_NullOptions_ThrowsArgumentNullException()
{
// Act & Assert
Should.Throw<ArgumentNullException>(
() => new CmsRepository(_connectionFactory, _logger, null!))
.ParamName.ShouldBe("options");
}
[Fact]
public void Constructor_ValidParameters_CreatesInstance()
{
// Act
var repository = new CmsRepository(_connectionFactory, _logger, _options);
// Assert
repository.ShouldNotBeNull();
}
#endregion
#region GetMisDataAsync Tests
[Fact]
public async Task GetMisDataAsync_ConnectionFails_ThrowsConnectionException()
{
// Arrange
_connectionFactory.CreateCmsConnectionAsync(Arg.Any<CancellationToken>())
.ThrowsAsync(new ConnectionException("Test connection error", "CMS"));
var repository = new CmsRepository(_connectionFactory, _logger, _options);
// Act & Assert
var ex = await Should.ThrowAsync<ConnectionException>(
async () =>
{
await foreach (var _ in repository.GetMisDataAsync())
{
}
});
ex.DataSource.ShouldBe("CMS");
}
[Fact]
public async Task GetMisDataAsync_UsesCmsConnection()
{
// Arrange
_connectionFactory.CreateCmsConnectionAsync(Arg.Any<CancellationToken>())
.ThrowsAsync(new ConnectionException("Test connection error", "CMS"));
var repository = new CmsRepository(_connectionFactory, _logger, _options);
// Act
try
{
await foreach (var _ in repository.GetMisDataAsync())
{
}
}
catch (ConnectionException)
{
// Expected
}
// Assert - verify correct connection factory method was called
await _connectionFactory.Received(1).CreateCmsConnectionAsync(Arg.Any<CancellationToken>());
}
#endregion
#region Cancellation Tests
[Fact]
public async Task GetMisDataAsync_CancellationRequested_ThrowsOperationCanceledException()
{
// Arrange
using var cts = new CancellationTokenSource();
cts.Cancel();
_connectionFactory.CreateCmsConnectionAsync(Arg.Any<CancellationToken>())
.ThrowsAsync(new OperationCanceledException(cts.Token));
var repository = new CmsRepository(_connectionFactory, _logger, _options);
// Act & Assert
await Should.ThrowAsync<OperationCanceledException>(
async () =>
{
await foreach (var _ in repository.GetMisDataAsync(ct: cts.Token))
{
}
});
}
#endregion
#region Incremental Sync Tests
[Fact]
public async Task GetMisDataAsync_WithLastUpdateDT_UsesFilteredQuery()
{
// Arrange
var lastUpdate = new DateTime(2024, 1, 15, 10, 30, 0);
_connectionFactory.CreateCmsConnectionAsync(Arg.Any<CancellationToken>())
.ThrowsAsync(new ConnectionException("Test connection error", "CMS"));
var repository = new CmsRepository(_connectionFactory, _logger, _options);
// Act & Assert - this just verifies the method accepts the parameter
await Should.ThrowAsync<ConnectionException>(
async () =>
{
await foreach (var _ in repository.GetMisDataAsync(lastUpdate))
{
}
});
}
[Fact]
public async Task GetMisDataAsync_WithoutLastUpdateDT_UsesFullQuery()
{
// Arrange
_connectionFactory.CreateCmsConnectionAsync(Arg.Any<CancellationToken>())
.ThrowsAsync(new ConnectionException("Test connection error", "CMS"));
var repository = new CmsRepository(_connectionFactory, _logger, _options);
// Act & Assert
await Should.ThrowAsync<ConnectionException>(
async () =>
{
await foreach (var _ in repository.GetMisDataAsync())
{
}
});
}
#endregion
#region Timeout Configuration Tests
[Fact]
public void Constructor_UsesMisDataTimeout()
{
// Arrange
var customOptions = Microsoft.Extensions.Options.Options.Create(new DataAccessOptions
{
MisDataTimeoutSeconds = 999999
});
// Act
var repository = new CmsRepository(_connectionFactory, _logger, customOptions);
// Assert
repository.ShouldNotBeNull();
// The timeout value is internal, verified through behavior
}
[Fact]
public void Constructor_DefaultMisDataTimeout_Is60000Seconds()
{
// Arrange
var defaultOptions = Microsoft.Extensions.Options.Options.Create(new DataAccessOptions());
// Act
var repository = new CmsRepository(_connectionFactory, _logger, defaultOptions);
// Assert
repository.ShouldNotBeNull();
// Default timeout of 60000 seconds is verified implicitly
}
#endregion
}
@@ -1,674 +0,0 @@
using JdeScoping.DataAccess.Options;
using JdeScoping.DataAccess.Exceptions;
using JdeScoping.DataAccess.Interfaces;
using JdeScoping.DataAccess.Repositories;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using NSubstitute;
using NSubstitute.ExceptionExtensions;
using Shouldly;
using Xunit;
namespace JdeScoping.DataAccess.Tests;
/// <summary>
/// Unit tests for JdeRepository.
/// </summary>
public class JdeRepositoryTests
{
private readonly IDbConnectionFactory _connectionFactory;
private readonly ILogger<JdeRepository> _logger;
private readonly IOptions<DataAccessOptions> _options;
public JdeRepositoryTests()
{
_connectionFactory = Substitute.For<IDbConnectionFactory>();
_logger = Substitute.For<ILogger<JdeRepository>>();
_options = Microsoft.Extensions.Options.Options.Create(new DataAccessOptions
{
DefaultTimeoutSeconds = 30,
LotUsageTimeoutSeconds = 60,
ProductionSchema = "PRODDTA",
ArchiveSchema = "ARCDTAPD",
StageSchema = "JDESTAGE"
});
}
#region Constructor Tests
[Fact]
public void Constructor_NullConnectionFactory_ThrowsArgumentNullException()
{
// Act & Assert
Should.Throw<ArgumentNullException>(
() => new JdeRepository(null!, _logger, _options))
.ParamName.ShouldBe("connectionFactory");
}
[Fact]
public void Constructor_NullLogger_ThrowsArgumentNullException()
{
// Act & Assert
Should.Throw<ArgumentNullException>(
() => new JdeRepository(_connectionFactory, null!, _options))
.ParamName.ShouldBe("logger");
}
[Fact]
public void Constructor_NullOptions_ThrowsArgumentNullException()
{
// Act & Assert
Should.Throw<ArgumentNullException>(
() => new JdeRepository(_connectionFactory, _logger, null!))
.ParamName.ShouldBe("options");
}
[Fact]
public void Constructor_ValidParameters_CreatesInstance()
{
// Act
var repository = new JdeRepository(_connectionFactory, _logger, _options);
// Assert
repository.ShouldNotBeNull();
}
#endregion
#region Schema Replacement Tests - Work Orders
[Fact]
public async Task GetWorkOrdersAsync_ConnectionFails_ThrowsConnectionException()
{
// Arrange
_connectionFactory.CreateJdeConnectionAsync(Arg.Any<CancellationToken>())
.ThrowsAsync(new ConnectionException("Test connection error", "JDE"));
var repository = new JdeRepository(_connectionFactory, _logger, _options);
// Act & Assert
var ex = await Should.ThrowAsync<ConnectionException>(
async () =>
{
await foreach (var _ in repository.GetWorkOrdersAsync())
{
}
});
ex.DataSource.ShouldBe("JDE");
}
[Fact]
public async Task GetWorkOrdersArchiveAsync_ConnectionFails_ThrowsConnectionException()
{
// Arrange
_connectionFactory.CreateJdeConnectionAsync(Arg.Any<CancellationToken>())
.ThrowsAsync(new ConnectionException("Test connection error", "JDE"));
var repository = new JdeRepository(_connectionFactory, _logger, _options);
// Act & Assert
var ex = await Should.ThrowAsync<ConnectionException>(
async () =>
{
await foreach (var _ in repository.GetWorkOrdersArchiveAsync())
{
}
});
ex.DataSource.ShouldBe("JDE");
}
#endregion
#region Schema Replacement Tests - Work Order Steps
[Fact]
public async Task GetWorkOrderStepsAsync_ConnectionFails_ThrowsConnectionException()
{
// Arrange
_connectionFactory.CreateJdeConnectionAsync(Arg.Any<CancellationToken>())
.ThrowsAsync(new ConnectionException("Test connection error", "JDE"));
var repository = new JdeRepository(_connectionFactory, _logger, _options);
// Act & Assert
var ex = await Should.ThrowAsync<ConnectionException>(
async () =>
{
await foreach (var _ in repository.GetWorkOrderStepsAsync())
{
}
});
ex.DataSource.ShouldBe("JDE");
}
[Fact]
public async Task GetWorkOrderStepsArchiveAsync_ConnectionFails_ThrowsConnectionException()
{
// Arrange
_connectionFactory.CreateJdeConnectionAsync(Arg.Any<CancellationToken>())
.ThrowsAsync(new ConnectionException("Test connection error", "JDE"));
var repository = new JdeRepository(_connectionFactory, _logger, _options);
// Act & Assert
await Should.ThrowAsync<ConnectionException>(
async () =>
{
await foreach (var _ in repository.GetWorkOrderStepsArchiveAsync())
{
}
});
}
#endregion
#region Schema Replacement Tests - Work Order Times
[Fact]
public async Task GetWorkOrderTimesAsync_ConnectionFails_ThrowsConnectionException()
{
// Arrange
_connectionFactory.CreateJdeConnectionAsync(Arg.Any<CancellationToken>())
.ThrowsAsync(new ConnectionException("Test connection error", "JDE"));
var repository = new JdeRepository(_connectionFactory, _logger, _options);
// Act & Assert
await Should.ThrowAsync<ConnectionException>(
async () =>
{
await foreach (var _ in repository.GetWorkOrderTimesAsync())
{
}
});
}
[Fact]
public async Task GetWorkOrderTimesArchiveAsync_ConnectionFails_ThrowsConnectionException()
{
// Arrange
_connectionFactory.CreateJdeConnectionAsync(Arg.Any<CancellationToken>())
.ThrowsAsync(new ConnectionException("Test connection error", "JDE"));
var repository = new JdeRepository(_connectionFactory, _logger, _options);
// Act & Assert
await Should.ThrowAsync<ConnectionException>(
async () =>
{
await foreach (var _ in repository.GetWorkOrderTimesArchiveAsync())
{
}
});
}
#endregion
#region Schema Replacement Tests - Work Order Routings
[Fact]
public async Task GetWorkOrderRoutingsAsync_ConnectionFails_ThrowsConnectionException()
{
// Arrange
_connectionFactory.CreateJdeConnectionAsync(Arg.Any<CancellationToken>())
.ThrowsAsync(new ConnectionException("Test connection error", "JDE"));
var repository = new JdeRepository(_connectionFactory, _logger, _options);
// Act & Assert
await Should.ThrowAsync<ConnectionException>(
async () =>
{
await foreach (var _ in repository.GetWorkOrderRoutingsAsync())
{
}
});
}
#endregion
#region Schema Replacement Tests - Work Order Components
[Fact]
public async Task GetWorkOrderComponentsAsync_ConnectionFails_ThrowsConnectionException()
{
// Arrange
_connectionFactory.CreateJdeConnectionAsync(Arg.Any<CancellationToken>())
.ThrowsAsync(new ConnectionException("Test connection error", "JDE"));
var repository = new JdeRepository(_connectionFactory, _logger, _options);
// Act & Assert
await Should.ThrowAsync<ConnectionException>(
async () =>
{
await foreach (var _ in repository.GetWorkOrderComponentsAsync())
{
}
});
}
[Fact]
public async Task GetWorkOrderComponentsArchiveAsync_ConnectionFails_ThrowsConnectionException()
{
// Arrange
_connectionFactory.CreateJdeConnectionAsync(Arg.Any<CancellationToken>())
.ThrowsAsync(new ConnectionException("Test connection error", "JDE"));
var repository = new JdeRepository(_connectionFactory, _logger, _options);
// Act & Assert
await Should.ThrowAsync<ConnectionException>(
async () =>
{
await foreach (var _ in repository.GetWorkOrderComponentsArchiveAsync())
{
}
});
}
#endregion
#region Schema Replacement Tests - Lots
[Fact]
public async Task GetLotsAsync_ConnectionFails_ThrowsConnectionException()
{
// Arrange
_connectionFactory.CreateJdeConnectionAsync(Arg.Any<CancellationToken>())
.ThrowsAsync(new ConnectionException("Test connection error", "JDE"));
var repository = new JdeRepository(_connectionFactory, _logger, _options);
// Act & Assert
await Should.ThrowAsync<ConnectionException>(
async () =>
{
await foreach (var _ in repository.GetLotsAsync())
{
}
});
}
#endregion
#region Schema Replacement Tests - Lot Usages
[Fact]
public async Task GetLotUsagesAsync_ConnectionFails_ThrowsConnectionException()
{
// Arrange
_connectionFactory.CreateJdeConnectionAsync(Arg.Any<CancellationToken>())
.ThrowsAsync(new ConnectionException("Test connection error", "JDE"));
var repository = new JdeRepository(_connectionFactory, _logger, _options);
// Act & Assert
await Should.ThrowAsync<ConnectionException>(
async () =>
{
await foreach (var _ in repository.GetLotUsagesAsync())
{
}
});
}
[Fact]
public async Task GetLotUsagesArchiveAsync_ConnectionFails_ThrowsConnectionException()
{
// Arrange
_connectionFactory.CreateJdeConnectionAsync(Arg.Any<CancellationToken>())
.ThrowsAsync(new ConnectionException("Test connection error", "JDE"));
var repository = new JdeRepository(_connectionFactory, _logger, _options);
// Act & Assert
await Should.ThrowAsync<ConnectionException>(
async () =>
{
await foreach (var _ in repository.GetLotUsagesArchiveAsync())
{
}
});
}
#endregion
#region JDE Stage Connection Tests - Lot Locations
[Fact]
public async Task GetLotLocationsAsync_UsesJdeStageConnection()
{
// Arrange
_connectionFactory.CreateJdeStageConnectionAsync(Arg.Any<CancellationToken>())
.ThrowsAsync(new ConnectionException("Test connection error", "JDEStage"));
var repository = new JdeRepository(_connectionFactory, _logger, _options);
// Act & Assert - verify it uses Stage connection
var ex = await Should.ThrowAsync<ConnectionException>(
async () =>
{
await foreach (var _ in repository.GetLotLocationsAsync())
{
}
});
ex.DataSource.ShouldBe("JDEStage");
}
#endregion
#region Reference Data Tests
[Fact]
public async Task GetItemsAsync_ConnectionFails_ThrowsConnectionException()
{
// Arrange
_connectionFactory.CreateJdeConnectionAsync(Arg.Any<CancellationToken>())
.ThrowsAsync(new ConnectionException("Test connection error", "JDE"));
var repository = new JdeRepository(_connectionFactory, _logger, _options);
// Act & Assert
await Should.ThrowAsync<ConnectionException>(
async () =>
{
await foreach (var _ in repository.GetItemsAsync())
{
}
});
}
[Fact]
public async Task GetUsersAsync_ConnectionFails_ThrowsConnectionException()
{
// Arrange
_connectionFactory.CreateJdeConnectionAsync(Arg.Any<CancellationToken>())
.ThrowsAsync(new ConnectionException("Test connection error", "JDE"));
var repository = new JdeRepository(_connectionFactory, _logger, _options);
// Act & Assert
await Should.ThrowAsync<ConnectionException>(
async () =>
{
await foreach (var _ in repository.GetUsersAsync())
{
}
});
}
[Fact]
public async Task GetBranchesAsync_ConnectionFails_ThrowsConnectionException()
{
// Arrange
_connectionFactory.CreateJdeConnectionAsync(Arg.Any<CancellationToken>())
.ThrowsAsync(new ConnectionException("Test connection error", "JDE"));
var repository = new JdeRepository(_connectionFactory, _logger, _options);
// Act & Assert
await Should.ThrowAsync<ConnectionException>(
async () =>
{
await foreach (var _ in repository.GetBranchesAsync())
{
}
});
}
[Fact]
public async Task GetProfitCentersAsync_ConnectionFails_ThrowsConnectionException()
{
// Arrange
_connectionFactory.CreateJdeConnectionAsync(Arg.Any<CancellationToken>())
.ThrowsAsync(new ConnectionException("Test connection error", "JDE"));
var repository = new JdeRepository(_connectionFactory, _logger, _options);
// Act & Assert
await Should.ThrowAsync<ConnectionException>(
async () =>
{
await foreach (var _ in repository.GetProfitCentersAsync())
{
}
});
}
[Fact]
public async Task GetWorkCentersAsync_ConnectionFails_ThrowsConnectionException()
{
// Arrange
_connectionFactory.CreateJdeConnectionAsync(Arg.Any<CancellationToken>())
.ThrowsAsync(new ConnectionException("Test connection error", "JDE"));
var repository = new JdeRepository(_connectionFactory, _logger, _options);
// Act & Assert
await Should.ThrowAsync<ConnectionException>(
async () =>
{
await foreach (var _ in repository.GetWorkCentersAsync())
{
}
});
}
[Fact]
public async Task GetStatusCodesAsync_UsesJdeStageConnection()
{
// Arrange
_connectionFactory.CreateJdeStageConnectionAsync(Arg.Any<CancellationToken>())
.ThrowsAsync(new ConnectionException("Test connection error", "JDEStage"));
var repository = new JdeRepository(_connectionFactory, _logger, _options);
// Act & Assert - verify it uses Stage connection
var ex = await Should.ThrowAsync<ConnectionException>(
async () =>
{
await foreach (var _ in repository.GetStatusCodesAsync())
{
}
});
ex.DataSource.ShouldBe("JDEStage");
}
[Fact]
public async Task GetFunctionCodesAsync_ConnectionFails_ThrowsConnectionException()
{
// Arrange
_connectionFactory.CreateJdeConnectionAsync(Arg.Any<CancellationToken>())
.ThrowsAsync(new ConnectionException("Test connection error", "JDE"));
var repository = new JdeRepository(_connectionFactory, _logger, _options);
// Act & Assert
await Should.ThrowAsync<ConnectionException>(
async () =>
{
await foreach (var _ in repository.GetFunctionCodesAsync())
{
}
});
}
[Fact]
public async Task GetOrgHierarchyAsync_ConnectionFails_ThrowsConnectionException()
{
// Arrange
_connectionFactory.CreateJdeConnectionAsync(Arg.Any<CancellationToken>())
.ThrowsAsync(new ConnectionException("Test connection error", "JDE"));
var repository = new JdeRepository(_connectionFactory, _logger, _options);
// Act & Assert
await Should.ThrowAsync<ConnectionException>(
async () =>
{
await foreach (var _ in repository.GetOrgHierarchyAsync())
{
}
});
}
[Fact]
public async Task GetRouteMastersAsync_ConnectionFails_ThrowsConnectionException()
{
// Arrange
_connectionFactory.CreateJdeConnectionAsync(Arg.Any<CancellationToken>())
.ThrowsAsync(new ConnectionException("Test connection error", "JDE"));
var repository = new JdeRepository(_connectionFactory, _logger, _options);
// Act & Assert
await Should.ThrowAsync<ConnectionException>(
async () =>
{
await foreach (var _ in repository.GetRouteMastersAsync())
{
}
});
}
#endregion
#region Cancellation Tests
[Fact]
public async Task GetWorkOrdersAsync_CancellationRequested_ThrowsOperationCanceledException()
{
// Arrange
using var cts = new CancellationTokenSource();
cts.Cancel();
_connectionFactory.CreateJdeConnectionAsync(Arg.Any<CancellationToken>())
.ThrowsAsync(new OperationCanceledException(cts.Token));
var repository = new JdeRepository(_connectionFactory, _logger, _options);
// Act & Assert
await Should.ThrowAsync<OperationCanceledException>(
async () =>
{
await foreach (var _ in repository.GetWorkOrdersAsync(ct: cts.Token))
{
}
});
}
[Fact]
public async Task GetLotUsagesAsync_CancellationRequested_ThrowsOperationCanceledException()
{
// Arrange
using var cts = new CancellationTokenSource();
cts.Cancel();
_connectionFactory.CreateJdeConnectionAsync(Arg.Any<CancellationToken>())
.ThrowsAsync(new OperationCanceledException(cts.Token));
var repository = new JdeRepository(_connectionFactory, _logger, _options);
// Act & Assert
await Should.ThrowAsync<OperationCanceledException>(
async () =>
{
await foreach (var _ in repository.GetLotUsagesAsync(ct: cts.Token))
{
}
});
}
#endregion
#region Incremental Sync Tests
[Fact]
public async Task GetWorkOrdersAsync_WithLastUpdateDT_UsesFilteredQuery()
{
// Arrange
var lastUpdate = new DateTime(2024, 1, 15, 10, 30, 0);
_connectionFactory.CreateJdeConnectionAsync(Arg.Any<CancellationToken>())
.ThrowsAsync(new ConnectionException("Test connection error", "JDE"));
var repository = new JdeRepository(_connectionFactory, _logger, _options);
// Act & Assert - this just verifies the method accepts the parameter
await Should.ThrowAsync<ConnectionException>(
async () =>
{
await foreach (var _ in repository.GetWorkOrdersAsync(lastUpdate))
{
}
});
}
[Fact]
public async Task GetLotsAsync_WithLastUpdateDT_UsesFilteredQuery()
{
// Arrange
var lastUpdate = new DateTime(2024, 1, 15, 10, 30, 0);
_connectionFactory.CreateJdeConnectionAsync(Arg.Any<CancellationToken>())
.ThrowsAsync(new ConnectionException("Test connection error", "JDE"));
var repository = new JdeRepository(_connectionFactory, _logger, _options);
// Act & Assert
await Should.ThrowAsync<ConnectionException>(
async () =>
{
await foreach (var _ in repository.GetLotsAsync(lastUpdate))
{
}
});
}
#endregion
#region Options Configuration Tests
[Fact]
public void Constructor_UsesConfiguredSchemas()
{
// Arrange
var customOptions = Microsoft.Extensions.Options.Options.Create(new DataAccessOptions
{
ProductionSchema = "CUSTOM_PROD",
ArchiveSchema = "CUSTOM_ARC",
StageSchema = "CUSTOM_STG"
});
// Act
var repository = new JdeRepository(_connectionFactory, _logger, customOptions);
// Assert
repository.ShouldNotBeNull();
// The schema values are internal, verified through integration tests
}
[Fact]
public void Constructor_UsesConfiguredTimeouts()
{
// Arrange
var customOptions = Microsoft.Extensions.Options.Options.Create(new DataAccessOptions
{
DefaultTimeoutSeconds = 120,
LotUsageTimeoutSeconds = 999999
});
// Act
var repository = new JdeRepository(_connectionFactory, _logger, customOptions);
// Assert
repository.ShouldNotBeNull();
// The timeout values are internal, verified through integration tests
}
#endregion
}
@@ -1,6 +1,3 @@
using JdeScoping.Core.Models;
using JdeScoping.Core.Models.Enums;
using JdeScoping.Core.Models.Inventory;
using JdeScoping.Core.Models.Search;
using JdeScoping.Core.ViewModels;
using JdeScoping.DataAccess.Options;
@@ -31,8 +28,7 @@ public class LotFinderRepositoryTests
_logger = Substitute.For<ILogger<LotFinderRepository>>();
_options = Microsoft.Extensions.Options.Options.Create(new DataAccessOptions
{
DefaultTimeoutSeconds = 30,
RebuildIndexTimeoutSeconds = 60
DefaultTimeoutSeconds = 30
});
}
@@ -77,160 +73,6 @@ public class LotFinderRepositoryTests
#endregion
#region RebuildIndicesAsync - Table Name Validation Tests
[Theory]
[InlineData("Branch")]
[InlineData("DataUpdate")]
[InlineData("FunctionCode")]
[InlineData("Item")]
[InlineData("JdeUser")]
[InlineData("Lot")]
[InlineData("LotLocation")]
[InlineData("LotUsage_Curr")]
[InlineData("LotUsage_Hist")]
[InlineData("MisData")]
[InlineData("OrgHierarchy")]
[InlineData("ProfitCenter")]
[InlineData("RouteMaster")]
[InlineData("Search")]
[InlineData("StatusCode")]
[InlineData("WorkCenter")]
[InlineData("WorkOrder_Curr")]
[InlineData("WorkOrder_Hist")]
[InlineData("WorkOrderComponent_Curr")]
[InlineData("WorkOrderComponent_Hist")]
[InlineData("WorkOrderRouting")]
[InlineData("WorkOrderStep_Curr")]
[InlineData("WorkOrderStep_Hist")]
[InlineData("WorkOrderTime_Curr")]
[InlineData("WorkOrderTime_Hist")]
public async Task RebuildIndicesAsync_ValidTableName_DoesNotThrowArgumentException(string tableName)
{
// Arrange - expect connection exception since we have no real connection
_connectionFactory.CreateLotFinderConnectionAsync(Arg.Any<CancellationToken>())
.ThrowsAsync(new ConnectionException("Test connection", "LotFinderDB"));
var repository = new LotFinderRepository(_connectionFactory, _logger, _options);
// Act & Assert - should throw QueryException (wrapped ConnectionException), not ArgumentException
var ex = await Should.ThrowAsync<QueryException>(
async () => await repository.RebuildIndicesAsync(tableName));
ex.QueryName.ShouldBe("SQL_REBUILD_INDICES");
}
[Theory]
[InlineData("InvalidTable")]
[InlineData("DropTable")]
[InlineData("Users")]
[InlineData("sys.tables")]
[InlineData("'; DROP TABLE Users; --")]
[InlineData("WorkOrder")]
[InlineData("branch")] // Case-insensitive should still work
public async Task RebuildIndicesAsync_InvalidTableName_ThrowsArgumentException(string tableName)
{
// Arrange
var repository = new LotFinderRepository(_connectionFactory, _logger, _options);
// Act & Assert
// Note: "branch" is case-insensitive match for "Branch", so it should NOT throw
if (tableName.Equals("branch", StringComparison.OrdinalIgnoreCase))
{
// Case-insensitive match - will try to connect and throw QueryException
_connectionFactory.CreateLotFinderConnectionAsync(Arg.Any<CancellationToken>())
.ThrowsAsync(new ConnectionException("Test connection", "LotFinderDB"));
await Should.ThrowAsync<QueryException>(
async () => await repository.RebuildIndicesAsync(tableName));
}
else
{
var ex = await Should.ThrowAsync<ArgumentException>(
async () => await repository.RebuildIndicesAsync(tableName));
ex.ParamName.ShouldBe("tableName");
ex.Message.ShouldContain($"Invalid table name: {tableName}");
}
}
#endregion
#region TruncateTableAsync - Table Name Validation Tests
[Theory]
[InlineData("Branch")]
[InlineData("Item")]
[InlineData("WorkOrder_Curr")]
public async Task TruncateTableAsync_ValidTableName_DoesNotThrowArgumentException(string tableName)
{
// Arrange - expect connection exception since we have no real connection
_connectionFactory.CreateLotFinderConnectionAsync(Arg.Any<CancellationToken>())
.ThrowsAsync(new ConnectionException("Test connection", "LotFinderDB"));
var repository = new LotFinderRepository(_connectionFactory, _logger, _options);
// Act & Assert - should throw QueryException (wrapped ConnectionException), not ArgumentException
await Should.ThrowAsync<QueryException>(
async () => await repository.TruncateTableAsync(tableName));
}
[Theory]
[InlineData("InvalidTable")]
[InlineData("'; DELETE FROM Users; --")]
public async Task TruncateTableAsync_InvalidTableName_ThrowsArgumentException(string tableName)
{
// Arrange
var repository = new LotFinderRepository(_connectionFactory, _logger, _options);
// Act & Assert
var ex = await Should.ThrowAsync<ArgumentException>(
async () => await repository.TruncateTableAsync(tableName));
ex.ParamName.ShouldBe("tableName");
ex.Message.ShouldContain($"Invalid table name: {tableName}");
}
#endregion
#region BulkInsertAsync - Table Name Validation Tests
[Theory]
[InlineData("Branch")]
[InlineData("Item")]
public async Task BulkInsertAsync_ValidTableName_DoesNotThrowArgumentException(string tableName)
{
// Arrange - expect connection exception since we have no real connection
_connectionFactory.CreateLotFinderConnectionAsync(Arg.Any<CancellationToken>())
.ThrowsAsync(new ConnectionException("Test connection", "LotFinderDB"));
var repository = new LotFinderRepository(_connectionFactory, _logger, _options);
var records = new List<Item>();
// Act & Assert - should throw QueryException (wrapped ConnectionException), not ArgumentException
await Should.ThrowAsync<QueryException>(
async () => await repository.BulkInsertAsync(tableName, records));
}
[Theory]
[InlineData("InvalidTable")]
[InlineData("'; TRUNCATE TABLE Users; --")]
public async Task BulkInsertAsync_InvalidTableName_ThrowsArgumentException(string tableName)
{
// Arrange
var repository = new LotFinderRepository(_connectionFactory, _logger, _options);
var records = new List<Item>();
// Act & Assert
var ex = await Should.ThrowAsync<ArgumentException>(
async () => await repository.BulkInsertAsync(tableName, records));
ex.ParamName.ShouldBe("tableName");
ex.Message.ShouldContain($"Invalid table name: {tableName}");
}
#endregion
#region Connection Exception Handling Tests
[Fact]
@@ -315,38 +157,6 @@ public class LotFinderRepositoryTests
ex.QueryName.ShouldBe(SqlObjects.SubmitSearch);
}
[Fact]
public async Task UpdateSearchStatusAsync_ConnectionFails_ThrowsQueryException()
{
// Arrange
_connectionFactory.CreateLotFinderConnectionAsync(Arg.Any<CancellationToken>())
.ThrowsAsync(new ConnectionException("Test connection error", "LotFinderDB"));
var repository = new LotFinderRepository(_connectionFactory, _logger, _options);
// Act & Assert
var ex = await Should.ThrowAsync<QueryException>(
async () => await repository.UpdateSearchStatusAsync(1, SearchStatus.Running));
ex.QueryName.ShouldBe("SQL_UPDATE_SEARCH_STATUS");
}
[Fact]
public async Task UpdateSearchResultsAsync_ConnectionFails_ThrowsQueryException()
{
// Arrange
_connectionFactory.CreateLotFinderConnectionAsync(Arg.Any<CancellationToken>())
.ThrowsAsync(new ConnectionException("Test connection error", "LotFinderDB"));
var repository = new LotFinderRepository(_connectionFactory, _logger, _options);
// Act & Assert
var ex = await Should.ThrowAsync<QueryException>(
async () => await repository.UpdateSearchResultsAsync(1, [1, 2, 3]));
ex.QueryName.ShouldBe("SQL_UPDATE_SEARCH_RESULTS");
}
#endregion
#region Reference Data Lookup Exception Handling Tests
@@ -415,22 +225,6 @@ public class LotFinderRepositoryTests
ex.QueryName.ShouldBe("SQL_SEARCH_WORK_CENTERS");
}
[Fact]
public async Task LookupWorkCentersAsync_ConnectionFails_ThrowsQueryException()
{
// Arrange
_connectionFactory.CreateLotFinderConnectionAsync(Arg.Any<CancellationToken>())
.ThrowsAsync(new ConnectionException("Test connection error", "LotFinderDB"));
var repository = new LotFinderRepository(_connectionFactory, _logger, _options);
// Act & Assert
var ex = await Should.ThrowAsync<QueryException>(
async () => await repository.LookupWorkCentersAsync(["WC01"]));
ex.QueryName.ShouldBe("SQL_LOOKUP_WORK_CENTERS");
}
[Fact]
public async Task SearchProfitCentersAsync_ConnectionFails_ThrowsQueryException()
{
@@ -447,22 +241,6 @@ public class LotFinderRepositoryTests
ex.QueryName.ShouldBe("SQL_SEARCH_PROFIT_CENTERS");
}
[Fact]
public async Task LookupProfitCentersAsync_ConnectionFails_ThrowsQueryException()
{
// Arrange
_connectionFactory.CreateLotFinderConnectionAsync(Arg.Any<CancellationToken>())
.ThrowsAsync(new ConnectionException("Test connection error", "LotFinderDB"));
var repository = new LotFinderRepository(_connectionFactory, _logger, _options);
// Act & Assert
var ex = await Should.ThrowAsync<QueryException>(
async () => await repository.LookupProfitCentersAsync(["PC01"]));
ex.QueryName.ShouldBe("SQL_LOOKUP_PROFIT_CENTERS");
}
[Fact]
public async Task SearchUsersAsync_ConnectionFails_ThrowsQueryException()
{
@@ -479,22 +257,6 @@ public class LotFinderRepositoryTests
ex.QueryName.ShouldBe("SQL_SEARCH_USERS");
}
[Fact]
public async Task LookupUsersAsync_ConnectionFails_ThrowsQueryException()
{
// Arrange
_connectionFactory.CreateLotFinderConnectionAsync(Arg.Any<CancellationToken>())
.ThrowsAsync(new ConnectionException("Test connection error", "LotFinderDB"));
var repository = new LotFinderRepository(_connectionFactory, _logger, _options);
// Act & Assert
var ex = await Should.ThrowAsync<QueryException>(
async () => await repository.LookupUsersAsync(["USER01"]));
ex.QueryName.ShouldBe("SQL_LOOKUP_USERS");
}
[Fact]
public async Task LookupLotsAsync_ConnectionFails_ThrowsQueryException()
{
@@ -532,38 +294,6 @@ public class LotFinderRepositoryTests
ex.QueryName.ShouldBe("SQL_GET_LAST_DATA_UPDATES");
}
[Fact]
public async Task GetTableSpecAsync_ConnectionFails_ThrowsQueryException()
{
// Arrange
_connectionFactory.CreateLotFinderConnectionAsync(Arg.Any<CancellationToken>())
.ThrowsAsync(new ConnectionException("Test connection error", "LotFinderDB"));
var repository = new LotFinderRepository(_connectionFactory, _logger, _options);
// Act & Assert
var ex = await Should.ThrowAsync<QueryException>(
async () => await repository.GetTableSpecAsync("Item"));
ex.QueryName.ShouldBe("SQL_GET_TABLE_COLUMNS");
}
[Fact]
public async Task PostProcessMisDataAsync_ConnectionFails_ThrowsQueryException()
{
// Arrange
_connectionFactory.CreateLotFinderConnectionAsync(Arg.Any<CancellationToken>())
.ThrowsAsync(new ConnectionException("Test connection error", "LotFinderDB"));
var repository = new LotFinderRepository(_connectionFactory, _logger, _options);
// Act & Assert
var ex = await Should.ThrowAsync<QueryException>(
async () => await repository.PostProcessMisDataAsync());
ex.QueryName.ShouldBe("SQL_POSTPROCESS_MISDATA");
}
#endregion
#region Cancellation Tests
@@ -1,4 +1,4 @@
using JdeScoping.DataAccess.Models.Results;
using JdeScoping.Core.Models.SearchResults;
using Shouldly;
using Xunit;
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,7 @@
{
"providerType": "SqlServer",
"connectionString": "Server=localhost,1434;Database=ScopingTool;User Id=sa;Password=ScopingTool_SA_2024Dev;TrustServerCertificate=true",
"query": "SELECT * FROM dbo.WorkOrderStep_Curr",
"outputPath": "./output/workorderstep-curr-15.pb.zstd",
"compressionLevel": 15
}
@@ -0,0 +1,7 @@
{
"providerType": "SqlServer",
"connectionString": "Server=localhost,1434;Database=ScopingTool;User Id=sa;Password=ScopingTool_SA_2024Dev;TrustServerCertificate=true",
"query": "SELECT * FROM dbo.WorkOrderStep_Curr",
"outputPath": "./output/workorderstep-curr-19.pb.zstd",
"compressionLevel": 19
}
@@ -0,0 +1,7 @@
{
"providerType": "SqlServer",
"connectionString": "Server=localhost,1434;Database=ScopingTool;User Id=sa;Password=ScopingTool_SA_2024Dev;TrustServerCertificate=true",
"query": "SELECT * FROM dbo.WorkOrderStep_Curr",
"outputPath": "./output/workorderstep-curr-22.pb.zstd",
"compressionLevel": 22
}
@@ -0,0 +1,7 @@
{
"providerType": "SqlServer",
"connectionString": "Server=localhost,1434;Database=ScopingTool;User Id=sa;Password=ScopingTool_SA_2024Dev;TrustServerCertificate=true",
"query": "SELECT * FROM dbo.WorkOrderStep_Curr",
"outputPath": "./output/workorderstep-curr-3.pb.zstd",
"compressionLevel": 3
}
@@ -0,0 +1,7 @@
{
"providerType": "SqlServer",
"connectionString": "Server=localhost,1434;Database=ScopingTool;User Id=sa;Password=ScopingTool_SA_2024Dev;TrustServerCertificate=true",
"query": "SELECT * FROM dbo.WorkOrderStep_Curr",
"outputPath": "./output/workorderstep-curr-5.pb.zstd",
"compressionLevel": 5
}
@@ -0,0 +1,7 @@
{
"providerType": "SqlServer",
"connectionString": "Server=localhost,1434;Database=ScopingTool;User Id=sa;Password=ScopingTool_SA_2024Dev;TrustServerCertificate=true",
"query": "SELECT * FROM dbo.WorkOrderStep_Curr",
"outputPath": "./output/workorderstep-curr-7.pb.zstd",
"compressionLevel": 7
}