using System.Text.Json; using FLSDAL.CustomCode.QuickRating; using FLSDAL.DTO.QuickRating; using GB5Shared.DaprCache; using GB5Shared.DTO.Framework.Login; using GB5Shared.GenerateAutoNumber; using GB5Shared.Resource.Response; using GB5Shared.Telemetry; using Microsoft.Extensions.Logging; namespace FLSBLL.QuickRating; public class QuickRatingBLL : IQuickRatingBLL { private readonly IQuickRatingDAL _dal; private readonly AutoNumber _autoNumber; private readonly KeyInvalidate _keyInvalidate; private readonly ILogger _logger; public QuickRatingBLL( IQuickRatingDAL dal, AutoNumber autoNumber, KeyInvalidate keyInvalidate, ILogger logger) { _dal = dal; _autoNumber = autoNumber; _keyInvalidate = keyInvalidate; _logger = logger; } public async Task GetWidgetDataAsync(GetQuickRatingConfigRequest req, LoginDTO login, CancellationToken ct) { try { GB5Trace.Step("get-quickrating-widget", new { req.ObjectTypeId, req.ObjectId }); var config = await _dal.GetConfigAsync(req.ObjectTypeId, req.BizTransactionTypeId, login, ct).ConfigureAwait(false); if (config is null) return "{}"; var summary = await _dal.GetSummaryAsync(req.ObjectTypeId, req.ObjectId, login, ct).ConfigureAwait(false); var summaryDto = BuildSummaryDto(summary); var existingRating = await _dal.GetUserRatingAsync( req.ObjectTypeId, req.ObjectId, req.SubmitterId, login, ct).ConfigureAwait(false); var widgetDto = new QuickRatingWidgetDto( QuickRatingConfigId : config.QuickRatingConfigId, ScaleType : config.ScaleType, ScaleMin : config.ScaleMin, ScaleMax : config.ScaleMax, RatingLabelLow : config.RatingLabelLow, RatingLabelHigh : config.RatingLabelHigh, AllowComment : config.AllowComment, CommentMaxLength : config.CommentMaxLength, ThankYouLabel : config.ThankYouLabel, ShowSummaryAfterSubmit : config.ShowSummaryAfterSubmit, DisplayMode : config.DisplayMode, IdentityMode : config.IdentityMode, WidgetLabel : config.ConfigName, CurrentSummary : summaryDto, ExistingRating : existingRating?.Rating, ExistingComment : existingRating?.CommentText); return JsonSerializer.Serialize(widgetDto); } catch (Exception ex) { GB5Trace.MarkFailed("get-quickrating-widget-failed", ex); _logger.LogError(ex, "GetWidgetData failed for object {TypeId}/{Id}", req.ObjectTypeId, req.ObjectId); throw; } } public async Task SubmitRatingAsync(SubmitQuickRatingRequest req, LoginDTO login, CancellationToken ct) { try { GB5Trace.Step("validate-quickrating", new { req.ObjectTypeId, req.ObjectId, req.RatingValue }); // Load config for range validation var config = await _dal.GetConfigAsync(req.ObjectTypeId, req.BizTransactionTypeId, login, ct).ConfigureAwait(false); if (config is null) return "Quick rating not configured for this object type."; if (req.RatingValue < config.ScaleMin || req.RatingValue > config.ScaleMax) return $"Rating must be between {config.ScaleMin} and {config.ScaleMax}."; int ratingId; // If ALLOWMULTIRATING=0, check for existing rating from this submitter if (config.AllowMultiRating == 0) { var existing = await _dal.GetUserRatingAsync( req.ObjectTypeId, req.ObjectId, req.SubmitterId, login, ct).ConfigureAwait(false); if (existing is not null) { // Amend existing rating instead of inserting a new row GB5Trace.Step("amend-quickrating", new { existing.QuickRatingId }); await _dal.AmendRatingAsync(existing.QuickRatingId, req.RatingValue, req.Comment, login, ct) .ConfigureAwait(false); ratingId = existing.QuickRatingId; } else { ratingId = await InsertNewRatingAsync(config, req, login, ct).ConfigureAwait(false); } } else { ratingId = await InsertNewRatingAsync(config, req, login, ct).ConfigureAwait(false); } var summaryAutoNum = await _autoNumber.GetNumberAsync(1, "QUICKRATINGSUMMARY", login).ConfigureAwait(false); var updatedSummary = await _dal.UpdateSummaryAsync( config.QuickRatingConfigId, req.ObjectTypeId, req.ObjectId, summaryAutoNum.StartNumber, login, ct).ConfigureAwait(false); await _keyInvalidate.InvalidateCache($"QuickRating:{login.ClientId}").ConfigureAwait(false); var result = new QuickRatingSubmitResultDto( QuickRatingId : ratingId, RatingValue : req.RatingValue, UpdatedSummary: BuildSummaryDto(updatedSummary) ?? new QuickRatingSummaryDto(0, null, null, null, null, null, null, null, null, 0)); return JsonSerializer.Serialize(result); } catch (Exception ex) { GB5Trace.MarkFailed("save-quickrating-failed", ex); _logger.LogError(ex, "SubmitRating failed for object {TypeId}/{Id}", req.ObjectTypeId, req.ObjectId); throw; } } private async Task InsertNewRatingAsync(QuickRatingConfigRow config, SubmitQuickRatingRequest req, LoginDTO login, CancellationToken ct) { GB5Trace.Step("save-quickrating", new { req.ObjectTypeId, req.ObjectId }); var autoNum = await _autoNumber.GetNumberAsync(1, "QUICKRATING", login).ConfigureAwait(false); int quickRatingId = autoNum.StartNumber; return await _dal.SaveRatingAsync( quickRatingId, config.QuickRatingConfigId, req.ObjectTypeId, req.ObjectId, req.RatingValue, req.Comment, req.SubmitterType, req.SubmitterId, login, ct).ConfigureAwait(false); } public async Task GetEntityRatingsAsync(GetEntityRatingsRequest req, LoginDTO login, CancellationToken ct) { try { GB5Trace.Step("get-quickrating-entity", new { req.ObjectTypeId, req.ObjectId, req.PageNo }); var rows = await _dal.GetRatingsPagedAsync(req.ObjectTypeId, req.ObjectId, req.PageNo, req.PageSize, login, ct) .ConfigureAwait(false); var total = await _dal.GetRatingsCountAsync(req.ObjectTypeId, req.ObjectId, login, ct) .ConfigureAwait(false); return JsonSerializer.Serialize(new { TotalCount = total, PageNo = req.PageNo, PageSize = req.PageSize, Data = rows }); } catch (Exception ex) { GB5Trace.MarkFailed("get-quickrating-entity-failed", ex); _logger.LogError(ex, "GetEntityRatings failed for object {TypeId}/{Id}", req.ObjectTypeId, req.ObjectId); throw; } } public async Task GetTrendAsync(int objectTypeId, int objectId, LoginDTO login, CancellationToken ct) { try { GB5Trace.Step("get-quickrating-trend", new { objectTypeId, objectId }); var trend = await _dal.GetTrendAsync(objectTypeId, objectId, login, ct).ConfigureAwait(false); return JsonSerializer.Serialize(trend); } catch (Exception ex) { GB5Trace.MarkFailed("get-quickrating-trend-failed", ex); _logger.LogError(ex, "GetTrend failed for object {TypeId}/{Id}", objectTypeId, objectId); throw; } } public async Task SaveConfigAsync(SaveQuickRatingConfigRequest req, LoginDTO login, CancellationToken ct) { try { int quickRatingConfigId = req.QuickRatingConfigId; // PK convention: QuickRatingConfigId == 0 → insert (new AutoNumber); non-zero → update existing row. if (quickRatingConfigId == 0) { bool duplicate = await _dal.ConfigExistsForObjectAsync(req.ObjectTypeId, req.BizTransactionTypeId, 0, login, ct).ConfigureAwait(false); if (duplicate) return $"A Quick Rating config already exists for ObjectType {req.ObjectTypeId} / BizType {req.BizTransactionTypeId}."; var autoNum = await _autoNumber.GetNumberAsync(1, "QUICKRATINGCONFIG", login).ConfigureAwait(false); quickRatingConfigId = autoNum.StartNumber; GB5Trace.Step("save-quickratingconfig", new { req.ObjectTypeId, req.BizTransactionTypeId }); await _dal.InsertConfigAsync(quickRatingConfigId, req, login, ct).ConfigureAwait(false); } else { bool duplicate = await _dal.ConfigExistsForObjectAsync(req.ObjectTypeId, req.BizTransactionTypeId, quickRatingConfigId, login, ct).ConfigureAwait(false); if (duplicate) return $"A Quick Rating config already exists for ObjectType {req.ObjectTypeId} / BizType {req.BizTransactionTypeId}."; GB5Trace.Step("update-quickratingconfig", new { quickRatingConfigId }); bool updated = await _dal.UpdateConfigAsync(quickRatingConfigId, req, login, ct).ConfigureAwait(false); if (!updated) return $"Quick Rating config {quickRatingConfigId} not found."; } await _keyInvalidate.InvalidateCache($"QuickRatingWidget:{login.ClientId}").ConfigureAwait(false); return $"{SuccessResponse.SaveSuccessMessage} {quickRatingConfigId}"; } catch (Exception ex) { GB5Trace.MarkFailed("save-quickratingconfig-failed", ex); _logger.LogError(ex, "SaveConfig failed for objectType {TypeId}", req.ObjectTypeId); throw; } } public async Task GetConfigByIdAsync(int quickRatingConfigId, LoginDTO login, CancellationToken ct) { try { GB5Trace.Step("get-quickratingconfig-byid", new { quickRatingConfigId }); return await _dal.GetConfigByIdAsync(quickRatingConfigId, login, ct).ConfigureAwait(false); } catch (Exception ex) { GB5Trace.MarkFailed("get-quickratingconfig-byid-failed", ex); _logger.LogError(ex, "GetConfigById failed for id {Id}", quickRatingConfigId); throw; } } public async Task> GetSelectListQuickRatingConfig( int firstNumber, int maxResult, LoginDTO login) { try { if (firstNumber <= 0 && maxResult <= 0) { firstNumber = -1; maxResult = -1; } return await _dal.GetSelectListQuickRatingConfig(firstNumber, maxResult, login).ConfigureAwait(false); } catch (Exception ex) { GB5Trace.MarkFailed("get-selectlist-quickratingconfig-failed", ex); _logger.LogError(ex, "GetSelectListQuickRatingConfig failed"); throw; } } public async Task DeleteConfigAsync(int quickRatingConfigId, LoginDTO login, CancellationToken ct) { try { GB5Trace.Step("delete-quickratingconfig", new { quickRatingConfigId }); bool deleted = await _dal.DeleteConfigAsync(quickRatingConfigId, login, ct).ConfigureAwait(false); if (!deleted) return $"Quick Rating config {quickRatingConfigId} not found."; await _keyInvalidate.InvalidateCache($"QuickRatingWidget:{login.ClientId}").ConfigureAwait(false); return SuccessResponse.DeleteSuccessMessage; } catch (Exception ex) { GB5Trace.MarkFailed("delete-quickratingconfig-failed", ex); _logger.LogError(ex, "DeleteConfig failed for id {Id}", quickRatingConfigId); throw; } } private static QuickRatingSummaryDto? BuildSummaryDto(QuickRatingSummaryRow? summary) { if (summary is null) return null; Dictionary? dist = null; if (!string.IsNullOrWhiteSpace(summary.RatingDistJson)) { try { dist = JsonSerializer.Deserialize>(summary.RatingDistJson); } catch { dist = null; } } return new QuickRatingSummaryDto( TotalRatings : summary.RatingCount, AvgRating : summary.AvgRating, MinRating : summary.MinRating, MaxRating : summary.MaxRating, RatingDist : dist, NpsPromoters : summary.NpsPromoters, NpsPassives : summary.NpsPassives, NpsDetractors: summary.NpsDetractors, NetScore : summary.NpsScore, CommentCount : 0); } }