Skip to content
Snippets Groups Projects
Commit 3b54d2de authored by Aaron Dötsch's avatar Aaron Dötsch
Browse files

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.
parent ad3d6df8
Branches
No related tags found
No related merge requests found
-- 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;
...@@ -20,6 +20,8 @@ model User { ...@@ -20,6 +20,8 @@ model User {
transactionsVerified Transaction[] @relation("verifiedBy") transactionsVerified Transaction[] @relation("verifiedBy")
itemTransactions ItemTransaction[] itemTransactions ItemTransaction[]
cards UserCard[] cards UserCard[]
moneyTransfersSent MoneyTransfer[] @relation("from")
moneyTransfersReceived MoneyTransfer[] @relation("to")
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
comment String @default("") comment String @default("")
} }
...@@ -86,3 +88,13 @@ model Restock { ...@@ -86,3 +88,13 @@ model Restock {
cost Int cost Int
createdAt DateTime @default(now()) 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
}
export const Codes = { export const Codes = {
Pfand: "PFAND", Pfand: "PFAND",
Custom: "CUSTOM" Custom: "CUSTOM",
MoneyTransfer: "TRANSFER"
}; };
<script> <script>
import { onDestroy, onMount } from "svelte"; import { onDestroy, onMount } from "svelte";
import { browser, dev } from "$app/environment"; import { browser, dev } from "$app/environment";
import { goto } from "$app/navigation"; import { goto, invalidateAll } from "$app/navigation";
import { addMessage, MessageType } from "$lib/messages"; import { addMessage, MessageType } from "$lib/messages";
import { Codes } from "$lib/customcodes"; import { Codes } from "$lib/customcodes";
import { addInputHandler, removeInputHandler } from "$lib/inputhandler"; import { addInputHandler, removeInputHandler } from "$lib/inputhandler";
...@@ -12,15 +12,17 @@ import ItemList from "../components/ItemList.svelte"; ...@@ -12,15 +12,17 @@ import ItemList from "../components/ItemList.svelte";
export let data; export let data;
const items = data.items || []; $: items = data.items || [];
const categories = data.categories || []; let categories = data.categories || [];
const usercard = data.user.card; let usercard = data.user.card;
let categoryElements = categories.map(category => ({name: category.name, element: null})); let categoryElements = categories.map(category => ({name: category.name, element: null}));
let elementsInView = categories.map(() => false); let elementsInView = categories.map(() => false);
$: categoryInView = elementsInView.lastIndexOf(true); $: categoryInView = elementsInView.lastIndexOf(true);
let cart = []; 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(){ function emptyCart(){
cart = []; cart = [];
resetTimer(); resetTimer();
...@@ -79,7 +81,7 @@ import ItemList from "../components/ItemList.svelte"; ...@@ -79,7 +81,7 @@ import ItemList from "../components/ItemList.svelte";
if(cart.length === 0){ if(cart.length === 0){
return logout(); 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", { fetch("/api/buy", {
method: "POST", method: "POST",
body: JSON.stringify({card: usercard, items}), body: JSON.stringify({card: usercard, items}),
...@@ -95,13 +97,29 @@ import ItemList from "../components/ItemList.svelte"; ...@@ -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){ function handleCode(code){
if(specialCodes[code]){ if(specialCodes[code]){
specialCodes[code](); specialCodes[code]();
}else { }else {
const item = items.find(item => item.code === code); const item = items.find(item => item.code === code);
if(item){ if(item){
addToCart(item); addToCart(item.code);
}else if(code === usercard){ }else if(code === usercard){
buy(); buy();
}else if(code.startsWith("#" + Codes.Custom + "-")){ }else if(code.startsWith("#" + Codes.Custom + "-")){
...@@ -120,6 +138,15 @@ import ItemList from "../components/ItemList.svelte"; ...@@ -120,6 +138,15 @@ import ItemList from "../components/ItemList.svelte";
else else
addMessage(MessageType.ERROR, "Der Preis muss größer als 0 sein."); 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{ }else{
addMessage(MessageType.INFO, "Unbekannter Code"); addMessage(MessageType.INFO, "Unbekannter Code");
} }
...@@ -213,13 +240,13 @@ import ItemList from "../components/ItemList.svelte"; ...@@ -213,13 +240,13 @@ import ItemList from "../components/ItemList.svelte";
{#each categories as category, i} {#each categories as category, i}
<h2 bind:this={categoryElements[i].element}>{category.name}</h2> <h2 bind:this={categoryElements[i].element}>{category.name}</h2>
<div class="item-list"> <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> </div>
{/each} {/each}
</div> </div>
</div> </div>
<div class="cart"> <div class="cart">
<Cart cart={cart} removeFromCart={removeItemAt} buy={buy} /> <Cart cart={cartItems} removeFromCart={removeItemAt} buy={buy} />
</div> </div>
</div> </div>
</div> </div>
import { getPriceModifier } from "../../admin/api/articles"; import { getPriceModifier } from "../../admin/api/articles";
import { perms } from "$lib/perms"; import { perms } from "$lib/perms";
import { getArticles } from "../articles"; import { getArticles } from "../articles";
import { buyArticles } from "../transactions"; import { buyArticles, transferMoney } from "../transactions";
import { getUser } from "../users"; import { getUser, getUserById } from "../users";
import { Codes } from "$lib/customcodes"; import { Codes } from "$lib/customcodes";
...@@ -22,17 +22,33 @@ export async function POST(event) { ...@@ -22,17 +22,33 @@ export async function POST(event) {
try{ try{
const data = await event.request.json(); 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}}); 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}}); 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 articles = (await getArticles()).reduce((acc, item) => {acc[item.code] = item; return acc;}, {});
const priceModifier = getPriceModifier(user.balance, !!(user.perms & perms.NO_PREMIUM)); 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}}); 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}}); return new Response(JSON.stringify(res), {headers: {'content-type': 'application/json', 'status': 200}});
}catch(e){ }catch(e){
return new Response(JSON.stringify({message: "Invalid data"}), {headers: {'content-type': 'application/json', 'status': 400}}); 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: default:
return new Response("Unknown endpoint", {status: 400}); return new Response("Unknown endpoint", {status: 400});
} }
......
...@@ -28,3 +28,31 @@ export async function buyArticles(userId, card, items, noBalance=false) { ...@@ -28,3 +28,31 @@ export async function buyArticles(userId, card, items, noBalance=false) {
]); ]);
return { user, itemTransactions }; 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 };
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment