student vor 1 Monat
Ursprung
Commit
84fa08037d

BIN
API NATCOM LOTTERY.docx


+ 87 - 0
website/Areas/Millions/Controllers/HomeController.cs

@@ -23,6 +23,9 @@ namespace LotteryWebApp.Areas.Millions.Controllers
     public class HomeController : BaseController
     {
         private static readonly log4net.ILog log = log4net.LogManager.GetLogger(typeof(Program));
+        private const string FreeTicketAutoCheckSessionKey = "Millions.FreeTicket.AutoChecked";
+        private const string FreeTicketPromotionCodeSessionKey = "Millions.FreeTicket.PromotionCode";
+        private const string FreeTicketTransactionSessionKey = "Millions.FreeTicket.TransactionId";
 
         IConfiguration configuration;
         private readonly IWebHostEnvironment webHostEnvironment;
@@ -938,6 +941,63 @@ namespace LotteryWebApp.Areas.Millions.Controllers
             return model;
         }
 
+        [HttpPost]
+        [ValidateAntiForgeryToken]
+        public IActionResult GetFreeTicket(string type)
+        {
+            try
+            {
+                var token = HttpContext.Session.GetComplexData<string>("token");
+                var msisdn = HttpContext.Session.GetComplexData<string>("msisdn");
+                log.Info($"[Millions.GetFreeTicket] START - msisdn={msisdn}");
+
+                if (string.IsNullOrEmpty(token) || string.IsNullOrEmpty(msisdn))
+                {
+                    log.Info($"[Millions.GetFreeTicket] Session expired - msisdn={msisdn}");
+                    return Json(new { responseCode = Code.SESSION_EXPIRED, responseMessage = "Session expired" });
+                }
+
+                if (type == "1")
+                {
+                    if (HttpContext.Session.GetString(FreeTicketAutoCheckSessionKey) == "1")
+                    {
+                        log.Info($"[Millions.GetFreeTicket] Auto-check skipped for current session - msisdn={msisdn}");
+                        return Json(new { responseCode = "", responseMessage = "Auto-check already completed", skipped = true });
+                    }
+
+                    // Set before the downstream call to prevent concurrent Home requests
+                    // from triggering multiple automatic checks in the same login session.
+                    HttpContext.Session.SetString(FreeTicketAutoCheckSessionKey, "1");
+                }
+
+                string lang = CultureInfo.CurrentCulture.Name;
+                string langValue = (lang.StartsWith("en") || lang.StartsWith("fr")) ? "0" : "1";
+                GetFreeTicketRequest request = new GetFreeTicketRequest
+                {
+                    requestId = Guid.NewGuid().ToString(),
+                    gameId = Constants.Millions_CODE,
+                    type = type,
+                    msisdn = msisdn,
+                    language = langValue,
+                    token = token
+                };
+
+                GetFreeTicketResponse response = api.GetFreeTicketApi(configuration, request);
+                HttpContext.Session.Remove(FreeTicketPromotionCodeSessionKey);
+                if (!string.IsNullOrWhiteSpace(response?.code))
+                {
+                    HttpContext.Session.SetString(FreeTicketPromotionCodeSessionKey, response.code.Trim());
+                }
+                log.Info($"[Millions.GetFreeTicket] Response - code={response?.responseCode}, msg={response?.responseMessage}, voucher={response?.code}");
+                return Json(response);
+            }
+            catch (Exception ex)
+            {
+                log.Error("[Millions.GetFreeTicket] EXCEPTION", ex);
+                return Json(new { responseCode = Code.ERROR, responseMessage = ex.Message });
+            }
+        }
+
         [HttpPost]
         [ValidateAntiForgeryToken]
         public IActionResult TermResult(string termType)
@@ -993,6 +1053,18 @@ namespace LotteryWebApp.Areas.Millions.Controllers
                 request.language = CultureInfo.CurrentCulture.Name.StartsWith("en") ? "0" : "1";
                 request.requestId = Guid.NewGuid().ToString();
 
+                var sessionPromotionCode = HttpContext.Session.GetString(FreeTicketPromotionCodeSessionKey);
+                var isValidPromotionCode = !string.IsNullOrWhiteSpace(request.promotionCode)
+                    && string.Equals(request.promotionCode, sessionPromotionCode, StringComparison.Ordinal);
+                if (isValidPromotionCode)
+                {
+                    request.gameId = Constants.Millions_CODE;
+                }
+                else
+                {
+                    request.promotionCode = null;
+                }
+
                 // DEBUG: Log request data
                 var ticketDebug = request.ticket != null ? string.Join("; ", request.ticket.Select(t => $"code={t.code}, money={t.money}")) : "NULL";
                 log.Info($"[ConfirmTicketData] gameId={request.gameId}, msisdn={request.msisdn}, tickets=[{ticketDebug}]");
@@ -1000,6 +1072,11 @@ namespace LotteryWebApp.Areas.Millions.Controllers
                 ConfirmTicketDataResponse response = request.channelPayment == Constants.NATCASH_WALLET_TICKET
                     ? api.BuyTicketV2Api(configuration, request)
                     : api.ConfirmTicketDataApi(configuration, request);
+
+                if (isValidPromotionCode && response.responseCode == Code.SUCCESS && !string.IsNullOrWhiteSpace(response.transId))
+                {
+                    HttpContext.Session.SetString(FreeTicketTransactionSessionKey, response.transId);
+                }
                 
                 // DEBUG: Log response
                 log.Info($"[ConfirmTicketData] Response: code={response.responseCode}, msg={response.responseMessage}, transId={response.transId}");
@@ -1098,11 +1175,21 @@ namespace LotteryWebApp.Areas.Millions.Controllers
                 request.language = CultureInfo.CurrentCulture.Name.StartsWith("en") ? "0" : "1";
                 request.channel = configuration.GetSection("channel").Value;
 
