diff --git a/backend/.env.template b/backend/.env.template index 4af10ad..e031608 100644 --- a/backend/.env.template +++ b/backend/.env.template @@ -5,4 +5,8 @@ PGDATABASE="spiken" PGHOST="localhost" PGUSER="spiken" PGPASSWORD="spiken" -NODE_ENV="development" \ No newline at end of file +NODE_ENV="development" + +BANK_ACCOUNT_NUMMER="FI123" +BANK_ACCOUNT_NAME="Org RF" +BANK_ACCOUNT_REF="123" \ No newline at end of file diff --git a/backend/src/admin/index.ts b/backend/src/admin/index.ts index f435e88..04362b7 100644 --- a/backend/src/admin/index.ts +++ b/backend/src/admin/index.ts @@ -17,14 +17,17 @@ const adminMiddleware = bot.use(async (ctx, next) => { }) bot.command('admin', async (ctx) => { - const admin_message = - 'Följande admin kommandon existerar:\n' + - '/add_product För att lägga till en produkt\n' + - '/edit_product För att ändra en produkt\n' + - '/delete_product För att ta bort en produkt\n' + - '/exportera CSV-dump av alla transaktioner\n' + - '/historia_all Se de senaste händelserna för alla användare\n' + - '/saldo_upload För att lägga till transaktioner manuellt\n' + const admin_message = [ + 'Följande admin kommandon existerar:', + '/add_product För att lägga till en produkt', + '/edit_product För att ändra en produkt', + '/delete_product För att ta bort en produkt', + '/exportera CSV-dump av alla transaktioner', + '/historia_all Se de senaste händelserna för alla användare', + '/saldo_all Se alal användares saldo', + '/saldo_upload För att lägga till transaktioner manuellt', + '/shame Skickar ett meddelande till alla med negativ saldo som påminner dem att betala. Lägg till _ för att endast pinga folk under -.', + ].join('\n') return ctx.reply(admin_message) }) diff --git a/backend/src/admin/saldo.ts b/backend/src/admin/saldo.ts index bc637ae..a2e7840 100644 --- a/backend/src/admin/saldo.ts +++ b/backend/src/admin/saldo.ts @@ -3,14 +3,28 @@ import { parse } from 'csv-parse/sync' import { exportTransactions, exportTransactionTemplate, + getAllBalances, purchaseItemForMember, } from '../transactions.js' import { createCsv, formatTransaction } from '../utils.js' import { config } from '../config.js' import { ContextWithScenes } from './scene.js' +//#region Misc + const bot = new Composer() +const formattedAccountString = + '
' +
+  `Mottagare: ${config.bankAccount.name}\n` +
+  `Kontonummer: ${config.bankAccount.number}\n` +
+  `Referensnummer: ${config.bankAccount.ref}\n` +
+  '
' + +//endregion + +//#region Export + const exportCommand = bot.command('exportera', async (ctx) => { const res = await exportTransactions() const csv = createCsv(res) @@ -20,12 +34,17 @@ const exportCommand = bot.command('exportera', async (ctx) => { }) }) +//endregion + +//#region Historia all + const allHistoryCommand = bot.command('historia_all', async (ctx) => { const history = await exportTransactions() const historyString = '```' + history.rows + .sort((a, b) => a.created_at.getTime() - b.created_at.getTime()) .map((row) => formatTransaction( row.user_name, @@ -40,6 +59,26 @@ const allHistoryCommand = bot.command('historia_all', async (ctx) => { return ctx.reply(historyString, { parse_mode: 'Markdown' }) }) +//endregion + +//#region Saldo all + +const allSaldoCommand = bot.command('saldo_all', async (ctx) => { + const balances = (await getAllBalances()).sort( + (a, b) => b.balance - a.balance + ) + + const historyString = + `User saldos:
` +
+    balances.map((b) => `${b.userName}: ${b.balance}`).join('\n') +
+    '
' + + return ctx.reply(historyString, { parse_mode: 'HTML' }) +}) + +//endregion + +//#region Manual saldo update const saldoTemplateCommand = bot.command('saldo_template', async (ctx) => { const csv = createCsv(await exportTransactionTemplate()) ctx.replyWithDocument({ @@ -187,9 +226,47 @@ const saldoUploadCommand = bot.command('saldo_upload', async (ctx) => { await ctx.scene.enter('saldo_upload_scene') }) -export default Composer.compose([ - exportCommand, - allHistoryCommand, - saldoTemplateCommand, - saldoUploadCommand, -]) +//endregion + +//#region Shame + +/** + * The command sends a message to each user that has a saldo lower than the cut-off with a default cut-off of 0. + * I.e sending `/shame` will send a message to all users with negative score, + * while ending `shame_20` will send a message to all users with a balance of less than -20. + */ +const shameCommand = bot.hears(/^\/shame(?:_(\d+))?$/, async (ctx) => { + const saldoCutOff = ctx.match[1] ? Number(ctx.match[1]) : 0 + + const balances = (await getAllBalances()).filter( + (obj) => obj.balance < -saldoCutOff + ) + + for await (const { userId, balance } of balances) { + const message = + `Ert saldo är nu ${balance.toFixed( + 2 + )}€. Det skulle vara att föredra att Ert saldo hålls positivt. ` + + `Ni kan betala in på Er spik genom att skicka en summa, dock helst minst ${-balance.toFixed( + 2 + )}€, till följande konto: ` + + formattedAccountString + await ctx.telegram.sendMessage(userId, message, { + parse_mode: 'HTML', + }) + } + + if (balances.length > 0) { + const adminMessage = + `Följande användare pingades med en cut-off av -${saldoCutOff}€:
` +
+      balances.map((b) => `${b.userName}: ${b.balance}`).join('\n') +
+      '
' + ctx.telegram.sendMessage(config.adminChatId, adminMessage, { + parse_mode: 'HTML', + }) + } +}) + +//endregion + +export default bot diff --git a/backend/src/config.ts b/backend/src/config.ts index f09d724..9602c3c 100644 --- a/backend/src/config.ts +++ b/backend/src/config.ts @@ -5,10 +5,18 @@ const processEnvSchema = z.object({ BOT_TOKEN: z.string(), CHAT_ID: z.string().transform((val) => Number(val)), ADMIN_CHAT_ID: z.string().transform((val) => Number(val)), + BANK_ACCOUNT_NUMMER: z.string(), + BANK_ACCOUNT_NAME: z.string(), + BANK_ACCOUNT_REF: z.string(), }) const typedProcessEnv = processEnvSchema.parse(process.env) export const config = { botToken: typedProcessEnv.BOT_TOKEN, chatId: typedProcessEnv.CHAT_ID, adminChatId: typedProcessEnv.ADMIN_CHAT_ID, + bankAccount : { + number: typedProcessEnv.BANK_ACCOUNT_NUMMER, + name: typedProcessEnv.BANK_ACCOUNT_NAME, + ref: typedProcessEnv.BANK_ACCOUNT_REF, + } } diff --git a/backend/src/index.ts b/backend/src/index.ts index 2ad7b44..6888638 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -21,10 +21,7 @@ import { /* Toiveiden tynnyri: -- Admin interface, lägg till/ta bort saldo -- Skamlistan, posta alla med negativt i chatten med en @ - En 'vapaa myynti' command med description och summa -- "Undo" funktionalitet */ const bot = new Telegraf(config.botToken) @@ -168,15 +165,16 @@ products.forEach(({ name, description, price_cents }) => { bot.command('historia', async (ctx) => { const history = await exportTransactionsForOneUser(ctx.from.id, 30) - const parsedHistory = history.rows.map( - ({ created_at, description, amount_cents }) => { + const parsedHistory = history.rows + .map(({ created_at, description, amount_cents }) => { return { created_at, description, amount_cents, } - } - ) + }) + .sort((a, b) => a.created_at.getTime() - b.created_at.getTime()) + const saldo = await getBalanceForMember(ctx.from.id) var res = `Ditt nuvarande saldo är ${saldo}. Här är din historia:\`\`\`` parsedHistory.forEach((row) => { diff --git a/backend/src/transactions.ts b/backend/src/transactions.ts index 7cd0ea5..ffe2bc5 100644 --- a/backend/src/transactions.ts +++ b/backend/src/transactions.ts @@ -52,6 +52,27 @@ export const getBalanceForMember = async (userId: number) => { } } +export const getAllBalances = async (): Promise< + { userId: string; userName: string; balance: number }[] +> => { + const res = await pool.query( + `--sql + SELECT DISTINCT t.user_id, t.user_name, grouped.balance + FROM transactions t, ( + SELECT user_id, SUM (amount_cents) AS balance + FROM transactions + GROUP BY user_id + ) grouped WHERE t.user_id = grouped.user_id` + ) + return res.rows.map((row) => { + return { + userId: row.user_id, + userName: row.user_name, + balance: Number(row.balance) / 100, + } + }) +} + export const exportTransactions = async (): Promise< QueryResult > => {