Select Git revision
transactions.ts
Code owners
Assign users and groups as approvers for specific file changes. Learn more.
transactions.ts 7.18 KiB
import { db } from "$lib/server/database";
/**
* Warning! The passed vouchers will only be activated. To also make the user pay for them,
* make sure that they are also included in the items array using the Voucher Custom Code.
*/
export async function buyArticles(userId, card, items, vouchers, noBalance=false) {
const [user, ...itemTransactions] = await db.$transaction([
db.user.update({
where: { id: userId },
data: {
balance: {
decrement: noBalance===true ? 0 : items.reduce((sum, item) => sum + item.price + item.premium, 0)
},
}
}),
...items.map(item => db.item.update({
where: { code: item.code },
data: {
stock: { decrement: 1 },
bought: { increment: 1 },
transactions: {
create: {
price: item.price,
premium: item.premium,
card,
user: { connect: { id: userId } }
}
}
}
})),
...vouchers.map(voucher => db.voucher.update({
where: { code: voucher.code },
data: {
activated: true,
}
}))
]);
return { user, itemTransactions };
}
export async function transferMoney(fromId, card, toId, amount){
const [to, from] = await db.$transaction([
db.user.update({
where: { id: toId },
data: {
balance: {
increment: amount
},
moneyTransfersReceived: {
create: {
amount,
card,
from: { connect: { id: fromId } }
}
}
}
}),
db.user.update({
where: { id: fromId },
data: {
balance: {
decrement: amount
}
}
})
]);
return { to, from };
}
/**
* Warning! This function returns null if the voucher doesn't exist, is already used, or is expired.
* Returns the amount/price of the voucher if it is not activated yet.
*/
export async function useVoucher(userId, voucherCode){
const voucher = await db.voucher.findUnique({ where: { code: voucherCode } });
// if voucher doesn't exist, is already used, or is expired, return null
if(!voucher || voucher.usedById || (voucher.expiresAt && voucher.expiresAt <= new Date())) return null;
// if voucher is not activated yet, return price
if(!voucher.activated) return voucher.amount;
return db.voucher.update({
where: { code: voucherCode },
data: {
usedAt: new Date(),
usedBy: {
connect: { id: userId },
update: {
balance: {
increment: voucher.amount
}
}
}
}
});
}
export async function getMoneyTransactions(userId, beforeId?: number, limit=100) {
return db.transaction.findMany({
take: limit,
where: { userId, id: { lt: beforeId } },
orderBy: { id: "desc" }
});
}
export async function getItemTransactions(userId, beforeId?: number, limit=100) {
return db.itemTransaction.findMany({
take: limit,
where: { userId, id: { lt: beforeId } },
orderBy: { id: "desc" },
include: { item: true }
});
}
export async function getMoneyTransfers(userId, beforeId?: number, limit=100) {
return db.moneyTransfer.findMany({
take: limit,
where: { OR: [{ fromId: userId }, { toId: userId }], id: { lt: beforeId } },
orderBy: { id: "desc" },
include: { from: true, to: true }
});
}
export async function getUsedVouchers(usedById, beforeId: number, limit=100) {
const args: {skip: 1, cursor: {id: number}}|{} = beforeId ? { skip: 1, cursor: { id: beforeId } } : {}; // skip cursor if present
return db.voucher.findMany({
take: limit,
...args,
where: { usedById },
orderBy: [{ id: "desc" }, { usedAt: "desc" }] // in case usedAt is the same for multiple vouchers, order by id to make sure the order is consistent
});
}
type BeforeIds = {
moneyTransactions?: number,
itemTransactions?: number,
moneyTransfers?: number,
voucherUses?: number
};
export async function fetchTransactions(userId: number, balanceAfterwards: number, before: BeforeIds, limit: number) {
/*
For every type of transaction that changes the user's balance the limit of transactions to return
is used. This means at most we fetch 4*limit transactions from the database.
Since we don't know how many transactions of each type are within the last [limit] transactions
we have to fetch all of them and then sort them by date. Afterwards we only take the first [limit]
transactions. We can't take more than [limit] transactions because we don't know if we have fetched
the correct next transaction.
(That is not entirely true. We could return transactions until we have [limit] of one type. We
could do that, but I don't think it's worth it and it would mean that the function gets a
"limit" parameter and returns more than [limit] transactions. I'd rather have data be fetched
multiple times but have the function behave as expected.)
*/
/*
[limit]+1 items are fetched because then we can check if there are more items to fetch.
*/
type MoneyTransaction = {
type: "transaction",
name: "Aufladen" | "Auszahlung",
difference: number,
createdAt: Date,
verified: boolean,
id: number,
balance?: number
};
type ItemTransaction = {
type: "item",
name: string,
difference: number,
premium: number,
createdAt: Date,
image: string,
id: number,
balance?: number
};
type MoneyTransfer = {
type: "transfer",
name: string,
difference: number,
createdAt: Date,
id: number,
balance?: number
};
type VoucherUse = {
type: "voucher",
name: string,
difference: number,
createdAt: Date,
id: number,
balance?: number
};
const moneyTransactions: MoneyTransaction[] = (await getMoneyTransactions(userId, before.moneyTransactions, limit+1)).map(transaction => ({
type: "transaction",
name: transaction.amount > 0 ? "Aufladen" : "Auszahlung",
difference: transaction.amount,
createdAt: transaction.createdAt,
verified: !!transaction.verifiedById,
id: transaction.id
}));
const itemTransactions: ItemTransaction[] = (await getItemTransactions(userId, before.itemTransactions, limit+1)).map(transaction => ({
type: "item",
name: transaction.item.name,
difference: -(transaction.price + transaction.premium),
premium: transaction.premium,
createdAt: transaction.createdAt,
image: transaction.item.image,
id: transaction.id
}));
const moneyTransfers: MoneyTransfer[] = (await getMoneyTransfers(userId, before.moneyTransfers, limit+1)).map(transfer => ({
type: "transfer",
name: transfer.fromId === userId ? `Gesendet an ${transfer.to.name}` : `Empfangen von ${transfer.from.name}`,
difference: transfer.fromId === userId ? -transfer.amount : transfer.amount,
createdAt: transfer.createdAt,
id: transfer.id
}));
const voucherUses: VoucherUse[] = (await getUsedVouchers(userId, before.voucherUses, limit+1)).map(voucher => ({
type: "voucher",
name: `Gutschein ${voucher.code} eingelöst`,
difference: voucher.amount,
createdAt: voucher.usedAt, // use usedAt because voucher creation time is not relevant
id: voucher.id
}));
let transactions = [...moneyTransactions, ...itemTransactions, ...moneyTransfers, ...voucherUses]
.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
const hasMore = transactions.length > limit;
if(transactions.length > limit) transactions = transactions.filter((_,i)=>i<limit);
if (transactions.length > 0) {
transactions[0].balance = balanceAfterwards;
for (let i = 1; i < transactions.length; i++)
transactions[i].balance = transactions[i - 1].balance - transactions[i - 1].difference;
}
return {transactions, hasMore};
}