diff --git a/frontend/src/features/deposit/Deposit.module.css b/frontend/src/features/deposit/Deposit.module.css new file mode 100644 index 0000000..07db7dc --- /dev/null +++ b/frontend/src/features/deposit/Deposit.module.css @@ -0,0 +1,13 @@ +.userList { + display: grid; + grid-template-columns: repeat(12, 100px); + row-gap: 16px; + height: 400px; + overflow-y: auto; +} +.amountSection { + margin-top: 32px; +} +.checkout { + margin-top: 32px; +} \ No newline at end of file diff --git a/frontend/src/features/deposit/Deposit.tsx b/frontend/src/features/deposit/Deposit.tsx index ea78e6c..220f906 100644 --- a/frontend/src/features/deposit/Deposit.tsx +++ b/frontend/src/features/deposit/Deposit.tsx @@ -1,5 +1,86 @@ -import React from "react"; +import { useMutation, useQuery } from "@apollo/client"; +import React, { useState } from "react"; + +import { useToast } from "../../components/ToastProvider"; +import { UserCard } from "../../components/UserCard"; +import { AllUsersDocument, CreateDepositDocument, User } from "../../types"; +import styles from "./Deposit.module.css"; export const Deposit: React.FC = () => { - return
Hi from deposit
; + const { data, loading } = useQuery(AllUsersDocument); + const [createDeposit] = useMutation(CreateDepositDocument); + const [selectedUser, setSelectedUser] = useState(""); + const [amount, setAmount] = useState(""); + const allUsers = data?.allUsers || []; + const { showToast } = useToast(); + + const onSubmit = async () => { + const amountInt = parseInt(amount, 10) || 0; + const variables = { userId: selectedUser, amount: amountInt }; + let res; + try { + res = await createDeposit({ variables }); + } catch (e) { + console.log("woops", e); + showToast("Kunne ikke sette inn penger", "danger"); + return; + } + + if (res.data?.createDeposit?.__typename === "CreateDepositSuccess") { + const { transaction } = res.data?.createDeposit; + showToast( + `${transaction.user.firstName} har satt inn${transaction.amount}. Ny saldo er ${transaction.user.profile?.balance}.` + ); + reset(); + return; + } + if (res.data?.createDeposit?.__typename === "FormErrors") { + showToast(res.data?.createDeposit.message, "danger"); + return; + } + if (res.data?.createDeposit?.__typename === "AboveMaxSaldo") { + showToast( + `${res.data.createDeposit.message}. Ny saldo ville vært ${res.data.createDeposit.above} mer enn maksimalt ${res.data.createDeposit.maxSaldo}.`, + "danger" + ); + } + }; + const reset = () => { + setSelectedUser(""); + setAmount(""); + }; + + return ( +
+
+

Hvem skal sette inn penger?

+ {/* TODO: search as you type */} + +
+ {allUsers.map((user: User) => ( + { + setSelectedUser(user.id); + }} + user={user} + /> + ))} +
+
+
+

Hvor mye?

+ setAmount(event.target.value)} /> +
+
+ {" "} + +
+
+ ); }; diff --git a/frontend/src/features/deposit/createDeposit.graphql b/frontend/src/features/deposit/createDeposit.graphql new file mode 100644 index 0000000..aea6bf4 --- /dev/null +++ b/frontend/src/features/deposit/createDeposit.graphql @@ -0,0 +1,32 @@ +mutation CreateDeposit($userId: ID!, $amount: Int!) { + createDeposit(userId: $userId, amount:$amount) { + ... on CreateDepositSuccess { + transaction { + id + amount + user { + id + firstName + lastName + profile { + balance + } + } + } + } + ... on AboveMaxSaldo { + error + message + maxSaldo + above + } + ... on FormErrors { + error + message + fields { + field + message + } + } + } +} diff --git a/frontend/src/features/register/Register.tsx b/frontend/src/features/register/Register.tsx index 5c95b1e..af913c3 100644 --- a/frontend/src/features/register/Register.tsx +++ b/frontend/src/features/register/Register.tsx @@ -131,7 +131,7 @@ export const Register: React.FC = () => { {" "} - diff --git a/frontend/src/types.ts b/frontend/src/types.ts index fc1df12..2efdd42 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -14,6 +14,21 @@ export type Scalars = { DateTime: any; }; +export type AboveMaxSaldo = Error & { + __typename?: 'AboveMaxSaldo'; + above: Scalars['Float']; + error: Scalars['Boolean']; + maxSaldo: Scalars['Float']; + message: Scalars['String']; +}; + +export type CreateDepositResponse = AboveMaxSaldo | CreateDepositSuccess | FormErrors; + +export type CreateDepositSuccess = { + __typename?: 'CreateDepositSuccess'; + transaction: Transaction; +}; + export type CreateOrderResponse = CreateOrderSuccess | FormErrors | InsufficientFunds; export type CreateOrderSuccess = { @@ -49,10 +64,17 @@ export type InsufficientFunds = Error & { export type Mutation = { __typename?: 'Mutation'; + createDeposit: CreateDepositResponse; createOrder: CreateOrderResponse; }; +export type MutationCreateDepositArgs = { + amount: Scalars['Int']; + userId: Scalars['ID']; +}; + + export type MutationCreateOrderArgs = { customerId: Scalars['ID']; isExternal?: Scalars['Boolean']; @@ -98,6 +120,14 @@ export type QueryAllProductsArgs = { filters?: Maybe; }; +export type Transaction = { + __typename?: 'Transaction'; + amount: Scalars['Float']; + created: Scalars['DateTime']; + id: Scalars['ID']; + user: User; +}; + export type User = { __typename?: 'User'; firstName: Scalars['String']; @@ -114,6 +144,14 @@ export type UserProfile = { lastPurchaseDate: Scalars['DateTime']; }; +export type CreateDepositMutationVariables = Exact<{ + userId: Scalars['ID']; + amount: Scalars['Int']; +}>; + + +export type CreateDepositMutation = { __typename?: 'Mutation', createDeposit: { __typename?: 'AboveMaxSaldo', error: boolean, message: string, maxSaldo: number, above: number } | { __typename?: 'CreateDepositSuccess', transaction: { __typename?: 'Transaction', id: string, amount: number, user: { __typename?: 'User', id: string, firstName: string, lastName: string, profile?: { __typename?: 'UserProfile', balance: number } | null | undefined } } } | { __typename?: 'FormErrors', error: boolean, message: string, fields: Array<{ __typename?: 'FieldError', field: string, message: string }> } }; + export type AllProductsQueryVariables = Exact<{ active: Scalars['Boolean']; }>; @@ -136,6 +174,7 @@ export type CreateOrderMutationVariables = Exact<{ export type CreateOrderMutation = { __typename?: 'Mutation', createOrder: { __typename?: 'CreateOrderSuccess', order: { __typename?: 'Order', id: string, orderSum: number, customer?: { __typename?: 'User', id: string } | null | undefined } } | { __typename?: 'FormErrors', error: boolean, message: string, fields: Array<{ __typename?: 'FieldError', field: string, message: string }> } | { __typename?: 'InsufficientFunds', error: boolean, message: string, amountLacking: number } }; +export const CreateDepositDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateDeposit"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"userId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"amount"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createDeposit"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"userId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"userId"}}},{"kind":"Argument","name":{"kind":"Name","value":"amount"},"value":{"kind":"Variable","name":{"kind":"Name","value":"amount"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"CreateDepositSuccess"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"transaction"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"amount"}},{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"firstName"}},{"kind":"Field","name":{"kind":"Name","value":"lastName"}},{"kind":"Field","name":{"kind":"Name","value":"profile"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"balance"}}]}}]}}]}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"AboveMaxSaldo"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"error"}},{"kind":"Field","name":{"kind":"Name","value":"message"}},{"kind":"Field","name":{"kind":"Name","value":"maxSaldo"}},{"kind":"Field","name":{"kind":"Name","value":"above"}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"FormErrors"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"error"}},{"kind":"Field","name":{"kind":"Name","value":"message"}},{"kind":"Field","name":{"kind":"Name","value":"fields"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"field"}},{"kind":"Field","name":{"kind":"Name","value":"message"}}]}}]}}]}}]}}]} as unknown as DocumentNode; export const AllProductsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"allProducts"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"active"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Boolean"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"allProducts"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filters"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"active"},"value":{"kind":"Variable","name":{"kind":"Name","value":"active"}}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"imageUrl"}},{"kind":"Field","name":{"kind":"Name","value":"salePriceExt"}},{"kind":"Field","name":{"kind":"Name","value":"salePriceInt"}}]}}]}}]} as unknown as DocumentNode; export const AllUsersDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"allUsers"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"allUsers"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"firstName"}},{"kind":"Field","name":{"kind":"Name","value":"lastName"}},{"kind":"Field","name":{"kind":"Name","value":"profile"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"balance"}},{"kind":"Field","name":{"kind":"Name","value":"image"}},{"kind":"Field","name":{"kind":"Name","value":"lastPurchaseDate"}}]}}]}}]}}]} as unknown as DocumentNode; export const CreateOrderDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateOrder"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"customerId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"orderLines"}},"type":{"kind":"NonNullType","type":{"kind":"ListType","type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"OrderLineInput"}}}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"isExternal"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Boolean"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createOrder"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"customerId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"customerId"}}},{"kind":"Argument","name":{"kind":"Name","value":"orderLines"},"value":{"kind":"Variable","name":{"kind":"Name","value":"orderLines"}}},{"kind":"Argument","name":{"kind":"Name","value":"isExternal"},"value":{"kind":"Variable","name":{"kind":"Name","value":"isExternal"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"CreateOrderSuccess"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"order"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"customer"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}},{"kind":"Field","name":{"kind":"Name","value":"orderSum"}}]}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"InsufficientFunds"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"error"}},{"kind":"Field","name":{"kind":"Name","value":"message"}},{"kind":"Field","name":{"kind":"Name","value":"amountLacking"}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"FormErrors"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"error"}},{"kind":"Field","name":{"kind":"Name","value":"message"}},{"kind":"Field","name":{"kind":"Name","value":"fields"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"field"}},{"kind":"Field","name":{"kind":"Name","value":"message"}}]}}]}}]}}]}}]} as unknown as DocumentNode; \ No newline at end of file diff --git a/main/api/mutations.py b/main/api/mutations.py index 0a98fa4..2c65c15 100644 --- a/main/api/mutations.py +++ b/main/api/mutations.py @@ -1,4 +1,5 @@ import strawberry +from django.conf import settings from django.contrib.auth import get_user_model from django.db import transaction from django.shortcuts import get_object_or_404 @@ -6,7 +7,7 @@ from strawberry import ID from main import models from main.api.errors import Error, FormErrors, FieldError -from main.api.types import Order +from main.api.types import Order, Transaction, UserProfile UserModel = get_user_model() @@ -27,12 +28,12 @@ class InsufficientFunds(Error): amount_lacking: float -Response = strawberry.union("CreateOrderResponse", [CreateOrderSuccess, InsufficientFunds, FormErrors]) +CreateOrderResponse = strawberry.union("CreateOrderResponse", [CreateOrderSuccess, InsufficientFunds, FormErrors]) @strawberry.mutation @transaction.atomic -def create_order(customer_id: ID, order_lines: list[OrderLineInput], is_external: bool = False) -> Response: +def create_order(customer_id: ID, order_lines: list[OrderLineInput], is_external: bool = False) -> CreateOrderResponse: # clean and validate try: customer = UserModel.objects.get(pk=customer_id) @@ -61,7 +62,9 @@ def create_order(customer_id: ID, order_lines: list[OrderLineInput], is_external order_sum += ol.amount * price if not is_external and customer.profile.balance <= order_sum: - return InsufficientFunds(message='Du har ikke nok penger på bok.', amount_lacking=abs(order_sum - customer.profile.balance)) + return InsufficientFunds( + message="Du har ikke nok penger på bok.", amount_lacking=abs(order_sum - customer.profile.balance) + ) # create order = models.Order.objects.create(customer=customer, order_sum=order_sum) @@ -70,3 +73,51 @@ def create_order(customer_id: ID, order_lines: list[OrderLineInput], is_external models.OrderLine.objects.bulk_create(ols) return CreateOrderSuccess(order=order) + + +@strawberry.type +class CreateDepositSuccess: + transaction: Transaction + + +@strawberry.type +class AboveMaxSaldo(Error): + max_saldo: float + above: float + + +CreateDepositResponse = strawberry.union("CreateDepositResponse", [CreateDepositSuccess, AboveMaxSaldo, FormErrors]) + + +@strawberry.mutation +@transaction.atomic +def create_deposit(user_id: ID, amount: int) -> CreateDepositResponse: + # clean and validate + try: + user = UserModel.objects.get(pk=user_id) + except (UserModel.DoesNotExist, ValueError): + message = "Du har ikke valgt hvem som sette inn penger." + return FormErrors( + message=message, + fields=[FieldError(field="user_id", message=message)], + ) + if amount <= 0: + message = "Du har ikke valgt beløp som skal settes inn." + return FormErrors( + message=message, + fields=[FieldError(field="amount", message=message)], + ) + # 1337 is special <3 + if amount + user.profile.balance > settings.BBS_SALDO_MAX and amount + user.profile.balance != 1337: + return AboveMaxSaldo( + message='Du prøver å sette inn for mye penger', + max_saldo=settings.BBS_SALDO_MAX, + above=amount + user.profile.balance - settings.BBS_SALDO_MAX, + ) + + trans = models.Transaction.objects.create(user=user, amount=amount) + profile = trans.user.profile + profile.balance += trans.amount + profile.save() + + return CreateDepositSuccess(transaction=trans) diff --git a/main/api/schema.py b/main/api/schema.py index 54dd248..75be7bc 100644 --- a/main/api/schema.py +++ b/main/api/schema.py @@ -15,6 +15,7 @@ class Query: @strawberry.type class Mutation: create_order = mutations.create_order + create_deposit = mutations.create_deposit schema = strawberry.Schema(query=Query, mutation=Mutation) diff --git a/main/api/types.py b/main/api/types.py index 06000af..feaee05 100644 --- a/main/api/types.py +++ b/main/api/types.py @@ -48,6 +48,14 @@ class User: @strawberry_django.type(models.Order) class Order: id: auto - customer: 'User' + customer: "User" order_sum: auto created: auto + + +@strawberry_django.type(models.Transaction) +class Transaction: + id: auto + user: "User" + amount: auto + created: auto diff --git a/schema.graphql b/schema.graphql index 9e5e043..a63d554 100644 --- a/schema.graphql +++ b/schema.graphql @@ -1,3 +1,16 @@ +type AboveMaxSaldo implements Error { + message: String! + error: Boolean! + maxSaldo: Float! + above: Float! +} + +union CreateDepositResponse = CreateDepositSuccess | AboveMaxSaldo | FormErrors + +type CreateDepositSuccess { + transaction: Transaction! +} + union CreateOrderResponse = CreateOrderSuccess | InsufficientFunds | FormErrors type CreateOrderSuccess { @@ -32,6 +45,7 @@ type InsufficientFunds implements Error { type Mutation { createOrder(customerId: ID!, orderLines: [OrderLineInput!]!, isExternal: Boolean! = false): CreateOrderResponse! + createDeposit(userId: ID!, amount: Int!): CreateDepositResponse! } type Order { @@ -65,6 +79,13 @@ type Query { allOrders: [Order!]! } +type Transaction { + id: ID! + user: User! + amount: Float! + created: DateTime! +} + type User { id: ID! firstName: String!