1
0
Fork 0
mirror of https://github.com/dancojocaru2000/foxbank.git synced 2025-06-19 11:02:28 +03:00

Compare commits

...

9 commits

Author SHA1 Message Date
DariusTFox24
1c56bf2a60
Merge pull request #11 from dancojocaru2000/Frontend
Finished frontend implementation with api
2022-01-04 01:25:25 +02:00
DariusTFox24
57d9ff8ed0 Finished frontend implementation with api 2022-01-04 01:22:27 +02:00
649e2c729f
Merge pull request #10 from dancojocaru2000/Backend
Currency exchange in Backend
2022-01-03 21:57:28 +02:00
c7c8f3c765
Added transfer between different currency accounts 2022-01-03 21:49:38 +02:00
9a1e1b4fce
Added basic currency exchange data 2022-01-03 21:42:21 +02:00
e68709092b
Fixed datetime not having correct timezone 2022-01-03 21:42:03 +02:00
DariusTFox24
ab53e05287
Merge pull request #9 from dancojocaru2000/Frontend
Frontend updates
2022-01-03 14:28:47 +02:00
1250b12049
Merge pull request #8 from dancojocaru2000/Backend
Hotfix: notification for received transfer has wrong id
2022-01-03 14:25:32 +02:00
28ac7a970e
Hotfix: notification for received transfer has wrong id 2022-01-03 11:45:18 +02:00
17 changed files with 234 additions and 59 deletions

View file

@ -23,6 +23,7 @@
"rollup-plugin-svelte": "^7.0.0", "rollup-plugin-svelte": "^7.0.0",
"rollup-plugin-terser": "^7.0.0", "rollup-plugin-terser": "^7.0.0",
"svelte": "^3.0.0", "svelte": "^3.0.0",
"svelte-keydown": "^0.4.0",
"svelte-preprocess": "^4.9.8" "svelte-preprocess": "^4.9.8"
} }
}, },
@ -1947,6 +1948,12 @@
"node": ">= 8" "node": ">= 8"
} }
}, },
"node_modules/svelte-keydown": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/svelte-keydown/-/svelte-keydown-0.4.0.tgz",
"integrity": "sha512-a0S5m+u78FE1jgpuSCZPtgh/KhwNAO9t8kkcBkK1sK9ZiVC+Iu5XOCqIu1F+PEyenRTIxGaA4yr6KAsYFB6kkQ==",
"dev": true
},
"node_modules/svelte-preprocess": { "node_modules/svelte-preprocess": {
"version": "4.9.8", "version": "4.9.8",
"resolved": "https://registry.npmjs.org/svelte-preprocess/-/svelte-preprocess-4.9.8.tgz", "resolved": "https://registry.npmjs.org/svelte-preprocess/-/svelte-preprocess-4.9.8.tgz",
@ -3700,6 +3707,12 @@
"integrity": "sha512-4DrCEJoBvdR689efHNSxIQn2pnFwB7E7j2yLEJtHE/P8hxwZWIphCtJ8are7bjl/iVMlcEf5uh5pJ68IwR09vQ==", "integrity": "sha512-4DrCEJoBvdR689efHNSxIQn2pnFwB7E7j2yLEJtHE/P8hxwZWIphCtJ8are7bjl/iVMlcEf5uh5pJ68IwR09vQ==",
"dev": true "dev": true
}, },
"svelte-keydown": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/svelte-keydown/-/svelte-keydown-0.4.0.tgz",
"integrity": "sha512-a0S5m+u78FE1jgpuSCZPtgh/KhwNAO9t8kkcBkK1sK9ZiVC+Iu5XOCqIu1F+PEyenRTIxGaA4yr6KAsYFB6kkQ==",
"dev": true
},
"svelte-preprocess": { "svelte-preprocess": {
"version": "4.9.8", "version": "4.9.8",
"resolved": "https://registry.npmjs.org/svelte-preprocess/-/svelte-preprocess-4.9.8.tgz", "resolved": "https://registry.npmjs.org/svelte-preprocess/-/svelte-preprocess-4.9.8.tgz",

View file

@ -17,6 +17,7 @@
"rollup-plugin-svelte": "^7.0.0", "rollup-plugin-svelte": "^7.0.0",
"rollup-plugin-terser": "^7.0.0", "rollup-plugin-terser": "^7.0.0",
"svelte": "^3.0.0", "svelte": "^3.0.0",
"svelte-keydown": "^0.4.0",
"svelte-preprocess": "^4.9.8" "svelte-preprocess": "^4.9.8"
}, },
"dependencies": { "dependencies": {

View file

@ -21,7 +21,7 @@
export let balance="5425"; export let balance="5425";
export let iban="RONFOX62188921"; export let iban="RONFOX62188921";
export let isExpanded=false; export let isExpanded=false;
let transactions=[]; export let transactions=[];
export let accountId; export let accountId;
let copied = false; let copied = false;
@ -55,14 +55,6 @@
}); });
} }
onMount( () => {
gettransactions($token, accountId).then( result => {
if(result.status == "success") {
transactions = result.transactions;
transactions.sort((e1, e2) => new Date(e2.datetime) - new Date(e1.datetime));
}
});
})
</script> </script>
@ -82,7 +74,7 @@
<div> <div>
<DetailField class="p-1 flex-shrink"> <DetailField class="p-1 flex-shrink">
<h2 class='font-sans mt-3 mb-2 pl-2 text-4xl text-gray-50'>Balance: <span style="color: #264D59">{balance}</span>{currency}</h2> <h2 class='font-sans mt-3 mb-2 pl-2 text-4xl text-gray-50'>Balance: <span style="color: #264D59">{amountToString(balance)}</span>{currency}</h2>
</DetailField> </DetailField>
</div> </div>

