+
+
+
diff --git a/frontend/src/shared/@kbx/ui/contracts.ts b/frontend/src/shared/@kbx/ui/contracts.ts
new file mode 100644
index 00000000..907a7487
--- /dev/null
+++ b/frontend/src/shared/@kbx/ui/contracts.ts
@@ -0,0 +1,4 @@
+/**
+ * Re-export contracts for UI components
+ */
+export * from '../contracts'
diff --git a/frontend/src/shared/@kbx/ui/index.ts b/frontend/src/shared/@kbx/ui/index.ts
new file mode 100644
index 00000000..07d4c984
--- /dev/null
+++ b/frontend/src/shared/@kbx/ui/index.ts
@@ -0,0 +1,50 @@
+/**
+ * @kbx/ui — KBX Foundation v60 UI Components
+ * Phase 1 + Phase 2 + Phase 3.5 + Phase 4: Complete library
+ */
+
+// Core Components (Phase 1)
+export { default as KbxSectionHeader } from './components/KbxSectionHeader.vue'
+export { default as KbxValidationSummary } from './components/KbxValidationSummary.vue'
+
+// Template Components (Phase 1)
+export { default as KbxTransactionTemplate } from './components/KbxTransactionTemplate.vue'
+export { default as KbxMasterTemplate } from './components/KbxMasterTemplate.vue'
+export { default as KbxQueueTemplate } from './components/KbxQueueTemplate.vue'
+export { default as KbxReconcileTemplate } from './components/KbxReconcileTemplate.vue'
+
+// Basic Components (Phase 2)
+export { default as KbxButton } from './components/KbxButton.vue'
+export { default as KbxStatusTag } from './components/KbxStatusTag.vue'
+
+// Form Fields (Phase 2)
+export { default as KbxInput } from './components/KbxInput.vue'
+export { default as KbxSelect } from './components/KbxSelect.vue'
+export { default as KbxDateField } from './components/KbxDateField.vue'
+export { default as KbxNumberField } from './components/KbxNumberField.vue'
+export { default as KbxTextarea } from './components/KbxTextarea.vue'
+export { default as KbxCheckbox } from './components/KbxCheckbox.vue'
+
+// Specialized Fields (Phase 4)
+export { default as KbxMoneyField } from './components/KbxMoneyField.vue'
+export { default as KbxQuantityField } from './components/KbxQuantityField.vue'
+export { default as KbxRadio } from './components/KbxRadio.vue'
+
+// Form Layout (Phase 4)
+export { default as KbxFormGrid } from './components/KbxFormGrid.vue'
+export { default as KbxFormSection } from './components/KbxFormSection.vue'
+
+// Composite Components (Phase 2)
+export { default as KbxDataGrid } from './components/KbxDataGrid.vue'
+export { default as KbxDialog } from './components/KbxDialog.vue'
+export { default as KbxDrawer } from './components/KbxDrawer.vue'
+export { default as KbxTabs } from './components/KbxTabs.vue'
+export { default as KbxLookup } from './components/KbxLookup.vue'
+
+// Wrapper Components (Phase 3.5)
+export { default as KbxScreenFrame } from './components/KbxScreenFrame.vue'
+export { default as KbxTemplateStateBoundary } from './components/KbxTemplateStateBoundary.vue'
+export { default as KbxSummaryBar } from './components/KbxSummaryBar.vue'
+
+// Re-export contracts for component usage
+export * from './contracts'
diff --git a/src/KArtSell.Host/Jobs/ShadowRunJob.cs b/src/KArtSell.Host/Jobs/ShadowRunJob.cs
index 7f03e088..6e68995c 100644
--- a/src/KArtSell.Host/Jobs/ShadowRunJob.cs
+++ b/src/KArtSell.Host/Jobs/ShadowRunJob.cs
@@ -272,7 +272,8 @@ public sealed class ShadowRunJob(
foreach (var order in orders.Where(o => o.FilledPrice.HasValue))
{
- var cost = order.Quantity * order.FilledPrice.Value;
+ var filledPrice = order.FilledPrice!.Value;
+ var cost = order.Quantity * filledPrice;
// Get fee schedule for this order's date
var fee = feeSchedule.FirstOrDefault(f => f.EffectiveDate <= order.FilledDate);
diff --git a/src/KArtSell.Modules.ModelOperations/ShadowRun/Services/KrxDataService.cs b/src/KArtSell.Modules.ModelOperations/ShadowRun/Services/KrxDataService.cs
index 480b1dbb..130473e1 100644
--- a/src/KArtSell.Modules.ModelOperations/ShadowRun/Services/KrxDataService.cs
+++ b/src/KArtSell.Modules.ModelOperations/ShadowRun/Services/KrxDataService.cs
@@ -1,6 +1,7 @@
using System.Net;
using System.Text.Json;
using Dapper;
+using KArtSell.BuildingBlocks.Time;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
using Npgsql;
@@ -18,6 +19,7 @@ public sealed class KrxDataService : IKrxDataService
private readonly IMemoryCache _cache;
private readonly ILogger _logger;
private readonly NpgsqlDataSource _dataSource;
+ private readonly IClock _clock;
private const int CacheDurationMinutes = 1440; // 24 hours
private const int RecentDaysWindow = 7; // Last 7 days: always refresh (mutable data)
@@ -55,11 +57,13 @@ public sealed class KrxDataService : IKrxDataService
HttpClient httpClient,
IMemoryCache cache,
ILogger logger,
+ IClock clock,
NpgsqlDataSource? dataSource = null)
{
_httpClient = httpClient;
_cache = cache;
_logger = logger;
+ _clock = clock;
_dataSource = dataSource!;
}
@@ -117,7 +121,7 @@ public sealed class KrxDataService : IKrxDataService
LogFetchingOhlcv(_logger, ticker, startDate, endDate, null);
// Incremental fetching: Skip old, immutable data that was already collected
- var today = DateOnly.FromDateTime(DateTime.UtcNow.Date);
+ var today = DateOnly.FromDateTime(_clock.UtcNow.Date);
var lastSuccessfulImport = await GetLastSuccessfulImportDateAsync(cancellationToken);
// Strategy: Only fetch data from 7 days ago onwards (last 7 days always fresh)
diff --git a/tests/KArtSell.Integration.Tests/KrxDataServiceTests.cs b/tests/KArtSell.Integration.Tests/KrxDataServiceTests.cs
index f42c0a52..13378b79 100644
--- a/tests/KArtSell.Integration.Tests/KrxDataServiceTests.cs
+++ b/tests/KArtSell.Integration.Tests/KrxDataServiceTests.cs
@@ -1,4 +1,5 @@
using Xunit;
+using KArtSell.BuildingBlocks.Time;
using KArtSell.Modules.ModelOperations.ShadowRun.Services;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
@@ -36,7 +37,7 @@ public sealed class KrxDataServiceTests : IAsyncLifetime
public async Task GetDailyOhlcvAsync_ReturnsBarsForTickerAndDateRange()
{
// Arrange
- var service = new KrxDataService(_httpClient, _cache, _logger);
+ var service = new KrxDataService(_httpClient, _cache, _logger, new SystemClock());
var ticker = "005930"; // Samsung
var startDate = new DateOnly(2024, 1, 2);
var endDate = new DateOnly(2024, 1, 5);
@@ -60,7 +61,7 @@ public sealed class KrxDataServiceTests : IAsyncLifetime
public async Task GetDailyOhlcvAsync_CacheHit_ReturnsCachedData()
{
// Arrange
- var service = new KrxDataService(_httpClient, _cache, _logger);
+ var service = new KrxDataService(_httpClient, _cache, _logger, new SystemClock());
var ticker = "005930";
var startDate = new DateOnly(2024, 1, 2);
var endDate = new DateOnly(2024, 1, 5);
@@ -85,7 +86,7 @@ public sealed class KrxDataServiceTests : IAsyncLifetime
public async Task GetFeeScheduleAsync_ReturnsFeeEntries()
{
// Arrange
- var service = new KrxDataService(_httpClient, _cache, _logger);
+ var service = new KrxDataService(_httpClient, _cache, _logger, new SystemClock());
var startDate = new DateOnly(2024, 1, 2);
var endDate = new DateOnly(2024, 1, 31);