import dotenv from 'dotenv' import { Client, GatewayIntentBits } from 'discord.js' import cron from 'node-cron' dotenv.config() const barsyApiUrl = 'https://vkashtibar.barsyonline.com/endpoints/json/' const botToken = process.env.DISCORD_BOT_TOKEN const channelId = process.env.DISCORD_BAROVCI_CHANNEL_ID const barsyUser = process.env.BARSY_USER const barsyPassword = process.env.BARSY_PASSWORD const client = new Client({ intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMessages], }) function getPreviousDay() { const date = new Date() date.setDate(date.getDate() - 1) const year = date.getFullYear() const month = String(date.getMonth() + 1).padStart(2, '0') const day = String(date.getDate()).padStart(2, '0') return `${year}-${month}-${day}` } function getToday() { const date = new Date() const year = date.getFullYear() const month = String(date.getMonth() + 1).padStart(2, '0') const day = String(date.getDate()).padStart(2, '0') return `${year}-${month}-${day}` } function formatCurrency(value) { return new Intl.NumberFormat('bg-BG', { style: 'currency', currency: 'BGN' }).format(value) } async function fetchRelevantCategories() { try { const response = await fetch(`${barsyApiUrl}Categories_getlist`, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: 'Basic ' + btoa(`${barsyUser}:${barsyPassword}`), }, body: JSON.stringify({ action_type: 'values', columns: [ 'cat_id', 'cat_name', 'parent_id', 'cat_path_ids', 'child_articles_cnt', 'cat_public_view', ], }), }) const data = await response.json() if (!Array.isArray(data)) { console.error('Invalid categories response format:', data) return {} } const relevantCategories = {} data.forEach((row) => { // Include only top-level categories or categories with articles if ( !row.parent_id || // Top-level category row.child_articles_cnt > 0 // Category contains articles ) { relevantCategories[row.cat_id] = row.cat_name } }) console.log('Relevant categories:', relevantCategories) return relevantCategories } catch (error) { console.error('Error fetching relevant categories:', error) return {} } } async function fetchSalesByCategory(catId) { try { const previousDay = getPreviousDay() const response = await fetch(`${barsyApiUrl}Reports_sales_by_articles`, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: 'Basic ' + btoa(`${barsyUser}:${barsyPassword}`), }, body: JSON.stringify({ action_type: 'values', filters: { cat_id: catId, ref_date: previousDay, }, columns: ['oborot', 'total'], rows: 200, }), }) const data = await response.json() const rows = data.rows if (!Array.isArray(rows)) { console.warn(`No sales data for category ${catId}`) return 0 } const totalRevenue = rows.reduce( (sum, row) => sum + parseFloat(row.total || 0), 0 ) return totalRevenue } catch (error) { console.error(`Error fetching sales for category ${catId}:`, error) return 0 } } async function fetchSalesGroupedByRelevantCategories() { const relevantCategories = await fetchRelevantCategories() if (Object.keys(relevantCategories).length === 0) { return 'Няма релевантни категории.' } const categoryRevenue = {} for (const [catId, categoryName] of Object.entries(relevantCategories)) { const revenue = await fetchSalesByCategory(catId) if (revenue > 0) { categoryRevenue[categoryName] = revenue } } if (Object.keys(categoryRevenue).length === 0) { return 'Няма приходи за релевантните категории.' } // Format the result const sortedCategories = Object.entries(categoryRevenue) .sort((a, b) => b[1] - a[1]) .map(([category, revenue]) => `• ${category}: ${formatCurrency(revenue)}`) const totalRevenue = Object.values(categoryRevenue).reduce( (sum, revenue) => sum + revenue, 0 ) return `📊 Продажби по категории:\n${sortedCategories.join( '\n' )}\n\n💰 Общ приход: ${totalRevenue.toFixed(2)} BGN` } // Helper function to send revenue to Discord async function sendRevenueToDiscord(revenueMessage) { const channel = await client.channels.fetch(channelId) if (!channel) { console.error('Channel not found!') return } await channel.send(revenueMessage) } async function fetchSalesByType(paymethodId) { try { const previousDay = getPreviousDay() const today = getToday() const response = await fetch(`${barsyApiUrl}Reports_sales_by_accounts`, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: 'Basic ' + btoa(`${barsyUser}:${barsyPassword}`), }, body: JSON.stringify({ action_type: 'values', filters: { ref_date: [`${previousDay} 01:00:00`, `${today} 00:03:00`], paymethod_id: paymethodId, // 1 - cash, 2 - card }, rows: 200, }), }) if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`) } const data = await response.json() const totalOborot = data.rows.reduce((sum, row) => sum + row.total, 0) return totalOborot } catch (error) { console.error('Error fetching sales by type:', error) return 0 } } async function fetchCashTotal() { const cashTotal = await fetchSalesByType(1) return `💵 Общо в брой: ${formatCurrency(cashTotal)}` } async function fetchCardTotal() { const cardTotal = await fetchSalesByType(2) return `💳 Общо с карта: ${formatCurrency(cardTotal)}` } async function fetchFullDiscountsByClient() { try { const previousDay = getPreviousDay() const today = getToday() const response = await fetch(`${barsyApiUrl}Reports_sales_by_accounts`, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: 'Basic ' + btoa(`${barsyUser}:${barsyPassword}`), }, body: JSON.stringify({ action_type: 'values', filters: { ref_date: [`${previousDay} 02:59:59`, `${today} 00:03:00`], }, columns: ['oborot', 'client_name', 'discount', 'total', 'discounts', 'user_name'], rows: 1000, }), }) if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`) } const data = await response.json() // Filter for 100% discounts - looking for discount of -100 const fullDiscounts = data.rows.filter(row => { const discountValue = parseFloat(row.discount || 0) return discountValue === -100 }) // Group by client_name and sum up oborot const discountsByClient = {} // For anonymous clients, track by staff member const anonymousDiscountsByStaff = {} fullDiscounts.forEach(row => { const clientName = row.client_name || 'Неизвестен клиент' const oborot = parseFloat(row.oborot) || 0 const staffName = row.user_name || 'Неизвестен персонал' // Track total by client if (!discountsByClient[clientName]) { discountsByClient[clientName] = 0 } discountsByClient[clientName] += oborot // Track anonymous discounts by staff if (clientName === 'Анонимен') { if (!anonymousDiscountsByStaff[staffName]) { anonymousDiscountsByStaff[staffName] = 0 } anonymousDiscountsByStaff[staffName] += oborot } }) // Format the result if (Object.keys(discountsByClient).length === 0) { return '🎁 Няма 100% отстъпки' } const sortedClients = Object.entries(discountsByClient) .sort((a, b) => b[1] - a[1]) .map(([client, oborot]) => `• ${client}: ${formatCurrency(oborot)}`) const totalDiscount = Object.values(discountsByClient).reduce( (sum, oborot) => sum + oborot, 0 ) let result = `🎁 100% отстъпки по клиенти:\n${sortedClients.join('\n')}\n\nОбщо отстъпки: ${formatCurrency(totalDiscount)}` // Add breakdown of anonymous discounts by staff if there are any if (Object.keys(anonymousDiscountsByStaff).length > 0) { const sortedStaff = Object.entries(anonymousDiscountsByStaff) .sort((a, b) => b[1] - a[1]) .map(([staff, oborot]) => ` - ${staff}: ${formatCurrency(oborot)}`) result += `\n\n📊 Разбивка на отстъпки "Анонимен" по персонал:\n${sortedStaff.join('\n')}` } return result } catch (error) { console.error('Error fetching full discounts:', error) return '🎁 Грешка при извличане на отстъпки' } } // Schedule the daily task client.once('ready', () => { cron.schedule( '30 9 * * *', async () => { const revenueMessage = await fetchSalesGroupedByRelevantCategories() const cashTotalMessage = await fetchCashTotal() const cardTotalMessage = await fetchCardTotal() const discountsMessage = await fetchFullDiscountsByClient() const fullMessage = `${revenueMessage}\n\n${cashTotalMessage}\n${cardTotalMessage}\n\n${discountsMessage}` await sendRevenueToDiscord(fullMessage) }, { timezone: 'Europe/Sofia' } ) }) // Start the bot client.login(botToken)