Selaa lähdekoodia

thêm hủy sub

student 3 viikkoa sitten
vanhempi
commit
ff540deb34

BIN
API_Sub_MILLION.docx


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

@@ -337,6 +337,15 @@ namespace LotteryWebApp.Areas.Millions.Controllers
                 UserStatus userStatus = HttpContext.Session.GetComplexData<UserStatus>("userStatus");
                 model.userStatus = userStatus;
                 model.profile = profile;
+
+                var msisdn = HttpContext.Session.GetComplexData<string>("msisdn") ?? userStatus?.msisdn ?? profile?.users;
+                bool isSub = false;
+                if (!string.IsNullOrEmpty(msisdn))
+                {
+                    var subInfo = api.SubMillionInfoApi(configuration, msisdn);
+                    isSub = subInfo != null && subInfo.code == SubMillionCode.SUCCESS && subInfo.data != null && subInfo.data.isSub;
+                }
+                ViewBag.IsSub = isSub;
             }
             catch (Exception ex)
             {
@@ -1000,6 +1009,105 @@ namespace LotteryWebApp.Areas.Millions.Controllers
             }
         }
 
+        [HttpPost]
+        [ValidateAntiForgeryToken]
+        public IActionResult CheckSub()
+        {
+            try
+            {
+                var token = HttpContext.Session.GetComplexData<string>("token");
+                var msisdn = HttpContext.Session.GetComplexData<string>("msisdn");
+                log.Info($"[Millions.CheckSub] START - msisdn={msisdn}");
+
+                if (string.IsNullOrEmpty(token) || string.IsNullOrEmpty(msisdn))
+                {
+                    return Json(new { responseCode = Code.SESSION_EXPIRED, responseMessage = "Session expired" });
+                }
+
+                SubMillionResponse response = api.SubMillionInfoApi(configuration, msisdn);
+                log.Info($"[Millions.CheckSub] Response - msisdn={msisdn}, code={response.code}, msg={response.message}, isSub={response.data?.isSub}");
+
+                if (response.code == SubMillionCode.SUCCESS)
+                {
+                    return Json(new { responseCode = Code.SUCCESS, isSub = response.data != null && response.data.isSub });
+                }
+                if (response.code == SubMillionCode.NOT_REGISTERED)
+                {
+                    return Json(new { responseCode = Code.SUCCESS, isSub = false });
+                }
+                return Json(new { responseCode = Code.ERROR, responseMessage = response.message, subCode = response.code });
+            }
+            catch (Exception ex)
+            {
+                log.Error("[Millions.CheckSub] EXCEPTION", ex);
+                return Json(new { responseCode = Code.ERROR, responseMessage = ex.Message });
+            }
+        }
+
+        [HttpPost]
+        [ValidateAntiForgeryToken]
+        public IActionResult RegisterSub()
+        {
+            try
+            {
+                var token = HttpContext.Session.GetComplexData<string>("token");
+                var msisdn = HttpContext.Session.GetComplexData<string>("msisdn");
+                log.Info($"[Millions.RegisterSub] START - msisdn={msisdn}");
+
+                if (string.IsNullOrEmpty(token) || string.IsNullOrEmpty(msisdn))
+                {
+                    return Json(new { responseCode = Code.SESSION_EXPIRED, responseMessage = "Session expired" });
+                }
+
+                SubMillionResponse response = api.SubMillionRegisterApi(configuration, msisdn);
+                log.Info($"[Millions.RegisterSub] Response - msisdn={msisdn}, code={response.code}, msg={response.message}");
+
+                // code 2 (Already registered) is treated as registered
+                if (response.code == SubMillionCode.SUCCESS || response.code == SubMillionCode.ALREADY_REGISTERED)
+                {
+                    return Json(new { responseCode = Code.SUCCESS, subCode = response.code });
+                }
+                return Json(new { responseCode = Code.FAILURE, responseMessage = response.message, subCode = response.code });
+            }
+            catch (Exception ex)
+            {
+                log.Error("[Millions.RegisterSub] EXCEPTION", ex);
+                return Json(new { responseCode = Code.ERROR, responseMessage = ex.Message });
+            }
+        }
+
+        [HttpPost]
+        [ValidateAntiForgeryToken]
+        public IActionResult CancelSub()
+        {
+            try
+            {
+                var token = HttpContext.Session.GetComplexData<string>("token");
+                var msisdn = HttpContext.Session.GetComplexData<string>("msisdn");
+                log.Info($"[Millions.CancelSub] START - msisdn={msisdn}");
+
+                if (string.IsNullOrEmpty(token) || string.IsNullOrEmpty(msisdn))
+                {
+                    return Json(new { responseCode = Code.SESSION_EXPIRED, responseMessage = "Session expired" });
+                }
+
+                SubMillionResponse response = api.SubMillionCancelApi(configuration, msisdn);
+                log.Info($"[Millions.CancelSub] Response - msisdn={msisdn}, code={response.code}, msg={response.message}");
+
+                // code 0 (Success) or code 6 (Not registered / already canceled) are both treated as success
+                if (response.code == SubMillionCode.SUCCESS || response.code == SubMillionCode.NOT_REGISTERED)
+                {
+                    return Json(new { responseCode = Code.SUCCESS, subCode = response.code });
+                }
+                return Json(new { responseCode = Code.FAILURE, responseMessage = response.message, subCode = response.code });
+            }
+            catch (Exception ex)
+            {
+                log.Error("[Millions.CancelSub] EXCEPTION", ex);
+                return Json(new { responseCode = Code.ERROR, responseMessage = ex.Message });
+            }
+        }
+
         [HttpPost]
         [ValidateAntiForgeryToken]
         public IActionResult TermResult(string termType)

