From 3b54d2deae873e693c511710ccb8fd1ffb36cca9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aaron=20D=C3=B6tsch?= <aaron@fsmpi.rwth-aachen.de> Date: Sun, 5 Mar 2023 22:10:44 +0100 Subject: [PATCH] Add possibility to transfer money to other users By using the code #TRANSFER-targetUserId-amount you can now transfer money to other people. For that you obviously need the id of the target user which you can't see anywhere as of now. That will be something for the future to add. --- .../migration.sql | 16 +++++++ prisma/schema.prisma | 12 ++++++ src/lib/customcodes.js | 3 +- src/routes/+page.svelte | 43 +++++++++++++++---- src/routes/api/[slug]/+server.js | 24 +++++++++-- src/routes/api/transactions.js | 28 ++++++++++++ 6 files changed, 113 insertions(+), 13 deletions(-) create mode 100644 prisma/migrations/20230305155931_add_money_transfers/migration.sql diff --git a/prisma/migrations/20230305155931_add_money_transfers/migration.sql b/prisma/migrations/20230305155931_add_money_transfers/migration.sql new file mode 100644 index 0000000..efbc90b --- /dev/null +++ b/prisma/migrations/20230305155931_add_money_transfers/migration.sql @@ -0,0 +1,16 @@ +-- CreateTable +CREATE TABLE "MoneyTransfer" ( + "id" SERIAL NOT NULL, + "amount" INTEGER NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "fromId" INTEGER NOT NULL, + "toId" INTEGER NOT NULL, + + CONSTRAINT "MoneyTransfer_pkey" PRIMARY KEY ("id") +); + +-- AddForeignKey +ALTER TABLE "MoneyTransfer" ADD CONSTRAINT "MoneyTransfer_fromId_fkey" FOREIGN KEY ("fromId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "MoneyTransfer" ADD CONSTRAINT "MoneyTransfer_toId_fkey" FOREIGN KEY ("toId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 488a449..576094a 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -20,6 +20,8 @@ model User { transactionsVerified Transaction[] @relation("verifiedBy") itemTransactions ItemTransaction[] cards UserCard[] + moneyTransfersSent MoneyTransfer[] @relation("from") + moneyTransfersReceived MoneyTransfer[] @relation("to") updatedAt DateTime @updatedAt comment String @default("") } @@ -86,3 +88,13 @@ model Restock { cost Int createdAt DateTime @default(now()) } + +model MoneyTransfer { + id Int @id @default(autoincrement()) + amount Int + createdAt DateTime @default(now()) + from User @relation("from", fields: [fromId], references: [id]) + fromId Int + to User @relation("to", fields: [toId], references: [id]) + toId Int +} diff --git a/src/lib/customcodes.js b/src/lib/customcodes.js index a228f55..df64f71 100644 --- a/src/lib/customcodes.js +++ b/src/lib/customcodes.js @@ -1,4 +1,5 @@ export const Codes = { Pfand: "PFAND", - Custom: "CUSTOM" + Custom: "CUSTOM", + MoneyTransfer: "TRANSFER" }; diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 9ffb34b..bfee8f6 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -1,7 +1,7 @@ <script> import { onDestroy, onMount } from "svelte"; import { browser, dev } from "$app/environment"; -import { goto } from "$app/navigation"; +import { goto, invalidateAll } from "$app/navigation"; import { addMessage, MessageType } from "$lib/messages"; import { Codes } from "$lib/customcodes"; import { addInputHandler, removeInputHandler } from "$lib/inputhandler"; @@ -12,15 +12,17 @@ import ItemList from "../components/ItemList.svelte"; export let data; - const items = data.items || []; - const categories = data.categories || []; - const usercard = data.user.card; + $: items = data.items || []; + let categories = data.categories || []; + let usercard = data.user.card; let categoryElements = categories.map(category => ({name: category.name, element: null})); let elementsInView = categories.map(() => false); $: categoryInView = elementsInView.lastIndexOf(true); let cart = []; + // if string, then it is a code, else it is a custom item + $: cartItems = cart.map(code => typeof code === "string" ? items.find(item => item.code === code) : code); function emptyCart(){ cart = []; resetTimer(); @@ -79,7 +81,7 @@ import ItemList from "../components/ItemList.svelte"; if(cart.length === 0){ return logout(); } - const items = cart.map(item => ({code: item.code, price: item.price, premium: item.premium})); + const items = cartItems.map(item => ({code: item.code, price: item.price, premium: item.premium})); fetch("/api/buy", { method: "POST", body: JSON.stringify({card: usercard, items}), @@ -95,13 +97,29 @@ import ItemList from "../components/ItemList.svelte"; }); } + function transferMoney(target, amount){ + fetch("/api/transfer", { + method: "POST", + body: JSON.stringify({card: usercard, target, amount}), + headers: { "Content-Type": "application/json" } + }).then(r => r.json()).then(r => { + if(r.message){ + addMessage(MessageType.ERROR, r.message); + } else { + // Überweisung erfolgreich + addMessage(MessageType.SUCCESS, "Überweisung erfolgreich"); + invalidateAll(); + } + }); + } + function handleCode(code){ if(specialCodes[code]){ specialCodes[code](); }else { const item = items.find(item => item.code === code); if(item){ - addToCart(item); + addToCart(item.code); }else if(code === usercard){ buy(); }else if(code.startsWith("#" + Codes.Custom + "-")){ @@ -120,6 +138,15 @@ import ItemList from "../components/ItemList.svelte"; else addMessage(MessageType.ERROR, "Der Preis muss größer als 0 sein."); } + }else if(code.startsWith("#" + Codes.MoneyTransfer + "-")){ + const parts = code.substring(2 + Codes.MoneyTransfer.length).split("-"); + if(parts.length !== 2) return addMessage(MessageType.ERROR, "Ungültiger Code"); + const to = parseInt(parts[0]); + const amount = parseInt(parts[1]); + if(!Number.isInteger(to) || !Number.isInteger(amount) || to < 0 || amount < 0) return addMessage(MessageType.ERROR, "Ungültiger Code"); + if(amount > data.user.balance) return addMessage(MessageType.ERROR, "Nicht genügend Guthaben"); + // TODO add confirmation + transferMoney(to, amount); }else{ addMessage(MessageType.INFO, "Unbekannter Code"); } @@ -213,13 +240,13 @@ import ItemList from "../components/ItemList.svelte"; {#each categories as category, i} <h2 bind:this={categoryElements[i].element}>{category.name}</h2> <div class="item-list"> - <ItemList items={items.filter(item=>item.categoryId===category.id)} onClick={addToCart} priceModifier={x=>x} bind:inView={elementsInView[i]} /> + <ItemList items={items.filter(item=>item.categoryId===category.id)} onClick={item=>addToCart(item.code)} priceModifier={x=>x} bind:inView={elementsInView[i]} /> </div> {/each} </div> </div> <div class="cart"> - <Cart cart={cart} removeFromCart={removeItemAt} buy={buy} /> + <Cart cart={cartItems} removeFromCart={removeItemAt} buy={buy} /> </div> </div> </div> diff --git a/src/routes/api/[slug]/+server.js b/src/routes/api/[slug]/+server.js index f2d69ab..e4946d5 100644 --- a/src/routes/api/[slug]/+server.js +++ b/src/routes/api/[slug]/+server.js @@ -1,8 +1,8 @@ import { getPriceModifier } from "../../admin/api/articles"; import { perms } from "$lib/perms"; import { getArticles } from "../articles"; -import { buyArticles } from "../transactions"; -import { getUser } from "../users"; +import { buyArticles, transferMoney } from "../transactions"; +import { getUser, getUserById } from "../users"; import { Codes } from "$lib/customcodes"; @@ -22,17 +22,33 @@ export async function POST(event) { try{ const data = await event.request.json(); if(!data.card || !data.items) return new Response(JSON.stringify({message: "Missing required fields"}), {headers: {'content-type': 'application/json', 'status': 400}}); - const user = await getUser(data.card); + const user = await getUser(data.card); // TODO probably switch to using event.locals.user if(!user) return new Response(JSON.stringify({message: "User not found"}), {headers: {'content-type': 'application/json', 'status': 404}}); const articles = (await getArticles()).reduce((acc, item) => {acc[item.code] = item; return acc;}, {}); const priceModifier = getPriceModifier(user.balance, !!(user.perms & perms.NO_PREMIUM)); if(!isValidCart(data.items, articles, priceModifier)) return new Response(JSON.stringify({message: "Invalid cart"}), {headers: {'content-type': 'application/json', 'status': 400}}); - const res = buyArticles(user.id, data.card, data.items, !!(user.perms & perms.NO_BALANCE)); + const res = await buyArticles(user.id, data.card, data.items, !!(user.perms & perms.NO_BALANCE)); return new Response(JSON.stringify(res), {headers: {'content-type': 'application/json', 'status': 200}}); }catch(e){ return new Response(JSON.stringify({message: "Invalid data"}), {headers: {'content-type': 'application/json', 'status': 400}}); } } + case "transfer": { + try{ + const data = await event.request.json(); + if(!data.card || !Number.isInteger(data.amount) || data.amount <= 0 || !data.target) return new Response(JSON.stringify({message: "Missing required fields"}), {headers: {'content-type': 'application/json', 'status': 400}}); + const user = await getUser(data.card); // TODO probably switch to using event.locals.user + if(!user) return new Response(JSON.stringify({message: "User not found"}), {headers: {'content-type': 'application/json', 'status': 404}}); + if(user.balance < data.amount) return new Response(JSON.stringify({message: "Not enough balance"}), {headers: {'content-type': 'application/json', 'status': 400}}); + const target = await getUserById(data.target); + if(!target) return new Response(JSON.stringify({message: "Target not found"}), {headers: {'content-type': 'application/json', 'status': 404}}); + const { from } = await transferMoney(user.id, target.id, data.amount); + // don't return the to-user, because it contains the target's information + return new Response(JSON.stringify(from), {headers: {'content-type': 'application/json', 'status': 200}}); + }catch(e){ + return new Response(JSON.stringify({message: "Invalid data"}), {headers: {'content-type': 'application/json', 'status': 400}}); + } + } default: return new Response("Unknown endpoint", {status: 400}); } diff --git a/src/routes/api/transactions.js b/src/routes/api/transactions.js index 442cf42..ee1d042 100644 --- a/src/routes/api/transactions.js +++ b/src/routes/api/transactions.js @@ -28,3 +28,31 @@ export async function buyArticles(userId, card, items, noBalance=false) { ]); return { user, itemTransactions }; } + +export async function transferMoney(fromId, toId, amount){ + const [to, from] = await db.$transaction([ + db.user.update({ + where: { id: toId }, + data: { + balance: { + increment: amount + }, + moneyTransfersReceived: { + create: { + amount, + from: { connect: { id: fromId } } + } + } + } + }), + db.user.update({ + where: { id: fromId }, + data: { + balance: { + decrement: amount + } + } + }) + ]); + return { to, from }; +} -- GitLab