Skip to content

Commit

Permalink
Shame (#9)
Browse files Browse the repository at this point in the history
* Add shame command

* Move balance calc to sql

* Add cutoff possibilty

* Add admin info

* Refactor and add saldo_all command

* Sort history correctly

* Fix negative amount and euro mark

* Fix language typos

* Move bank account info to env

* Change minor string readibility
  • Loading branch information
christiansegercrantz authored Apr 23, 2024
1 parent 23a2096 commit f8bc511
Show file tree
Hide file tree
Showing 6 changed files with 133 additions and 22 deletions.
6 changes: 5 additions & 1 deletion backend/.env.template
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,8 @@ PGDATABASE="spiken"
PGHOST="localhost"
PGUSER="spiken"
PGPASSWORD="spiken"
NODE_ENV="development"
NODE_ENV="development"

BANK_ACCOUNT_NUMMER="FI123"
BANK_ACCOUNT_NAME="Org RF"
BANK_ACCOUNT_REF="123"
19 changes: 11 additions & 8 deletions backend/src/admin/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 _<nummer> för att endast pinga folk under -<nummer>.',
].join('\n')
return ctx.reply(admin_message)
})

Expand Down
89 changes: 83 additions & 6 deletions backend/src/admin/saldo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<ContextWithScenes>()

const formattedAccountString =
'<pre>' +
`Mottagare: ${config.bankAccount.name}\n` +
`Kontonummer: ${config.bankAccount.number}\n` +
`Referensnummer: ${config.bankAccount.ref}\n` +
'</pre>'

//endregion

//#region Export

const exportCommand = bot.command('exportera', async (ctx) => {
const res = await exportTransactions()
const csv = createCsv(res)
Expand All @@ -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,
Expand All @@ -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:<pre>` +
balances.map((b) => `${b.userName}: ${b.balance}`).join('\n') +
'</pre>'

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({
Expand Down Expand Up @@ -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 <b>${balance.toFixed(
2
)}€</b>. 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}€:<pre>` +
balances.map((b) => `${b.userName}: ${b.balance}`).join('\n') +
'</pre>'
ctx.telegram.sendMessage(config.adminChatId, adminMessage, {
parse_mode: 'HTML',
})
}
})

//endregion

export default bot
8 changes: 8 additions & 0 deletions backend/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
}
12 changes: 5 additions & 7 deletions backend/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<ContextWithScenes>(config.botToken)
Expand Down Expand Up @@ -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) => {
Expand Down
21 changes: 21 additions & 0 deletions backend/src/transactions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Transaction>
> => {
Expand Down

0 comments on commit f8bc511

Please sign in to comment.