+ 171 - 2
website/Areas/Millions/Views/Home/GameHome.cshtml

@@ -6,7 +6,9 @@
 @using LotteryWebApp.Languages;
 @using LotteryWebApp.Controllers;
 @using LotteryWebApp.Common;
-
+@{
+    string SubText(string key) => Lang.ResourceManager.GetString(key, System.Globalization.CultureInfo.CurrentUICulture) ?? key;
+}
 
 
 @section Styles {
@@ -180,7 +182,7 @@
         <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;">
+            <a onclick="onPlayNow('@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;">
                 <div class="play-now-ring rounded-full" style="width:100%;height:100%;padding:4px;">
                     <div class="play-now-btn rounded-full flex flex-col items-center justify-center text-white text-[32px] font-black border-[1.8px] border-white animate-pulse-play" style="width:100%;height:100%;line-height:24px;">
                         <span class="uppercase">@Lang.millions_play</span>
@@ -214,6 +216,87 @@
         </div>
     </div>
 
+    <div id="subMillionModal" class="sub-modal fixed inset-0 z-[100] flex items-center justify-center hidden px-6 font-bricolage">
+        <div class="sub-modal__panel relative w-full max-w-[343px] bg-white rounded-[28px] overflow-hidden flex flex-col animate__animated animate__zoomIn animate__faster">
+            <button type="button" onclick="closeSubModal()" class="sub-modal__close" aria-label="@SubText("millions_close")">
+                <i class="fa-solid fa-xmark"></i>
+            </button>
+
+            <div id="subChoiceView" class="w-full flex flex-col">
+                <!-- Header -->
+                <div class="sub-modal__hero relative px-6 pt-7 pb-14 text-center overflow-hidden">
+                    <span class="sub-modal__badge">MILLIONS</span>
+                    <h2 class="text-gold-gradient font-[800] text-[26px] leading-tight mt-3 relative z-[1]">@SubText("sub_million_popup_title")</h2>
+                </div>
+                <img src="/Millions/assets/images/crown-balls.png" alt="" aria-hidden="true" class="sub-modal__crown" />
+
+                <!-- Body -->
+                <div class="px-6 pb-6 pt-2 flex flex-col">
+                    <p class="text-[#4a4a4a] text-[14px] leading-snug text-center mb-4">@SubText("sub_million_popup_desc")</p>
+
+                    <ul class="sub-modal__benefits mb-6">
+                        <li><span class="sub-modal__check"><i class="fa-solid fa-ticket"></i></span>@SubText("sub_million_benefit_daily")</li>
+                        <li><span class="sub-modal__check"><i class="fa-solid fa-bolt"></i></span>@SubText("sub_million_benefit_auto")</li>
+                        <li><span class="sub-modal__check"><i class="fa-solid fa-crown"></i></span>@SubText("sub_million_benefit_jackpot")</li>
+                    </ul>
+
+                    <button type="button" id="subRegisterBtn" onclick="registerSub()" class="sub-modal__primary">
+                        <span class="sub-modal__btn-label">@SubText("sub_million_btn_subscribe")</span>
+                        <span class="sub-modal__spinner" aria-hidden="true"></span>
+                    </button>
+                    <button type="button" onclick="buySingleTicket()" class="sub-modal__secondary mt-3">
+                        @SubText("sub_million_btn_buy_single")
+                    </button>
+                </div>
+            </div>
+
+            <div id="subResultView" class="w-full flex-col items-center hidden px-6 pt-8 pb-6">
+                <div id="subResultSuccess" class="hidden w-full flex-col items-center">
+                    <div class="sub-modal__result-icon sub-modal__result-icon--success mb-4">
+                        <i class="fa-solid fa-check"></i>
+                    </div>
+                    <h3 class="text-[#0a1f44] font-[800] text-[22px] leading-tight text-center mb-3">
+                        @SubText("sub_million_register_success")
+                    </h3>
+                    <p class="text-[#4a4a4a] text-[13.5px] leading-relaxed text-center mb-2 px-1">
+                        @SubText("sub_million_register_success_desc")
+                    </p>
+                    <p class="text-[#0a1f44] font-[700] text-[14px] leading-snug text-center mb-6">
+                        @SubText("sub_million_register_success_prompt")
+                    </p>
+                    <button type="button" onclick="buySingleTicket()" class="sub-modal__primary w-full">
+                        @SubText("sub_million_btn_buy_more")
+                    </button>
+                    <button type="button" onclick="closeSubModal()" class="sub-modal__btn-close mt-3 w-full">
+                        @SubText("millions_close")
+                    </button>
+                </div>
+                <div id="subResultFail" class="hidden w-full flex-col items-center">
+                    <div class="sub-modal__result-icon sub-modal__result-icon--fail mb-4">
+                        <i class="fa-solid fa-exclamation"></i>
+                    </div>
+                    <p class="text-black font-[800] text-[20px] leading-snug text-center mb-6">
+                        @SubText("sub_million_register_fail")
+                    </p>
+                    <button type="button" onclick="closeSubModal()" class="sub-modal__primary w-full">
+                        @SubText("millions_close")
+                    </button>
+                </div>
+                <div id="subResultInvalidMsisdn" class="hidden w-full flex-col items-center">
+                    <div class="sub-modal__result-icon sub-modal__result-icon--fail mb-4">
+                        <i class="fa-solid fa-phone-slash"></i>
+                    </div>
+                    <p class="text-black font-[800] text-[20px] leading-snug text-center mb-6">
+                        @SubText("sub_million_invalid_msisdn")
+                    </p>
+                    <button type="button" onclick="closeSubModal()" class="sub-modal__primary w-full">
+                        @SubText("millions_close")
+                    </button>
+                </div>
+            </div>
+        </div>
+    </div>
+
     @await Html.PartialAsync("~/Areas/Millions/Views/Shared/_BottomNavbar.cshtml")
 
 </div>
@@ -291,6 +374,92 @@
             });
         }
 
