Spaces:
Paused
Paused
| 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 = `π <b>BTS:</b>\n${state.addressText}\nπ€ <b>F.I.O:</b> ${state.fullName}`; | |
| } else if (state.location) { | |
| locInfo = `π <a href="https://www.google.com/maps?q=${state.location.latitude},${state.location.longitude}">Lokatsiya</a>`; | |
| } else { | |
| locInfo = "π Olib ketish"; | |
| } | |
| const adminText = `π <b>Yangi buyurtma!</b>\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; | |