# Modélisation d'un système bancaire Une banque locale souhaite informatiser la gestion de ses titulaires et de leurs comptes. Vous êtes chargé(e) de concevoir et d’implémenter le schéma relationnel de base permettant de gérer : 1. Les clients de la banque, 2. Les comptes bancaires, 3. Le lien entre les clients et les comptes. 4. Les dépots et les retraits d'argent. 5. Les virements entre compte. 6. Les devises et les taux de change. L'objectif de ces travaux pratiques est de : - Concevoir un **modèle relationnel** à partir d’un scénario réaliste. - Utiliser les **contraintes d’intégrité** pour garantir la cohérence des données. - Manipuler des **jointures** et des **relations n–n**. - Comprendre la notion d’**héritage logique** en base de données. La diffusion de cette application est internationale, vous vous efforcerez d'utiliser des termes anglais pour nommer les entités et les propriétés. Réferrez pour cela vous au [glossaire](banque.glossaire.md) Pour les entités vous utiliserez le singuler et écrirez le tout en minuscule. - Séance 1 : [Le schéma Entités-Relations](banque.erd.md) # Séance 2 : Implémentation du modèle Voir les adresses des serveurs [postgreSQL](https://sources.neotech.fr/Universite/tp/src/branch/main/geii3_2025.md) Voir la syntaxe de [postgreSQL](syntaxe.md) ### 1. Titulaires Création de la table `holder`. ```sql create table holder ( "id" bigint primary key generated always as identity, "type" text, "created_at" timestamp ); ``` - entier sur 64 bits : `bigint` - clé primaire : `primary key` - incrément automatique : `generated always as identity` Création de la table `person`. ```sql create table person ( "id" bigint primary key references holder(id), "firstname" text, "lastname" text, "birthdate" date ); ``` - Créer un compte individuel pour _Françoise Zanetti_, née le 12 avril 1995. - Créer une entreprise nommée _Boulangerie de Valorgue_, créée le 19 août 2014, numéro d’immatriculation FR19803269968. - Ajouter un nouveau titulaire : _Justin Hébrard_ né le 11/03/1993. ```sql create table company ( "id" bigint primary key references holder(id), "name" text, "creation_date" date ); ``` ```sql create table bank ( "id" bigint primary key references holder(id), "name" text ); ``` ```sql insert into holder (type) values ('BANK') returning holder; ``` ```sql insert into bank (id, name) values (1, 'Banque de l''Est'); ``` #### 1.1 Contraintes à respecter - Chaque `person` ou `company` doit correspondre à exactement un seul `holder`. - La suppression d’un `holder` doit supprimer automatiquement la ligne correspondante dans `person` ou `company`. - La banque souhaite désormais que toute personne titulaire d’un compte ait au moins 15 ans à la date de création de sa fiche. Il n'y a pas de restriction sur l'âge de la société. - Le type doit être contraint à `'PERSON'` ou `'COMPANY'`. Il existe deux méthodes pour gérer le type. 1. vérifier par la commande `CHECK` la validité de la valeur. 2. utiliser une énumération `enum`. ```sql create type holder_type as enum ('BANK', 'PERSON', 'COMPANY'); ``` #### 1.2 Vérifications - Lister tous les titulaires. Pour réutiliser rapidement la requête enregistrer la dans une vue. - Supprimer un titulaire, vérifier que cela supprime l'individu ou la société correspondante. #### 1.4 L'intégrité des données Lorsque l’on tente d'insèrer une nouvelle personne qui n'a pas l'âge requis. La ligne dans `holder` est d'abord créée, puis l'insertion dans `person` échoue à cause de la vérification d'âge. Mais la ligne du titulaire est toujours présente **sans être rattachée** à une personne. On parle alors d'enregistrement **orphelin**. Chaque commande SQL est exécutée indépendamment. Si la deuxième commande échoue, la première n’est pas annulée automatiquement. Réalisez l’insertion d’un titulaire complet (dans holder et person) à l’aide d’une **transaction**. Testez le cas où la contrainte d’âge échoue et vérifiez que rien n’est inséré dans holder. ```sql begin; ... commit; ``` #### 1.5 Procédure stockée Pour fiabiliser le process et être sûr que l'execution s'effectue toujours dans une transaction, nous allons encapsuler la création d’un titulaire dans une **procédure stockée**. Créez une **procédure stockée** appelée `create_person_holder`. Cette procédure prend en paramètre : * le prénom (`p_firstname text`) * le nom (`p_lastname text`) * la date de naissance (`p_birthdate date`) Cette procédure doit : * Vérifier que la personne a **au moins 15 ans** ; * Créer automatiquement un enregistrement dans `holder` de type `'PERSON'`; * Récupérer l’identifiant généré et créer la fiche dans `person` ; * Afficher un message de confirmation. 3. Si la personne a moins de 15 ans : * La procédure doit **refuser la création** et afficher une erreur claire. >[!NOTE] > La procédure garantit l’**atomicité** : soit tout est créé, soit rien. ### 2. Les comptes ```sql create table account ( "id" bigint primary key generated always as identity, "creation_date" date default current_date, "balance" decimal check (balance >= 0), "currency_code" text references currency (code) ); ``` - Le solde des comptes ne peuvent être négatifs. - Créez un compte joint à 50/50 pour Françoise et Justin. Écrire une requête pour vérifier la somme des parts Jusqu’à présent, nous savons créer des titulaires (`holder`) de façon sûre. Il est temps de leur ouvrir des **comptes bancaires**. Un compte doit toujours : * être associé à **au moins un titulaire**, * et la somme des parts (`share`) des titulaires doit être exactement **égale à 1** (c’est-à-dire 100 % du compte). Nous allons donc écrire une **procédure stockée** `create_account` qui vérifie ces règles avant d’enregistrer les données. 1. Créez une **procédure stockée** `create_account` qui : * prend en paramètre : * `p_holders int[]` (tableau des identifiants de titulaires), * `p_shares numeric[]` (tableau des parts correspondantes). * vérifie que : * le nombre d’éléments dans `p_holders` et `p_shares` est identique ; * la somme des parts est **exactement égale à 1** ; * chaque `holder_id` existe dans la table `holder`. 2. Si tout est correct : * crée un nouveau compte dans `account` ; * insère les lignes correspondantes dans `account_holder`. 3. Si une condition échoue : * la procédure doit lever une **erreur explicite** (`RAISE EXCEPTION`) ; * aucune insertion ne doit être faite (transaction annulée). * Les tableaux peuvent être parcourus avec une boucle : ```sql for i in 1..array_length(p_holders, 1) loop ... end loop; ``` * Pour vérifier la somme : ```sql select sum(unnest(p_shares)); ``` (ou additionner dans la boucle) * Vous pouvez lever une erreur personnalisée : ```sql raise exception 'La somme des parts doit être égale à 1 (%.4f)', somme; ``` #### Exemples d’appel ```sql call create_account( 'FR761234567890', 'Compte commun', array[1, 5], array[0.5, 0.5] ); ``` Crée un compte partagé 50/50 entre les titulaires 1 et 2. ```sql call create_account( 'FR009999999999', 'Compte déséquilibré', array[1, 5], array[0.7, 0.4] ); ``` Doit refuser la création avec une erreur claire : ``` ERROR: La somme des parts (1.1000) doit être égale à 1.0000 ``` ### 3. Monnaies et taux de change ```sql create table currency ( "code" text primary key ); ``` ```sql insert into currency values ('EUR'); insert into currency values ('YEN'); ``` ```sql create table exchange_rate ( "currency_code" text references currency(code) on delete cascade, "date" date , "rate" decimal not null, primary key (currency_code, date) ); ``` ```sql insert into exchange_rate values ('YEN', '2025-11-03', 177.57), ('YEN', '2025-11-04', 176.39), ('YEN', '2025-11-05', 176.67), ('YEN', '2025-11-06', 177.15), ('YEN', '2025-11-07', 176.99); ``` ### 4. Les opérations et transactions Implémenter les transactions et opérations. ```sql create table transaction ( "id" bigint primary key generated always as identity, "transaction_date" timestamp default current_timestamp, "amount" decimal check (amount > 0) ); ``` ```sql create table operation ( "id" bigint primary key generated always as identity, "transaction_id" bigint references transaction(id), "account_id" bigint references account(id), "amount" decimal check (amount > 0), "direction" text check (direction in ('DEBIT', 'CREDIT')) ); ``` #### Dépôts et retraits ```sql create or replace procedure add_retrait ( p_account_id bigint, p_amount decimal ) language plpgsql as $$ declare v_id bigint begin end; $$; ``` # Séance 3 : Exploitation des données [Utiliser la correction](banque.correction.sql) de la base de données ## 1. Vue : taux de change de la veille Créer une vue *yesterday_exchange_rates* qui affiche pour **chaque devise** son taux de change de la veille présent dans `exchange_rate`. Si aucun taux du jour n’existe, la ligne ne doit pas apparaître. ## 2. Fonction : dernier taux connu pour une devise et une date données Créer une fonction *latest_exchange_rate* qui donne, pour une devise, son taux de change le plus récent : ## 3. Vue : liste détaillées des titulaires Créer une vue *holder_details* permettant d’afficher **tous les titulaires** (banque, personne ou entreprise) sous une forme unifiée. La vue doit contenir : * l’identifiant du titulaire * son type * un champ `display_name` calculé ainsi : * pour un titulaire de type *PERSON* : *firstname lastname* * pour un titulaire de type *COMPANY* : *name* * pour un titulaire de type *BANK* : *name* ## 4. Vue : liste des comptes avec devise et solde Créer une vue *account_detail* affichant : * l’identifiant du compte * la date d’ouverture * la devise * le solde * le nombre de titulaires du compte ## 5. Vue : comptes par titulaire Créer une vue *holder_accounts* permettant de lister les comptes détenus par chaque titulaire, avec : * le titulaire (id et type) * le nom du titulaire * l’identifiant du compte * la part détenue (`share`) * le solde total du compte La vue doit fusionner les informations venant de **holder, person et company**. ## 6. Vue : opérations enrichies Créer une vue *operation_details* affichant les opérations avec : * la date de l’opération * le compte impacté * la direction (DEBIT ou CREDIT) * le montant de l’opération * le montant signé (crédit positif, débit négatif) * le solde du compte **après l’opération** (bonus : fenêtre analytique) ## 7. Vue : solde converti en EUR Créer une vue *account_balance_eur* pour afficher : * compte * devise d’origine * solde original * taux de change correspondant à la date du jour * solde converti en EUR (solde × taux) ## 8. Vue : transactions complètes Créer une vue *transaction_summary* affichant un regroupement par transaction : * id de la transaction * date * montant total de la transaction (somme des opérations) * liste des comptes concernés (optionnel : concaténation) ## 9. Vue : comptes en découvert imminent Créer une vue *accounts_at_risk* listant les comptes dont le solde est inférieur à 50 (dans leur devise), ou qui auraient un solde négatif s'ils effectuaient un débit supplémentaire de 20. > Vérification simple : `balance - 20 < 0 OR balance < 50`. ## 10. Vue : âge des personnes Créer une vue *person_age* indiquant : * id * nom complet * date de naissance * âge en années (utiliser `age()`) ## 11. Vue : répartition des parts d’un compte Créer une vue *account_shares* affichant : * id du compte * nombre de titulaires * somme des parts * une colonne booléenne `is_valid` vérifiant si la somme = 1 > Objectif : vérifier que les parts des comptes joints sont bien réparties. ## 12. Vue : solde par titulaire Créer une vue *holder_total_balance* indiquant pour chaque titulaire : * id * type * nom * somme des soldes de tous ses comptes (pondérée par `share`), calcul : `total = SUM(share × balance)` ## 13. Vue : opérations d’un compte en sens unique Créer une vue *account_debits* listant uniquement les opérations de type DEBIT, avec : * date * compte * montant négatif Créer une seconde vue *account_credits* (montants positifs). ## 14. Vue : liste des entreprises avec ancienneté Créer une vue *company_age* indiquant : * id * name * registration_number * age de l’entreprise en années (`age(current_date, created_at)`) ## 15. Vue : recapitulatif bancaire complet Créer une vue *bank_overview* qui croise : * les titulaires * leurs comptes * leurs transactions * leurs opérations