+        // ===== Sub MILLION =====
+        // null: unknown (check failed) -> fall back to the existing buy flow
+        var isSubMillion = null;
+        var pendingPlayTermType = null;
+        var isRegisteringSub = false;
+
+        var subCheckRequest = $.ajax({
+            url: subDomain + '/Millions/Home/CheckSub',
+            type: 'POST',
+            data: { __RequestVerificationToken: $('input[name="__RequestVerificationToken"]').val() }
+        }).done(function (res) {
+            if (res && res.responseCode === "0") {
+                isSubMillion = res.isSub === true;
+            }
+        });
+
+        function onPlayNow(termType) {
+            subCheckRequest.always(function () {
+                if (isSubMillion === false) {
+                    openSubModal(termType);
+                } else {
+                    checkTerm(termType);
+                }
+            });
+        }
+
+        function openSubModal(termType) {
+            pendingPlayTermType = termType;
+            $("#subResultView").addClass("hidden").removeClass("flex");
+            $("#subChoiceView").removeClass("hidden").addClass("flex");
+            $("#subRegisterBtn").prop("disabled", false).removeClass("is-loading");
+            $("#subMillionModal").removeClass("hidden").addClass("flex");
+        }
+
+        function closeSubModal() {
+            $("#subMillionModal").addClass("hidden").removeClass("flex");
+        }
+
+        $("#subMillionModal").on("click", function (e) {
+            if (e.target === this && !isRegisteringSub) closeSubModal();
+        });
+
+        function showSubResult(resultId) {
+            $("#subResultSuccess, #subResultFail, #subResultInvalidMsisdn").addClass("hidden").removeClass("flex");
+            $("#" + resultId).removeClass("hidden").addClass("flex");
+            $("#subChoiceView").addClass("hidden").removeClass("flex");
+            $("#subResultView").removeClass("hidden").addClass("flex");
+        }
+
+        function buySingleTicket() {
+            closeSubModal();
+            checkTerm(pendingPlayTermType || '@Constants.Millions_CODE');
+        }
+
+        function registerSub() {
+            if (isRegisteringSub) return;
+            isRegisteringSub = true;
+            $("#subRegisterBtn").prop("disabled", true).addClass("is-loading");
+
+            $.ajax({
+                url: subDomain + '/Millions/Home/RegisterSub',
+                type: 'POST',
+                data: { __RequestVerificationToken: $('input[name="__RequestVerificationToken"]').val() },
+                success: function (res) {
+                    if (res.responseCode === "0") {
+                        isSubMillion = true;
+                        showSubResult("subResultSuccess");
+                    } else if (res.responseCode === "@Code.SESSION_EXPIRED") {
+                        window.location.href = subDomain + "/Account/Login";
+                        return;
+                    } else if (res.subCode === 5) {
+                        showSubResult("subResultInvalidMsisdn");
+                    } else {
+                        showSubResult("subResultFail");
+                    }
+                },
+                error: function () {
+                    showSubResult("subResultFail");
+                },
+                complete: function () {
+                    isRegisteringSub = false;
+                    $("#subRegisterBtn").removeClass("is-loading");
+                }
+            });
+        }
+
         function startCountdown() {
             const targetHour = 23;
             const now = new Date();

+ 150 - 4
website/Areas/Millions/Views/Home/More.cshtml

@@ -3,8 +3,10 @@
     ViewData["Title"] = "Millions - More";
     ViewData["ActiveTab"] = "More";
     Layout = "~/Areas/Millions/Views/Shared/_Layout.cshtml";
+    string SubText(string key) => Lang.ResourceManager.GetString(key, System.Globalization.CultureInfo.CurrentUICulture) ?? key;
 }
 @using LotteryWebApp.Languages;
+@using LotteryWebApp.Common;
 
 <script src="https://cdn.tailwindcss.com"></script>
 <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
@@ -22,7 +24,7 @@
     }
 }
 
-<div class="main-container animate__animated animate__fadeIn min-h-screen relative flex flex-col pb-24 overflow-x-hidden" style="background-color: #EAEAEA !important;">
+<div class="main-container animate__animated animate__fadeIn min-h-screen relative flex flex-col pb-24 overflow-x-hidden font-bricolage" style="background-color: #EAEAEA !important;">
     
     <!-- User Profile Header (Center Aligned, Red Background) -->
     <div class="w-full bg-[#0062FF] flex flex-col items-center justify-center pt-8 pb-6 px-6 relative z-10 border-none m-0">
@@ -72,6 +74,25 @@
             <i class="fa-solid fa-chevron-right text-gray-400 text-sm opacity-80"></i>
         </div>
 
+        <!-- Item: Millions Subscription (Only visible if currently subscribed) -->
+        <div id="subCancelMenuItem" class="@(ViewBag.IsSub == true ? "flex" : "hidden") items-center justify-between py-[18px] border-b-2 border-dashed border-gray-100 transition-colors">
+            <div class="flex items-center gap-4">
+                <div class="w-6 flex items-center justify-center text-[#0062FF]">
+                    <i class="fa-solid fa-ticket text-[20px]"></i>
+                </div>
+                <div>
+                    <div class="flex items-center gap-2">
+                        <span class="text-[#212121] font-bold text-[13px]">@SubText("sub_million_package_name")</span>
+                        <span class="text-[10px] font-extrabold text-[#0062FF] bg-[#0062FF]/10 px-2 py-0.5 rounded-full uppercase">@SubText("sub_million_active_badge")</span>
+                    </div>
+                    <span class="text-[11px] text-[#797979]">5 HTG/@SubText("sub_million_benefit_daily_short")</span>
+                </div>
+            </div>
+            <button type="button" onclick="openCancelSubModal()" class="text-[12px] font-bold text-[#e53935] bg-red-50 hover:bg-red-100 active:scale-95 transition-all px-3 py-1.5 rounded-lg border border-red-200 cursor-pointer">
+                @SubText("sub_million_cancel_btn")
+            </button>
+        </div>
+
         <!-- Item: How to play Pick 10 
         <div class="flex items-center justify-between py-[18px] border-b-2 border-dashed border-gray-100 cursor-pointer transition-colors active:bg-gray-50" onclick="window.location.href='/Millions/Home/HowToPlay'">
             <div class="flex items-center gap-4">