+                request.gameId = Constants.Millions_CODE;
+                var freeTicketTransactionId = HttpContext.Session.GetString(FreeTicketTransactionSessionKey);
+                var isFreeTicketTransaction = !string.IsNullOrWhiteSpace(request.transIdByTicket)
+                    && string.Equals(request.transIdByTicket, freeTicketTransactionId, StringComparison.Ordinal);
+
                 ConfirmBuyingTicketResponse response = isWalletPayment
                     ? api.ConfirmBuyingTicketV2Api(configuration, request, Constants.WALLET_PAYMENT_CHANNEL, request.walletPin)
                     : api.ConfirmBuyingTicketApi(configuration, request);
                 if (response.responseCode == Code.SUCCESS)
                 {
+                    if (isFreeTicketTransaction)
+                    {
+                        HttpContext.Session.Remove(FreeTicketPromotionCodeSessionKey);
+                        HttpContext.Session.Remove(FreeTicketTransactionSessionKey);
+                    }
                     UpdateUserStatus(msisdn, token);
                 }
                 return Json(response);

+ 33 - 11
website/Areas/Millions/Views/Home/BuyTicket.cshtml

@@ -6,6 +6,14 @@
 }
 @using LotteryWebApp.Languages;
 @using LotteryWebApp.Common;
