using Dapr.Client; using GB5Shared.DTO.Framework.Login; using GB5Shared.Resource.GB5Solution; using Microsoft.Extensions.Logging; using MMDAL.CustomCode.StockValuation; using MMDAL.DTO.StockValuation; using Newtonsoft.Json; namespace MMBLL.StockValuation; public class StockValuationBLL : IStockValuationBLL { private readonly IStockValuationDAL _dal; private readonly DaprClient _dapr; private readonly ILogger _logger; public StockValuationBLL( IStockValuationDAL dal, DaprClient dapr, ILogger logger) { _dal = dal; _dapr = dapr; _logger = logger; } public async Task StartBatchRunAsync( StockValuationFilterDTO filter, LoginDTO login, CancellationToken ct) { try { int ouid = filter.OUIDs?.FirstOrDefault() ?? login.WorkOUId; if (await _dal.IsPeriodLockedAsync(ouid, filter.ToDate, login, ct).ConfigureAwait(false)) return MMResource.PeriodAlreadyLocked; bool alreadyActive = await _dal.IsRunActiveAsync( ouid, filter.RunType, filter.FromDate, filter.ToDate, login, ct).ConfigureAwait(false); if (alreadyActive) return MMResource.ValuationRunAlreadyActive; var run = new StockValuationRunDTO { OUID = ouid, RunType = filter.RunType, PeriodFrom = filter.FromDate, PeriodTo = filter.ToDate, Status = 0, // Queued CreatedById = login.UserId, CreatedOn = DateTime.UtcNow }; long runId = await _dal.InsertValuationRunAsync(run, login, ct).ConfigureAwait(false); var jobPayload = new StockValuationJobDTO { StockValuationRunId = runId, Filter = filter, LoginToken = JsonConvert.SerializeObject(login) }; await _dapr.PublishEventAsync( "pubsub", "mm.stockvaluation.batch-requested", jobPayload, ct).ConfigureAwait(false); _logger.LogInformation( "Valuation batch run {RunId} queued for OUID {OUID} period {From}-{To}", runId, ouid, filter.FromDate, filter.ToDate); return MMResource.ValuationRunStarted; } catch (Exception ex) { _logger.LogError(ex, "StartBatchRunAsync failed for OUID {OUID}", login.WorkOUId); return MMResource.ValuationRunFailed; } } public async Task RunBatchAsync( StockValuationFilterDTO filter, long runId, LoginDTO login, CancellationToken ct) { int ouid = filter.OUIDs?.FirstOrDefault() ?? login.WorkOUId; try { _logger.LogInformation( "Batch valuation run {RunId} starting for OUID {OUID} {From}-{To}", runId, ouid, filter.FromDate, filter.ToDate); // Compute weighted average and apply to TSTOCKLEDGER // (uses BATCH_STEP1/2/3 QBs via StockLedgerCostDAL batch path) await _dal.ApplyMakeItemCostAsync(ouid, filter.FromDate, filter.ToDate, login, ct) .ConfigureAwait(false); // Sync TSTOCKPOSITION from final TSTOCKLEDGER values await _dal.SyncStockPositionFromLedgerAsync(ouid, filter.FromDate, filter.ToDate, login, ct) .ConfigureAwait(false); // Take a period-end snapshot so future recosting doesn't need full history await _dal.InsertBalanceSnapshotAsync(ouid, filter.ToDate, login, ct) .ConfigureAwait(false); await _dal.UpdateRunDoneAsync(runId, login, ct).ConfigureAwait(false); await _dapr.PublishEventAsync( "pubsub", "mm.stockvaluation.batch-completed", new { RunId = runId, OUID = ouid, Period = filter.ToDate }, ct).ConfigureAwait(false); _logger.LogInformation("Batch valuation run {RunId} completed", runId); } catch (Exception ex) { _logger.LogError(ex, "Batch valuation run {RunId} failed", runId); await _dal.UpdateRunFailedAsync(runId, ex.Message, login, ct).ConfigureAwait(false); throw; } } public async Task RunPerpetualRerunAsync( StockValuationFilterDTO filter, LoginDTO login, CancellationToken ct) { int ouid = filter.OUIDs?.FirstOrDefault() ?? login.WorkOUId; bool alreadyActive = await _dal.IsRunActiveAsync( ouid, 2, filter.FromDate, filter.ToDate, login, ct).ConfigureAwait(false); if (alreadyActive) { _logger.LogWarning( "Perpetual rerun already active for OUID {OUID} {From}-{To} — skipping", ouid, filter.FromDate, filter.ToDate); return; } var run = new StockValuationRunDTO { OUID = ouid, RunType = 2, // Perpetual-Rerun PeriodFrom = filter.FromDate, PeriodTo = filter.ToDate, Status = 1, // Running StartedAt = DateTime.UtcNow, CreatedById = login.UserId, CreatedOn = DateTime.UtcNow }; long runId = await _dal.InsertValuationRunAsync(run, login, ct).ConfigureAwait(false); try { // Sync TSTOCKPOSITION from reprocessed TSTOCKLEDGER data await _dal.SyncStockPositionFromLedgerAsync(ouid, filter.FromDate, filter.ToDate, login, ct) .ConfigureAwait(false); await _dal.UpdateRunDoneAsync(runId, login, ct).ConfigureAwait(false); _logger.LogInformation( "Perpetual rerun {RunId} completed for OUID {OUID} {From}-{To}", runId, ouid, filter.FromDate, filter.ToDate); } catch (Exception ex) { _logger.LogError(ex, "Perpetual rerun {RunId} failed", runId); await _dal.UpdateRunFailedAsync(runId, ex.Message, login, ct).ConfigureAwait(false); throw; } } public async Task GetValuationRunsAsync( int ouid, DateOnly fromDate, DateOnly toDate, LoginDTO login, CancellationToken ct) { var runs = await _dal.GetValuationRunsAsync(ouid, fromDate, toDate, login, ct) .ConfigureAwait(false); return JsonConvert.SerializeObject(runs); } public async Task CertifyPeriodSnapshotAsync( int ouid, DateOnly periodEndDate, LoginDTO login, CancellationToken ct) { await _dal.CertifySnapshotAsync(ouid, periodEndDate, login, ct).ConfigureAwait(false); _logger.LogInformation( "Period snapshot certified: OUID {OUID} as of {Date}", ouid, periodEndDate); } public async Task GetCostWorkingsAsync( int itemId, int skuId, int storeId, int ouid, DateOnly asOfDate, LoginDTO login, CancellationToken ct) { // Opening balance: latest certified snapshot before asOfDate var snapshot = await _dal.GetLastCertifiedSnapshotAsync(ouid, asOfDate, login, ct) .ConfigureAwait(false); // The StockValuationQB.GET_COST_WORKINGS_WA query fetches TSTOCKLEDGER rows // from snapshot date (or inception) through asOfDate — called directly via DAL var workings = await _dal.GetCostWorkingsAsync( itemId, skuId, storeId, ouid, asOfDate, login, ct).ConfigureAwait(false); var response = new { ItemId = itemId, SKUId = skuId, StoreId = storeId, AsOfDate = asOfDate, OpeningBalance = snapshot is null ? null : new { AsOf = snapshot.SnapshotDate, Qty = snapshot.ClosingQty, Value = snapshot.ClosingValue, Cost = snapshot.ClosingCost, Source = "CertifiedSnapshot" }, Transactions = workings }; return JsonConvert.SerializeObject(response); } public async Task RecoverStuckRunsAsync(LoginDTO login, CancellationToken ct) { await _dal.MarkStuckRunsFailedAsync(login, ct).ConfigureAwait(false); } }