@@ -92,8 +113,6 @@
         </div>
         -->
 
-
-
         <!-- Item: Contact Us -->
         <div class="flex items-center justify-between py-[18px] cursor-pointer transition-colors active:bg-gray-50">
             <div class="flex items-center gap-4">
@@ -138,10 +157,53 @@
     <partial name="_BottomNavbar" />
 </div>
 
+<!-- Cancel Subscription Modal -->
+<div id="cancelSubModal" class="fixed inset-0 z-[100] flex items-center justify-center hidden px-6 font-bricolage" style="background: rgba(5, 0, 60, 0.72); backdrop-filter: blur(4px); -webkit-backdrop-filter: blur(4px);">
+    <div class="relative w-full max-w-[343px] bg-white rounded-[24px] overflow-hidden flex flex-col p-6 animate__animated animate__zoomIn animate__faster shadow-2xl">
+        <!-- Close button in top-right -->
+        <button type="button" onclick="closeCancelSubModal()" class="absolute top-3 right-3 w-8 h-8 rounded-full bg-gray-100 hover:bg-gray-200 flex items-center justify-center text-gray-500 transition-colors text-sm cursor-pointer">
+            <i class="fa-solid fa-xmark"></i>
+        </button>
+
+        <!-- Confirm View -->
+        <div id="cancelSubConfirmView" class="w-full flex flex-col items-center pt-2">
+            <div class="w-16 h-16 rounded-full bg-red-50 border-4 border-red-100 flex items-center justify-center text-[#e53935] text-2xl mb-4">
+                <i class="fa-solid fa-bell-slash"></i>
+            </div>
+            <h3 class="text-[#0a1f44] font-[800] text-[20px] leading-tight text-center mb-2">
+                @SubText("sub_million_cancel_confirm_title")
+            </h3>
+            <p class="text-[#4a4a4a] text-[13px] leading-relaxed text-center mb-6 px-1">
+                @SubText("sub_million_cancel_confirm_desc")
+            </p>
+            <button type="button" id="confirmCancelSubBtn" onclick="executeCancelSub()" class="w-full bg-[#e53935] hover:bg-[#d32f2f] text-white font-[800] text-[16px] py-3 rounded-[14px] shadow-[0_4px_12px_rgba(229,57,53,0.3)] active:scale-95 transition-all flex items-center justify-center gap-2 cursor-pointer">
+                <span>@SubText("sub_million_cancel_confirm_yes")</span>
+                <i id="cancelSpinner" class="fa-solid fa-spinner fa-spin hidden"></i>
+            </button>
+            <button type="button" onclick="closeCancelSubModal()" class="mt-2.5 w-full py-2.5 rounded-[14px] text-gray-600 bg-gray-100 hover:bg-gray-200 active:scale-95 font-bold text-[14px] transition-all text-center cursor-pointer">
+                @SubText("sub_million_cancel_keep")
+            </button>
+        </div>
 
+        <!-- Result View -->
+        <div id="cancelSubResultView" class="w-full flex-col items-center pt-2 hidden">
+            <div id="cancelResultSuccessIcon" class="w-16 h-16 rounded-full bg-emerald-50 border-4 border-emerald-100 flex items-center justify-center text-emerald-600 text-2xl mb-4 hidden">
+                <i class="fa-solid fa-check"></i>
+            </div>
+            <div id="cancelResultFailIcon" class="w-16 h-16 rounded-full bg-red-50 border-4 border-red-100 flex items-center justify-center text-[#e53935] text-2xl mb-4 hidden">
+                <i class="fa-solid fa-exclamation"></i>
+            </div>
+            <h3 id="cancelResultTitle" class="text-[#0a1f44] font-[800] text-[20px] leading-tight text-center mb-2"></h3>
+            <p id="cancelResultMessage" class="text-[#4a4a4a] text-[13px] leading-relaxed text-center mb-6 px-1"></p>
+            <button type="button" onclick="closeCancelSubModal()" class="w-full bg-[#0062FF] hover:bg-[#0052d9] text-white font-[800] text-[16px] py-3 rounded-[14px] shadow-lg active:scale-95 transition-all cursor-pointer">
+                @Lang.v2_close
+            </button>
+        </div>
+    </div>
+</div>
 