View file

@ -42,21 +42,39 @@
}) })
setContext("user", user); setContext("user", user);
const refreshAccounts = writable(null);
setContext("refreshAccounts", refreshAccounts);
const accounts = readable(null, set => { const accounts = readable(null, set => {
const unsubscribe = userToken.subscribe(token => { function getAccounts(token){
if(token == null) { if(token==null){
set(null); set(null);
}else{ }else{
getaccountlist(token) getaccountlist(token)
.then(result =>{ .then(result => {
set(result); set(result);
}) })
} }
}
let token = null;
refreshAccounts.set( () => {
getAccounts(token);
});
const unsubscribe = userToken.subscribe(newToken => {
token = newToken;
getAccounts(token);
}) })
const intervalId = setInterval(() => {
getAccounts(token);
}, 10000);
return () => { return () => {
unsubscribe(); unsubscribe();
clearInterval(intervalId);
} }
}) })

View file

@ -39,6 +39,7 @@
<div class="w-full max-w-md self-start border-solid border mb-3"></div> <div class="w-full max-w-md self-start border-solid border mb-3"></div>
<div class="flex flex-col flex-grow pl-8 pr-10 relative scroller overflow-auto overflow-x-hidden max-h-full min-h-0"> <div class="flex flex-col flex-grow pl-8 pr-10 relative scroller overflow-auto overflow-x-hidden max-h-full min-h-0">
{#if notifications.length > 0}
{#each notifications as notification,i (i)} {#each notifications as notification,i (i)}
<div on:click={() => onNotificationClick(notification.id)} in:slide={{delay:100*i}} out:slide={{delay:50*(notifications.length-i)}}> <div on:click={() => onNotificationClick(notification.id)} in:slide={{delay:100*i}} out:slide={{delay:50*(notifications.length-i)}}>
@ -68,6 +69,14 @@
</DetailField> </DetailField>
</div> </div>
{/each} {/each}
{:else}
<DetailField class="relative my-3 py-1 flex-shrink min-w-transaction max-w-4xl">
<div class='font-sans text-gray-50 text-2xl mt-2 mx-4 border-b-1'>
No notifications.
</div>
</DetailField>
{/if}
</div> </div>
<div class="m-2"></div> <div class="m-2"></div>

View file

@ -12,6 +12,7 @@
import { getContext } from "svelte"; import { getContext } from "svelte";
const token = getContext("token"); const token = getContext("token");
const refreshAccounts = getContext("refreshAccounts");
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
@ -32,6 +33,9 @@
const result = await createaccount($token, name, currency, type); const result = await createaccount($token, name, currency, type);
if(result.status == "success") { if(result.status == "success") {
dispatch("createPopup",{type:"create_acc_success"}); dispatch("createPopup",{type:"create_acc_success"});
if($refreshAccounts){
$refreshAccounts();
}
}else{ }else{
dispatch("createPopup",{type:"create_acc_failed", reason:"Failed to create account. Error:"+result.status}); dispatch("createPopup",{type:"create_acc_failed", reason:"Failed to create account. Error:"+result.status});
} }

View file

@ -9,7 +9,7 @@
</script> </script>
<main> <main>
<input type={isPassword ? "password" : "text"} placeholder={placeholder} value={value} on:input={handleInput} class="placeholder-gray-300 p-3 text-gray-50 w-full text-3xl"> <input on:keydown type={isPassword ? "password" : "text"} placeholder={placeholder} value={value} on:input={handleInput} class="placeholder-gray-300 p-3 text-gray-50 w-full text-3xl">
</main> </main>
<style> <style>

View file

@ -24,6 +24,19 @@
} }
async function enter(event) {
if (event.key === 'Enter') {
const result = await login(username, code);
if(result.status == "success") {
dispatch("loginSuccess",{
token: result.token,
});
}else{
alert(result.code);
}
}
}
</script> </script>
<main class="h-full"> <main class="h-full">
@ -35,7 +48,7 @@
</div> </div>
<div class="m-3 flex-shrink"> <div class="m-3 flex-shrink">
<InputField placeholder="Code" isPassword={true} bind:value={code}></InputField> <InputField on:keydown={enter} placeholder="Code" isPassword={true} bind:value={code}></InputField>
</div> </div>
<div class="m-3 flex-shrink"> <div class="m-3 flex-shrink">