+@using Microsoft.AspNetCore.Http;
+@{
+    var isFreeTicketMode = Context.Request.Query["freeTicketMode"] == "1"
+        && Model.termType == Constants.Millions_CODE;
+    var freeTicketPromotionCode = isFreeTicketMode
+        ? Context.Session.GetString("Millions.FreeTicket.PromotionCode")
+        : null;
+}
 
 @section Styles {
     <link rel="stylesheet" href="/Millions/css/buy-ticket.css" asp-append-version="true" />
@@ -239,12 +247,15 @@ else
 
     <!-- Ticket Scrollable List Area -->
     <div id="ticketContainer" class="ticket-scroll-area p-4 flex flex-col gap-6 bg-[#F5F5F5] pb-6">
-        @for (int i = 1; i <= 2; i++)
+        @for (int i = 1; i <= (isFreeTicketMode ? 1 : 2); i++)
         {
             <div class="ticket-card relative group active:bg-gray-50 transition-colors cursor-pointer animate__animated animate__fadeIn">
-                <button onclick="removeEntry(this)" class="absolute -right-2 top-2 w-7 h-7 bg-[#FF0000] text-white rounded-full flex items-center justify-center text-[11px] shadow-sm z-[10] hover:bg-red-700 transition-all border-2 border-red-600">
-                    <i class="fa-solid fa-xmark"></i>
-                </button>
+                @if (!isFreeTicketMode)
+                {
+                    <button onclick="removeEntry(this)" class="absolute -right-2 top-2 w-7 h-7 bg-[#FF0000] text-white rounded-full flex items-center justify-center text-[11px] shadow-sm z-[10] hover:bg-red-700 transition-all border-2 border-red-600">
+                        <i class="fa-solid fa-xmark"></i>
+                    </button>
+                }
                 <div class="absolute left-2 top-4 w-9 h-9 rounded-full border-[3px] border-white flex items-center justify-center text-white font-black text-sm shadow-md ticket-index">@i</div>
                 <div class="flex flex-col gap-1 pl-8">
                     <span class="text-[16px] font-extrabold text-[#333]">@Lang.millions_select_10_lucky_numbers</span>
@@ -280,11 +291,14 @@ else
             </div>
         }
 
-        <div class="w-full pt-2" id="addButtonWrapper">
-            <button onclick="addNewTicket()" class="w-fit min-w-[220px] mx-auto bg-[#2B83F2] text-white font-black text-md py-2.5 px-6 rounded-xl shadow-md border-b-[4px] border-[#1F66D6] active:translate-y-[1px] active:border-b-[1px] transition-all flex items-center justify-center gap-2">
-                @Lang.millions_add_more_ticket
-            </button>
-        </div>
+        @if (!isFreeTicketMode)
+        {
+            <div class="w-full pt-2" id="addButtonWrapper">
+                <button onclick="addNewTicket()" class="w-fit min-w-[220px] mx-auto bg-[#2B83F2] text-white font-black text-md py-2.5 px-6 rounded-xl shadow-md border-b-[4px] border-[#1F66D6] active:translate-y-[1px] active:border-b-[1px] transition-all flex items-center justify-center gap-2">
+                    @Lang.millions_add_more_ticket
+                </button>
+            </div>
+        }
         <div class="h-4"></div>
     </div>
 
@@ -512,6 +526,8 @@ else
     <script>
 
         const maxBalls = @(Model.termType == Constants.PIC10_BASIC_CODE ? 10 : (Model.termType == Constants.Millions_CODE ? 6 : (Model.termType == Constants.PIC10_BIGSMALL_CODE ? 13 : 15)));
+        const isFreeTicketMode = @(isFreeTicketMode ? "true" : "false");
+        const freeTicketPromotionCode = @Html.Raw(System.Text.Json.JsonSerializer.Serialize(freeTicketPromotionCode));
 
         function reindexTickets() {
             $(".ticket-card").each(function(index) {
@@ -942,6 +958,8 @@ else
         }
 
         function addNewTicket() {
+            if (isFreeTicketMode) return;
+
             var container = $("#ticketContainer");
             var wrapper = $("#addButtonWrapper");
             var currentIndex = $(".ticket-card").length + 1;
@@ -1016,6 +1034,8 @@ else
                 });
             } else {
                 $(".ticket-card").each(function() {
+                    if (isFreeTicketMode && tickets.length >= 1) return false;
+
                     const balls = $(this).find(".ball-filled");
                     if (balls.length === maxBalls) {
                         const selectedNumbers = [];
@@ -1047,9 +1067,10 @@ else
             hidePaymentChannelModal();
 
             const requestData = {
-                gameId: "@Model.termType",
+                gameId: isFreeTicketMode ? "@Constants.Millions_CODE" : "@Model.termType",
                 ticket: pendingTickets,
-                channelPayment: channelPayment
+                channelPayment: channelPayment,
+                promotionCode: isFreeTicketMode ? freeTicketPromotionCode : null
             };
 
             $.ajax({
@@ -1355,6 +1376,7 @@ else
 
             const finalData = {
                 transIdByTicket: currentTransId,
+                gameId: isFreeTicketMode ? "@Constants.Millions_CODE" : "@Model.termType",
                 paymentCode: currentChannelPayment === '@Constants.NATCASH_WALLET_TICKET' ? null : otpCode,
                 walletPin: currentChannelPayment === '@Constants.NATCASH_WALLET_TICKET' ? otpCode : null,
                 channelPayment: currentChannelPayment

+ 3 - 1
website/Areas/Millions/Views/Home/GameHome.cshtml

@@ -177,7 +177,7 @@
         </div>
 
         <!-- Play now button + decorative balls -->
-        <div class="relative shrink-0 mt-4 z-10">
+        <div id="playNowSection" class="relative shrink-0 mt-4 z-10">
             <img src="/Millions/assets/images/play-decor.png" alt="" class="block w-full h-auto pointer-events-none"/>
 
             <a onclick="checkTerm('@Constants.Millions_CODE')" class="play-now-outer rounded-full animate-pulse-play" style="position:absolute; left:50%; top:50%; transform:translate(-50%,-50%); width:155px; height:155px; padding:11px; cursor:pointer;">
@@ -218,6 +218,8 @@
 
 </div>
 
+@await Html.PartialAsync("~/Areas/Millions/Views/Home/PopupFreeTickit.cshtml")
+
 @section Scripts {
     <script src="/Millions/js/all.min.js"></script>
     <script>

+ 21 - 10
website/Areas/Millions/Views/Home/History.cshtml

@@ -169,7 +169,7 @@
             if (nextBtn) nextBtn.disabled = currentSeqPage >= totalPages;
         }
 
-        function showTicketDetail(billCode, money, moneyWin, date, channel, payMethod, ticketCode, gameId, drawTime, ticketId) {
+        function showTicketDetail(billCode, money, moneyWin, date, channel, payMethod, paymentMessage, ticketCode, gameId, drawTime, ticketId) {
             document.getElementById('detailBillCode').innerText = '#' + (billCode || '-');
             document.getElementById('detailTicketCode').innerText = '#' + (ticketId || '-');
             document.getElementById('detailMoney').innerText = formatMoneyV2(money || '0') + ' HTG';
@@ -179,24 +179,35 @@
             document.getElementById('detailChannel').innerText = channel || 'App';
             
             // Logic for Game Name and Color
-            let gameName = "@Lang.millions_classic_pick_10";
-            let gameColor = "#0062FF"; // Red
+            const normalizedGameId = String(gameId || '').trim();
+            let gameName = @Html.Raw(System.Text.Json.JsonSerializer.Serialize(Lang.millions));
+            let gameColor = "#0062FF";
             
-            if (gameId === "31") {
+            if (normalizedGameId === "30") {
+                gameName = @Html.Raw(System.Text.Json.JsonSerializer.Serialize(Lang.millions_classic_pick_10));
+            } else if (normalizedGameId === "31") {
                 gameName = "@Lang.millions_big_small";
-                gameColor = "#00A3FF"; // Blue
-            } else if (gameId === "32") {
+                gameColor = "#00A3FF";
+            } else if (normalizedGameId === "32") {
                 gameName = "@Lang.millions_odd_even";
-                gameColor = "#9333EA"; // Purple
+                gameColor = "#9333EA";
+            } else if (normalizedGameId === "33") {
+                gameName = @Html.Raw(System.Text.Json.JsonSerializer.Serialize(Lang.millions));
             }
             
             const gameEl = document.getElementById('detailGameName');
             gameEl.innerText = gameName;
             gameEl.style.color = gameColor;
 
-            let payMethodText = "Main Account";
-            if(payMethod == "1") payMethodText = "Wallet";
-            else if(payMethod == "0") payMethodText = "Main Account";
+            const normalizedPayMethod = String(payMethod || '').trim().toUpperCase();
+            const normalizedPaymentMessage = String(paymentMessage || '').trim().toUpperCase();
+            const isWalletPayment = normalizedPaymentMessage.includes('WALLET')
+                || normalizedPaymentMessage.includes('NATCASH')
+                || normalizedPayMethod === '1'
+                || normalizedPayMethod === 'WALLET';
+            const payMethodText = isWalletPayment
+                ? @Html.Raw(System.Text.Json.JsonSerializer.Serialize(Lang.payment_wallet))
+                : @Html.Raw(System.Text.Json.JsonSerializer.Serialize(Lang.payment_main_account));
 
             document.getElementById('detailPaymentMethod').innerText = payMethodText;
 

+ 279 - 0
website/Areas/Millions/Views/Home/PopupFreeTickit.cshtml

@@ -0,0 +1,279 @@
+@using LotteryWebApp.Languages
+@using Microsoft.AspNetCore.Http
+@{
+    var shouldAutoCheckFreeTicket = Context.Session.GetString("Millions.FreeTicket.AutoChecked") != "1";
+}
+
+<link rel="stylesheet" href="~/Millions/css/popup-free-ticket.css" asp-append-version="true" />
+
+<aside class="free-ticket-popup" aria-labelledby="freeTicketTitle">
+    <a class="free-ticket-popup__card"
+       href="#"
+       id="freeTicketClaimButton"
+       aria-label="Get your free ticket">
+        <img class="free-ticket-popup__image"
+             src="~/Millions/assets/images/free-ticket-floating-icon.png"
+             alt="" aria-hidden="true" />
+        <strong class="free-ticket-popup__ticket-text">
+            @Lang.millions_free<br />@Lang.millions_ticket
+        </strong>
+        <strong class="free-ticket-popup__button-text" id="freeTicketTitle">
+            @Lang.millions_free @Lang.millions_ticket
+        </strong>
+    </a>
+</aside>
+
+<div class="free-ticket-result" id="freeTicketResultModal" role="dialog" aria-modal="true"
+     aria-labelledby="freeTicketResultTitle" aria-describedby="freeTicketResultDescription" hidden>
+    <div class="free-ticket-result__backdrop" data-free-ticket-close></div>
+    <section class="free-ticket-result__panel">
+        <h2 class="free-ticket-visually-hidden" id="freeTicketResultTitle">@Lang.free_ticket_waiting_title</h2>
+        <p class="free-ticket-visually-hidden" id="freeTicketResultDescription">@Lang.free_ticket_waiting_description</p>
+        <img class="free-ticket-result__image"
+             src="~/Millions/assets/images/free-ticket-success.png"
+             alt="" aria-hidden="true" />
+        <button type="button" class="free-ticket-result__action free-ticket-result__action--dismiss-icon"
+                data-free-ticket-close aria-label="@Lang.millions_close"></button>
+        <a class="free-ticket-result__action free-ticket-result__action--use"
+           href="@Url.Action("BuyTicket", "Home", new { area = "Millions", freeTicketMode = "1" })"
+           aria-label="@Lang.free_ticket_buy_now"></a>
+        <button type="button" class="free-ticket-result__action free-ticket-result__action--later"
+                data-free-ticket-close aria-label="@Lang.millions_close"></button>
+    </section>
+</div>
+
+<div class="free-ticket-error-43" id="freeTicketError43Modal" role="dialog" aria-modal="true"
+     aria-labelledby="freeTicketError43Title" aria-describedby="freeTicketError43Description" hidden>
+    <div class="free-ticket-error-43__backdrop" data-free-ticket-error-43-close></div>
+    <section class="free-ticket-error-43__panel">
+        <h2 class="free-ticket-visually-hidden" id="freeTicketError43Title">@Lang.free_ticket_error_43_title</h2>
+        <p class="free-ticket-visually-hidden" id="freeTicketError43Description">
+            @Lang.free_ticket_error_43_description
+        </p>
+        <img class="free-ticket-error-43__image"
+             src="~/Millions/assets/images/free-ticket-error-43.png"
+             alt="" aria-hidden="true" />
+        <button type="button" class="free-ticket-error-43__action free-ticket-error-43__action--dismiss-icon"
+                data-free-ticket-error-43-close aria-label="@Lang.millions_close"></button>
+        <a class="free-ticket-error-43__action free-ticket-error-43__action--buy"
+           href="@Url.Action("BuyTicket", "Home", new { area = "Millions" })"
+           aria-label="@Lang.millions_buy_ticket_label"></a>
+        <button type="button" class="free-ticket-error-43__action free-ticket-error-43__action--close"
+                data-free-ticket-error-43-close aria-label="@Lang.millions_close"></button>
+    </section>
+</div>
+
+<div class="free-ticket-error-44" id="freeTicketError44Modal" role="dialog" aria-modal="true"
+     aria-labelledby="freeTicketError44Title" aria-describedby="freeTicketError44Description" hidden>
+    <div class="free-ticket-error-44__backdrop" data-free-ticket-error-44-close></div>
+    <section class="free-ticket-error-44__panel">
+        <h2 class="free-ticket-visually-hidden" id="freeTicketError44Title">@Lang.free_ticket_waiting_title</h2>
+        <p class="free-ticket-visually-hidden" id="freeTicketError44Description">
+            @Lang.free_ticket_waiting_description
+        </p>
+        <img class="free-ticket-error-44__image"
+             src="~/Millions/assets/images/free-ticket-error-44.png"
+             alt="" aria-hidden="true" />
+        <button type="button" class="free-ticket-error-44__action free-ticket-error-44__action--dismiss-icon"
+                data-free-ticket-error-44-close aria-label="@Lang.millions_close"></button>
+        <a class="free-ticket-error-44__action free-ticket-error-44__action--buy"
+           href="@Url.Action("BuyTicket", "Home", new { area = "Millions", freeTicketMode = "1" })"
+           aria-label="@Lang.free_ticket_buy_now"></a>
+        <button type="button" class="free-ticket-error-44__action free-ticket-error-44__action--close"
+                data-free-ticket-error-44-close aria-label="@Lang.millions_close"></button>
+    </section>
+</div>
+
+<script>
+    (function () {
+        var popup = document.querySelector('.free-ticket-popup');
+        var claimButton = document.getElementById('freeTicketClaimButton');
+        var resultModal = document.getElementById('freeTicketResultModal');
+        var error43Modal = document.getElementById('freeTicketError43Modal');
+        var error44Modal = document.getElementById('freeTicketError44Modal');
+        var error43ReturnFocus = null;
+        var error44ReturnFocus = null;
+        if (!popup) return;
+
+        function showFreeTicketResult(result) {
+            if (!resultModal || !result || String(result.responseCode) !== '0' || !result.code) {
+                return false;
+            }
+
+            resultModal.hidden = false;
+            document.body.classList.add('free-ticket-result-open');
+            return true;
+        }
+
+        function showFreeTicketError43() {
+            if (!error43Modal) return false;
+
+            error43ReturnFocus = document.activeElement;
+            error43Modal.hidden = false;
+            document.body.classList.add('free-ticket-result-open');
+            var closeButton = error43Modal.querySelector('[data-free-ticket-error-43-close]');
+            if (closeButton) closeButton.focus();
+            return true;
+        }
+
+        function closeFreeTicketError43() {
+            if (!error43Modal) return;
+
+            error43Modal.hidden = true;
+            document.body.classList.remove('free-ticket-result-open');
+            if (error43ReturnFocus && typeof error43ReturnFocus.focus === 'function') {
+                error43ReturnFocus.focus();
+            }
+        }
+
+        if (error43Modal) {
+            error43Modal.querySelectorAll('[data-free-ticket-error-43-close]').forEach(function (closeButton) {
+                closeButton.addEventListener('click', closeFreeTicketError43);
+            });
+
+            error43Modal.addEventListener('keydown', function (event) {
+                if (event.key === 'Escape') closeFreeTicketError43();
+            });
+        }
+
+        function showFreeTicketError44(result) {
+            if (!error44Modal) return false;
+
+            error44ReturnFocus = document.activeElement;
+            error44Modal.hidden = false;
+            document.body.classList.add('free-ticket-result-open');
+            var closeButton = error44Modal.querySelector('[data-free-ticket-error-44-close]');
+            if (closeButton) closeButton.focus();
+            return true;
+        }
+
+        function closeFreeTicketError44() {
+            if (!error44Modal) return;
+
+            error44Modal.hidden = true;
+            document.body.classList.remove('free-ticket-result-open');
+            if (error44ReturnFocus && typeof error44ReturnFocus.focus === 'function') {
+                error44ReturnFocus.focus();
+            }
+        }
+
+        if (error44Modal) {
+            error44Modal.querySelectorAll('[data-free-ticket-error-44-close]').forEach(function (closeButton) {
+                closeButton.addEventListener('click', closeFreeTicketError44);
+            });
+
+            error44Modal.addEventListener('keydown', function (event) {
+                if (event.key === 'Escape') closeFreeTicketError44();
+            });
+        }
+
+        if (resultModal) {
+            resultModal.querySelectorAll('[data-free-ticket-close]').forEach(function (closeButton) {
+                closeButton.addEventListener('click', function () {
+                    resultModal.hidden = true;
+                    document.body.classList.remove('free-ticket-result-open');
+                });
+            });
+        }
+
+        if (claimButton) {
+            var freeTicketMessages = {
+                '38': @Html.Raw(System.Text.Json.JsonSerializer.Serialize(Lang.get_free_ticket_code_38)),
+                '39': @Html.Raw(System.Text.Json.JsonSerializer.Serialize(Lang.get_free_ticket_code_39)),
+                '40': @Html.Raw(System.Text.Json.JsonSerializer.Serialize(Lang.get_free_ticket_code_40)),
+                '41': @Html.Raw(System.Text.Json.JsonSerializer.Serialize(Lang.get_free_ticket_code_41)),
+                '42': @Html.Raw(System.Text.Json.JsonSerializer.Serialize(Lang.get_free_ticket_code_42)),
+                '43': @Html.Raw(System.Text.Json.JsonSerializer.Serialize(Lang.get_free_ticket_code_43)),
+                '44': @Html.Raw(System.Text.Json.JsonSerializer.Serialize(Lang.get_free_ticket_code_44))
+            };
+
+            if (@(shouldAutoCheckFreeTicket ? "true" : "false") && document.getElementById('playNowSection')) {
+                document.addEventListener('DOMContentLoaded', function () {
+                    var initialVerificationToken = document.querySelector('input[name="__RequestVerificationToken"]');
+                    $.ajax({
+                        url: subDomain + '@Url.Action("GetFreeTicket", "Home", new { area = "Millions" })',
+                        type: 'POST',
+                        data: {
+                            __RequestVerificationToken: initialVerificationToken ? initialVerificationToken.value : '',
+                            type: '1'
+                        },
+                        success: function (result) {
+                            if (result && String(result.responseCode) === '0' && result.code) {
+                                showFreeTicketError44(result);
+                            }
+                        }
+                    });
+                });
+            }
+
+            claimButton.addEventListener('click', function (event) {
+                event.preventDefault();
+                if (claimButton.dataset.loading === 'true') return;
+
+                var verificationToken = document.querySelector('input[name="__RequestVerificationToken"]');
+                claimButton.dataset.loading = 'true';
+                claimButton.classList.add('free-ticket-popup__card--loading');
+
+                $.ajax({
+                    url: subDomain + '@Url.Action("GetFreeTicket", "Home", new { area = "Millions" })',
+                    type: 'POST',
+                    data: {
+                        __RequestVerificationToken: verificationToken ? verificationToken.value : ''
+                    },
+                    success: function (result) {
+                        if (showFreeTicketResult(result)) {
+                            return;
+                        }
+
+                        var responseCode = result && result.responseCode
+                            ? String(result.responseCode)
+                            : '';
+                        if (responseCode === '43' && showFreeTicketError43()) {
+                            return;
+                        }
+                        if (responseCode === '44' && showFreeTicketError44(result)) {
+                            return;
+                        }
+                        var responseMessage = freeTicketMessages[responseCode]
+                            || (result && result.responseMessage)
+                            || 'Unable to get free ticket.';
+
+                        window.alert(responseMessage);
+                    },
+                    error: function () {
+                        window.alert('Unable to get free ticket.');
+                    },
+                    complete: function () {
+                        claimButton.dataset.loading = 'false';
+                        claimButton.classList.remove('free-ticket-popup__card--loading');
+                    }
+                });
+            });
+        }
+
+        function alignFreeTicketIcon() {
+            var millionsFrame = document.querySelector('.main-content, .main-container');
+            var playNowSection = document.getElementById('playNowSection');
+            if (!millionsFrame) {
+                popup.style.left = '';
+                popup.style.right = '10px';
+                return;
+            }
+
+            var frameRect = millionsFrame.getBoundingClientRect();
+            var popupWidth = popup.getBoundingClientRect().width;
+            popup.style.left = Math.max(8, frameRect.right - popupWidth - 15) + 'px';
+            popup.style.right = 'auto';
+
+            if (playNowSection) {
+                var playRect = playNowSection.getBoundingClientRect();
+                popup.style.top = (playRect.top - 7) + 'px';
+                popup.style.bottom = 'auto';
+            }
+        }
+
+        alignFreeTicketIcon();
+        window.addEventListener('resize', alignFreeTicketIcon, { passive: true });
+        window.addEventListener('scroll', alignFreeTicketIcon, { passive: true, capture: true });
+    })();
+</script>

+ 1 - 1
website/Areas/Millions/Views/Home/_TermUserTicketHistory.cshtml

@@ -99,7 +99,7 @@
                         <i class="fa-solid fa-circle-info text-[12px]"></i>
                         <span class="text-[10px] font-bold uppercase tracking-tight">@Lang.millions_tap_to_see_detail</span>
                     </div>
-                    <button onclick="showTicketDetail('@item.billCode', '@item.money', '@item.moneyWin', '@item.createDate', '@item.channel', '@item.channelPayment', '@item.code', '@item.gameId', '@item.termRandomDate', '@item.id')" 
+                    <button onclick="showTicketDetail('@item.billCode', '@item.money', '@item.moneyWin', '@item.createDate', '@item.channel', '@item.channelPayment', '@item.paymentMessage', '@item.code', '@item.gameId', '@item.termRandomDate', '@item.id')"
                             class="bg-[#00A3FF] text-white px-5 py-1.5 rounded-lg font-black text-[12px] shadow-[0_4px_10px_rgba(0,163,255,0.3)] active:scale-95 transition-all border-[1.5px] border-white uppercase tracking-wide">
                         @Lang.millions_detail
                     </button>

+ 15 - 0
website/Controllers/HomeController.cs

@@ -91,6 +91,13 @@ namespace LotteryWebApp.Controllers
                 // check auto login ?uuid=681471d23b83f0a463e2dd5671ea0fc5
                 // ?uuid=681471d23b83f0a463e2dd5671ea0fc5&mcuid=6c6948a89908d83aec8c05be256970a1&mcapp=mocha
 
+                if (CheckAuthToken())
+                {
+                    return Redirect(
+                        GetParameter(Constants.SUB_DOMAIN) + "/Account/ChooseApp"
+                    );
+                }
+
                 if (!CheckAuthToken())
                 {
                     //String uri = HttpContext.Request.QueryString.ToUriComponent().ToString();
@@ -165,6 +172,10 @@ namespace LotteryWebApp.Controllers
                                     userStatusRequest
                                 );
                                 HttpContext.Session.SetComplexData("userStatus", userStatusGet);
+
+                                return Redirect(
+                                    GetParameter(Constants.SUB_DOMAIN) + "/Account/ChooseApp"
+                                );
                             }
                             else
                             {
@@ -280,6 +291,10 @@ namespace LotteryWebApp.Controllers
                                 userStatusRequest
                             );
                             HttpContext.Session.SetComplexData("userStatus", userStatusGet);
+
+                            return Redirect(
+                                GetParameter(Constants.SUB_DOMAIN) + "/Account/ChooseApp"
+                            );
                         }
                         else
                         {

+ 12 - 0
website/Languages/Lang.Designer.cs

@@ -5814,5 +5814,17 @@ namespace LotteryWebApp.Languages {
         public static string payment_wallet_desc { get { return ResourceManager.GetString("payment_wallet_desc", resourceCulture); } }
         public static string payment_security_message { get { return ResourceManager.GetString("payment_security_message", resourceCulture); } }
         public static string payment_wallet_pin_instruction { get { return ResourceManager.GetString("payment_wallet_pin_instruction", resourceCulture); } }
+        public static string get_free_ticket_code_38 { get { return ResourceManager.GetString("get_free_ticket_code_38", resourceCulture); } }
+        public static string get_free_ticket_code_39 { get { return ResourceManager.GetString("get_free_ticket_code_39", resourceCulture); } }
+        public static string get_free_ticket_code_40 { get { return ResourceManager.GetString("get_free_ticket_code_40", resourceCulture); } }
+        public static string get_free_ticket_code_41 { get { return ResourceManager.GetString("get_free_ticket_code_41", resourceCulture); } }
+        public static string get_free_ticket_code_42 { get { return ResourceManager.GetString("get_free_ticket_code_42", resourceCulture); } }
+        public static string get_free_ticket_code_43 { get { return ResourceManager.GetString("get_free_ticket_code_43", resourceCulture); } }
+        public static string get_free_ticket_code_44 { get { return ResourceManager.GetString("get_free_ticket_code_44", resourceCulture); } }
+        public static string free_ticket_error_43_title { get { return ResourceManager.GetString("free_ticket_error_43_title", resourceCulture); } }
+        public static string free_ticket_error_43_description { get { return ResourceManager.GetString("free_ticket_error_43_description", resourceCulture); } }
+        public static string free_ticket_waiting_title { get { return ResourceManager.GetString("free_ticket_waiting_title", resourceCulture); } }
+        public static string free_ticket_waiting_description { get { return ResourceManager.GetString("free_ticket_waiting_description", resourceCulture); } }
+        public static string free_ticket_buy_now { get { return ResourceManager.GetString("free_ticket_buy_now", resourceCulture); } }
     }
 }

+ 12 - 0
website/Languages/Lang.fr.resx

@@ -2043,4 +2043,16 @@ We’ll help you create an account in a few easy steps.</value>
   <data name="payment_wallet_desc" xml:space="preserve"><value>Secure payment with wallet PIN</value></data>
   <data name="payment_security_message" xml:space="preserve"><value>Your payment information is protected</value></data>
   <data name="payment_wallet_pin_instruction" xml:space="preserve"><value>Enter your WALLET PIN</value></data>
+  <data name="get_free_ticket_code_38" xml:space="preserve"><value>Promotion code invalid</value></data>
+  <data name="get_free_ticket_code_39" xml:space="preserve"><value>Promotion code has already been used</value></data>
+  <data name="get_free_ticket_code_40" xml:space="preserve"><value>Promotion is not active</value></data>
+  <data name="get_free_ticket_code_41" xml:space="preserve"><value>Ticket request does not exist</value></data>
+  <data name="get_free_ticket_code_42" xml:space="preserve"><value>Your transaction was unsuccessful</value></data>
+  <data name="get_free_ticket_code_43" xml:space="preserve"><value>Good luck next time</value></data>
+  <data name="get_free_ticket_code_44" xml:space="preserve"><value>The code has been sent</value></data>
+  <data name="free_ticket_error_43_title" xml:space="preserve"><value>Good luck next time!</value></data>
+  <data name="free_ticket_error_43_description" xml:space="preserve"><value>You did not receive a free ticket today. Come back tomorrow and try your luck again.</value></data>
+  <data name="free_ticket_waiting_title" xml:space="preserve"><value>Your FREE ticket is waiting!</value></data>
+  <data name="free_ticket_waiting_description" xml:space="preserve"><value>You have 1 free Millions ticket that has not been used. Use it for today's draw.</value></data>
+  <data name="free_ticket_buy_now" xml:space="preserve"><value>Buy now</value></data>
 </root>

+ 12 - 0
website/Languages/Lang.resx

@@ -2044,4 +2044,16 @@ Apre yo fin tire 20 nimewo, sistem nan konte konbyen nimewo ki enpe oswa pe: &lt
   <data name="payment_wallet_desc" xml:space="preserve"><value>Peman sekirize avèk PIN wallet la</value></data>
   <data name="payment_security_message" xml:space="preserve"><value>Enfòmasyon peman ou pwoteje</value></data>
   <data name="payment_wallet_pin_instruction" xml:space="preserve"><value>Antre PIN WALLET ou</value></data>
+  <data name="get_free_ticket_code_38" xml:space="preserve"><value>Kòd pwomosyon an pa valid</value></data>
+  <data name="get_free_ticket_code_39" xml:space="preserve"><value>Kòd pwomosyon an deja itilize</value></data>
+  <data name="get_free_ticket_code_40" xml:space="preserve"><value>Pwomosyon an pa aktif</value></data>
+  <data name="get_free_ticket_code_41" xml:space="preserve"><value>Demann tikè a pa egziste</value></data>
+  <data name="get_free_ticket_code_42" xml:space="preserve"><value>Tranzaksyon ou an pa reyisi</value></data>
+  <data name="get_free_ticket_code_43" xml:space="preserve"><value>Bòn chans pwochen fwa</value></data>
+  <data name="get_free_ticket_code_44" xml:space="preserve"><value>Yo voye kòd la</value></data>
+  <data name="free_ticket_error_43_title" xml:space="preserve"><value>Bòn chans pwochen fwa!</value></data>
+  <data name="free_ticket_error_43_description" xml:space="preserve"><value>Ou pa t resevwa yon tikè gratis jodi a. Retounen demen epi eseye chans ou ankò.</value></data>
+  <data name="free_ticket_waiting_title" xml:space="preserve"><value>Tikè GRATIS ou ap tann ou!</value></data>
+  <data name="free_ticket_waiting_description" xml:space="preserve"><value>Ou gen 1 tikè Millions gratis ki poko itilize. Sèvi avè l pou tiraj jodi a.</value></data>
+  <data name="free_ticket_buy_now" xml:space="preserve"><value>Achte kounye a</value></data>
 </root>

+ 32 - 0
website/Service/ApiFunctions.cs

@@ -247,6 +247,38 @@ namespace LotteryWebApp.Service
             return deptObj;
         }
 
+        public GetFreeTicketResponse GetFreeTicketApi(IConfiguration configuration, GetFreeTicketRequest getFreeTicketRequest)
+        {
+            getFreeTicketRequest.channel = configuration.GetSection("channel").Value;
+
+            String url = configuration.GetSection("get_free_ticket").Value;
+            if (String.IsNullOrWhiteSpace(url))
+            {
+                log.Error("Missing configuration: get_free_ticket");
+                return new GetFreeTicketResponse
+                {
+                    responseCode = Code.ERROR,
+                    responseMessage = "Missing configuration: get_free_ticket"
+                };
+            }
+
+            String jsonData = SendPost(getFreeTicketRequest, ApiConstants.SERVICE_ID, url);
+            try
+            {
+                GetFreeTicketResponse deptObj = JsonSerializer.Deserialize<GetFreeTicketResponse>(jsonData);
+                return deptObj;
+            }
+            catch (System.Text.Json.JsonException ex)
+            {
+                log.Error($"GetFreeTicketApi returned invalid JSON: {jsonData}", ex);
+                return new GetFreeTicketResponse
+                {
+                    responseCode = Code.ERROR,
+                    responseMessage = $"Get free ticket API returned an invalid response: {jsonData}"
+                };
+            }
+        }
+
         public GetTopWininerResponse GetTopWinnerApi(IConfiguration configuration, GetTopWinnerRequest userTicketRequest)
         {
             userTicketRequest.serviceId = ApiConstants.SERVICE_ID;

+ 21 - 0
website/Service/ApiModels.cs

@@ -293,6 +293,7 @@ namespace LotteryWebApp.Service
         public string walletPin { get; set; }
         public string channelPayment { get; set; }
         public string transIdByTicket { get; set; }
+        public string gameId { get; set; }
     }
 
     public class ConfirmBuyingTicketResponse
@@ -355,6 +356,26 @@ namespace LotteryWebApp.Service
 
     }
 
+    public class GetFreeTicketRequest : Posting
+    {
+        public string requestId { get; set; }
+        public string gameId { get; set; }
+        public string type { get; set; }
+        public string msisdn { get; set; }
+        public string channel { get; set; }
+        public string language { get; set; }
+        public string token { get; set; }
+    }
+
+    public class GetFreeTicketResponse
+    {
+        public string code { get; set; }
+        public string money { get; set; }
+        public string expireDate { get; set; }
+        public string responseCode { get; set; }
+        public string responseMessage { get; set; }
+    }
+
     public class GetTopWininerResponse
     {
         public string responseCode { get; set; }

+ 3 - 0
website/appsettings.json

@@ -38,6 +38,9 @@
   "buy_ticketv2": "http://171.244.50.177:8900/api/loto/buyTicketV2/data",
   "confirm_buying_ticketv2": "http://171.244.50.177:8900/api/loto/confirmTicketV2/data",
 
+  "get_free_ticket": "http://171.244.50.177:8900/api/loto/getPromotionCode/data",
+
+
   "Logging": {
     "LogLevel": {
       "Default": "Information",

BIN
website/wwwroot/Millions/assets/images/free-ticket-error-43.png


BIN
website/wwwroot/Millions/assets/images/free-ticket-error-44.png


BIN
website/wwwroot/Millions/assets/images/free-ticket-floating-icon.png


BIN
website/wwwroot/Millions/assets/images/free-ticket-success.png


+ 314 - 0
website/wwwroot/Millions/css/popup-free-ticket.css

@@ -0,0 +1,314 @@
+.free-ticket-popup,
+.free-ticket-popup * {
+    box-sizing: border-box;
+}
+
+.free-ticket-popup {
+    position: fixed;
+    z-index: 90;
+    right: 10px;
+    bottom: calc(88px + env(safe-area-inset-bottom));
+    width: 70px;
+    height: 81px;
+    font-family: Arial, Helvetica, sans-serif;
+}
+
+.free-ticket-popup__card {
+    position: absolute;
+    right: 0;
+    bottom: 0;
+    display: block;
+    width: 70px;
+    height: 81px;
+    color: inherit;
+    cursor: pointer;
+    filter: drop-shadow(0 3px 4px rgba(0, 35, 105, .3));
+    text-decoration: none;
+    transform-origin: center bottom;
+    will-change: transform, filter;
+}
+
+.free-ticket-popup__image {
+    display: block;
+    width: 100%;
+    height: 100%;
+    object-fit: contain;
+}
+
+.free-ticket-popup__ticket-text,
+.free-ticket-popup__button-text {
+    position: absolute;
+    z-index: 2;
+    color: #06429b;
+    font-weight: 900;
+    text-align: center;
+    text-transform: uppercase;
+    text-shadow: 0 1px rgba(255, 255, 255, .35);
+    pointer-events: none;
+}
+
+.free-ticket-popup__ticket-text {
+    top: 22px;
+    left: 15px;
+    width: 39px;
+    transform: rotate(3deg);
+    font-size: 8px;
+    line-height: .82;
+}
+
+.free-ticket-popup__button-text {
+    top: auto;
+    right: 14px;
+    bottom: 16px;
+    left: 14px;
+    display: flex;
+    height: 9px;
+    align-items: center;
+    justify-content: center;
+    overflow: hidden;
+    font-size: 5px;
+    line-height: 9px;
+    text-overflow: ellipsis;
+    white-space: nowrap;
+}
+
+.free-ticket-popup__card:hover,
+.free-ticket-popup__card:focus-visible {
+    transform: translateY(-3px) scale(1.06);
+    filter: brightness(1.06) drop-shadow(0 4px 6px rgba(0, 35, 105, .42));
+    outline: 1px solid rgba(112, 183, 255, .9);
+    outline-offset: 1px;
+}
+
+.free-ticket-popup__card:active {
+    transform: translateY(0) scale(.96);
+}
+
+.free-ticket-popup__card--loading {
+    animation: free-ticket-loading .75s linear infinite !important;
+    cursor: wait;
+    opacity: .72;
+}
+
+.free-ticket-popup--claimed {
+    display: none;
+}
+
+body.free-ticket-result-open {
+    overflow: hidden;
+}
+
+.free-ticket-result[hidden] {
+    display: none;
+}
+
+.free-ticket-error-43[hidden],
+.free-ticket-error-44[hidden] {
+    display: none;
+}
+
+.free-ticket-visually-hidden {
+    position: absolute;
+    width: 1px;
+    height: 1px;
+    padding: 0;
+    margin: -1px;
+    overflow: hidden;
+    clip: rect(0, 0, 0, 0);
+    white-space: nowrap;
+    border: 0;
+}
+
+.free-ticket-error-43,
+.free-ticket-error-44 {
+    position: fixed;
+    z-index: 230;
+    inset: 0;
+    display: grid;
+    place-items: center;
+    padding: clamp(10px, 4vw, 24px);
+    font-family: Arial, Helvetica, sans-serif;
+}
+
+.free-ticket-error-43__backdrop,
+.free-ticket-error-44__backdrop {
+    position: absolute;
+    inset: 0;
+    background: rgba(0, 20, 72, .68);
+    backdrop-filter: blur(3px);
+}
+
+.free-ticket-error-43__panel,
+.free-ticket-error-44__panel {
+    position: relative;
+    z-index: 1;
+    width: min(380px, 100%, calc((100dvh - 32px) * 547 / 437));
+    aspect-ratio: 547 / 437;
+    overflow: hidden;
+    border-radius: clamp(12px, 3vw, 18px);
+    box-shadow: 0 22px 55px rgba(0, 15, 70, .45);
+}
+
+.free-ticket-error-43__image,
+.free-ticket-error-44__image {
+    display: block;
+    width: 100%;
+    height: 100%;
+    object-fit: contain;
+}
+
+.free-ticket-error-43__action,
+.free-ticket-error-44__action {
+    position: absolute;
+    z-index: 2;
+    display: block;
+    border: 0;
+    border-radius: 8px;
+    background: transparent;
+    cursor: pointer;
+    color: transparent;
+}
+
+.free-ticket-error-43__action--dismiss-icon,
+.free-ticket-error-44__action--dismiss-icon {
+    top: 2.5%;
+    right: 1.8%;
+    width: 7.5%;
+    height: 10%;
+}
+
+.free-ticket-error-43__action--buy,
+.free-ticket-error-44__action--buy {
+    top: 79.4%;
+    left: 12.3%;
+    width: 36.8%;
+    height: 8.8%;
+}
+
+.free-ticket-error-43__action--close,
+.free-ticket-error-44__action--close {
+    top: 79.4%;
+    left: 51.1%;
+    width: 35.9%;
+    height: 8.8%;
+}
+
+.free-ticket-error-43__action:focus-visible,
+.free-ticket-error-44__action:focus-visible {
+    outline: 3px solid #ffd447;
+    outline-offset: 2px;
+}
+
+@media (max-width: 359px) {
+    .free-ticket-error-43,
+    .free-ticket-error-44 {
+        padding: 8px;
+    }
+}
+
+.free-ticket-result {
+    position: fixed;
+    z-index: 220;
+    inset: 0;
+    display: grid;
+    place-items: center;
+    padding: 20px;
+    font-family: Arial, Helvetica, sans-serif;
+}
+
+.free-ticket-result__backdrop {
+    position: absolute;
+    inset: 0;
+    background: rgba(0, 20, 72, .68);
+    backdrop-filter: blur(3px);
+}
+
+.free-ticket-result__panel {
+    position: relative;
+    z-index: 1;
+    width: min(380px, 100%, calc((100dvh - 32px) * 520 / 412));
+    aspect-ratio: 520 / 412;
+    overflow: hidden;
+    border-radius: clamp(12px, 3vw, 18px);
+    box-shadow: 0 22px 55px rgba(0, 15, 70, .45);
+}
+
+.free-ticket-result__image {
+    display: block;
+    width: 100%;
+    height: 100%;
+    object-fit: contain;
+}
+
+.free-ticket-result__action {
+    position: absolute;
+    z-index: 2;
+    display: block;
+    border: 0;
+    border-radius: 8px;
+    background: transparent;
+    cursor: pointer;
+    color: transparent;
+}
+
+.free-ticket-result__action--dismiss-icon {
+    top: 1.8%;
+    right: 1.5%;
+    width: 8%;
+    height: 11%;
+}
+
+.free-ticket-result__action--use {
+    top: 80.1%;
+    left: 9.2%;
+    width: 39.2%;
+    height: 10.1%;
+}
+
+.free-ticket-result__action--later {
+    top: 80.1%;
+    left: 51%;
+    width: 39.6%;
+    height: 10.1%;
+}
+
+.free-ticket-result__action:focus-visible {
+    outline: 3px solid #ffd447;
+    outline-offset: 2px;
+}
+
+@media (max-width: 359px) {
+    .free-ticket-popup {
+        right: 8px;
+    }
+}
+
+@media (prefers-reduced-motion: no-preference) {
+    .free-ticket-popup__card {
+        animation: free-ticket-attention 2.6s ease-in-out infinite;
+    }
+
+    .free-ticket-popup__card:hover,
+    .free-ticket-popup__card:focus-visible,
+    .free-ticket-popup__card:active {
+        animation-play-state: paused;
+    }
+}
+
+@keyframes free-ticket-attention {
+    0%, 68%, 100% {
+        transform: translateY(0) rotate(0);
+        filter: drop-shadow(0 3px 4px rgba(0, 35, 105, .3));
+    }
+    73% { transform: translateY(-3px) rotate(-3deg); }
+    78% {
+        transform: translateY(-4px) rotate(3deg) scale(1.04);
+        filter: brightness(1.07) drop-shadow(0 0 5px rgba(255, 201, 40, .8));
+    }
+    83% { transform: translateY(-3px) rotate(-2deg); }
+    88% { transform: translateY(0) rotate(0); }
+}
+
+@keyframes free-ticket-loading {
+    to { transform: rotate(360deg); }
+}