+@section Scripts {
 <script>
-
     function setLanguage(newLang) {
         fetch(subDomain + '/Home/SetCulture?lang=' + newLang)
             .then(response => {
@@ -151,6 +213,90 @@
                 location.reload();
             });
     }
+
+    function openCancelSubModal() {
+        $('#cancelSubResultView').addClass('hidden').removeClass('flex');
+        $('#cancelSubConfirmView').removeClass('hidden').addClass('flex');
+        $('#cancelSubModal').removeClass('hidden').addClass('flex');
+    }
+
+    function closeCancelSubModal() {
+        $('#cancelSubModal').addClass('hidden').removeClass('flex');
+    }
+
+    function showCancelResult(isSuccess, message) {
+        $('#cancelSubConfirmView').addClass('hidden').removeClass('flex');
+        if (isSuccess) {
+            $('#cancelResultSuccessIcon').removeClass('hidden');
+            $('#cancelResultFailIcon').addClass('hidden');
+            $('#cancelResultTitle').text('@Html.Raw(SubText("sub_million_cancel_success_title"))');
+        } else {
+            $('#cancelResultSuccessIcon').addClass('hidden');
+            $('#cancelResultFailIcon').removeClass('hidden');
+            $('#cancelResultTitle').text('Failed');
+        }
+        $('#cancelResultMessage').text(message);
+        $('#cancelSubResultView').removeClass('hidden').addClass('flex');
+    }
+
+    function executeCancelSub() {
+        var $btn = $('#confirmCancelSubBtn');
+        $btn.prop('disabled', true).addClass('opacity-70');
+        $('#cancelSpinner').removeClass('hidden');
+
+        $.ajax({
+            url: subDomain + '/Millions/Home/CancelSub',
+            type: 'POST',
+            data: {
+                __RequestVerificationToken: $('input[name="__RequestVerificationToken"]').val()
+            },
+            success: function (res) {
+                if (res && res.responseCode === "0") {
+                    $('#subCancelMenuItem').addClass('hidden').removeClass('flex');
+                    showCancelResult(true, "@Html.Raw(SubText("sub_million_cancel_success_desc"))");
+                } else if (res && res.responseCode === "@Code.SESSION_EXPIRED") {
+                    window.location.href = subDomain + "/Account/Login";
+                } else {
+                    var msg = (res && res.responseMessage) || "@Html.Raw(SubText("sub_million_cancel_fail"))";
+                    showCancelResult(false, msg);
+                }
+            },
+            error: function () {
+                showCancelResult(false, "@Html.Raw(SubText("sub_million_cancel_fail"))");
+            },
+            complete: function () {
+                $btn.prop('disabled', false).removeClass('opacity-70');
+                $('#cancelSpinner').addClass('hidden');
+            }
+        });
+    }
+
+    $(document).ready(function () {
+        $('#cancelSubModal').on('click', function(e) {
+            if (e.target === this) closeCancelSubModal();
+        });
+
+        // Sync subscription status with backend API
+        $.ajax({
+            url: subDomain + '/Millions/Home/CheckSub',
+            type: 'POST',
+            global: false,
+            data: {
+                __RequestVerificationToken: $('input[name="__RequestVerificationToken"]').val()
+            },
+            success: function (res) {
+                if (res && res.responseCode === "0") {
+                    if (res.isSub === true) {
+                        $('#subCancelMenuItem').removeClass('hidden').addClass('flex');
+                    } else {
+                        $('#subCancelMenuItem').addClass('hidden').removeClass('flex');
+                    }
+                }
+            }
+        });
+    });
 </script>
+}
+
 
 

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

@@ -2149,4 +2149,28 @@ We’ll help you create an account in a few easy steps.</value>
   <data name="free_ticket_error_45_footer" xml:space="preserve"><value>Don't miss your chance to win up to 30,000,000 HTG!</value></data>
   <data name="free_ticket_banner_cta" xml:space="preserve"><value>Tap here to get your</value></data>
   <data name="free_ticket_banner_highlight" xml:space="preserve"><value>FREE TICKET!</value></data>
+  <data name="sub_million_popup_title" xml:space="preserve"><value>Subscribe to MILLIONS</value></data>
+  <data name="sub_million_popup_desc" xml:space="preserve"><value>Subscribe to automatically receive a MILLIONS ticket every day, or buy a single ticket now.</value></data>
+  <data name="sub_million_btn_subscribe" xml:space="preserve"><value>Subscribe</value></data>
+  <data name="sub_million_btn_buy_single" xml:space="preserve"><value>Buy single ticket</value></data>
+  <data name="sub_million_register_success" xml:space="preserve"><value>Subscription Successful!</value></data>
+  <data name="sub_million_register_success_desc" xml:space="preserve"><value>You have successfully subscribed to Millions for 5 HTG/day (auto-renewal). Please check your SMS for ticket details.</value></data>
+  <data name="sub_million_register_success_prompt" xml:space="preserve"><value>Would you like to buy more tickets?</value></data>
+  <data name="sub_million_btn_buy_more" xml:space="preserve"><value>Buy more ticket</value></data>
+  <data name="sub_million_register_fail" xml:space="preserve"><value>Subscription failed. Please try again later.</value></data>
+  <data name="sub_million_invalid_msisdn" xml:space="preserve"><value>Your phone number is not eligible for this package.</value></data>
+  <data name="sub_million_benefit_daily" xml:space="preserve"><value>Get a MILLIONS ticket every day</value></data>
+  <data name="sub_million_benefit_auto" xml:space="preserve"><value>Automatic, no need to buy every day</value></data>
+  <data name="sub_million_benefit_jackpot" xml:space="preserve"><value>Chance to win up to 30,000,000 HTG</value></data>
+  <data name="sub_million_package_name" xml:space="preserve"><value>MILLIONS Subscription</value></data>
+  <data name="sub_million_active_badge" xml:space="preserve"><value>Active</value></data>
+  <data name="sub_million_benefit_daily_short" xml:space="preserve"><value>day</value></data>
+  <data name="sub_million_cancel_btn" xml:space="preserve"><value>Cancel</value></data>
+  <data name="sub_million_cancel_confirm_title" xml:space="preserve"><value>Cancel Subscription?</value></data>
+  <data name="sub_million_cancel_confirm_desc" xml:space="preserve"><value>Are you sure you want to cancel your Millions subscription (5 HTG/day)? You will no longer automatically receive daily tickets.</value></data>
+  <data name="sub_million_cancel_confirm_yes" xml:space="preserve"><value>Yes, Cancel Subscription</value></data>
+  <data name="sub_million_cancel_keep" xml:space="preserve"><value>Keep Subscription</value></data>
+  <data name="sub_million_cancel_success_title" xml:space="preserve"><value>Subscription Canceled</value></data>
+  <data name="sub_million_cancel_success_desc" xml:space="preserve"><value>You have successfully canceled your Millions subscription.</value></data>
+  <data name="sub_million_cancel_fail" xml:space="preserve"><value>Failed to cancel subscription. Please try again later.</value></data>
 </root>

