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)