View file

@ -6,8 +6,8 @@
import GreenButton from './GreenButton.svelte'; import GreenButton from './GreenButton.svelte';
import { fade, fly, slide } from 'svelte/transition'; import { fade, fly, slide } from 'svelte/transition';
import { flip } from 'svelte/animate'; import { flip } from 'svelte/animate';
import { logout, whoami, getaccountlist, getnotificationlist } from './api'; import { logout, whoami, getaccountlist, getnotificationlist, getForex } from './api';
import { amountToString } from './utils'; import { amountToString } from './utils';
const token = getContext("token"); const token = getContext("token");
const user = getContext("user"); const user = getContext("user");
@ -20,7 +20,7 @@ import { amountToString } from './utils';
$: username = $user.user.username; $: username = $user.user.username;
$: email = $user.user.email; $: email = $user.user.email;
$: notifications = $notificationsStore ? $notificationsStore.notifications : []; $: notifications = $notificationsStore ? $notificationsStore.notifications : [];
let totalbalance = "2455.22"; let totalbalance = "0.00";
let maincurrency = "RON"; let maincurrency = "RON";
let expandedAccount = null; let expandedAccount = null;
let showAllAccounts = true; let showAllAccounts = true;
@ -30,12 +30,21 @@ import { amountToString } from './utils';
return { return {
name: account.customName ? account.customName : `${account.accountType} Account`, name: account.customName ? account.customName : `${account.accountType} Account`,
currency: account.currency, currency: account.currency,
balance: amountToString(account.balance), balance: account.balance,
iban: account.iban.replace(/(.{4})/g, "$1 "), iban: account.iban.replace(/(.{4})/g, "$1 "),
id: account.id, id: account.id,
transactions: account.transactions,
} }
}) : []; }) : [];
$: {
Promise.all(accounts.map(account => {
return getForex(account.currency, maincurrency, account.balance);
})).then( balances => {
const sum = balances.reduce((acc, current) => acc+current, 0);
totalbalance = amountToString(Math.round(sum));
})
}
function dispatchLogout(){ function dispatchLogout(){
if (confirm("Log out?")) { if (confirm("Log out?")) {
@ -92,7 +101,7 @@ import { amountToString } from './utils';
{/if} {/if}
<div class="self-center m-0"> <div class="self-center m-0">
{#if showAllAccounts} {#if showAllAccounts && (accounts.length < 3) }
<div in:slide={{delay:500*accounts.length, duration:250*accounts.length}}> <div in:slide={{delay:500*accounts.length, duration:250*accounts.length}}>
<GreenButton on:click={createAccount}><Icon icon="akar-icons:plus" color="rgba(249, 250, 251, 1)" width="26" height="26" /></GreenButton> <GreenButton on:click={createAccount}><Icon icon="akar-icons:plus" color="rgba(249, 250, 251, 1)" width="26" height="26" /></GreenButton>
</div> </div>

View file

@ -20,6 +20,7 @@
let amount=0.00; let amount=0.00;
let description=""; let description="";
const token = getContext("token"); const token = getContext("token");
const refreshAccounts = getContext("refreshAccounts");
async function create(){ async function create(){
@ -36,6 +37,9 @@
await createtransaction($token, receiveriban, Math.round(amount*100), account.id, description).then( result => { await createtransaction($token, receiveriban, Math.round(amount*100), account.id, description).then( result => {
if(result.status == "success") { if(result.status == "success") {
dispatch("createPopup",{type:"send_money_success"}); dispatch("createPopup",{type:"send_money_success"});
if($refreshAccounts){
$refreshAccounts();
}
} }
}); });

View file

@ -67,7 +67,18 @@ export async function getaccountlist(token) {
}, },
}); });
return (await result.json()); const data = await result.json();
if(data.status == "success"){
for(let i=0; i < data.accounts.length; i++){
const transactionsreq = await gettransactions(token, data.accounts[i].id);
if (transactionsreq.status == "success") {
data.accounts[i].transactions = transactionsreq.transactions;
}
}
}
return data;
} catch (error) { } catch (error) {
return { return {
status: "error", status: "error",
@ -146,7 +157,12 @@ export async function getnotificationlist(token) {
}, },
}); });
return (await result.json()); const data = await result.json();
if(data.status == "success") {
data.notifications.sort((e1, e2) => new Date(e2.datetime) - new Date(e1.datetime));
}
return data;
} catch (error) { } catch (error) {
return { return {
status: "error", status: "error",
@ -233,7 +249,12 @@ export async function gettransactions(token, id) {
}, },
}); });
return (await result.json()); const data = await result.json();
if(data.status == "success") {
data.transactions.sort((e1, e2) => new Date(e2.datetime) - new Date(e1.datetime));
}
return data;
} catch (error) { } catch (error) {
return { return {
status: "error", status: "error",
@ -241,3 +262,26 @@ export async function gettransactions(token, id) {
} }
} }
} }
export async function getForex(from, to, amount) {
try {
const result = await fetch(new URL("/forex/"+from+"/"+to, baseURL), {
method: "GET",
headers: {
"Content-Type": "application/json",
},
});
const data = await result.json();
if(data.status == "success") {
const exchanged = amount * data.rate;
return exchanged;
}
return null;
} catch (error) {
return null;
}
}