+ 24 - 0
website/Languages/Lang.resx

@@ -2150,4 +2150,28 @@ Apre yo fin tire 20 nimewo, sistem nan konte konbyen nimewo ki enpe oswa pe: &lt
   <data name="free_ticket_error_45_footer" xml:space="preserve"><value>Pa rate chans ou pou genyen jiska 30,000,000 HTG!</value></data>
   <data name="free_ticket_banner_cta" xml:space="preserve"><value>Peze la pou jwenn</value></data>
   <data name="free_ticket_banner_highlight" xml:space="preserve"><value>TIKÈ GRATIS ou!</value></data>
+  <data name="sub_million_popup_title" xml:space="preserve"><value>Abòne nan MILLIONS</value></data>
+  <data name="sub_million_popup_desc" xml:space="preserve"><value>Abòne pou resevwa yon tikè MILLIONS chak jou otomatikman, oswa achte yon tikè kounye a.</value></data>
+  <data name="sub_million_btn_subscribe" xml:space="preserve"><value>Abòne</value></data>
+  <data name="sub_million_btn_buy_single" xml:space="preserve"><value>Achte yon tikè</value></data>
+  <data name="sub_million_register_success" xml:space="preserve"><value>Abònman an reyisi!</value></data>
+  <data name="sub_million_register_success_desc" xml:space="preserve"><value>Ou fin abòne nan Millions pou 5 HTG/jou (renouvèlman otomatik). Tanpri tcheke SMS ou pou detay sou tikè a.</value></data>
+  <data name="sub_million_register_success_prompt" xml:space="preserve"><value>Èske ou ta renmen achte plis tikè?</value></data>
+  <data name="sub_million_btn_buy_more" xml:space="preserve"><value>Achte plis tikè</value></data>
+  <data name="sub_million_register_fail" xml:space="preserve"><value>Abònman an echwe. Tanpri eseye ankò pita.</value></data>
+  <data name="sub_million_invalid_msisdn" xml:space="preserve"><value>Nimewo telefòn ou pa kalifye pou pakè sa a.</value></data>
+  <data name="sub_million_benefit_daily" xml:space="preserve"><value>Resevwa yon tikè MILLIONS chak jou</value></data>
+  <data name="sub_million_benefit_auto" xml:space="preserve"><value>Otomatik, pa bezwen achte chak jou</value></data>
+  <data name="sub_million_benefit_jackpot" xml:space="preserve"><value>Chans pou genyen jiska 30,000,000 HTG</value></data>
+  <data name="sub_million_package_name" xml:space="preserve"><value>Abònman MILLIONS</value></data>
+  <data name="sub_million_active_badge" xml:space="preserve"><value>Aktif</value></data>
+  <data name="sub_million_benefit_daily_short" xml:space="preserve"><value>jou</value></data>
+  <data name="sub_million_cancel_btn" xml:space="preserve"><value>Anile</value></data>
+  <data name="sub_million_cancel_confirm_title" xml:space="preserve"><value>Anile abònman an?</value></data>
+  <data name="sub_million_cancel_confirm_desc" xml:space="preserve"><value>Èske ou sèten ou vle anile abònman Millions ou a (5 HTG/jou)? Ou p ap resevwa tikè chak jou otomatikman ankò.</value></data>
+  <data name="sub_million_cancel_confirm_yes" xml:space="preserve"><value>Wi, anile abònman an</value></data>
+  <data name="sub_million_cancel_keep" xml:space="preserve"><value>Kenbe abònman an</value></data>
+  <data name="sub_million_cancel_success_title" xml:space="preserve"><value>Abònman anile</value></data>
+  <data name="sub_million_cancel_success_desc" xml:space="preserve"><value>Ou fin anile abònman Millions ou a avèk siksè.</value></data>
+  <data name="sub_million_cancel_fail" xml:space="preserve"><value>Echèk pou anile abònman an. Tanpri eseye ankò pita.</value></data>
 </root>

+ 49 - 1
website/Service/ApiFunctions.cs

@@ -1,4 +1,4 @@
-using LotteryWebApp.Common;
+using LotteryWebApp.Common;
 using Microsoft.Extensions.Configuration;
 using Newtonsoft.Json;
 using System;
@@ -319,6 +319,54 @@ namespace LotteryWebApp.Service
             return deptObj;
         }
 
