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 0000000000000000000000000000000000000000..efbc90b6852d22f99bbc2c4383fa6297c4b56db5 --- /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 488a449e2f894ba309167c404297d0c5b5aa2945..576094a20590fe4d23b5dd8bb150fcf8ac8fc1b0 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 a228f55aa2937a928d3c09ed0dc4ed3991fb5b56..df64f710f4ffa586b6d65f03347c9fd37b56a46d 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 9ffb34b0f6488d6ffb1f04f1208ae02b46d8dab1..bfee8f6ab991e136b1e7bc59fd34a8384a4caae9 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 f2d69ab7fcb2f7e24cd2e6f47e25a37cd56fc031..e4946d5ea92c2128c3cecf42ea3fa269fbe86268 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 442cf4212931063225cb54625ae270ddecf0b0c5..ee1d042977a2e292cd07d12679c2cfe3acac2309 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 }; +}