const { Scenes, Markup } = require('telegraf'); const Order = require('../../models/Order'); const Product = require('../../models/Product'); const User = require('../../models/User'); const config = require('../../config'); const userController = require('../../controllers/userController'); const formatPrice = (price) => { return price.toString().replace(/\B(?=(\d{3})+(?!\d))/g, " "); }; // Common Keyboard const navKeyboard = (ctx, i18n, extras = []) => { const back = i18n.btn_back_nav || "⬅️ Back"; const cancel = i18n.btn_cancel || "❌ Cancel"; const k = [[back, cancel]]; if (extras.length) k.unshift(...extras); return Markup.keyboard(k).resize().oneTime(); }; const checkNav = (ctx) => { if (!ctx.message || !ctx.message.text) return null; const text = ctx.message.text; const i18n = ctx.i18n || require('../../locales').uz; const btnCancel = i18n.btn_cancel || "❌ Cancel"; const btnBack = i18n.btn_back_nav || "⬅️ Back"; // Robust checking against localised strings if (text === btnCancel || text === '❌ Bekor qilish' || text === '❌ Отмена' || text === '❌ Cancel') { ctx.scene.leave(); ctx.reply(i18n.cancel_process || "Cancelled", Markup.removeKeyboard()); return 'STOP'; } if (text === btnBack || text === '⬅️ Ortga' || text === '⬅️ Назад' || text === '⬅️ Back') { return 'BACK'; } return null; }; async function finishOrder(ctx) { try { const state = ctx.wizard.state; const cart = ctx.session.cart || []; const i18n = ctx.i18n || require('../../locales').uz; for (const item of cart) { await Product.updateOne({ id: item.id }, { $inc: { quantity: -item.count } }); } // Save Address if New and Delivery (Yandex) if (state.deliveryMethod === i18n.delivery_yandex && state.isNewAddress && state.location) { await User.updateOne( { id: ctx.from.id }, { $push: { addresses: { name: `Manzil ${new Date().toLocaleDateString()}`, latitude: state.location.latitude, longitude: state.location.longitude } } } ); } const orderData = { id: Date.now(), userId: ctx.from.id, user: state.fullName || ctx.from.first_name, phone: state.phone, deliveryMethod: state.deliveryMethod, location: state.location, addressText: state.addressText, deliveryTime: state.deliveryTime || 'Standard', comment: state.comment, items: cart, total: state.total, paymentMethod: state.paymentMethod, status: 'new' }; const newOrder = new Order(orderData); await newOrder.save(); // Update User Phone if new await User.updateOne({ id: ctx.from.id }, { phone: state.phone }); ctx.reply(`${i18n.confirm_order}\n#${newOrder.id}\n${i18n.order_details_pay} ${state.paymentMethod}`, Markup.removeKeyboard()); // Notify Admins let locInfo = ""; if (state.deliveryMethod === i18n.delivery_bts) { locInfo = `📝 BTS:\n${state.addressText}\n👤 F.I.O: ${state.fullName}`; } else if (state.location) { locInfo = `📍 Lokatsiya`; } else { locInfo = "🏃 Olib ketish"; } const adminText = `🆕 Yangi buyurtma!\n#${newOrder.id}\n👤: ${newOrder.user}\n📞: ${newOrder.phone}\n🚚: ${state.deliveryMethod}\n${locInfo}\n🕒: ${state.deliveryTime || '-'}\n💬: ${state.comment || "Yo'q"}\n💰: ${state.paymentMethod}\n\n` + cart.map(item => `- ${item.name} (${item.count}x)`).join('\n') + `\n\nJami: ${formatPrice(newOrder.total)} ${i18n.currency}`; const btns = Markup.inlineKeyboard([Markup.button.callback("👁 Ko'rish", `admin_order_${newOrder.id}`)]); for (const adminId of config.ADMIN_IDS) { try { await ctx.telegram.sendMessage(adminId, adminText, { parse_mode: 'HTML', ...btns }); if (state.location) await ctx.telegram.sendLocation(adminId, state.location.latitude, state.location.longitude); if (state.paymentProof) await ctx.telegram.sendPhoto(adminId, state.paymentProof, { caption: `🧾 Chek (#${newOrder.id})` }); } catch (e) { console.error(e); } } ctx.session.cart = []; return ctx.scene.leave(); } catch (err) { console.error("ORDER_CREATION_ERROR:", err); // Handle Duplicate Key Error (Self-Healing Attempt) if (err.code === 11000) { ctx.reply("⚠️ Tizimda qisqa nosozlik (ID Duplicate). Iltimos, qayta 'Buyurtma berish' tugmasini bosing."); // Ideally we retry, but for now asking user is safer than infinite loop } else { const errorMsg = (err.message || "").substring(0, 50); ctx.reply(`❌ Tizim xatoligi yuz berdi: ${errorMsg}\n\nIltimos, adminga xabar bering.`); // Notify admin about the crash for (const adminId of config.ADMIN_IDS) { ctx.telegram.sendMessage(adminId, `🚨 **CRITICAL CHECKOUT ERROR**:\nUser: ${ctx.from.id}\nError: ${err.message}`, { parse_mode: 'Markdown' }).catch(e => console.error(e)); } } return ctx.scene.leave(); } } const checkoutScene = new Scenes.WizardScene( 'CHECKOUT_SCENE', // Step 0: Pre-flight Check & Phone async (ctx) => { const i18n = ctx.i18n || require('../../locales').uz; const cart = ctx.session.cart || []; ctx.wizard.state.i18n = i18n; // Save specifically for wizard reuse // 1. Stock Validation const conflicts = []; for (const item of cart) { const product = await Product.findOne({ id: item.id }); if (!product || product.quantity < item.count) { conflicts.push(`${item.name} (${product ? product.quantity : 0})`); } } if (conflicts.length > 0) { ctx.reply(`⚠️ ${i18n.cart_stock_err.replace('{item}', conflicts.join(', ')).replace('{stock}', '...')} \n\nIltimos savatni yangilang.`); ctx.scene.leave(); return; // Stop checkout } // 2. Phone Input const user = await User.findOne({ id: ctx.from.id }); ctx.wizard.state.user = user; // NUCLEAR DEFENSIVE FIX const btnPhone = (i18n && i18n.phone_button) ? i18n.phone_button : "📱 Raqamni yuborish"; const btnCancel = (i18n && i18n.btn_cancel) ? i18n.btn_cancel : "❌ Bekor qilish"; const msgText = (i18n && i18n.phone_prompt) ? i18n.phone_prompt : "Telefon raqamingizni yuboring:"; console.log("CHECKOUT_DEBUG: Initializing Step 0 with safe keys:", { btnPhone, btnCancel }); let keyboardRows = []; // Add Saved Phone if exists if (user && user.phone) { const savedTemplate = (i18n && i18n.checkout_phone_saved) ? i18n.checkout_phone_saved : "📱 Mening raqamim: {phone}"; const savedBtnText = savedTemplate.replace('{phone}', user.phone || ""); if (savedBtnText) keyboardRows.push([Markup.button.text(savedBtnText)]); } // Add Contact Request Button if (btnPhone) keyboardRows.push([Markup.button.contactRequest(btnPhone)]); // Add Cancel Button if (btnCancel) keyboardRows.push([Markup.button.text(btnCancel)]); // Filter any potentially empty rows just in case keyboardRows = keyboardRows.filter(row => row && row.length > 0 && row[0]); try { ctx.reply(msgText, Markup.keyboard(keyboardRows).resize().oneTime()); } catch (e) { console.error("CHECKOUT_DEBUG_CRASH:", e); ctx.reply("Checkout Error (Keyboard). Type /start"); } return ctx.wizard.next(); }, // Step 1: Handle Phone -> Delivery (ctx) => { const i18n = ctx.wizard.state.i18n || ctx.i18n; const nav = checkNav(ctx); if (nav === 'STOP') return; let phone = ''; if (ctx.message.contact) { phone = ctx.message.contact.phone_number; } else if (ctx.message.text) { // Check if user clicked "My Saved Phone" const savedText = i18n.checkout_phone_saved.replace('{phone}', ''); if (ctx.message.text.includes(savedText.trim()) && ctx.wizard.state.user && ctx.wizard.state.user.phone) { phone = ctx.wizard.state.user.phone; } else { if (!/^\+?[0-9]{9,15}$/.test(ctx.message.text.replace(/\s/g, ''))) { ctx.reply(i18n.error_phone, navKeyboard(ctx, i18n)); return; } phone = ctx.message.text; } } else { ctx.reply(i18n.phone_prompt, navKeyboard(ctx, i18n)); return; } ctx.wizard.state.phone = phone; ctx.reply(i18n.delivery_select, Markup.keyboard([ [i18n.delivery_yandex, i18n.delivery_bts], [i18n.delivery_pickup], [i18n.btn_back_nav, i18n.btn_cancel] ]).resize().oneTime()); return ctx.wizard.next(); }, // Step 2: Handle Delivery Selection async (ctx) => { const i18n = ctx.wizard.state.i18n || ctx.i18n; const nav = checkNav(ctx); if (nav === 'STOP') return; if (nav === 'BACK') { ctx.wizard.selectStep(0); return await ctx.wizard.steps[0](ctx); } const text = ctx.message.text.trim(); // Trim input ctx.wizard.state.deliveryMethod = text; if (text.includes('BTS') || text.includes('Почта')) { ctx.wizard.state.deliveryMethod = i18n.delivery_bts; ctx.reply("📝 " + i18n.settings_addr, navKeyboard(ctx, i18n)); return ctx.wizard.next(); // Step 3 } if (text.includes('Yandex') || text.includes('Taxi') || text.includes('Такси')) { ctx.wizard.state.deliveryMethod = i18n.delivery_yandex; // Yandex Explicit Check const user = ctx.wizard.state.user; const btns = []; if (user && user.addresses && user.addresses.length > 0) { user.addresses.forEach((addr, i) => { btns.push([i18n.checkout_location_saved.replace('{name}', addr.name || 'Saved Address')]); }); } btns.push([Markup.button.locationRequest(i18n.location_button)]); btns.push([i18n.btn_back_nav, i18n.btn_cancel]); ctx.reply(i18n.address_prompt, Markup.keyboard(btns).resize().oneTime()); return ctx.wizard.selectStep(5); // Step 5 } // Pickup Check (Robust) if (text.includes('Olib ketish') || text.includes('Самовывоз') || text.includes('Pickup')) { ctx.wizard.state.deliveryMethod = i18n.delivery_pickup; ctx.wizard.state.location = null; const Settings = require('../../models/Settings'); const storeSettings = await Settings.findOne({ key: 'store_location' }); // Optimized query placement if (storeSettings && storeSettings.value) { const { text, latitude, longitude } = storeSettings.value; await ctx.reply(`📍 **Bizning manzil:**\n${text}`, { parse_mode: 'Markdown' }); await ctx.replyWithLocation(latitude, longitude); } else { await ctx.reply("📍 **Bizning manzil:**\nToshkent sh... (Default)"); } ctx.reply("🕒 Iltimos, vaqtni yozing (Masalan: 14:30):", Markup.keyboard([ ["📅 Bugun", "📅 Ertaga"], [i18n.btn_back_nav, i18n.btn_cancel] ]).resize().oneTime()); return ctx.wizard.selectStep(6); } // Unknown Input ctx.reply(i18n.delivery_select, Markup.keyboard([ [i18n.delivery_yandex, i18n.delivery_bts], [i18n.delivery_pickup], [i18n.btn_back_nav, i18n.btn_cancel] ]).resize().oneTime()); return; // Stay in step }, // Step 3: BTS Address (ctx) => { const i18n = ctx.wizard.state.i18n || ctx.i18n; const nav = checkNav(ctx); if (nav === 'STOP') return; if (nav === 'BACK') { ctx.reply(i18n.delivery_select, Markup.keyboard([ [i18n.delivery_yandex, i18n.delivery_bts], [i18n.delivery_pickup], [i18n.btn_back_nav, i18n.btn_cancel] ]).resize().oneTime()); return ctx.wizard.selectStep(2); } ctx.wizard.state.addressText = ctx.message.text; ctx.reply("👤 F.I.O (To'liq):", navKeyboard(ctx, i18n)); return ctx.wizard.next(); }, // Step 4: BTS Name (ctx) => { const i18n = ctx.wizard.state.i18n || ctx.i18n; const nav = checkNav(ctx); if (nav === 'STOP') return; if (nav === 'BACK') return ctx.wizard.selectStep(3); ctx.wizard.state.fullName = ctx.message.text; ctx.wizard.state.paymentMethod = i18n.payment_card; // BTS is usually Card only logic from previous Code ctx.reply("📝 Izoh (Comment)?", Markup.keyboard([ ["Yo'q"], [i18n.btn_back_nav, i18n.btn_cancel] ]).resize()); return ctx.wizard.selectStep(7); }, // Step 5: Yandex Address async (ctx) => { const i18n = ctx.wizard.state.i18n || ctx.i18n; const nav = checkNav(ctx); if (nav === 'STOP') return; if (nav === 'BACK') { ctx.reply(i18n.delivery_select, Markup.keyboard([ [i18n.delivery_yandex, i18n.delivery_bts], [i18n.delivery_pickup], [i18n.btn_back_nav, i18n.btn_cancel] ]).resize().oneTime()); return ctx.wizard.selectStep(2); } let location = null; if (ctx.message.location) { location = ctx.message.location; ctx.wizard.state.isNewAddress = true; } else if (ctx.message.text) { // Find in saved const user = ctx.wizard.state.user; if (user && user.addresses) { // Fuzzy match or exact match depending on format // We formatted buttons as: i18n.checkout_location_saved.replace('{name}', addr.name) // Let's try to match by name const savedFormat = i18n.checkout_location_saved.replace('{name}', ''); const name = ctx.message.text.replace(savedFormat, '').trim(); // This might be tricky if "Saved: " is empty. // Simple loop match const found = user.addresses.find(a => ctx.message.text.includes(a.name)); if (found) location = { latitude: found.latitude, longitude: found.longitude }; } } if (!location) { ctx.reply("⚠️ " + i18n.settings_loc, navKeyboard(ctx, i18n)); return; } ctx.wizard.state.location = location; ctx.reply("🕒 Iltimos, vaqtni yozing (Masalan: 15:00):", Markup.keyboard([ ["📅 Bugun", "📅 Ertaga"], [i18n.btn_back_nav, i18n.btn_cancel] ]).resize().oneTime()); return ctx.wizard.next(); // To Step 6 }, // Step 6: Time (ctx) => { const i18n = ctx.wizard.state.i18n || ctx.i18n; const nav = checkNav(ctx); if (nav === 'STOP') return; if (nav === 'BACK') { // Logic to go back to Address return ctx.wizard.selectStep(5); } ctx.wizard.state.deliveryTime = ctx.message.text; ctx.reply("📝 Izoh (Comment)?", Markup.keyboard([ ["Yo'q"], [i18n.btn_back_nav, i18n.btn_cancel] ]).resize()); return ctx.wizard.next(); }, // Step 7: Comment & Total Calc (ctx) => { const i18n = ctx.wizard.state.i18n || ctx.i18n; const nav = checkNav(ctx); if (nav === 'STOP') return; if (nav === 'BACK') return ctx.wizard.selectStep(6); const text = ctx.message.text; ctx.wizard.state.comment = text === "Yo'q" ? "" : text; const cart = ctx.session.cart || []; const total = cart.reduce((acc, item) => acc + (item.price * item.count), 0); ctx.wizard.state.total = total; // BTS is strictly Card (handled via pre-set or just flow) if (ctx.wizard.state.deliveryMethod === i18n.delivery_bts) { // BTS flow usually requires Card payment proof, so we treat it same as Card // But existing code jumped to Step 9 directly? // Line 358 in original code: return ctx.wizard.selectStep(9); // We keep that behavior or merge it. // Original: 358: if (ctx.wizard.state.deliveryMethod === i18n.delivery_bts) ... selectStep(9) // Let's keep existing BTS logic if it was working, but here we are replacing the block. // The original code handled BTS specifically at line 358. // Let's replicate that logic first. ctx.reply(`📨 **BTS Pay**\n\n8600 ...`, navKeyboard(ctx, i18n)); return ctx.wizard.selectStep(9); } // Conditional Payment Buttons let paymentButtons = []; if (ctx.wizard.state.deliveryMethod === i18n.delivery_pickup) { // Pickup -> Cash AND Card paymentButtons.push([i18n.payment_cash, i18n.payment_card]); } else { // Yandex (Delivery) -> Card Only paymentButtons.push([i18n.payment_card]); } paymentButtons.push([i18n.btn_back_nav, i18n.btn_cancel]); ctx.reply(i18n.payment_select, Markup.keyboard(paymentButtons).resize().oneTime()); return ctx.wizard.next(); }, // Step 8: Payment Method async (ctx) => { const i18n = ctx.wizard.state.i18n || ctx.i18n; const nav = checkNav(ctx); if (nav === 'STOP') return; if (nav === 'BACK') return ctx.wizard.selectStep(7); const choice = ctx.message.text; // Validation: Verify if choice is allowed for current delivery method // Pickup Logic: Allow both Cash and Card (Relaxed) // Delivery Logic: Strict Card Only if (ctx.wizard.state.deliveryMethod === i18n.delivery_yandex && choice !== i18n.payment_card) { return ctx.reply("⚠️ Yetkazib berish uchun faqat Karta orqali to'lov mavjud.", Markup.keyboard([[i18n.payment_card], [i18n.btn_back_nav, i18n.btn_cancel]]).resize()); } if (choice === i18n.payment_cash) { ctx.wizard.state.paymentMethod = 'Naqd'; return await finishOrder(ctx); } else if (choice === i18n.payment_card) { ctx.wizard.state.paymentMethod = 'Karta'; ctx.reply(`💳 **Humo:** 9860 ...\n\nChek yuboring:`, navKeyboard(ctx, i18n)); return ctx.wizard.next(); } else { ctx.reply("Tanlang."); } }, // Step 9: Proof async (ctx) => { const i18n = ctx.wizard.state.i18n || ctx.i18n; const nav = checkNav(ctx); if (nav === 'STOP') return; if (nav === 'BACK') return ctx.wizard.selectStep(8); if (ctx.message.photo) { ctx.wizard.state.paymentProof = ctx.message.photo[ctx.message.photo.length - 1].file_id; return await finishOrder(ctx); } else { ctx.reply("⚠️ Chek rasmini yuboring."); } } ); module.exports = checkoutScene;