diff --git a/index.js b/index.js index 1e923fc..671b602 100644 --- a/index.js +++ b/index.js @@ -159,6 +159,37 @@ const promoStore = { cooldownDays: 0, // Feature 10: 0 = no cooldown; >0 = days between claims per user }; +const PROMO_AUDIENCE = { + NEW_USER: 'new_user', + EXISTING_USER: 'existing_user', +}; + +const PROMO_REQUIREMENT_TYPE = { + WAGER: 'wager', + NO_REQUIREMENT: 'no_requirement', +}; + +const PROMO_GLOBAL_COOLDOWN_MS = 24 * 60 * 60 * 1000; + +const DEFAULT_PROMO_CODES = [ + { + id: 1, + code: process.env.PROMO_CODE || 'Sports3.5', + audience: PROMO_AUDIENCE.NEW_USER, + requirementType: PROMO_REQUIREMENT_TYPE.WAGER, + amountSC: Number(process.env.PROMO_AMOUNT_SC) || 3.5, + notes: 'New user welcome bonus', + active: true, + createdAt: Date.now(), + updatedAt: Date.now(), + }, +]; + +const promoCodeStore = { + nextId: 2, + codes: DEFAULT_PROMO_CODES.map((code) => ({ ...code })), +}; + const giveawayStore = { running: new Map(), history: new Map(), @@ -491,6 +522,10 @@ function createRuntimeStateSnapshot() { bugreports: promoStore.bugreports || [], cooldownDays: promoStore.cooldownDays || 0, }, + promoCodeStore: { + nextId: promoCodeStore.nextId, + codes: promoCodeStore.codes, + }, referralStore: { boosts: referralStore.boosts || [], archivedBoosts: referralStore.archivedBoosts || [], @@ -602,6 +637,19 @@ function loadRuntimeState() { if (typeof raw.promoStore.cooldownDays === 'number') promoStore.cooldownDays = raw.promoStore.cooldownDays; } + if (raw.promoCodeStore) { + const loadedCodes = Array.isArray(raw.promoCodeStore.codes) + ? raw.promoCodeStore.codes + .map((entry) => normalizePromoCodeEntry(entry, entry && entry.id)) + .filter(Boolean) + : []; + promoCodeStore.codes = loadedCodes.length > 0 + ? loadedCodes + : DEFAULT_PROMO_CODES.map((code) => ({ ...code })); + const maxId = promoCodeStore.codes.reduce((acc, code) => Math.max(acc, Number(code.id) || 0), 0); + promoCodeStore.nextId = Math.max(Number(raw.promoCodeStore.nextId) || 0, maxId + 1, 2); + } + if (raw.referralStore) { referralStore.boosts = Array.isArray(raw.referralStore.boosts) ? raw.referralStore.boosts : []; referralStore.archivedBoosts = Array.isArray(raw.referralStore.archivedBoosts) ? raw.referralStore.archivedBoosts : []; @@ -693,6 +741,9 @@ function createDefaultUser(user) { hasJoinedChannel: false, hasJoinedGroup: false, claimedPromo: false, + claimedPromoAt: 0, + claimedPromoCodes: [], + lastAnyPromoClaimAt: 0, muted: false, optOutBroadcasts: false, pendingAction: null, @@ -809,6 +860,11 @@ function getUser(ctx) { if (user.unreachable === undefined) user.unreachable = false; if (!user.giveawayHistory) user.giveawayHistory = []; if (user.supportTicket === undefined) user.supportTicket = null; + if (!user.claimedPromoCodes) user.claimedPromoCodes = []; + if (!Array.isArray(user.claimedPromoCodes)) user.claimedPromoCodes = []; + if (!user.claimedPromoAt) user.claimedPromoAt = 0; + if (!user.lastAnyPromoClaimAt) user.lastAnyPromoClaimAt = 0; + if (user.claimedPromo && !user.claimedPromoAt) user.claimedPromoAt = Date.now(); // Migrate: ensure settings block exists on older user records if (!user.settings) { user.settings = { playMode: 'miniapp', showQuickCommands: true, tooltipsEnabled: false }; @@ -1592,6 +1648,83 @@ function promoText() { return `🎁 NEW USER BONUS — ONCE PER ACCOUNT/IP\nPromo Code: ${promoStore.code}\nAmount: ${promoStore.amountSC} SC each\nRemaining Claims: ${promoStore.remainingClaims}\nTotal Claims: ${promoStore.totalClaimLimit}\n\n${promoStore.bonusRule}`; } +function normalizePromoCodeEntry(raw, idHint = null) { + if (!raw || typeof raw !== 'object') return null; + const code = String(raw.code || '').trim(); + if (!code) return null; + const audience = raw.audience === PROMO_AUDIENCE.EXISTING_USER + ? PROMO_AUDIENCE.EXISTING_USER + : PROMO_AUDIENCE.NEW_USER; + const requirementType = raw.requirementType === PROMO_REQUIREMENT_TYPE.NO_REQUIREMENT + ? PROMO_REQUIREMENT_TYPE.NO_REQUIREMENT + : PROMO_REQUIREMENT_TYPE.WAGER; + const parsedAmount = Number(raw.amountSC); + const amountSC = Number.isFinite(parsedAmount) && parsedAmount > 0 ? parsedAmount : 0; + const parsedId = Number(raw.id); + const id = Number.isFinite(parsedId) && parsedId > 0 ? parsedId : Number(idHint || 0); + if (!id) return null; + return { + id, + code, + audience, + requirementType, + amountSC, + notes: typeof raw.notes === 'string' ? raw.notes.trim() : '', + active: raw.active !== false, + createdAt: Number(raw.createdAt) || Date.now(), + updatedAt: Number(raw.updatedAt) || Date.now(), + }; +} + +function getActivePromoCodeForUser(user) { + const wantsNewUser = !user.claimedPromo; + const audience = wantsNewUser ? PROMO_AUDIENCE.NEW_USER : PROMO_AUDIENCE.EXISTING_USER; + const candidates = promoCodeStore.codes + .filter((code) => code.active && code.audience === audience) + .sort((a, b) => b.updatedAt - a.updatedAt); + return candidates[0] || null; +} + +function getPromoClaimCooldownRemainingMs(user) { + if (!user.lastAnyPromoClaimAt) return 0; + const elapsed = Date.now() - Number(user.lastAnyPromoClaimAt || 0); + return Math.max(0, PROMO_GLOBAL_COOLDOWN_MS - elapsed); +} + +function formatCooldown(ms) { + const totalMinutes = Math.ceil(ms / 60000); + const hours = Math.floor(totalMinutes / 60); + const minutes = totalMinutes % 60; + if (hours <= 0) return `${minutes}m`; + return `${hours}h ${minutes}m`; +} + +function formatPromoCodeLine(code) { + const audience = code.audience === PROMO_AUDIENCE.NEW_USER ? 'new-user' : 'existing-user'; + const requirement = code.requirementType === PROMO_REQUIREMENT_TYPE.NO_REQUIREMENT ? 'no-requirement' : 'wager'; + return `#${code.id} | ${code.code} | ${audience} | ${requirement} | ${code.active ? 'active' : 'paused'}`; +} + +function promoCodeManagerText() { + const lines = promoCodeStore.codes + .slice() + .sort((a, b) => a.id - b.id) + .map((code) => `• ${formatPromoCodeLine(code)}`); + return [ + '🧩 *Promo Code Manager*', + '', + '*Global Rules:*', + '• User must be registered under *GambleCodez* affiliate', + '• 24h cooldown between any promo claims', + '', + '*Configured Codes:*', + lines.length ? lines.join('\n') : 'No promo codes configured yet.', + '', + 'Use buttons below to add or toggle codes.', + ].join('\n'); +} + + function wager30AdminKeyboard() { return Markup.inlineKeyboard([ [Markup.button.callback('📋 Open (Pending) Requests', 'w30_admin_pending')], @@ -1715,6 +1848,8 @@ function adminGiveawayToolsKeyboard() { function adminPromoToolsKeyboard() { return Markup.inlineKeyboard([ [Markup.button.callback('📄 View Promo', 'admin_view')], + [Markup.button.callback('🧩 Manage Promo Codes', 'admin_manage_promo_codes')], + [Markup.button.callback('➕ Add Promo Code', 'admin_promo_code_add')], [Markup.button.callback('✏️ Edit Promo Code', 'admin_edit_code')], [Markup.button.callback('💰 Edit SC Amount', 'admin_edit_amount')], [Markup.button.callback('🔢 Edit Claim Limit', 'admin_edit_limit')], @@ -2174,16 +2309,20 @@ async function deleteEphemeralBonusPrompt(ctx, user) { } async function sendEphemeralBonusPrompt(ctx, user) { + if (user.claimedPromo) return; + const activeCode = getActivePromoCodeForUser(user); + if (!activeCode) return; await deleteEphemeralBonusPrompt(ctx, user); const sent = await ctx.reply( `🎁 *Claim Your New User Bonus* -Enter promo code *${promoStore.code}* on the Runewager affiliate page to claim your ${promoStore.amountSC} SC new user bonus!`, +Enter promo code *${activeCode.code}* on the Runewager affiliate page to claim your ${activeCode.amountSC} SC new user bonus!`, { parse_mode: 'Markdown', ...Markup.inlineKeyboard([ [Markup.button.url('🎁 Enter Promo Code (Mini App)', LINKS.miniAppClaim)], + [Markup.button.callback('✅ I Have Claimed — Next Step', 'promo_confirm_claimed_next')], [Markup.button.callback('⬅️ Main Menu', 'to_main_menu')], ]), }, @@ -4098,11 +4237,27 @@ bot.command('discord', async (ctx) => { }); bot.command('promo', async (ctx) => { - await ctx.reply(promoText(), Markup.inlineKeyboard([ - [Markup.button.url('🔗 Sign Up', LINKS.runewagerSignup)], - [Markup.button.url('📋 Enter Promo', LINKS.promoInput)], - [Markup.button.callback('❓ How to Claim', 'menu_claim_bonus')], - ])); + const user = getUser(ctx); + const activeCode = getActivePromoCodeForUser(user); + if (!activeCode) { + await ctx.reply('No promo code is active for your account tier right now.'); + return; + } + const audienceLabel = activeCode.audience === PROMO_AUDIENCE.NEW_USER ? 'New User' : 'Existing User'; + const requirementLabel = activeCode.requirementType === PROMO_REQUIREMENT_TYPE.NO_REQUIREMENT ? 'No extra wager requirement' : 'Wager requirement applies'; + await ctx.reply( + `🎁 ${audienceLabel} Promo +Code: ${activeCode.code} +Amount: ${activeCode.amountSC} SC +Requirement: ${requirementLabel} + +${AFFILIATE_REMINDER_TEXT}`, + Markup.inlineKeyboard([ + [Markup.button.url('🔗 Sign Up', LINKS.runewagerSignup)], + [Markup.button.url('📋 Enter Promo', LINKS.promoInput)], + [Markup.button.callback('❓ How to Claim', 'menu_claim_bonus')], + ]), + ); }); bot.command('setpromo', async (ctx) => { @@ -4833,16 +4988,46 @@ bot.action('menu_claim_bonus', async (ctx) => { return; } if (!promoStore.active) { - await ctx.reply('This promo is currently paused. Please check back soon.'); + await ctx.reply('Promo claims are currently paused. Please check back soon.'); return; } if (promoStore.remainingClaims <= 0) { - await ctx.reply('This promo is fully claimed.'); + await ctx.reply('Promo claims are currently exhausted.'); + return; + } + + const activeCode = getActivePromoCodeForUser(user); + if (!activeCode) { + await ctx.reply(user.claimedPromo + ? 'No existing-user promo code is active yet. Please check back soon.' + : 'No new-user promo code is active yet. Please check back soon.'); + return; + } + + const cooldownRemaining = getPromoClaimCooldownRemainingMs(user); + if (cooldownRemaining > 0) { + await ctx.reply(`⏳ Promo cooldown active. Please wait *${formatCooldown(cooldownRemaining)}* before claiming another code.`, { parse_mode: 'Markdown' }); return; } + const audienceLabel = activeCode.audience === PROMO_AUDIENCE.NEW_USER ? 'NEW USER BONUS — ONCE PER ACCOUNT/IP' : 'EXISTING USER BONUS CODE'; + const requirementLabel = activeCode.requirementType === PROMO_REQUIREMENT_TYPE.NO_REQUIREMENT + ? 'No additional wagering requirement for this code.' + : 'This code includes a wager requirement as listed in your account terms.'; + await ctx.reply( - `${promoText()}\n\n${AFFILIATE_REMINDER_TEXT}\n\nSteps:\n1) Sign up on Runewager under GambleCodez\n2) Open the affiliate/promo page\n3) Enter the promo code shown above`, + `🎁 ${audienceLabel} +Promo Code: ${activeCode.code} +Amount: ${activeCode.amountSC} SC each + +${AFFILIATE_REMINDER_TEXT} + +Requirement: ${requirementLabel} + +Steps: +1) Stay registered under GambleCodez affiliate +2) Open the affiliate/promo page +3) Enter the promo code shown above`, { parse_mode: 'Markdown', ...Markup.inlineKeyboard([ @@ -4853,31 +5038,81 @@ bot.action('menu_claim_bonus', async (ctx) => { ]), }, ); - // Show promo entry screenshot so users know exactly where to type the code await sendPhoto(ctx, IMG_PROMO_ENTRY, '📸 Step 3: Enter the promo code here (Menu → Refer a Friend → Promo entry box)'); }); -bot.action('promo_confirm_claimed', async (ctx) => { - const user = getUser(ctx); - if (!consumeSmartButton(user.id, 'promo_confirm_claimed')) { - await ctx.answerCbQuery('Already processed'); - return; + +function applyPromoClaimIntent(user) { + const activeCode = getActivePromoCodeForUser(user); + if (!activeCode) { + return { ok: false, message: user.claimedPromo ? 'No existing-user promo codes are active right now.' : 'No new-user promo codes are active right now.' }; } + if (activeCode.audience === PROMO_AUDIENCE.NEW_USER && user.claimedPromo) { + return { ok: false, message: 'You already claimed a new-user code. Only existing-user codes are now available.' }; + } + + const cooldownRemaining = getPromoClaimCooldownRemainingMs(user); + if (cooldownRemaining > 0) { + return { ok: false, message: `Promo cooldown active. Please wait ${formatCooldown(cooldownRemaining)} before claiming another code.` }; + } + if (promoStore.remainingClaims <= 0 || !promoStore.active) { - await ctx.answerCbQuery(); - await ctx.reply('This promo is not available right now.'); - return; + return { ok: false, message: 'This promo is not available right now.' }; } + if (!promoStore.claimsByUser.has(user.id)) { promoStore.claimsByUser.add(user.id); promoStore.remainingClaims = Math.max(0, promoStore.remainingClaims - 1); } + user.claimedPromo = true; - registerPromoClaim(user, promoStore.code || 'unknown'); + if (!user.claimedPromoAt) user.claimedPromoAt = Date.now(); + user.lastAnyPromoClaimAt = Date.now(); + if (!Array.isArray(user.claimedPromoCodes)) user.claimedPromoCodes = []; + user.claimedPromoCodes.push({ + codeId: activeCode.id, + code: activeCode.code, + audience: activeCode.audience, + requirementType: activeCode.requirementType, + ts: Date.now(), + }); + if (user.claimedPromoCodes.length > 50) user.claimedPromoCodes = user.claimedPromoCodes.slice(-50); + + registerPromoClaim(user, activeCode.code || 'unknown'); addBadge(user, 'promo_claimed'); user.profileXP += 20; - trackAnalytics('promo_claimed', { userId: user.id, code: promoStore.code }); + trackAnalytics('promo_claimed', { + userId: user.id, + code: activeCode.code, + audience: activeCode.audience, + requirementType: activeCode.requirementType, + }); + adminLog('promo_claim_click', { + userId: user.id, + code: activeCode.code, + codeId: activeCode.id, + audience: activeCode.audience, + requirementType: activeCode.requirementType, + }); trackOnboardingProgress(user); + return { ok: true, code: activeCode }; +} + + +bot.action('promo_confirm_claimed', async (ctx) => { + const user = getUser(ctx); + if (!consumeSmartButton(user.id, 'promo_confirm_claimed')) { + await ctx.answerCbQuery('Already processed'); + return; + } + + const result = applyPromoClaimIntent(user); + if (!result.ok) { + await ctx.answerCbQuery(); + await ctx.reply(result.message); + return; + } + await ctx.answerCbQuery('Saved'); await ctx.reply( '✅ Promo claim intent saved! If you need help, contact @GambleCodez on Telegram or use the support link below.', @@ -4890,6 +5125,30 @@ bot.action('promo_confirm_claimed', async (ctx) => { await sendPersistentUserMenu(ctx, user); }); +bot.action('promo_confirm_claimed_next', async (ctx) => { + const user = getUser(ctx); + if (!consumeSmartButton(user.id, 'promo_confirm_claimed_next')) { + await ctx.answerCbQuery('Already processed'); + return; + } + + const result = applyPromoClaimIntent(user); + if (!result.ok) { + await ctx.answerCbQuery(); + await ctx.reply(result.message); + return; + } + + await ctx.answerCbQuery('Saved'); + await reactToMessage(ctx, '🎁'); + const nextStep = getNextOnboardingStep(user); + if (nextStep >= 5) { + await ctx.reply('🎉 Onboarding complete! Use /bonus to claim your 30 SC bonus.', Markup.inlineKeyboard([[Markup.button.callback('🎯 Claim 30 SC Bonus', 'w30_request_start')]])); + return; + } + await showOnboardingPrompt(ctx, user, nextStep); +}); + bot.action('w30_rules', async (ctx) => { await ctx.answerCbQuery(); await ctx.reply(WAGER_BONUS_RULES_TEXT, { @@ -5688,6 +5947,35 @@ bot.action('admin_view', async (ctx) => { await ctx.reply(promoText()); }); +bot.action('admin_manage_promo_codes', async (ctx) => { + if (!requireAdmin(ctx)) return; + await ctx.answerCbQuery(); + await replaceCallbackPanel(ctx, promoCodeManagerText(), { + parse_mode: 'Markdown', + ...Markup.inlineKeyboard([ + [Markup.button.callback('➕ Add Promo Code', 'admin_promo_code_add')], + [Markup.button.callback('🔁 Toggle Promo Code', 'admin_promo_code_toggle_prompt')], + [Markup.button.callback('⬅️ Promo Tools', 'admin_cat_promo')], + ]), + }); +}); + +bot.action('admin_promo_code_add', async (ctx) => { + if (!requireAdmin(ctx)) return; + const user = getUser(ctx); + user.pendingAction = { type: 'admin_promo_code_add_input' }; + await ctx.answerCbQuery(); + await ctx.reply(`Send new code as:\n||||\n\nExample:\nSPORTS5|existing_user|wager|5|Weekend reload`); +}); + +bot.action('admin_promo_code_toggle_prompt', async (ctx) => { + if (!requireAdmin(ctx)) return; + const user = getUser(ctx); + user.pendingAction = { type: 'admin_promo_code_toggle_input' }; + await ctx.answerCbQuery(); + await ctx.reply('Send the promo code ID to toggle active/paused state (example: 2).'); +}); + bot.action('admin_edit_code', async (ctx) => { if (!requireAdmin(ctx)) return; const user = getUser(ctx); @@ -6417,6 +6705,65 @@ bot.on('text', async (ctx) => { } // Admin promo edits + if (action.type === 'admin_promo_code_add_input') { + if (!requireAdmin(ctx)) return; + const parts = text.split('|').map((v) => v.trim()); + if (parts.length < 4) { + await ctx.reply('Invalid format. Use: ||||'); + return; + } + const [codeRaw, audienceRaw, requirementRaw, amountRaw, ...notesParts] = parts; + const audience = audienceRaw === PROMO_AUDIENCE.EXISTING_USER ? PROMO_AUDIENCE.EXISTING_USER : PROMO_AUDIENCE.NEW_USER; + const requirementType = requirementRaw === PROMO_REQUIREMENT_TYPE.NO_REQUIREMENT ? PROMO_REQUIREMENT_TYPE.NO_REQUIREMENT : PROMO_REQUIREMENT_TYPE.WAGER; + const amountSC = Number(amountRaw); + if (!codeRaw || !Number.isFinite(amountSC) || amountSC <= 0) { + await ctx.reply('Invalid code or amount. Try again.'); + return; + } + const normalizedCode = codeRaw.toUpperCase(); + const existing = promoCodeStore.codes.find((code) => code.code.toUpperCase() === normalizedCode); + if (existing) { + await ctx.reply('A promo code with that label already exists. Use a different code text.'); + return; + } + const now = Date.now(); + promoCodeStore.codes.push({ + id: promoCodeStore.nextId, + code: normalizedCode, + audience, + requirementType, + amountSC, + notes: notesParts.join('|').trim(), + active: true, + createdAt: now, + updatedAt: now, + }); + promoCodeStore.nextId += 1; + user.pendingAction = null; + adminLog('promo_code_added', { by: ctx.from.id, code: normalizedCode, audience, requirementType, amountSC }); + await ctx.reply(`✅ Added promo code: ${normalizedCode}`); + return; + } + if (action.type === 'admin_promo_code_toggle_input') { + if (!requireAdmin(ctx)) return; + const id = Number(text); + if (!Number.isInteger(id) || id <= 0) { + await ctx.reply('Please send a valid promo code ID number.'); + return; + } + const code = promoCodeStore.codes.find((entry) => entry.id === id); + if (!code) { + await ctx.reply('Promo code ID not found.'); + return; + } + code.active = !code.active; + code.updatedAt = Date.now(); + user.pendingAction = null; + adminLog('promo_code_toggled', { by: ctx.from.id, id: code.id, active: code.active, code: code.code }); + await ctx.reply(`✅ Promo #${code.id} (${code.code}) is now ${code.active ? 'active' : 'paused'}.`); + return; + } + if (action.type === 'admin_edit_code_input') { if (!requireAdmin(ctx)) return; user.pendingAction = { type: 'admin_edit_code_confirm', data: text }; @@ -6829,6 +7176,11 @@ bot.action('admin_edit_code_yes', async (ctx) => { const user = getUser(ctx); if (!user.pendingAction || user.pendingAction.type !== 'admin_edit_code_confirm') return; promoStore.code = user.pendingAction.data; + const baseNewCode = promoCodeStore.codes.find((code) => code.id === 1 || (code.audience === PROMO_AUDIENCE.NEW_USER && code.active)); + if (baseNewCode) { + baseNewCode.code = promoStore.code; + baseNewCode.updatedAt = Date.now(); + } user.pendingAction = null; adminLog('edit_code', { by: ctx.from.id, code: promoStore.code }); await ctx.answerCbQuery('Updated'); @@ -6840,6 +7192,11 @@ bot.action('admin_edit_amount_yes', async (ctx) => { const user = getUser(ctx); if (!user.pendingAction || user.pendingAction.type !== 'admin_edit_amount_confirm') return; promoStore.amountSC = user.pendingAction.data; + const baseNewCode = promoCodeStore.codes.find((code) => code.id === 1 || (code.audience === PROMO_AUDIENCE.NEW_USER && code.active)); + if (baseNewCode) { + baseNewCode.amountSC = promoStore.amountSC; + baseNewCode.updatedAt = Date.now(); + } user.pendingAction = null; adminLog('edit_amount', { by: ctx.from.id, amountSC: promoStore.amountSC }); await ctx.answerCbQuery('Updated'); @@ -9202,23 +9559,43 @@ bot.command('gwhistory', async (ctx) => { // ── Feature 23: Promo Eligibility Check ────────────────────────────────── bot.command('promocheck', async (ctx) => { const user = getUser(ctx); - if (user.claimedPromo) { await ctx.reply('✅ You have already claimed the promo code!'); return; } + const activeCode = getActivePromoCodeForUser(user); const issues = []; - if (!promoStore.active) issues.push('❌ The promo is currently *inactive*.'); - if (promoStore.remainingClaims <= 0) issues.push('❌ The promo has *no remaining claims*.'); - if (promoStore.claimsByUser && promoStore.claimsByUser.has(user.id)) issues.push('❌ You have *already claimed* this code.'); - if (promoStore.cooldownDays > 0 && user.wagerBonus && user.wagerBonus.lastRequestAt > 0) { - const cd = promoStore.cooldownDays * 86400000; - const elapsed = Date.now() - user.wagerBonus.lastRequestAt; - if (elapsed < cd) issues.push(`⏳ Cooldown active — ${Math.ceil((cd - elapsed) / 86400000)} day(s) remaining.`); - } - if (!issues.length) { - await ctx.reply(`✅ *You are eligible to claim the promo!*\n\nCode: \`${promoStore.code}\`\nAmount: *${promoStore.amountSC} SC*\nRemaining claims: *${promoStore.remainingClaims}*\n\nTap below to claim now.`, { parse_mode: 'Markdown', ...Markup.inlineKeyboard([[Markup.button.callback('🎁 Claim Promo Now', 'menu_claim_bonus')]]) }); + if (!promoStore.active) issues.push('❌ Promo claims are currently *inactive*.'); + if (promoStore.remainingClaims <= 0) issues.push('❌ Promo claims are currently *exhausted*.'); + if (!activeCode) issues.push('❌ No active promo code is configured for your account tier right now.'); + + const cooldownRemaining = getPromoClaimCooldownRemainingMs(user); + if (cooldownRemaining > 0) { + issues.push(`⏳ Cooldown active — *${formatCooldown(cooldownRemaining)}* remaining before any next code claim.`); + } + + if (!issues.length && activeCode) { + const requirementLabel = activeCode.requirementType === PROMO_REQUIREMENT_TYPE.NO_REQUIREMENT ? 'No extra wager requirement' : 'Wager requirement applies'; + await ctx.reply( + `✅ *You are eligible to claim a promo code!* + +Code: \`${activeCode.code}\` +Amount: *${activeCode.amountSC} SC* +Tier: *${activeCode.audience === PROMO_AUDIENCE.NEW_USER ? 'New User' : 'Existing User'}* +Requirement: *${requirementLabel}* + +Tap below to claim now.`, + { parse_mode: 'Markdown', ...Markup.inlineKeyboard([[Markup.button.callback('🎁 Claim Promo Now', 'menu_claim_bonus')]]) }, + ); } else { - await ctx.reply(`📋 *Promo Eligibility Check*\n\n${issues.join('\n')}\n\nContact support if you have questions.`, { parse_mode: 'Markdown', ...Markup.inlineKeyboard([[Markup.button.callback('📩 Contact Support', 'menu_bugreport')]]) }); + await ctx.reply(`📋 *Promo Eligibility Check* + +${issues.join('\n')} + +${AFFILIATE_REMINDER_TEXT}`, { + parse_mode: 'Markdown', + ...Markup.inlineKeyboard([[Markup.button.callback('📩 Contact Support', 'menu_bugreport')]]), + }); } }); + // ── Feature 24: Language Info ───────────────────────────────────────────── bot.command('language', async (ctx) => { const user = getUser(ctx); diff --git a/prod-run.sh b/prod-run.sh index f51f692..1a3c9b3 100755 --- a/prod-run.sh +++ b/prod-run.sh @@ -460,6 +460,13 @@ if is_port_listening "$PRECHECK_PORT"; then free_port_if_conflicted "$PRECHECK_PORT" "$PID" || true fi +PRECHECK_HEALTH_URL="$(resolve_health_url)" +PRECHECK_PORT="${PRECHECK_HEALTH_URL#*://127.0.0.1:}" +PRECHECK_PORT="${PRECHECK_PORT%%/*}" +if is_port_listening "$PRECHECK_PORT"; then + free_port_if_conflicted "$PRECHECK_PORT" "$PID" || true +fi + if command -v systemctl >/dev/null 2>&1 && [[ -f "$SERVICE_FILE" ]]; then if systemctl restart "${APP_NAME}.service" 2>&1; then sleep 3