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
No related branches found
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 {
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
}
export const Codes = {
Pfand: "PFAND",
Custom: "CUSTOM"
Custom: "CUSTOM",
MoneyTransfer: "TRANSFER"
};
<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>
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});
}
......
......@@ -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 };
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment