Skip to content
Snippets Groups Projects
Select Git revision
  • 8d605eee6a896608f1e7ab792a978136b0921128
  • master default protected
2 results

transactions.ts

Blame
  • 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};
    }