This commit is contained in:
2026-03-27 10:20:35 +01:00
parent b922a1ced0
commit fa9d7c7f43
48 changed files with 67256 additions and 0 deletions
+3
View File
@@ -0,0 +1,3 @@
COMPOSE_PROJECT_NAME=dragonbank
POSTGRES_DB=dragonbank
POSTGRES_USER=dragonadmin
+43
View File
@@ -0,0 +1,43 @@
FROM python:3.12-slim
LABEL maintainer="DragonBank Team"
LABEL description="DragonBank Backend API"
# Variables d'environnement
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
# Répertoire de travail
WORKDIR /app
# Installation des dépendances système
RUN apt-get update && apt-get install -y --no-install-recommends \
gcc \
libpq-dev \
curl \
&& rm -rf /var/lib/apt/lists/*
# Copie des fichiers de dépendances
COPY requirements.txt .
# Installation des dépendances Python
RUN pip install --no-cache-dir --upgrade pip && \
pip install --no-cache-dir -r requirements.txt
# Copie du code source
COPY app.py .
# Création d'un utilisateur non-root (sécurité)
RUN adduser --disabled-password --gecos '' appuser && \
chown -R appuser:appuser /app
USER appuser
# Exposition du port
EXPOSE 5000
# Healthcheck
HEALTHCHECK --interval=15s --timeout=5s --retries=3 \
CMD curl -f http://localhost:5000/api/health || exit 1
# Commande de démarrage
CMD ["python", "app.py"]
File diff suppressed because it is too large Load Diff
+6
View File
@@ -0,0 +1,6 @@
Flask==3.0.0
Flask-CORS==4.0.0
psycopg2-binary==2.9.9
bcrypt==4.1.2
PyJWT==2.8.0
gunicorn==21.2.0
+14
View File
@@ -0,0 +1,14 @@
FROM postgres:16-alpine
LABEL maintainer="DragonBank Team"
LABEL description="DragonBank PostgreSQL Database"
# Copie du script d'initialisation
COPY init.sql /docker-entrypoint-initdb.d/
# Exposition du port
EXPOSE 5432
# Healthcheck intégré
HEALTHCHECK --interval=10s --timeout=5s --retries=5 \
CMD pg_isready -U dragonadmin -d dragonbank || exit 1
+232
View File
@@ -0,0 +1,232 @@
-- ============================================
-- DragonBank - Schéma de Base de Données
-- ============================================
-- Extension pour UUID
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
CREATE EXTENSION IF NOT EXISTS "pgcrypto";
-- ============================================
-- TABLE: Utilisateurs
-- ============================================
CREATE TABLE IF NOT EXISTS users (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
email VARCHAR(255) UNIQUE NOT NULL,
password_hash VARCHAR(255) NOT NULL,
first_name VARCHAR(100) NOT NULL,
last_name VARCHAR(100) NOT NULL,
phone VARCHAR(20),
address TEXT,
date_of_birth DATE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
is_active BOOLEAN DEFAULT TRUE,
last_login TIMESTAMP WITH TIME ZONE
);
-- ============================================
-- TABLE: Types de Comptes
-- ============================================
CREATE TYPE account_type AS ENUM ('courant', 'livret_a', 'assurance_vie');
CREATE TYPE account_status AS ENUM ('active', 'closed', 'frozen');
-- ============================================
-- TABLE: Comptes Bancaires
-- ============================================
CREATE TABLE IF NOT EXISTS accounts (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
account_number VARCHAR(20) UNIQUE NOT NULL,
account_type account_type NOT NULL DEFAULT 'courant',
balance DECIMAL(15, 2) NOT NULL DEFAULT 0.00,
currency VARCHAR(3) DEFAULT 'EUR',
status account_status DEFAULT 'active',
interest_rate DECIMAL(5, 4) DEFAULT 0.0000,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
CONSTRAINT positive_balance CHECK (balance >= 0)
);
-- ============================================
-- TABLE: Bénéficiaires
-- ============================================
CREATE TYPE beneficiary_status AS ENUM ('pending', 'approved', 'rejected');
CREATE TABLE IF NOT EXISTS beneficiaries (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
beneficiary_name VARCHAR(200) NOT NULL,
bank_name VARCHAR(200) DEFAULT 'DragonBank',
account_number VARCHAR(34) NOT NULL,
iban VARCHAR(34),
bic VARCHAR(11),
status beneficiary_status DEFAULT 'approved',
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
UNIQUE(user_id, account_number)
);
-- ============================================
-- TABLE: Transactions / Virements
-- ============================================
CREATE TYPE transaction_type AS ENUM (
'virement_interne',
'virement_entre_personnes',
'virement_externe',
'depot',
'retrait',
'interets'
);
CREATE TYPE transaction_status AS ENUM ('pending', 'completed', 'failed', 'cancelled');
CREATE TABLE IF NOT EXISTS transactions (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
from_account_id UUID REFERENCES accounts(id),
to_account_id UUID REFERENCES accounts(id),
transaction_type transaction_type NOT NULL,
amount DECIMAL(15, 2) NOT NULL,
currency VARCHAR(3) DEFAULT 'EUR',
description TEXT,
status transaction_status DEFAULT 'pending',
reference VARCHAR(50) UNIQUE DEFAULT uuid_generate_v4()::text,
external_bank_name VARCHAR(200),
external_account_number VARCHAR(34),
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
executed_at TIMESTAMP WITH TIME ZONE,
CONSTRAINT positive_amount CHECK (amount > 0)
);
-- ============================================
-- TABLE: Historique des Intérêts
-- ============================================
CREATE TABLE IF NOT EXISTS interest_history (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
account_id UUID NOT NULL REFERENCES accounts(id) ON DELETE CASCADE,
amount DECIMAL(15, 2) NOT NULL,
rate DECIMAL(5, 4) NOT NULL,
balance_before DECIMAL(15, 2) NOT NULL,
balance_after DECIMAL(15, 2) NOT NULL,
calculated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- ============================================
-- TABLE: Sessions (pour la sécurité)
-- ============================================
CREATE TABLE IF NOT EXISTS sessions (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
token VARCHAR(500) NOT NULL,
ip_address VARCHAR(45),
user_agent TEXT,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
expires_at TIMESTAMP WITH TIME ZONE NOT NULL,
is_active BOOLEAN DEFAULT TRUE
);
-- ============================================
-- TABLE: Logs d'audit
-- ============================================
CREATE TABLE IF NOT EXISTS audit_logs (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id UUID REFERENCES users(id),
action VARCHAR(100) NOT NULL,
details JSONB,
ip_address VARCHAR(45),
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- ============================================
-- INDEX pour performance
-- ============================================
CREATE INDEX idx_accounts_user_id ON accounts(user_id);
CREATE INDEX idx_transactions_from ON transactions(from_account_id);
CREATE INDEX idx_transactions_to ON transactions(to_account_id);
CREATE INDEX idx_transactions_status ON transactions(status);
CREATE INDEX idx_transactions_created ON transactions(created_at);
CREATE INDEX idx_beneficiaries_user_id ON beneficiaries(user_id);
CREATE INDEX idx_sessions_user_id ON sessions(user_id);
CREATE INDEX idx_sessions_token ON sessions(token);
CREATE INDEX idx_audit_user_id ON audit_logs(user_id);
-- ============================================
-- FONCTION: Générer un numéro de compte
-- ============================================
CREATE OR REPLACE FUNCTION generate_account_number()
RETURNS VARCHAR(20) AS $$
DECLARE
new_number VARCHAR(20);
prefix VARCHAR(4) := 'DRG';
BEGIN
new_number := prefix || LPAD(FLOOR(RANDOM() * 10000000000)::TEXT, 13, '0');
WHILE EXISTS (SELECT 1 FROM accounts WHERE account_number = new_number) LOOP
new_number := prefix || LPAD(FLOOR(RANDOM() * 10000000000)::TEXT, 13, '0');
END LOOP;
RETURN new_number;
END;
$$ LANGUAGE plpgsql;
-- ============================================
-- FONCTION: Mise à jour du timestamp
-- ============================================
CREATE OR REPLACE FUNCTION update_updated_at()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- Triggers
CREATE TRIGGER update_users_timestamp
BEFORE UPDATE ON users
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
CREATE TRIGGER update_accounts_timestamp
BEFORE UPDATE ON accounts
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
-- ============================================
-- DONNÉES DE TEST
-- ============================================
-- Utilisateur de test (mot de passe: "password123")
INSERT INTO users (id, email, password_hash, first_name, last_name, phone, address, date_of_birth)
VALUES (
'a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11',
'jean.dupont@email.com',
'$2b$12$LJ3m4ys3GZ5aJfrRkOmU0OYm0MqfGBCqGY5nS5B1VZHvMKvSG1IHa',
'Jean',
'Dupont',
'+33612345678',
'12 Rue de la Paix, 75001 Paris',
'1990-05-15'
);
-- Deuxième utilisateur de test
INSERT INTO users (id, email, password_hash, first_name, last_name, phone, address, date_of_birth)
VALUES (
'b1eebc99-9c0b-4ef8-bb6d-6bb9bd380a22',
'marie.martin@email.com',
'$2b$12$LJ3m4ys3GZ5aJfrRkOmU0OYm0MqfGBCqGY5nS5B1VZHvMKvSG1IHa',
'Marie',
'Martin',
'+33698765432',
'5 Avenue des Champs-Élysées, 75008 Paris',
'1985-11-20'
);
-- Comptes bancaires de test
INSERT INTO accounts (user_id, account_number, account_type, balance, interest_rate)
VALUES
('a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11', 'DRG0000000001234', 'courant', 5000.00, 0.0000),
('a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11', 'DRG0000000001235', 'livret_a', 15000.00, 0.0300),
('b1eebc99-9c0b-4ef8-bb6d-6bb9bd380a22', 'DRG0000000005678', 'courant', 3200.00, 0.0000),
('b1eebc99-9c0b-4ef8-bb6d-6bb9bd380a22', 'DRG0000000005679', 'assurance_vie', 25000.00, 0.0200);
-- Bénéficiaire de test
INSERT INTO beneficiaries (user_id, beneficiary_name, bank_name, account_number)
VALUES
('a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11', 'Marie Martin', 'DragonBank', 'DRG0000000005678');
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO dragonadmin;
GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO dragonadmin;
+140
View File
@@ -0,0 +1,140 @@
version: '3.8'
services:
# ============================================
# BASE DE DONNÉES POSTGRES
# ============================================
db:
build:
context: ./db
dockerfile: Dockerfile
container_name: dragonbank-db
environment:
POSTGRES_DB: dragonbank
POSTGRES_USER: dragonadmin
POSTGRES_PASSWORD_FILE: /run/secrets/db_password
secrets:
- db_password
volumes:
- postgres_data:/var/lib/postgresql/data
- ./db/init.sql:/docker-entrypoint-initdb.d/init.sql
networks:
- dragonbank-backend-net
ports:
- "5432:5432"
healthcheck:
test: ["CMD-SHELL", "pg_isready -U dragonadmin -d dragonbank"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
restart: unless-stopped
# ============================================
# BACKEND API PYTHON (FLASK)
# ============================================
backend:
build:
context: ./backend
dockerfile: Dockerfile
container_name: dragonbank-backend
environment:
DATABASE_URL: postgresql://dragonadmin:dragonpass@db:5432/dragonbank
SECRET_KEY: dragonbank-super-secret-key-2024
FLASK_ENV: production
depends_on:
db:
condition: service_healthy
networks:
- dragonbank-backend-net
- dragonbank-frontend-net
ports:
- "5000:5000"
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:5000/api/health"]
interval: 15s
timeout: 5s
retries: 3
start_period: 20s
restart: unless-stopped
# ============================================
# FRONTEND PYTHON (FLASK)
# ============================================
frontend:
build:
context: ./frontend
dockerfile: Dockerfile
container_name: dragonbank-frontend
environment:
BACKEND_URL: http://backend:5000
SECRET_KEY: frontend-secret-key-2024
depends_on:
backend:
condition: service_healthy
networks:
- dragonbank-frontend-net
ports:
- "8080:8080"
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/"]
interval: 15s
timeout: 5s
retries: 3
start_period: 15s
restart: unless-stopped
# ============================================
# SERVICE D'INTÉRÊTS (BONUS)
# ============================================
interests:
build:
context: ./interests
dockerfile: Dockerfile
container_name: dragonbank-interests
environment:
DATABASE_URL: postgresql://dragonadmin:dragonpass@db:5432/dragonbank
INTEREST_RATE_LIVRET_A: 0.03
INTEREST_RATE_ASSURANCE_VIE: 0.02
INTERVAL_SECONDS: 86400
depends_on:
db:
condition: service_healthy
networks:
- dragonbank-backend-net
healthcheck:
test: ["CMD", "python", "-c", "import psycopg2; psycopg2.connect('postgresql://dragonadmin:dragonpass@db:5432/dragonbank')"]
interval: 30s
timeout: 10s
retries: 3
start_period: 15s
restart: unless-stopped
# ============================================
# SECRETS
# ============================================
secrets:
db_password:
file: ./secrets/db_password.txt
# ============================================
# VOLUMES
# ============================================
volumes:
postgres_data:
driver: local
# ============================================
# NETWORKS
# ============================================
networks:
dragonbank-backend-net:
driver: bridge
ipam:
config:
- subnet: 172.20.0.0/16
dragonbank-frontend-net:
driver: bridge
ipam:
config:
- subnet: 172.21.0.0/16
+30
View File
@@ -0,0 +1,30 @@
FROM python:3.12-slim
LABEL maintainer="DragonBank Team"
LABEL description="DragonBank Frontend Web App"
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
WORKDIR /app
RUN apt-get update && apt-get install -y --no-install-recommends \
curl \
&& rm -rf /var/lib/apt/lists/*
COPY requirements.txt .
RUN pip install --no-cache-dir --upgrade pip && \
pip install --no-cache-dir -r requirements.txt
COPY . .
RUN adduser --disabled-password --gecos '' appuser && \
chown -R appuser:appuser /app
USER appuser
EXPOSE 8080
HEALTHCHECK --interval=15s --timeout=5s --retries=3 \
CMD curl -f http://localhost:8080/ || exit 1
CMD ["python", "app.py"]
+357
View File
@@ -0,0 +1,357 @@
"""
DragonBank - Frontend Web Application
======================================
Interface web pour l'application bancaire DragonBank.
"""
import os
from functools import wraps
import requests
from flask import Flask, render_template, request, redirect, url_for, session, flash
app = Flask(__name__)
app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY', 'frontend-secret')
BACKEND_URL = os.environ.get('BACKEND_URL', 'http://backend:5000')
# ============================================
# UTILITAIRES
# ============================================
def api_request(method, endpoint, data=None, token=None):
"""Effectue une requête vers le backend API."""
url = f"{BACKEND_URL}{endpoint}"
headers = {'Content-Type': 'application/json'}
if token:
headers['Authorization'] = f'Bearer {token}'
try:
if method == 'GET':
resp = requests.get(url, headers=headers, timeout=10)
elif method == 'POST':
resp = requests.post(url, json=data, headers=headers, timeout=10)
elif method == 'DELETE':
resp = requests.delete(url, headers=headers, timeout=10)
else:
return None, 'Méthode non supportée'
return resp.json(), resp.status_code
except requests.exceptions.ConnectionError:
return {'error': 'Impossible de contacter le serveur'}, 503
except Exception as e:
return {'error': str(e)}, 500
def login_required(f):
"""Décorateur pour protéger les routes nécessitant une connexion."""
@wraps(f)
def decorated(*args, **kwargs):
if 'token' not in session:
flash('Veuillez vous connecter', 'warning')
return redirect(url_for('login'))
return f(*args, **kwargs)
return decorated
# ============================================
# ROUTES PUBLIQUES
# ============================================
@app.route('/')
def home():
"""Page d'accueil."""
if 'token' in session:
return redirect(url_for('dashboard'))
return render_template('login.html')
@app.route('/login', methods=['GET', 'POST'])
def login():
"""Page de connexion."""
if request.method == 'POST':
email = request.form.get('email')
password = request.form.get('password')
data, status = api_request('POST', '/api/auth/login', {
'email': email,
'password': password
})
if status == 200:
session['token'] = data['token']
session['user'] = data['user']
flash(f'Bienvenue {data["user"]["first_name"]} ! 🐉', 'success')
return redirect(url_for('dashboard'))
else:
flash(data.get('error', 'Erreur de connexion'), 'danger')
return render_template('login.html')
@app.route('/register', methods=['GET', 'POST'])
def register():
"""Page d'inscription."""
if request.method == 'POST':
form_data = {
'email': request.form.get('email'),
'password': request.form.get('password'),
'first_name': request.form.get('first_name'),
'last_name': request.form.get('last_name'),
'phone': request.form.get('phone', ''),
'address': request.form.get('address', ''),
'date_of_birth': request.form.get('date_of_birth') or None
}
# Vérifier confirmation mot de passe
if request.form.get('password') != request.form.get('confirm_password'):
flash('Les mots de passe ne correspondent pas', 'danger')
return render_template('register.html')
data, status = api_request('POST', '/api/auth/register', form_data)
if status == 201:
session['token'] = data['token']
session['user'] = data['user']
flash('Compte créé avec succès ! Un compte courant a été ouvert automatiquement. 🎉', 'success')
return redirect(url_for('dashboard'))
else:
flash(data.get('error', 'Erreur lors de l\'inscription'), 'danger')
return render_template('register.html')
@app.route('/logout')
def logout():
"""Déconnexion."""
session.clear()
flash('Vous êtes déconnecté', 'info')
return redirect(url_for('login'))
# ============================================
# ROUTES PROTÉGÉES - DASHBOARD
# ============================================
@app.route('/dashboard')
@login_required
def dashboard():
"""Tableau de bord principal."""
token = session['token']
accounts_data, _ = api_request('GET', '/api/accounts', token=token)
stats_data, _ = api_request('GET', '/api/stats', token=token)
transactions_data, _ = api_request('GET', '/api/transactions?limit=5', token=token)
return render_template('dashboard.html',
user=session.get('user', {}),
accounts=accounts_data.get('accounts', []),
stats=stats_data.get('stats', {}),
recent_transactions=transactions_data.get('transactions', [])
)
# ============================================
# ROUTES - COMPTES
# ============================================
@app.route('/accounts')
@login_required
def accounts():
"""Liste des comptes bancaires."""
token = session['token']
data, status = api_request('GET', '/api/accounts', token=token)
return render_template('accounts.html',
user=session.get('user', {}),
accounts=data.get('accounts', [])
)
@app.route('/accounts/open', methods=['GET', 'POST'])
@login_required
def open_account():
"""Ouverture d'un nouveau compte."""
if request.method == 'POST':
token = session['token']
form_data = {
'account_type': request.form.get('account_type'),
'initial_deposit': float(request.form.get('initial_deposit', 0))
}
data, status = api_request('POST', '/api/accounts', data=form_data, token=token)
if status == 201:
flash(f'Compte {form_data["account_type"]} ouvert avec succès ! 🎉', 'success')
return redirect(url_for('accounts'))
else:
flash(data.get('error', 'Erreur lors de l\'ouverture du compte'), 'danger')
return render_template('open_account.html', user=session.get('user', {}))
# ============================================
# ROUTES - BÉNÉFICIAIRES
# ============================================
@app.route('/beneficiaries')
@login_required
def beneficiaries():
"""Liste des bénéficiaires."""
token = session['token']
data, status = api_request('GET', '/api/beneficiaries', token=token)
return render_template('beneficiaries.html',
user=session.get('user', {}),
beneficiaries=data.get('beneficiaries', [])
)
@app.route('/beneficiaries/add', methods=['GET', 'POST'])
@login_required
def add_beneficiary():
"""Ajout d'un bénéficiaire."""
if request.method == 'POST':
token = session['token']
form_data = {
'beneficiary_name': request.form.get('beneficiary_name'),
'account_number': request.form.get('account_number'),
'bank_name': request.form.get('bank_name', 'DragonBank'),
'iban': request.form.get('iban', ''),
'bic': request.form.get('bic', '')
}
data, status = api_request('POST', '/api/beneficiaries', data=form_data, token=token)
if status == 201:
flash('Bénéficiaire ajouté avec succès !', 'success')
return redirect(url_for('beneficiaries'))
else:
flash(data.get('error', 'Erreur'), 'danger')
return render_template('add_beneficiary.html', user=session.get('user', {}))
@app.route('/beneficiaries/delete/<beneficiary_id>')
@login_required
def delete_beneficiary(beneficiary_id):
"""Suppression d'un bénéficiaire."""
token = session['token']
data, status = api_request('DELETE', f'/api/beneficiaries/{beneficiary_id}', token=token)
if status == 200:
flash('Bénéficiaire supprimé', 'success')
else:
flash(data.get('error', 'Erreur'), 'danger')
return redirect(url_for('beneficiaries'))
# ============================================
# ROUTES - VIREMENTS
# ============================================
@app.route('/transfer', methods=['GET', 'POST'])
@login_required
def transfer():
"""Page de virement (interne et entre personnes)."""
token = session['token']
accounts_data, _ = api_request('GET', '/api/accounts', token=token)
beneficiaries_data, _ = api_request('GET', '/api/beneficiaries', token=token)
if request.method == 'POST':
transfer_type = request.form.get('transfer_type')
amount = float(request.form.get('amount', 0))
from_account_id = request.form.get('from_account_id')
description = request.form.get('description', '')
if transfer_type == 'internal':
to_account_id = request.form.get('to_account_id')
data, status = api_request('POST', '/api/transfers/internal', {
'from_account_id': from_account_id,
'to_account_id': to_account_id,
'amount': amount,
'description': description
}, token=token)
elif transfer_type == 'person':
beneficiary_id = request.form.get('beneficiary_id')
data, status = api_request('POST', '/api/transfers/person', {
'from_account_id': from_account_id,
'beneficiary_id': beneficiary_id,
'amount': amount,
'description': description
}, token=token)
else:
flash('Type de virement invalide', 'danger')
return redirect(url_for('transfer'))
if status == 200:
flash(data.get('message', 'Virement effectué !'), 'success')
return redirect(url_for('dashboard'))
else:
flash(data.get('error', 'Erreur lors du virement'), 'danger')
return render_template('transfer.html',
user=session.get('user', {}),
accounts=accounts_data.get('accounts', []),
beneficiaries=beneficiaries_data.get('beneficiaries', [])
)
@app.route('/transfer/external', methods=['GET', 'POST'])
@login_required
def transfer_external():
"""Virement externe (depuis/vers autre banque)."""
token = session['token']
accounts_data, _ = api_request('GET', '/api/accounts', token=token)
if request.method == 'POST':
form_data = {
'account_id': request.form.get('account_id'),
'amount': float(request.form.get('amount', 0)),
'external_bank_name': request.form.get('external_bank_name'),
'external_account_number': request.form.get('external_account_number'),
'direction': request.form.get('direction'),
'description': request.form.get('description', '')
}
data, status = api_request('POST', '/api/transfers/external', form_data, token=token)
if status == 200:
flash(data.get('message', 'Virement externe effectué !'), 'success')
return redirect(url_for('dashboard'))
else:
flash(data.get('error', 'Erreur'), 'danger')
return render_template('transfer_external.html',
user=session.get('user', {}),
accounts=accounts_data.get('accounts', [])
)
# ============================================
# ROUTES - HISTORIQUE
# ============================================
@app.route('/transactions')
@login_required
def transactions():
"""Historique des transactions."""
token = session['token']
account_id = request.args.get('account_id', '')
endpoint = '/api/transactions?limit=100'
if account_id:
endpoint += f'&account_id={account_id}'
data, status = api_request('GET', endpoint, token=token)
accounts_data, _ = api_request('GET', '/api/accounts', token=token)
return render_template('transactions.html',
user=session.get('user', {}),
transactions=data.get('transactions', []),
accounts=accounts_data.get('accounts', []),
selected_account=account_id
)
# ============================================
# DÉMARRAGE
# ============================================
if __name__ == '__main__':
print("🐉 DragonBank Frontend starting on port 8080...")
app.run(host='0.0.0.0', port=8080, debug=False)
+2
View File
@@ -0,0 +1,2 @@
Flask==3.0.0
requests==2.31.0
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,81 @@
{% extends "base.html" %}
{% block title %}Mes Comptes{% endblock %}
{% block content %}
<div class="main-content">
<div class="page-header d-flex justify-content-between align-items-center">
<div>
<h1><i class="bi bi-wallet2"></i> Mes Comptes</h1>
<p>Gérez vos comptes bancaires</p>
</div>
<a href="{{ url_for('open_account') }}" class="btn btn-dragon">
<i class="bi bi-plus-circle"></i> Ouvrir un compte
</a>
</div>
<div class="row g-4">
{% for account in accounts %}
<div class="col-lg-6 fade-in delay-{{ loop.index }}">
<div class="account-card {{ account.account_type }}" style="padding: 2rem;">
<div class="d-flex justify-content-between align-items-start mb-3">
<span class="account-type-badge {{ account.account_type }}">
{% if account.account_type == 'courant' %}
<i class="bi bi-credit-card"></i> Compte Courant
{% elif account.account_type == 'livret_a' %}
<i class="bi bi-piggy-bank"></i> Livret A
{% elif account.account_type == 'assurance_vie' %}
<i class="bi bi-shield-check"></i> Assurance Vie
{% endif %}
</span>
<span class="status-badge completed">
<i class="bi bi-check-circle"></i> {{ account.status }}
</span>
</div>
<div class="account-balance mb-2">
{{ "%.2f"|format(account.balance) }} €
</div>
<div class="account-number mb-3">
<i class="bi bi-hash"></i> {{ account.account_number }}
</div>
<div class="d-flex justify-content-between align-items-center">
<small class="text-muted">
<i class="bi bi-calendar"></i>
Ouvert le {{ account.created_at[:10] if account.created_at else 'N/A' }}
</small>
{% if account.interest_rate > 0 %}
<small class="text-success fw-bold">
<i class="bi bi-graph-up-arrow"></i>
Taux: {{ "%.2f"|format(account.interest_rate * 100) }}%
</small>
{% endif %}
</div>
<div class="mt-3 d-flex gap-2">
<a href="{{ url_for('transactions', account_id=account.id) }}"
class="btn btn-sm btn-dragon-outline">
<i class="bi bi-clock-history"></i> Historique
</a>
<a href="{{ url_for('transfer') }}" class="btn btn-sm btn-dragon">
<i class="bi bi-arrow-left-right"></i> Virement
</a>
</div>
</div>
</div>
{% else %}
<div class="col-12">
<div class="section-card text-center py-5">
<i class="bi bi-wallet" style="font-size: 4rem; color: var(--dragon-text-light);"></i>
<h3 class="mt-3">Aucun compte bancaire</h3>
<p class="text-muted">Ouvrez votre premier compte pour commencer</p>
<a href="{{ url_for('open_account') }}" class="btn btn-dragon mt-2">
<i class="bi bi-plus-circle"></i> Ouvrir un compte
</a>
</div>
</div>
{% endfor %}
</div>
</div>
{% endblock %}
@@ -0,0 +1,70 @@
{% extends "base.html" %}
{% block title %}Ajouter un Bénéficiaire{% endblock %}
{% block content %}
<div class="main-content">
<div class="page-header">
<h1><i class="bi bi-person-plus"></i> Ajouter un Bénéficiaire</h1>
<p>Enregistrez un nouveau contact pour vos virements</p>
</div>
<div class="row justify-content-center">
<div class="col-md-6">
<div class="section-card fade-in">
<form method="POST" action="{{ url_for('add_beneficiary') }}">
<div class="form-group">
<label class="form-label">
<i class="bi bi-person"></i> Nom du bénéficiaire *
</label>
<input type="text" name="beneficiary_name" class="form-control"
placeholder="Ex: Marie Martin" required>
</div>
<div class="form-group">
<label class="form-label">
<i class="bi bi-bank"></i> Banque
</label>
<input type="text" name="bank_name" class="form-control"
value="DragonBank" placeholder="DragonBank">
</div>
<div class="form-group">
<label class="form-label">
<i class="bi bi-hash"></i> Numéro de compte *
</label>
<input type="text" name="account_number" class="form-control"
placeholder="Ex: DRG0000000005678" required>
<small class="text-muted">
Pour DragonBank, le numéro commence par DRG
</small>
</div>
<div class="form-group">
<label class="form-label">
<i class="bi bi-upc"></i> IBAN (optionnel)
</label>
<input type="text" name="iban" class="form-control"
placeholder="FR76 XXXX XXXX XXXX">
</div>
<div class="form-group">
<label class="form-label">
<i class="bi bi-building"></i> BIC (optionnel)
</label>
<input type="text" name="bic" class="form-control"
placeholder="DRGBFRPP">
</div>
<button type="submit" class="btn btn-dragon w-100 mt-3">
<i class="bi bi-check-circle"></i> Ajouter le bénéficiaire
</button>
<a href="{{ url_for('beneficiaries') }}" class="btn btn-dragon-outline w-100 mt-2">
<i class="bi bi-arrow-left"></i> Retour
</a>
</form>
</div>
</div>
</div>
</div>
{% endblock %}
+123
View File
@@ -0,0 +1,123 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>🐉 DragonBank - {% block title %}Accueil{% endblock %}</title>
<!-- Bootstrap 5 CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- Bootstrap Icons -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.2/font/bootstrap-icons.css" rel="stylesheet">
<!-- Custom CSS -->
<link href="{{ url_for('static', filename='style.css') }}" rel="stylesheet">
</head>
<body>
{% if session.get('token') %}
<!-- NAVBAR CONNECTÉ -->
<nav class="navbar navbar-expand-lg navbar-dark">
<div class="container-fluid">
<a class="navbar-brand" href="{{ url_for('dashboard') }}">
<span class="dragon-icon">🐉</span> DragonBank
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav me-auto">
<li class="nav-item">
<a class="nav-link {% if request.endpoint == 'dashboard' %}active{% endif %}"
href="{{ url_for('dashboard') }}">
<i class="bi bi-speedometer2"></i> Tableau de bord
</a>
</li>
<li class="nav-item">
<a class="nav-link {% if request.endpoint in ['accounts', 'open_account'] %}active{% endif %}"
href="{{ url_for('accounts') }}">
<i class="bi bi-wallet2"></i> Mes comptes
</a>
</li>
<li class="nav-item">
<a class="nav-link {% if request.endpoint in ['beneficiaries', 'add_beneficiary'] %}active{% endif %}"
href="{{ url_for('beneficiaries') }}">
<i class="bi bi-people"></i> Bénéficiaires
</a>
</li>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle {% if request.endpoint in ['transfer', 'transfer_external'] %}active{% endif %}"
href="#" role="button" data-bs-toggle="dropdown">
<i class="bi bi-arrow-left-right"></i> Virements
</a>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="{{ url_for('transfer') }}">
<i class="bi bi-arrow-repeat"></i> Virement interne / Personne
</a></li>
<li><a class="dropdown-item" href="{{ url_for('transfer_external') }}">
<i class="bi bi-bank"></i> Virement externe
</a></li>
</ul>
</li>
<li class="nav-item">
<a class="nav-link {% if request.endpoint == 'transactions' %}active{% endif %}"
href="{{ url_for('transactions') }}">
<i class="bi bi-clock-history"></i> Historique
</a>
</li>
</ul>
<ul class="navbar-nav">
<li class="nav-item me-3 d-flex align-items-center">
<span class="user-badge">
<i class="bi bi-person-circle"></i>
{{ session.get('user', {}).get('first_name', '') }}
{{ session.get('user', {}).get('last_name', '') }}
</span>
</li>
<li class="nav-item">
<a class="nav-link text-danger" href="{{ url_for('logout') }}">
<i class="bi bi-box-arrow-right"></i> Déconnexion
</a>
</li>
</ul>
</div>
</div>
</nav>
{% endif %}
<!-- FLASH MESSAGES -->
<div class="container-fluid mt-3">
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ category }} alert-dismissible fade show" role="alert">
{% if category == 'success' %}
<i class="bi bi-check-circle-fill"></i>
{% elif category == 'danger' %}
<i class="bi bi-exclamation-triangle-fill"></i>
{% elif category == 'warning' %}
<i class="bi bi-exclamation-circle-fill"></i>
{% else %}
<i class="bi bi-info-circle-fill"></i>
{% endif %}
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
{% endfor %}
{% endif %}
{% endwith %}
</div>
<!-- CONTENU PRINCIPAL -->
{% block content %}{% endblock %}
{% if session.get('token') %}
<footer class="footer">
<p>🐉 <strong>DragonBank</strong> &copy; 2024 — Votre banque de confiance |
Sécurisé par <a href="#">Dragon Shield™</a></p>
</footer>
{% endif %}
<!-- Bootstrap JS -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
{% block scripts %}{% endblock %}
</body>
</html>
@@ -0,0 +1,154 @@
{% extends "base.html" %}
{% block title %}Tableau de bord{% endblock %}
{% block content %}
<div class="main-content">
<!-- HEADER -->
<div class="page-header fade-in">
<h1>Bienvenue {{ user.get('first_name', '') }} 🐉</h1>
<p>Voici un aperçu de vos finances</p>
</div>
<!-- STATISTIQUES -->
<div class="row g-4 mb-4">
<div class="col-lg-3 col-md-6 fade-in delay-1">
<div class="stat-card primary">
<div class="stat-icon"><i class="bi bi-wallet2"></i></div>
<div class="stat-value">{{ "%.2f"|format(stats.get('total_balance', 0)) }} €</div>
<div class="stat-label">Solde total</div>
</div>
</div>
<div class="col-lg-3 col-md-6 fade-in delay-2">
<div class="stat-card success">
<div class="stat-icon"><i class="bi bi-credit-card"></i></div>
<div class="stat-value">{{ stats.get('total_accounts', 0) }}</div>
<div class="stat-label">Comptes actifs</div>
</div>
</div>
<div class="col-lg-3 col-md-6 fade-in delay-3">
<div class="stat-card info">
<div class="stat-icon"><i class="bi bi-arrow-left-right"></i></div>
<div class="stat-value">{{ stats.get('monthly_transactions', 0) }}</div>
<div class="stat-label">Transactions ce mois</div>
</div>
</div>
<div class="col-lg-3 col-md-6 fade-in delay-4">
<div class="stat-card warning">
<div class="stat-icon"><i class="bi bi-people"></i></div>
<div class="stat-value">{{ stats.get('total_beneficiaries', 0) }}</div>
<div class="stat-label">Bénéficiaires</div>
</div>
</div>
</div>
<div class="row g-4">
<!-- MES COMPTES -->
<div class="col-lg-7 fade-in delay-2">
<div class="section-card">
<div class="d-flex justify-content-between align-items-center mb-3">
<h3 class="section-title mb-0" style="border:none; padding:0;">
<i class="bi bi-wallet2"></i> Mes Comptes
</h3>
<a href="{{ url_for('open_account') }}" class="btn btn-sm btn-dragon">
<i class="bi bi-plus-circle"></i> Ouvrir un compte
</a>
</div>
{% if accounts %}
{% for account in accounts %}
<div class="account-card {{ account.account_type }}">
<div class="d-flex justify-content-between align-items-start">
<div>
<span class="account-type-badge {{ account.account_type }}">
{% if account.account_type == 'courant' %}
<i class="bi bi-credit-card"></i> Compte Courant
{% elif account.account_type == 'livret_a' %}
<i class="bi bi-piggy-bank"></i> Livret A
{% elif account.account_type == 'assurance_vie' %}
<i class="bi bi-shield-check"></i> Assurance Vie
{% endif %}
</span>
<div class="account-number mt-2">
{{ account.account_number }}
</div>
</div>
<div class="text-end">
<div class="account-balance">
{{ "%.2f"|format(account.balance) }} €
</div>
{% if account.interest_rate > 0 %}
<small class="text-success">
<i class="bi bi-graph-up-arrow"></i>
{{ "%.2f"|format(account.interest_rate * 100) }}% / jour
</small>
{% endif %}
</div>
</div>
</div>
{% endfor %}
{% else %}
<div class="text-center py-4 text-muted">
<i class="bi bi-wallet" style="font-size: 3rem;"></i>
<p class="mt-2">Aucun compte trouvé</p>
</div>
{% endif %}
</div>
</div>
<!-- ACTIONS RAPIDES + DERNIÈRES TRANSACTIONS -->
<div class="col-lg-5 fade-in delay-3">
<!-- Actions rapides -->
<div class="section-card mb-4">
<h3 class="section-title">
<i class="bi bi-lightning-charge"></i> Actions rapides
</h3>
<div class="d-grid gap-2">
<a href="{{ url_for('transfer') }}" class="btn btn-dragon">
<i class="bi bi-arrow-left-right"></i> Faire un virement
</a>
<a href="{{ url_for('add_beneficiary') }}" class="btn btn-gold">
<i class="bi bi-person-plus"></i> Ajouter un bénéficiaire
</a>
<a href="{{ url_for('transfer_external') }}" class="btn btn-success-custom">
<i class="bi bi-bank"></i> Virement externe
</a>
</div>
</div>
<!-- Dernières transactions -->
<div class="section-card">
<div class="d-flex justify-content-between align-items-center mb-3">
<h3 class="section-title mb-0" style="border:none; padding:0;">
<i class="bi bi-clock-history"></i> Dernières opérations
</h3>
<a href="{{ url_for('transactions') }}" class="btn btn-sm btn-dragon-outline">
Voir tout
</a>
</div>
{% if recent_transactions %}
{% for tx in recent_transactions %}
<div class="d-flex justify-content-between align-items-center py-2 border-bottom">
<div>
<div class="fw-bold" style="font-size: 0.9rem;">
{{ tx.description[:40] }}{% if tx.description|length > 40 %}...{% endif %}
</div>
<small class="text-muted">
{{ tx.created_at[:10] if tx.created_at else '' }}
</small>
</div>
<div>
<span class="fw-bold" style="color: {% if tx.transaction_type in ['depot', 'interets'] %}var(--dragon-success){% else %}var(--dragon-accent){% endif %};">
{{ "%.2f"|format(tx.amount) }} €
</span>
</div>
</div>
{% endfor %}
{% else %}
<p class="text-center text-muted py-3">Aucune transaction récente</p>
{% endif %}
</div>
</div>
</div>
</div>
{% endblock %}
+68
View File
@@ -0,0 +1,68 @@
{% extends "base.html" %}
{% block title %}Connexion{% endblock %}
{% block content %}
<div class="login-page">
<div class="login-container">
<div class="login-header">
<span class="dragon-logo">🐉</span>
<h1>DragonBank</h1>
<p>Votre banque. Votre pouvoir.</p>
</div>
<div class="login-card fade-in">
<h2>Connexion</h2>
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ category }} mb-3">{{ message }}</div>
{% endfor %}
{% endif %}
{% endwith %}
<form method="POST" action="{{ url_for('login') }}">
<div class="form-group">
<label class="form-label">
<i class="bi bi-envelope"></i> Email
</label>
<input type="email" name="email" class="form-control"
placeholder="votre@email.com" required>
</div>
<div class="form-group">
<label class="form-label">
<i class="bi bi-lock"></i> Mot de passe
</label>
<input type="password" name="password" class="form-control"
placeholder="••••••••" required>
</div>
<button type="submit" class="btn btn-dragon w-100 mt-3">
<i class="bi bi-box-arrow-in-right"></i> Se connecter
</button>
</form>
<hr class="my-4">
<p class="text-center text-muted mb-2">Pas encore de compte ?</p>
<a href="{{ url_for('register') }}" class="btn btn-dragon-outline w-100">
<i class="bi bi-person-plus"></i> Créer un compte
</a>
<div class="text-center mt-4">
<small class="text-muted">
<i class="bi bi-shield-check"></i>
Connexion sécurisée par Dragon Shield™
</small>
</div>
</div>
<div class="text-center mt-3">
<small style="color: rgba(255,255,255,0.5);">
Compte test: jean.dupont@email.com / password123
</small>
</div>
</div>
</div>
{% endblock %}
+102
View File
@@ -0,0 +1,102 @@
{% extends "base.html" %}
{% block title %}Inscription{% endblock %}
{% block content %}
<div class="login-page">
<div class="login-container" style="max-width: 550px;">
<div class="login-header">
<span class="dragon-logo">🐉</span>
<h1>DragonBank</h1>
<p>Rejoignez le clan du Dragon</p>
</div>
<div class="login-card fade-in">
<h2>Créer un compte</h2>
<form method="POST" action="{{ url_for('register') }}">
<div class="row">
<div class="col-md-6">
<div class="form-group">
<label class="form-label">Prénom *</label>
<input type="text" name="first_name" class="form-control"
placeholder="Jean" required>
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label class="form-label">Nom *</label>
<input type="text" name="last_name" class="form-control"
placeholder="Dupont" required>
</div>
</div>
</div>
<div class="form-group">
<label class="form-label">
<i class="bi bi-envelope"></i> Email *
</label>
<input type="email" name="email" class="form-control"
placeholder="votre@email.com" required>
</div>
<div class="form-group">
<label class="form-label">
<i class="bi bi-telephone"></i> Téléphone
</label>
<input type="tel" name="phone" class="form-control"
placeholder="+33 6 12 34 56 78">
</div>
<div class="form-group">
<label class="form-label">
<i class="bi bi-geo-alt"></i> Adresse
</label>
<input type="text" name="address" class="form-control"
placeholder="12 Rue de la Paix, 75001 Paris">
</div>
<div class="form-group">
<label class="form-label">
<i class="bi bi-calendar"></i> Date de naissance
</label>
<input type="date" name="date_of_birth" class="form-control">
</div>
<div class="row">
<div class="col-md-6">
<div class="form-group">
<label class="form-label">
<i class="bi bi-lock"></i> Mot de passe *
</label>
<input type="password" name="password" class="form-control"
placeholder="Min. 6 caractères" minlength="6" required>
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label class="form-label">
<i class="bi bi-lock-fill"></i> Confirmer *
</label>
<input type="password" name="confirm_password" class="form-control"
placeholder="Confirmer" required>
</div>
</div>
</div>
<button type="submit" class="btn btn-dragon w-100 mt-3">
<i class="bi bi-person-plus"></i> Créer mon compte
</button>
</form>
<hr class="my-3">
<p class="text-center">
<a href="{{ url_for('login') }}" class="text-decoration-none"
style="color: var(--dragon-accent);">
<i class="bi bi-arrow-left"></i> Déjà un compte ? Se connecter
</a>
</p>
</div>
</div>
</div>
{% endblock %}
@@ -0,0 +1,98 @@
{% extends "base.html" %}
{% block title %}Virement Externe{% endblock %}
{% block content %}
<div class="main-content">
<div class="page-header">
<h1><i class="bi bi-bank"></i> Virement Externe</h1>
<p>Envoyer ou recevoir un virement depuis une autre banque</p>
</div>
<div class="row justify-content-center">
<div class="col-md-7">
<div class="section-card fade-in">
<form method="POST" action="{{ url_for('transfer_external') }}">
<!-- Direction -->
<div class="form-group">
<label class="form-label fw-bold">
<i class="bi bi-arrow-down-up"></i> Direction
</label>
<select name="direction" class="form-select" required>
<option value="">-- Choisir --</option>
<option value="incoming">Réception (depuis une autre banque)</option>
<option value="outgoing">Envoi (vers une autre banque)</option>
</select>
</div>
<!-- Mon compte -->
<div class="form-group">
<label class="form-label fw-bold">
<i class="bi bi-wallet2"></i> Mon compte DragonBank
</label>
<select name="account_id" class="form-select" required>
<option value="">-- Choisir --</option>
{% for account in accounts %}
<option value="{{ account.id }}">
{{ account.account_type | upper }} -
{{ account.account_number }}
({{ "%.2f"|format(account.balance) }} €)
</option>
{% endfor %}
</select>
</div>
<!-- Banque externe -->
<div class="form-group">
<label class="form-label fw-bold">
<i class="bi bi-building"></i> Nom de la banque externe
</label>
<input type="text" name="external_bank_name" class="form-control"
placeholder="Ex: BNP Paribas, Société Générale..." required>
</div>
<!-- Compte externe -->
<div class="form-group">
<label class="form-label fw-bold">
<i class="bi bi-hash"></i> Numéro de compte externe
</label>
<input type="text" name="external_account_number" class="form-control"
placeholder="IBAN ou numéro de compte" required>
</div>
<!-- Montant -->
<div class="form-group">
<label class="form-label fw-bold">
<i class="bi bi-currency-euro"></i> Montant (€)
</label>
<input type="number" name="amount" class="form-control"
placeholder="0.00" min="0.01" step="0.01" required>
</div>
<!-- Description -->
<div class="form-group">
<label class="form-label">
<i class="bi bi-chat-left-text"></i> Motif (optionnel)
</label>
<input type="text" name="description" class="form-control"
placeholder="Ex: Transfert épargne">
</div>
<div class="alert alert-info mt-3">
<i class="bi bi-info-circle"></i>
<strong>Note:</strong> Les virements externes sont simulés dans cette démo.
En production, ils seraient traités via le réseau bancaire SEPA.
</div>
<button type="submit" class="btn btn-dragon w-100 mt-2">
<i class="bi bi-send"></i> Effectuer le virement
</button>
<a href="{{ url_for('dashboard') }}" class="btn btn-dragon-outline w-100 mt-2">
<i class="bi bi-arrow-left"></i> Annuler
</a>
</form>
</div>
</div>
</div>
</div>
{% endblock %}
+29
View File
@@ -0,0 +1,29 @@
FROM python:3.12-slim
LABEL maintainer="DragonBank Team"
LABEL description="DragonBank Interest Calculator Service"
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
WORKDIR /app
RUN apt-get update && apt-get install -y --no-install-recommends \
gcc \
libpq-dev \
&& rm -rf /var/lib/apt/lists/*
COPY requirements.txt .
RUN pip install --no-cache-dir --upgrade pip && \
pip install --no-cache-dir -r requirements.txt
COPY app.py .
RUN adduser --disabled-password --gecos '' appuser && \
chown -R appuser:appuser /app
USER appuser
HEALTHCHECK --interval=30s --timeout=10s --retries=3 \
CMD python -c "import psycopg2; psycopg2.connect('${DATABASE_URL}')" || exit 1
CMD ["python", "app.py"]
+191
View File
@@ -0,0 +1,191 @@
"""
DragonBank - Service de Calcul des Intérêts
============================================
Service indépendant qui calcule et applique les intérêts
sur les comptes épargne (Livret A, Assurance Vie).
Bonus: +3% journalier sur les comptes épargne.
"""
import os
import time
import decimal
from datetime import datetime, timezone
import psycopg2
import psycopg2.extras
import schedule
# ============================================
# CONFIGURATION
# ============================================
DATABASE_URL = os.environ.get(
'DATABASE_URL',
'postgresql://dragonadmin:dragonpass@db:5432/dragonbank'
)
INTEREST_RATE_LIVRET_A = float(os.environ.get('INTEREST_RATE_LIVRET_A', 0.03))
INTEREST_RATE_ASSURANCE_VIE = float(os.environ.get('INTEREST_RATE_ASSURANCE_VIE', 0.02))
INTERVAL_SECONDS = int(os.environ.get('INTERVAL_SECONDS', 86400)) # 24h par défaut
def get_db_connection():
"""Crée une connexion à la base de données."""
conn = psycopg2.connect(
DATABASE_URL,
cursor_factory=psycopg2.extras.RealDictCursor
)
return conn
def calculate_interests():
"""
Calcule et applique les intérêts sur tous les comptes épargne actifs.
- Livret A: INTEREST_RATE_LIVRET_A (par défaut 3%)
- Assurance Vie: INTEREST_RATE_ASSURANCE_VIE (par défaut 2%)
"""
print(f"\n{'='*60}")
print(f"🐉 CALCUL DES INTÉRÊTS - {datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M:%S UTC')}")
print(f"{'='*60}")
conn = get_db_connection()
try:
cur = conn.cursor()
# Récupérer tous les comptes épargne actifs
cur.execute('''
SELECT id, user_id, account_number, account_type, balance, interest_rate
FROM accounts
WHERE status = 'active'
AND account_type IN ('livret_a', 'assurance_vie')
AND balance > 0
''')
accounts = cur.fetchall()
total_interest_paid = decimal.Decimal('0.00')
accounts_processed = 0
for account in accounts:
account_id = str(account['id'])
balance = decimal.Decimal(str(account['balance']))
account_type = account['account_type']
# Déterminer le taux d'intérêt
if account_type == 'livret_a':
rate = decimal.Decimal(str(INTEREST_RATE_LIVRET_A))
elif account_type == 'assurance_vie':
rate = decimal.Decimal(str(INTEREST_RATE_ASSURANCE_VIE))
else:
continue
# Calculer les intérêts
interest_amount = (balance * rate).quantize(decimal.Decimal('0.01'))
if interest_amount <= 0:
continue
new_balance = balance + interest_amount
# Appliquer les intérêts
cur.execute(
'UPDATE accounts SET balance = %s, updated_at = NOW() WHERE id = %s',
(float(new_balance), account_id)
)
# Enregistrer dans l'historique des intérêts
cur.execute('''
INSERT INTO interest_history
(account_id, amount, rate, balance_before, balance_after)
VALUES (%s, %s, %s, %s, %s)
''', (
account_id,
float(interest_amount),
float(rate),
float(balance),
float(new_balance)
))
# Créer une transaction pour traçabilité
cur.execute('''
INSERT INTO transactions
(to_account_id, transaction_type, amount, description, status, executed_at)
VALUES (%s, 'interets', %s, %s, 'completed', NOW())
''', (
account_id,
float(interest_amount),
f"Intérêts {account_type.replace('_', ' ').title()} ({float(rate)*100:.1f}%)"
))
total_interest_paid += interest_amount
accounts_processed += 1
print(f"{account['account_number']} ({account_type}): "
f"{float(balance):.2f}€ → {float(new_balance):.2f}"
f"(+{float(interest_amount):.2f}€ à {float(rate)*100:.1f}%)")
conn.commit()
print(f"\n📊 Résumé:")
print(f" Comptes traités: {accounts_processed}")
print(f" Total intérêts versés: {float(total_interest_paid):.2f}")
print(f"{'='*60}\n")
except Exception as e:
conn.rollback()
print(f" ❌ ERREUR lors du calcul des intérêts: {e}")
finally:
conn.close()
def main():
"""Point d'entrée principal du service d'intérêts."""
print("🐉" + "="*58)
print(" DragonBank - Service de Calcul des Intérêts")
print("="*60)
print(f" Taux Livret A: {INTEREST_RATE_LIVRET_A*100:.1f}% / cycle")
print(f" Taux Assurance Vie: {INTEREST_RATE_ASSURANCE_VIE*100:.1f}% / cycle")
print(f" Intervalle: {INTERVAL_SECONDS} secondes")
print(f" Database: {DATABASE_URL.split('@')[1] if '@' in DATABASE_URL else 'configured'}")
print("="*60)
# Attendre que la DB soit prête
print("\n⏳ Attente de la connexion à la base de données...")
max_retries = 30
for i in range(max_retries):
try:
conn = get_db_connection()
conn.close()
print("✅ Connexion à la base de données établie !")
break
except Exception as e:
if i < max_retries - 1:
print(f" Tentative {i+1}/{max_retries} échouée: {e}")
time.sleep(2)
else:
print(f"❌ Impossible de se connecter à la DB après {max_retries} tentatives")
return
# Premier calcul immédiat
print("\n🚀 Premier calcul des intérêts...")
calculate_interests()
# Planifier les calculs suivants
# Pour la démo, on peut réduire l'intervalle
if INTERVAL_SECONDS < 3600:
# Si intervalle court (démo), utiliser un simple sleep
print(f"\n⏰ Mode démo: calcul toutes les {INTERVAL_SECONDS} secondes")
while True:
time.sleep(INTERVAL_SECONDS)
calculate_interests()
else:
# En production, calcul journalier
schedule.every(INTERVAL_SECONDS).seconds.do(calculate_interests)
print(f"\n⏰ Prochain calcul dans {INTERVAL_SECONDS} secondes")
while True:
schedule.run_pending()
time.sleep(60)
if __name__ == '__main__':
main()
+2
View File
@@ -0,0 +1,2 @@
psycopg2-binary==2.9.9
schedule==1.2.1
+1
View File
@@ -0,0 +1 @@
dragonpass