using Dapr.Client; using GB5Shared.DTO.Framework.Login; using GB5Shared.QueryExecutor; using GB5Shared.Resource.Response; using Microsoft.Extensions.Logging; namespace MMBLL.StockCostApport; public class StockCostApportBLL : IStockCostApportBLL { private readonly IQueryExecutor _qe; private readonly DaprClient _dapr; private readonly ILogger _logger; // SQL is inline here because apportionment logic is self-contained and doesn't // belong in the stock valuation QB files (different domain: charge allocation). private const string GET_GRN_ITEMS_FOR_APPORT = @" SELECT sl.ITEMID AS ItemId, sl.SKUID AS SKUId, sl.STOREID AS StoreId, sl.OUID AS OUID, SUM(sl.QUANTITY) AS ReceivedQty, SUM(sl.POSTEDVALUE) AS ItemValue FROM TSTOCKLEDGER sl JOIN MITEM mi ON mi.ITEMID = sl.ITEMID AND mi.ISSTOCKED = 1 WHERE sl.OBJECTTYPEID = @ObjectTypeId AND sl.OBJECTID = @ObjectId AND sl.STOCKPOSTTYPE = 0 GROUP BY sl.ITEMID, sl.SKUID, sl.STOREID, sl.OUID HAVING SUM(sl.QUANTITY) > 0"; private const string APPLY_CHARGE_TO_POSITION = @" UPDATE TSTOCKPOSITION SET AVERAGEVALUE = ROUND(ISNULL(AVERAGEVALUE, 0) + @ChargeShare, 4), AVERAGECOST = CASE WHEN QUANTITY > 0 THEN ROUND((ISNULL(AVERAGEVALUE, 0) + @ChargeShare) / QUANTITY, 4) ELSE AVERAGECOST END WHERE OUID = @OUID AND ITEMID = @ItemId AND SKUID = @SKUId AND STOREID = @StoreId"; public StockCostApportBLL( IQueryExecutor qe, DaprClient dapr, ILogger logger) { _qe = qe; _dapr = dapr; _logger = logger; } public async Task ApportionChargeAsync( int grnObjectTypeId, int grnObjectId, decimal totalChargeAmount, int chargeAccountId, LoginDTO login, CancellationToken ct) { try { // 1. Fetch stocked GRN lines with their received values var grnItems = (await _qe.QueryAsync( login, GET_GRN_ITEMS_FOR_APPORT, new { ObjectTypeId = grnObjectTypeId, ObjectId = grnObjectId }, cancellationToken: ct).ConfigureAwait(false)).ToList(); if (grnItems.Count == 0) return SuccessResponse.SaveSuccess; decimal totalGrnValue = grnItems.Sum(i => (decimal)i.ItemValue); if (totalGrnValue == 0m) return SuccessResponse.SaveSuccess; // 2. Apportion charge proportionally by item value and update TSTOCKPOSITION foreach (var item in grnItems) { decimal share = Math.Round( totalChargeAmount * ((decimal)item.ItemValue / totalGrnValue), 4); await _qe.ExecuteAsync( login, APPLY_CHARGE_TO_POSITION, new { ChargeShare = share, OUID = (int)item.OUID, ItemId = (int)item.ItemId, SKUId = (int)item.SKUId, StoreId = (int)item.StoreId }, cancellationToken: ct).ConfigureAwait(false); } // 3. Publish GL event for the charge account debit await _dapr.PublishEventAsync( "pubsub", "mm.stockvaluation.charge-apportioned", new { GrnObjectTypeId = grnObjectTypeId, GrnObjectId = grnObjectId, TotalChargeAmount = totalChargeAmount, ChargeAccountId = chargeAccountId, OUID = login.WorkOUId }, ct).ConfigureAwait(false); _logger.LogInformation( "Charge {Amount} apportioned across {Count} GRN items for doc {DocId}", totalChargeAmount, grnItems.Count, grnObjectId); return SuccessResponse.SaveSuccess; } catch (Exception ex) { _logger.LogError(ex, "ApportionChargeAsync failed for GRN {DocId}", grnObjectId); throw; } } }