+        public SubMillionResponse SubMillionInfoApi(IConfiguration configuration, string msisdn)
+        {
+            String url = configuration.GetSection("sub_million_info").Value;
+            return SendSubMillion(configuration, url, msisdn);
+        }
+
+        public SubMillionResponse SubMillionRegisterApi(IConfiguration configuration, string msisdn)
+        {
+            String url = configuration.GetSection("sub_million_register").Value;
+            return SendSubMillion(configuration, url, msisdn);
+        }
+
+        public SubMillionResponse SubMillionCancelApi(IConfiguration configuration, string msisdn)
+        {
+            String url = configuration.GetSection("sub_million_cancel").Value;
+            return SendSubMillion(configuration, url, msisdn);
+        }
+
+        private SubMillionResponse SendSubMillion(IConfiguration configuration, string url, string msisdn)
+        {
+            if (String.IsNullOrWhiteSpace(url))
+            {
+                log.Error("Missing configuration: sub_million url");
+                return new SubMillionResponse { code = SubMillionCode.ERROR, message = "Missing configuration: sub_million url" };
+            }
+
+            SubMillionRequest request = new SubMillionRequest
+            {
+                username = configuration.GetSection("sub_million_username").Value,
+                password = configuration.GetSection("sub_million_password").Value,
+                msisdn = msisdn,
+                packageCode = configuration.GetSection("sub_million_package_code").Value ?? "MILLION5",
+                channel = configuration.GetSection("channel").Value
+            };
+
+            String jsonData = SendPostObj(request, ApiConstants.SERVICE_ID, url);
+            try
+            {
+                SubMillionResponse response = JsonConvert.DeserializeObject<SubMillionResponse>(jsonData);
+                return response ?? new SubMillionResponse { code = SubMillionCode.ERROR, message = "Empty response" };
+            }
+            catch (Exception ex)
+            {
+                log.Error($"SubMillion {url} returned invalid JSON: {jsonData}", ex);
+                return new SubMillionResponse { code = SubMillionCode.ERROR, message = "Invalid response" };
+            }
+        }
+
         private async Task<string> SendFormDataPostAsync(string token, String url)
         {
 

+ 40 - 0
website/Service/ApiModels.cs

@@ -424,4 +424,44 @@ namespace LotteryWebApp.Service
         public string token { get; set; }
         public string role { get; set; }
     }
+
+    public class SubMillionRequest
+    {
+        public string username { get; set; }
+        public string password { get; set; }
+        public string msisdn { get; set; }
+        public string packageCode { get; set; }
+        public string channel { get; set; }
+    }
+
+    public class SubMillionResponse
+    {
+        public int code { get; set; }
+        public string message { get; set; }
+        public SubMillionInfo data { get; set; }
+    }
+
+    public class SubMillionInfo
+    {
+        public string msisdn { get; set; }
+        public string packageCode { get; set; }
+        public bool isSub { get; set; }
+        public string channel { get; set; }
+        public string registerTime { get; set; }
+        public string cancelTime { get; set; }
+        public string cancelType { get; set; }
+        public decimal chargedToday { get; set; }
+        public bool ticketToday { get; set; }
+    }
+
+    public static class SubMillionCode
+    {
+        public const int SUCCESS = 0;
+        public const int ALREADY_REGISTERED = 2;
+        public const int ERROR = 3;
+        public const int INVALID_ACCOUNT = 4;
+        public const int INVALID_MSISDN = 5;
+        public const int NOT_REGISTERED = 6;
+        public const int PACKAGE_NOT_FOUND = 7;
+    }
 }

+ 9 - 0
website/appsettings.json

@@ -40,6 +40,15 @@
 
   "get_free_ticket": "http://171.244.50.177:8900/api/loto/getPromotionCode/data",
 