View file

@ -5,6 +5,7 @@ from .accounts import bp as acc_bp
from .login import bp as login_bp from .login import bp as login_bp
from .transactions import bp as transactions_bp from .transactions import bp as transactions_bp
from .notifications import bp as notifications_bp from .notifications import bp as notifications_bp
from .forex import bp as forex_bp
class ApiWithErr(Api): class ApiWithErr(Api):
def handle_http_exception(self, error): def handle_http_exception(self, error):
@ -31,3 +32,4 @@ def init_apis(app: Flask):
api.register_blueprint(acc_bp, url_prefix='/accounts') api.register_blueprint(acc_bp, url_prefix='/accounts')
api.register_blueprint(transactions_bp, url_prefix='/transactions') api.register_blueprint(transactions_bp, url_prefix='/transactions')
api.register_blueprint(notifications_bp, url_prefix='/notifications') api.register_blueprint(notifications_bp, url_prefix='/notifications')
api.register_blueprint(forex_bp, url_prefix='/forex')

View file

@ -0,0 +1,26 @@
from flask.views import MethodView
from flask_smorest import Blueprint
from marshmallow import Schema, fields
from ..db_utils import get_forex_rate
from .. import returns
bp = Blueprint('forex', __name__, description='Foreign Exchange information')
class GetExchangeResult(returns.SuccessSchema):
rate = fields.Float(optional=True)
@bp.get('/<from_currency>/<to_currency>')
@bp.response(422, returns.ErrorSchema, description='Invalid currency')
@bp.response(200, GetExchangeResult)
def get_exchange(from_currency: str, to_currency: str):
"""Get exchange rate between two currencies"""
if from_currency == to_currency:
rate = 1
else:
rate = get_forex_rate(from_currency, to_currency)
if rate is None:
return returns.abort(returns.invalid_argument('currency'))
return returns.success(rate=rate)

View file

@ -1,4 +1,4 @@
from datetime import datetime from datetime import datetime, timezone
from flask.views import MethodView from flask.views import MethodView
from flask_smorest import Blueprint from flask_smorest import Blueprint
from marshmallow import Schema, fields from marshmallow import Schema, fields
@ -41,7 +41,7 @@ class NotificationsList(MethodView):
The usefulness of this endpoint is questionable besides debugging since it's a notification to self The usefulness of this endpoint is questionable besides debugging since it's a notification to self
""" """
now = datetime.now() now = datetime.now(timezone.utc).astimezone()
notification = Notification.new_notification(body, now, read) notification = Notification.new_notification(body, now, read)
insert_notification(decorators.user_id, notification) insert_notification(decorators.user_id, notification)
return returns.success(notification=notification) return returns.success(notification=notification)

