student 8 timmar sedan
förälder
incheckning
9e47aa6f34

+ 11 - 11
KittyFallWs/KittyFallWs/src/com/vas/webservices/KittyFallWs.java

@@ -1989,15 +1989,15 @@ public class KittyFallWs extends WebserviceAbstract {
                 } else {
                     msisdn = formatMsisdn(msisdn);
                     int moneyCharge = Common.ADD_SPIN_FEE;
-                    String fee = "0";
-                    if (fee != null && fee.length() > 0) {
-                        moneyCharge = Integer.parseInt(fee);
-                    }
-                    if (moneyCharge < 0) {
-                        response.setErrorCode(Common.ErrorCode.NOT_ENOUGH_MONEY);
-                        response.setContent("Charge fail");
-                        return response;
-                    }
+//                    String fee = "0";
+//                    if (fee != null && fee.length() > 0) {
+//                        moneyCharge = Integer.parseInt(fee);
+//                    }
+//                    if (moneyCharge < 0) {
+//                        response.setErrorCode(Common.ErrorCode.NOT_ENOUGH_MONEY);
+//                        response.setContent("Charge fail");
+//                        return response;
+//                    }
                     ProductInfo productInfo = getProductByName(packageName);
                     if (productInfo == null) {
                         response.setErrorCode(Common.ErrorCode.QUERY_ERROR);
@@ -2007,7 +2007,7 @@ public class KittyFallWs extends WebserviceAbstract {
                     String REQ = reqDf.format(new Date()) + "1" + msisdn.substring(Common.COUNTRY_CODE.length());
 
                     // charge
-                    String resultPaid = ws.chargeFeeOtp(msisdn, moneyCharge, REQ);
+                    String resultPaid = ws.chargeFeeOtp(msisdn, (int) productInfo.getFee(), REQ);
 //                    String resultPaid = "0";
                     boolean registered = false;
                     List<RegisterInfo> listRegInday = null;
@@ -2056,7 +2056,7 @@ public class KittyFallWs extends WebserviceAbstract {
                         
                         // Update Spin
                         db.addTurnSpin(msisdn,productInfo.getNumberSpin());
-                        logger.info("Buy turn success success " + msisdn + ", fee " + fee);
+                        logger.info("Buy turn success success " + msisdn + ", fee " + (int) productInfo.getFee());
                         
                         LuckySprin mLuckySprin = db.igetLuckySprin(msisdn);
                         if(mLuckySprin != null){

+ 110 - 0
Kitty_Fall/Kitty_Fall/Kitty_Fall.Apis/Business/Game/GameBusinessImpl.cs

@@ -19,6 +19,11 @@ namespace Kitty_Fall.Apis.Business.Game
         private const int DbMaxGameLevel = 50;
         private const long MaxAttemptScore = int.MaxValue;
         private const long MaxTotalScore = 999999999999;
+        private const decimal DataToMmkRate = 0.665m;
+        private const decimal LoyaltyToMmkRate = 1.33m;
+        private static readonly SemaphoreSlim RewardBudgetCacheLock = new(1, 1);
+        private static int cachedRewardBudgetDateKey;
+        private static decimal cachedRewardBudget;
 
         private readonly ModelContext dbContext;
         private readonly IConfiguration configuration;
@@ -832,6 +837,15 @@ namespace Kitty_Fall.Apis.Business.Game
                         .ThenByDescending(x => x.Weight)
                         .ThenBy(x => x.RewardOptionId)
                         .ToListAsync();
+
+                    var rewardBudget = await GetDailyExternalRewardBudgetAsync(now);
+                    var usedRewardBudget = await GetDailyExternalRewardCostAsync(now);
+                    rewardOptions = rewardOptions
+                        .Where(x => !IsExternalReward(x.RewardType)
+                            || usedRewardBudget + ConvertRewardToMmk(x.RewardType, x.RewardValue) <= rewardBudget)
+                        .ToList();
+
+                    log.Info($"Milestone reward budget checked. date={now:yyyy-MM-dd}, budgetMmk={rewardBudget}, usedMmk={usedRewardBudget}, eligibleOptions={rewardOptions.Count}");
                     rewardOption = SelectRewardOption(rewardOptions);
                     if (rewardOption != null)
                     {
@@ -1065,6 +1079,102 @@ namespace Kitty_Fall.Apis.Business.Game
             return positiveWeightOptions.FirstOrDefault(x => x.IsDefault == true) ?? positiveWeightOptions[0];
         }
 
+        private async Task<decimal> GetDailyExternalRewardBudgetAsync(DateTime now)
+        {
+            var budgetDate = now.Date;
+            var budgetDateKey = budgetDate.Year * 10000 + budgetDate.Month * 100 + budgetDate.Day;
+            if (Volatile.Read(ref cachedRewardBudgetDateKey) == budgetDateKey)
+            {
+                return cachedRewardBudget;
+            }
+
+            await RewardBudgetCacheLock.WaitAsync();
+            try
+            {
+                // Double-check sau khi lay lock: request khac co the da nap cache trong luc cho.
+                if (cachedRewardBudgetDateKey == budgetDateKey)
+                {
+                    return cachedRewardBudget;
+                }
+
+                var configuredRevenueText = GetParameter("REVENUE");
+                if (string.IsNullOrWhiteSpace(configuredRevenueText))
+                {
+                    configuredRevenueText = GetParameter("REVENUE_MIN");
+                }
+
+                var configuredRevenue = decimal.TryParse(
+                    configuredRevenueText,
+                    NumberStyles.Number,
+                    CultureInfo.InvariantCulture,
+                    out var parsedRevenue)
+                    ? Math.Max(0, parsedRevenue)
+                    : 0;
+
+                var previousDay = budgetDate.AddDays(-1);
+                var previousDayRevenue = await dbContext.ReportByTimes.AsNoTracking()
+                    .Where(x => x.Type == CommonConstants.DAY_REPORT
+                        && x.TimeReport >= previousDay
+                        && x.TimeReport < budgetDate)
+                    .SumAsync(x => x.Revenue ?? 0);
+
+                var previousDayRevenueLimit = Math.Max(0, previousDayRevenue) * 0.10m;
+                cachedRewardBudget = Math.Max(previousDayRevenueLimit, configuredRevenue);
+                Volatile.Write(ref cachedRewardBudgetDateKey, budgetDateKey);
+
+                log.Info($"Daily reward budget cache loaded. date={budgetDate:yyyy-MM-dd}, previousDayRevenue={previousDayRevenue}, configuredRevenue={configuredRevenue}, budgetMmk={cachedRewardBudget}");
+                return cachedRewardBudget;
+            }
+            finally
+            {
+                RewardBudgetCacheLock.Release();
+            }
+        }
+
+        private async Task<decimal> GetDailyExternalRewardCostAsync(DateTime now)
+        {
+            var start = now.Date;
+            var end = start.AddDays(1);
+            var totals = await dbContext.GameLevelAttempts.AsNoTracking()
+                .Where(x => x.Status == 2
+                    && x.IsValid != false
+                    && x.RewardValue != null
+                    && x.RewardValue > 0
+                    && (x.RewardType == "DATA" || x.RewardType == "LOYALTY")
+                    && (x.SubmitTime ?? x.CreatedTime) >= start
+                    && (x.SubmitTime ?? x.CreatedTime) < end)
+                .GroupBy(x => x.RewardType)
+                .Select(x => new
+                {
+                    RewardType = x.Key,
+                    TotalValue = x.Sum(y => y.RewardValue ?? 0)
+                })
+                .ToListAsync();
+
+            return totals.Sum(x => ConvertRewardToMmk(x.RewardType, x.TotalValue));
+        }
+
+        private static decimal ConvertRewardToMmk(string? rewardType, decimal rewardValue)
+        {
+            if (string.Equals(rewardType, "DATA", StringComparison.OrdinalIgnoreCase))
+            {
+                return rewardValue * DataToMmkRate;
+            }
+
+            if (string.Equals(rewardType, "LOYALTY", StringComparison.OrdinalIgnoreCase))
+            {
+                return rewardValue * LoyaltyToMmkRate;
+            }
+
+            return 0;
+        }
+
+        private static bool IsExternalReward(string? rewardType)
+        {
+            return string.Equals(rewardType, "DATA", StringComparison.OrdinalIgnoreCase)
+                || string.Equals(rewardType, "LOYALTY", StringComparison.OrdinalIgnoreCase);
+        }
+
         private async Task<GameMode?> GetGameModeAsync(string? gameType)
         {
             var modeCode = (gameType ?? "EASY").Trim().ToUpperInvariant();

+ 1 - 0
Kitty_Fall/Kitty_Fall/Kitty_Fall.Apis/appsettings.json

@@ -8,6 +8,7 @@
   "wsUrl": "http://127.0.0.1:8199/KittyFallWs?wsdl",
   "wsUser": "ws_KittyFall",
   "wsPass": "KittyFall@123A",
+  "REVENUE_MIN": "200000",
   "Logging": {
     "LogLevel": {
       "Default": "Information",

+ 121 - 8
Kitty_Fall/Kitty_Fall/Kitty_Fall.Woker/Services/GameRewardPayoutWorker.cs

@@ -14,8 +14,13 @@ namespace Kitty_Fall.Woker.Services
         private const byte StatusProcessing = 1;
         private const byte StatusSuccess = 2;
         private const byte StatusFailed = 3;
+        private const decimal DataToMmkRate = 0.665m;
+        private const decimal LoyaltyToMmkRate = 1.33m;
 
         private static readonly log4net.ILog log = log4net.LogManager.GetLogger(typeof(GameRewardPayoutWorker));
+        private static readonly SemaphoreSlim RewardBudgetCacheLock = new(1, 1);
+        private static int cachedRewardBudgetDateKey;
+        private static decimal cachedRewardBudget;
 
         private readonly IServiceScopeFactory scopeFactory;
         private readonly IConfiguration configuration;
@@ -95,13 +100,6 @@ namespace Kitty_Fall.Woker.Services
 
             try
             {
-                var limitResult = await ApplyLimitBeforePayoutAsync(dbContext, rewardLog, now, cancellationToken);
-                if (!limitResult.CanPayout)
-                {
-                    await dbContext.SaveChangesAsync(cancellationToken);
-                    return;
-                }
-
                 if (!IsExternalReward(rewardLog.RewardType))
                 {
                     // Fallback khong can goi WS thi danh dau xong de khong retry vo nghia.
@@ -114,6 +112,25 @@ namespace Kitty_Fall.Woker.Services
                     return;
                 }
 
+                var rewardBudget = await GetDailyExternalRewardBudgetAsync(dbContext, now, cancellationToken);
+                // PROCESSING da duoc tinh la giu cho ngan sach. Claim la UPDATE co dieu kien
+                // va commit truoc khi vao day, nen cac worker/instance sau se thay khoan nay.
+                var reservedRewardCost = await GetDailyReservedExternalRewardCostAsync(dbContext, now, cancellationToken);
+                var currentRewardCost = ConvertRewardToMmk(rewardLog.RewardType, rewardLog.RewardValue);
+                if (reservedRewardCost > rewardBudget)
+                {
+                    rewardLog.Status = StatusFailed;
+                    rewardLog.ResponseTime = now;
+                    rewardLog.ResponseCode = "DAILY_REWARD_BUDGET_EXCEEDED";
+                    rewardLog.ResponseMessage = Truncate(
+                        $"Daily reward budget exceeded. budgetMmk={rewardBudget}, reservedMmk={reservedRewardCost}, rewardMmk={currentRewardCost}",
+                        500);
+                    rewardLog.UpdatedTime = now;
+                    await dbContext.SaveChangesAsync(cancellationToken);
+                    log.Warn($"Reward payout blocked by daily budget. rewardLogId={rewardLog.RewardLogId}, budgetMmk={rewardBudget}, reservedMmk={reservedRewardCost}, rewardMmk={currentRewardCost}");
+                    return;
+                }
+
                 var payoutPackageName = GetPackageName(rewardLog);
                 var addPointRes = DotnetLib.Mytel.AddingPointLogic.AddingPointHandler(
                     log,
@@ -157,7 +174,6 @@ namespace Kitty_Fall.Woker.Services
                         AccountId = null,
                         Status = CommonConstants.UNCHARGE_STATUS
                     });
-                    await UpdateLimitUsageAsync(dbContext, rewardLog, now, cancellationToken);
                     await dbContext.SaveChangesAsync(cancellationToken);
                     await SendSuccessMtAsync(dbContext, rewardLog, cancellationToken);
                     return;
@@ -388,6 +404,103 @@ namespace Kitty_Fall.Woker.Services
                 || string.Equals(rewardType, "LOYALTY", StringComparison.OrdinalIgnoreCase);
         }
 
+        private async Task<decimal> GetDailyExternalRewardBudgetAsync(
+            ModelContext dbContext,
+            DateTime now,
+            CancellationToken cancellationToken)
+        {
+            var budgetDate = now.Date;
+            var budgetDateKey = budgetDate.Year * 10000 + budgetDate.Month * 100 + budgetDate.Day;
+            if (Volatile.Read(ref cachedRewardBudgetDateKey) == budgetDateKey)
+            {
+                return cachedRewardBudget;
+            }
+
+            await RewardBudgetCacheLock.WaitAsync(cancellationToken);
+            try
+            {
+                if (cachedRewardBudgetDateKey == budgetDateKey)
+                {
+                    return cachedRewardBudget;
+                }
+
+                var configuredRevenueText = configuration["REVENUE"];
+                if (string.IsNullOrWhiteSpace(configuredRevenueText))
+                {
+                    configuredRevenueText = configuration["REVENUE_MIN"];
+                }
+
+                var configuredRevenue = decimal.TryParse(
+                    configuredRevenueText,
+                    NumberStyles.Number,
+                    CultureInfo.InvariantCulture,
+                    out var parsedRevenue)
+                    ? Math.Max(0, parsedRevenue)
+                    : 0;
+
+                var previousDay = budgetDate.AddDays(-1);
+                var previousDayRevenue = await dbContext.ReportByTimes.AsNoTracking()
+                    .Where(x => x.Type == CommonConstants.DAY_REPORT
+                        && x.TimeReport >= previousDay
+                        && x.TimeReport < budgetDate)
+                    .SumAsync(x => x.Revenue ?? 0, cancellationToken);
+
+                var previousDayRevenueLimit = Math.Max(0, previousDayRevenue) * 0.10m;
+                cachedRewardBudget = Math.Max(previousDayRevenueLimit, configuredRevenue);
+                Volatile.Write(ref cachedRewardBudgetDateKey, budgetDateKey);
+
+                log.Info($"Worker daily reward budget cache loaded. date={budgetDate:yyyy-MM-dd}, previousDayRevenue={previousDayRevenue}, configuredRevenue={configuredRevenue}, budgetMmk={cachedRewardBudget}");
+                return cachedRewardBudget;
+            }
+            finally
+            {
+                RewardBudgetCacheLock.Release();
+            }
+        }
+
+        private static async Task<decimal> GetDailyReservedExternalRewardCostAsync(
+            ModelContext dbContext,
+            DateTime now,
+            CancellationToken cancellationToken)
+        {
+            var start = now.Date;
+            var end = start.AddDays(1);
+            var totals = await dbContext.GameRewardLogs.AsNoTracking()
+                .Where(x => (x.Status == StatusSuccess || x.Status == StatusProcessing)
+                    && x.RewardValue > 0
+                    && (x.RewardType == "DATA" || x.RewardType == "LOYALTY")
+                    && ((x.Status == StatusSuccess
+                            && (x.ResponseTime ?? x.CreatedTime) >= start
+                            && (x.ResponseTime ?? x.CreatedTime) < end)
+                        || (x.Status == StatusProcessing
+                            && (x.RequestTime ?? x.CreatedTime) >= start
+                            && (x.RequestTime ?? x.CreatedTime) < end)))
+                .GroupBy(x => x.RewardType)
+                .Select(x => new
+                {
+                    RewardType = x.Key,
+                    TotalValue = x.Sum(y => y.RewardValue)
+                })
+                .ToListAsync(cancellationToken);
+
+            return totals.Sum(x => ConvertRewardToMmk(x.RewardType, x.TotalValue));
+        }
+
+        private static decimal ConvertRewardToMmk(string? rewardType, decimal rewardValue)
+        {
+            if (string.Equals(rewardType, "DATA", StringComparison.OrdinalIgnoreCase))
+            {
+                return rewardValue * DataToMmkRate;
+            }
+
+            if (string.Equals(rewardType, "LOYALTY", StringComparison.OrdinalIgnoreCase))
+            {
+                return rewardValue * LoyaltyToMmkRate;
+            }
+
+            return 0;
+        }
+
         private string GetPackageName(GameRewardLog rewardLog)
         {
             if (string.Equals(rewardLog.RewardType, "DATA", StringComparison.OrdinalIgnoreCase))

+ 1 - 0
Kitty_Fall/Kitty_Fall/Kitty_Fall.Woker/appsettings.json

@@ -12,6 +12,7 @@
   // "UrlAddData": "http://10.201.5.123:9350/vas-gw/api/v1/add-prize",
   "UrlAddData": "http://127.0.0.1:9350/vas-gw/api/v1/add-prize",
   "Hmac256KeyAddData": "xxxxasaxxx",
+  "REVENUE_MIN": "200000",
 
   "RewardWorker": {
     // Worker chi goi moi reward NEW mot lan; loi/timeout/PROCESSING de admin doi soat, khong auto retry.