+  //"sub_million_info": "http://10.228.102.229:8120/api/sub/info",
+  //"sub_million_register": "http://10.228.102.229:8120/api/sub/register",
+  "sub_million_info": "http://171.244.50.177:8120/api/sub/info",
+  "sub_million_register": "http://171.244.50.177:8120/api/sub/register",
+  "sub_million_cancel": "http://171.244.50.177:8120/api/sub/cancel",
+  "sub_million_username": "lotto_sub",
+  "sub_million_password": "lotto_sub@123",
+  "sub_million_package_code": "MILLION5",
+
 
   "Logging": {
     "LogLevel": {

+ 195 - 0
website/wwwroot/Millions/css/game-home.css

@@ -46,3 +46,198 @@ html, body.millions-bg { background: #f5f7fb !important; }
         transform: scale(0.25) rotate(720deg);
     }
 }
+
+/* ===== Sub MILLION popup ===== */
+.sub-modal {
+    background: rgba(5, 0, 60, 0.72);
+    backdrop-filter: blur(4px);
+    -webkit-backdrop-filter: blur(4px);
+}
+.sub-modal__panel {
+    box-shadow: 0 24px 60px rgba(0, 20, 120, 0.45);
+}
+.sub-modal__close {
+    position: absolute;
+    top: 12px;
+    right: 12px;
+    z-index: 3;
+    width: 32px;
+    height: 32px;
+    border-radius: 9999px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    font-size: 16px;
+    color: #fff;
+    background: rgba(255, 255, 255, 0.18);
+    transition: background 0.2s;
+}
+.sub-modal__close:hover { background: rgba(255, 255, 255, 0.3); }
+
+.sub-modal__panel:has(#subResultView.flex) .sub-modal__close {
+    color: #797979;
+    background: #f1f3f8;
+}
+.sub-modal__hero {
+    background: linear-gradient(-1.8deg, #050068 3.87%, #0062ff 96.13%);
+}
+.sub-modal__hero::before,
+.sub-modal__hero::after {
+    content: "";
+    position: absolute;
+    border-radius: 9999px;
+    pointer-events: none;
+}
+.sub-modal__hero::before {
+    width: 180px;
+    height: 180px;
+    top: -70px;
+    left: -60px;
+    background: radial-gradient(circle, rgba(255, 255, 255, 0.16) 0%, rgba(255, 255, 255, 0) 70%);
+}
+.sub-modal__hero::after {
+    width: 220px;
+    height: 220px;
+    bottom: -120px;
+    right: -80px;
+    background: radial-gradient(circle, rgba(255, 187, 50, 0.28) 0%, rgba(255, 187, 50, 0) 70%);
+}
+.sub-modal__badge {
+    position: relative;
+    z-index: 1;
+    display: inline-block;
+    padding: 3px 12px;
+    border-radius: 9999px;
+    font-size: 11px;
+    font-weight: 800;
+    letter-spacing: 2px;
+    color: #3a2400;
+    background: linear-gradient(180deg, #fff1c9 0%, #ffbb32 100%);
+    box-shadow: 0 2px 6px rgba(0, 0, 0, 0.25);
+}
+.sub-modal__crown {
+    display: block;
+    width: 150px;
+    height: auto;
+    margin: -52px auto 0;
+    position: relative;
+    z-index: 2;
+    filter: drop-shadow(0 6px 10px rgba(0, 20, 120, 0.3));
+    animation: subCrownFloat 2.4s ease-in-out infinite;
+}
+@keyframes subCrownFloat {
+    0%, 100% { transform: translateY(0); }
+    50% { transform: translateY(-5px); }
+}
+.sub-modal__benefits {
+    display: flex;
+    flex-direction: column;
+    gap: 10px;
+    padding: 14px;
+    border-radius: 16px;
+    background: #f3f6ff;
+}
+.sub-modal__benefits li {
+    display: flex;
+    align-items: center;
+    gap: 10px;
+    font-size: 14px;
+    font-weight: 600;
+    color: #1a1a2e;
+    line-height: 1.3;
+}
+.sub-modal__check {
+    flex-shrink: 0;
+    width: 28px;
+    height: 28px;
+    border-radius: 9999px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    font-size: 13px;
+    color: #fff;
+    background: linear-gradient(180deg, #0400ff 0%, #0062ff 100%);
+}
+.sub-modal__primary {
+    position: relative;
+    width: 100%;
+    padding: 13px 16px;
+    border-radius: 14px;
+    font-size: 18px;
+    font-weight: 800;
+    color: #fff;
+    background: linear-gradient(180deg, #0062ff 0%, #001ec7 100%);
+    box-shadow: 0 6px 16px rgba(0, 62, 255, 0.35), inset 0 1px 0 rgba(255, 255, 255, 0.35);
+    transition: transform 0.15s, opacity 0.2s;
+}
+.sub-modal__primary:active { transform: scale(0.97); }
+.sub-modal__primary:disabled { opacity: 0.75; }
+.sub-modal__secondary {
+    width: 100%;
+    padding: 11px 16px;
+    border-radius: 14px;
+    font-size: 16px;
+    font-weight: 700;
+    color: #0062ff;
+    background: #fff;
+    border: 2px solid #cfdcff;
+    transition: transform 0.15s, border-color 0.2s;
+}
+.sub-modal__secondary:hover { border-color: #0062ff; }
+.sub-modal__secondary:active { transform: scale(0.97); }
+.sub-modal__spinner {
+    display: none;
+    width: 20px;
+    height: 20px;
+    border-radius: 9999px;
+    border: 3px solid rgba(255, 255, 255, 0.35);
+    border-top-color: #fff;
+    animation: subSpin 0.8s linear infinite;
+}
+.sub-modal__primary.is-loading .sub-modal__btn-label { visibility: hidden; }
+.sub-modal__primary.is-loading .sub-modal__spinner {
+    display: block;
+    position: absolute;
+    top: 50%;
+    left: 50%;
+    margin: -10px 0 0 -10px;
+}
+@keyframes subSpin { to { transform: rotate(360deg); } }
+.sub-modal__result-icon {
+    width: 84px;
+    height: 84px;
+    border-radius: 9999px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    font-size: 38px;
+    color: #fff;
+}
+.sub-modal__result-icon--success {
+    background: linear-gradient(180deg, #2ee59d 0%, #0fb36b 100%);
+    box-shadow: 0 0 0 10px rgba(15, 179, 107, 0.12);
+}
+.sub-modal__result-icon--fail {
+    background: linear-gradient(180deg, #ff7a7a 0%, #e53935 100%);
+    box-shadow: 0 0 0 10px rgba(229, 57, 53, 0.12);
+}
+.sub-modal__btn-close {
+    width: 100%;
+    padding: 12px 16px;
+    border-radius: 14px;
+    font-size: 16px;
+    font-weight: 700;
+    color: #4b5563;
+    background: #f3f4f6;
+    border: 1px solid #e5e7eb;
+    transition: transform 0.15s, background-color 0.2s, color 0.2s;
+    cursor: pointer;
+}
+.sub-modal__btn-close:hover {
+    background: #e5e7eb;
+    color: #1f2937;
+}
+.sub-modal__btn-close:active {
+    transform: scale(0.97);
+}
+

+ 25 - 0
website_natcash/LotteryWebAppNatcash.sln

@@ -0,0 +1,25 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.14.37027.9 d17.14
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LotteryWebAppNatcash", "LotteryWebAppNatcash.csproj", "{9107796C-9D02-8F9A-F124-A907296D8D8C}"
+EndProject
+Global
+	GlobalSection(SolutionConfigurationPlatforms) = preSolution
+		Debug|Any CPU = Debug|Any CPU
+		Release|Any CPU = Release|Any CPU
+	EndGlobalSection
+	GlobalSection(ProjectConfigurationPlatforms) = postSolution
+		{9107796C-9D02-8F9A-F124-A907296D8D8C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{9107796C-9D02-8F9A-F124-A907296D8D8C}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{9107796C-9D02-8F9A-F124-A907296D8D8C}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{9107796C-9D02-8F9A-F124-A907296D8D8C}.Release|Any CPU.Build.0 = Release|Any CPU
+	EndGlobalSection
+	GlobalSection(SolutionProperties) = preSolution
+		HideSolutionNode = FALSE
+	EndGlobalSection
+	GlobalSection(ExtensibilityGlobals) = postSolution
+		SolutionGuid = {EF4737E1-9EC2-40DA-B074-9A41CB4655B3}
+	EndGlobalSection
+EndGlobal