View file

@ -1,4 +1,4 @@
from datetime import date, datetime from datetime import date, datetime, timezone
from flask.views import MethodView from flask.views import MethodView
from flask_smorest import Blueprint from flask_smorest import Blueprint
from marshmallow import Schema, fields from marshmallow import Schema, fields
@ -6,7 +6,7 @@ from marshmallow import Schema, fields
import re import re
from ..decorators import ensure_logged_in from ..decorators import ensure_logged_in
from ..db_utils import get_transactions, get_account, get_accounts, insert_transaction, whose_account, insert_notification from ..db_utils import get_transactions, get_account, get_accounts, insert_transaction, whose_account, insert_notification, get_forex_rate
from ..models import Account, Notification, Transaction from ..models import Account, Notification, Transaction
from ..utils.iban import check_iban from ..utils.iban import check_iban
from .. import decorators, returns from .. import decorators, returns
@ -75,22 +75,25 @@ class TransactionsList(MethodView):
if not check_iban(destination_iban): if not check_iban(destination_iban):
return returns.abort(returns.INVALID_IBAN) return returns.abort(returns.INVALID_IBAN)
date = datetime.now() date = datetime.now(timezone.utc).astimezone()
# Check if transaction is to another FoxBank account # Check if transaction is to another FoxBank account
reverse_transaction = None reverse_transaction = None
if destination_iban[4:8] == 'FOXB': if destination_iban[4:8] == 'FOXB':
for acc in get_accounts(): for acc in get_accounts():
if destination_iban == acc.iban: if destination_iban == acc.iban:
rate = get_forex_rate(account.currency, acc.currency)
reverse_transaction = Transaction.new_transaction( reverse_transaction = Transaction.new_transaction(
date_time=date, date_time=date,
transaction_type='receive_transfer', transaction_type='receive_transfer',
status='processed', status='processed',
other_party={'iban': account.iban,}, other_party={'iban': account.iban,},
extra={ extra={
'currency': account.currency, 'currency': acc.currency,
'amount': -amount, 'amount': int(-amount * rate),
'description': description, 'description': description,
'originalAmount': -amount,
'originalCurrency': account.currency,
}, },
) )
insert_transaction(acc.id, reverse_transaction) insert_transaction(acc.id, reverse_transaction)
@ -100,7 +103,7 @@ class TransactionsList(MethodView):
date_time=date, date_time=date,
read=False, read=False,
) )
insert_notification(acc.id, notification) insert_notification(whose_account(acc.id), notification)
break break
else: else:
return returns.abort(returns.NOT_FOUND) return returns.abort(returns.NOT_FOUND)

View file

@ -306,5 +306,35 @@ class Module(ModuleType):
) )
self.db.commit() self.db.commit()
@get_db
def get_forex_rate(self, from_currency: str, to_currency: str) -> float | None:
if from_currency == to_currency:
return 1.0
cur = self.db.cursor()
if from_currency == 'RON' or to_currency == 'RON':
currency_pairs = [(from_currency, to_currency)]
else:
currency_pairs = [(from_currency, 'RON'), ('RON', to_currency)]
amount = 1.0
for currency_pair in currency_pairs:
to_select = 'to_ron'
if currency_pair[0] == 'RON':
to_select = 'from_ron'
cur.execute(
f'select {to_select} from exchange where currency = ?',
(currency_pair[1] if currency_pair[0] == 'RON' else currency_pair[0],),
)
rate = cur.fetchone()
if rate is None:
amount = None
break
rate = rate[0]
amount *= rate
return amount
sys.modules[__name__] = Module(__name__) sys.modules[__name__] = Module(__name__)

View file

@ -60,6 +60,13 @@ create table users_notifications (
foreign key (notification_id) references notifications (id) foreign key (notification_id) references notifications (id)
); );
create table exchange (
id integer primary key autoincrement,
currency text not null,
to_ron real not null,
from_ron real not null
);
create view V_account_balance as create view V_account_balance as
select select
accounts_transactions.account_id as "account_id", accounts_transactions.account_id as "account_id",