diff --git a/Domains/cli/MiniProjects/banking-system/.gitignore b/Domains/cli/MiniProjects/banking-system/.gitignore new file mode 100644 index 00000000..528af2f0 --- /dev/null +++ b/Domains/cli/MiniProjects/banking-system/.gitignore @@ -0,0 +1,2 @@ +target/ +*.s3db diff --git a/Domains/cli/MiniProjects/banking-system/Cargo.lock b/Domains/cli/MiniProjects/banking-system/Cargo.lock new file mode 100644 index 00000000..5a877f7e --- /dev/null +++ b/Domains/cli/MiniProjects/banking-system/Cargo.lock @@ -0,0 +1,440 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "anstream" +version = "0.6.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys", +] + +[[package]] +name = "banking-system" +version = "0.1.0" +dependencies = [ + "clap", + "rand", + "rusqlite", +] + +[[package]] +name = "bitflags" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "clap" +version = "4.5.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c2cfd7bf8a6017ddaa4e32ffe7403d547790db06bd171c1c53926faab501623" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a4c05b9e80c5ccd3a7ef080ad7b6ba7d6fc00a985b8b157197075677c82c7a0" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" + +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + +[[package]] +name = "fallible-iterator" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" + +[[package]] +name = "fallible-streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashlink" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" +dependencies = [ + "hashbrown", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "libc" +version = "0.2.177" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" + +[[package]] +name = "libsqlite3-sys" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "133c182a6a2c87864fe97778797e46c7e999672690dc9fa3ee8e241aa4a9c13f" +dependencies = [ + "pkg-config", + "vcpkg", +] + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e0f6df8eaa422d97d72edcd152e1451618fed47fabbdbd5a8864167b1d4aff7" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom", +] + +[[package]] +name = "rusqlite" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "165ca6e57b20e1351573e3729b958bc62f0e48025386970b6e4d29e7a7e71f3f" +dependencies = [ + "bitflags", + "fallible-iterator", + "fallible-streaming-iterator", + "hashlink", + "libsqlite3-sys", + "smallvec", +] + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "syn" +version = "2.0.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da58917d35242480a05c2897064da0a80589a2a0476c9a3f2fdc83b53502e917" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "462eeb75aeb73aea900253ce739c8e18a67423fadf006037cd3ff27e82748a06" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "wasip2" +version = "1.0.1+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "wit-bindgen" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" + +[[package]] +name = "zerocopy" +version = "0.8.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/Domains/cli/MiniProjects/banking-system/Cargo.toml b/Domains/cli/MiniProjects/banking-system/Cargo.toml new file mode 100644 index 00000000..811ece4b --- /dev/null +++ b/Domains/cli/MiniProjects/banking-system/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "banking-system" +version = "0.1.0" +edition = "2024" +description = "A small CLI banking system (account creation, login, deletion) using SQLite persistence." +authors = ["AksharGoyal"] +license = "MIT" +readme = "README.md" +keywords = ["cli", "banking", "rust", "sqlite"] +categories = ["command-line-utilities"] + +[dependencies] +clap = { version = "4.5.50", features = ["derive"] } +rand = "0.9.2" +rusqlite = { version = "0.37.0", features = ["backup"] } \ No newline at end of file diff --git a/Domains/cli/MiniProjects/banking-system/README.md b/Domains/cli/MiniProjects/banking-system/README.md new file mode 100644 index 00000000..23f44f13 --- /dev/null +++ b/Domains/cli/MiniProjects/banking-system/README.md @@ -0,0 +1,117 @@ +# Simple Banking App (rust) + +A small CLI banking system written in Rust for learning and demo purposes. +Provides account creation, login, simple operations and account deletion via a single binary. + +**Contributor:** AksharGoyal + +--- + +## Features +- Create accounts with randomly generated 10-digit account numbers and 6-digit PINs +- Login using account number + PIN +- Delete accounts +- Persistent storage using SQLite (via rusqlite) so accounts survive between runs +- CLI parsing and subcommands handled with clap for a familiar UX and easy extension +- Random data generation uses rand for account numbers and PINs + +Dependency mapping (from Cargo.toml): +- clap — command-line parsing and subcommands +- rand — secure-ish random generation for account numbers and PINs +- rusqlite — persistent SQLite backend (database file is created in the working directory by default) + +## Prerequisites +- Rust toolchain (rustc + cargo) installed + - Install from https://rustup.rs if needed + +## Tech stack +- Language: Rust +- Build / package manager: cargo +- CLI parsing: clap +- Random generation: rand +- Persistence: rusqlite +- Testing: cargo test +- Formatting: rustfmt (cargo fmt) +- Linting: clippy (cargo clippy) + +## Quickstart +1. Change into the project directory: + ```sh + cd ./Domains/cli/MiniProjects/banking-system # Assuming you are in ProjectHive repo + ``` + +2. Install the CLI locally: + ```sh + cargo install --path . + ``` + This installs a `banking-system` binary into your cargo bin directory (usually ~/.cargo/bin). + +3. Run commands directly (either with the installed binary or via cargo): + ```sh + # using installed binary + banking-system create + + # or using cargo (no install required) + cargo run -- create + ``` + +## Commands / Usage + +- Create a new account + ```sh + banking-system create + ``` + Example output: + ``` + YOUR NEW ACCOUNT: 6046334535 + YOUR PIN: 688154 + ``` + +- Login to an existing account + ```sh + banking-system login + ``` + Example: + ```sh + banking-system login 6046334535 688154 + ``` + +- Delete an existing account + ```sh + banking-system delete + ``` + Example output: + ``` + banking-system delete 6046334535 688154 + >>> DELETED ACCOUNT: 6046334535 + ``` + +Notes: +- You can run any command via `cargo run -- ...` while developing. +- Keep your generated account numbers and PINs safe for subsequent operations. + +## Testing +Run the unit tests: +```sh +cargo test -- --test-threads=1 +``` +(Tests are run single-threaded to avoid concurrency issues with any shared test state.) + +## Development tips +- Format code with: + ```sh + cargo fmt + ``` +- Check for common issues: + ```sh + cargo clippy -- -D warnings + ``` +- To iterate quickly without installing: + ```sh + cargo run -- + ``` +- To uninstall the app & clean everything: + ```sh + cargo uninstall banking-system + cargo clean + ``` diff --git a/Domains/cli/MiniProjects/banking-system/src/cli.rs b/Domains/cli/MiniProjects/banking-system/src/cli.rs new file mode 100644 index 00000000..c8949487 --- /dev/null +++ b/Domains/cli/MiniProjects/banking-system/src/cli.rs @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: Unlicense + +use clap::{Parser, Subcommand}; + +#[derive(Parser, Debug)] +#[command(name = "bank", version)] +pub struct Opts { + /// Account subcommand + #[command(subcommand)] + pub account: AccountOpts, +} + +#[derive(Subcommand, Debug)] +#[command(name = "account", about = "A simple bank system in Rust")] +pub enum AccountOpts { + /// Login into account with account number. This will get you to the Main Menu. + #[command(name = "login")] + Login { + #[arg(help = "Account number of user")] + account: String, + #[arg(help = "PIN of the account")] + pin: String, + }, + /// Delete into account with account number. Your account will be gone forever. + #[command(name = "delete")] + Delete { + #[arg(help = "Account number of user")] + account: String, + #[arg(help = "PIN of the account.")] + pin: String, + }, + /// Create new account. This will have a randomly generated PIN. + #[command(name = "create")] + Create, +} diff --git a/Domains/cli/MiniProjects/banking-system/src/database.rs b/Domains/cli/MiniProjects/banking-system/src/database.rs new file mode 100644 index 00000000..5444886f --- /dev/null +++ b/Domains/cli/MiniProjects/banking-system/src/database.rs @@ -0,0 +1,290 @@ +// SPDX-License-Identifier: Unlicense + +use std::path::PathBuf; + +use crate::luhn::AccountNumber; + +use rand::prelude::*; +use rand::rng; +use rusqlite::{Connection, Result}; + +#[derive(Debug)] +pub struct Account { + pub id: u64, + pub account_number: String, + pub balance: usize, + pub pin: String, +} + +fn database_path() -> PathBuf { + PathBuf::from("bank.s3db") +} + +pub fn initialise_bankdb() -> Result { + let db = Connection::open(database_path())?; + + let command = "CREATE TABLE IF NOT EXISTS account( +id INTEGER PRIMARY KEY, +account_number TEXT, +pin TEXT DEFAULT '000000', +balance INTEGER DEFAULT 0 +) +"; + + db.execute(command, ())?; + Ok(db) +} + +pub fn create_account(data: &AccountNumber, balance: u64) -> Result<()> { + let db = initialise_bankdb()?; + let account_number = data.to_string(); + let mut stmt = db.prepare("SELECT id, account_number, balance, pin FROM account")?; + let accounts = stmt.query_map([], |row| { + Ok(Account { + id: row.get(0)?, + account_number: row.get(1)?, + balance: row.get(2)?, + pin: row.get(3)?, + }) + })?; + + let get_latest_max_id = { + let mut x = 0; + for account in accounts.flatten() { + if account.id > x { + x = account.id + } + } + x + }; + + let newest_max_id = get_latest_max_id + 1; + let mut rng = rng(); + let mut pin: Vec = Vec::new(); + + // Six digit pin + for _ in 1..=6 { + let y = rng.random_range(0..=9).to_string(); + pin.push(y); + } + + let pin: String = String::from_iter(pin); + + let new_account = Account { + id: newest_max_id, + account_number, + balance: balance.try_into().unwrap(), + pin, + }; + + db.execute( + "INSERT INTO account (id, account_number, pin, balance) VALUES (?1, ?2, ?3, ?4)", + ( + &new_account.id, + &new_account.account_number, + &new_account.pin, + &new_account.balance, + ), + )?; + Ok(()) +} + +pub fn deposit(amount: &str, pin: &str, account_number: &str) -> Result<()> { + let db = initialise_bankdb()?; + let query_string = format!( + "SELECT pin FROM account where account_number='{}';", + account_number + ); + + let pin_from_db: String = db.query_row(&query_string, [], |row| row.get(0))?; + + let correct_pin = { pin_from_db == pin }; + + if correct_pin { + db.execute( + "UPDATE account SET balance = balance + ?1 WHERE account_number=?2", + (amount, account_number), + )?; + + let query_string = format!( + "SELECT balance FROM account where account_number='{}';", + account_number + ); + + let amount_from_db: usize = db.query_row(&query_string, [], |row| row.get(0))?; + + println!( + "The account number `{}` now has a balance of `{}`.\n", + &account_number, &amount_from_db + ); + } else { + eprintln!("Wrong pin. Try again..."); + } + Ok(()) +} + +pub fn transfer( + amount: &str, + pin: &str, + account_number1: &str, + account_number2: &str, +) -> Result<()> { + if *account_number1 == *account_number2 { + eprintln!("Cannot perform a transfer to the same account!"); + return Ok(()); + }; + + let db = initialise_bankdb()?; + let query_string = format!( + "SELECT pin FROM account where account_number='{}';", + account_number1 + ); + + let pin_from_db: String = db.query_row(&query_string, [], |row| row.get(0))?; + + let correct_pin = { pin_from_db == pin }; + if correct_pin { + let query_string = format!( + "SELECT balance FROM account where account_number='{}';", + account_number1 + ); + + let amount_from_db: usize = db.query_row(&query_string, [], |row| row.get(0))?; + + println!( + "The account number `{}` has a balance of `{}`.\n", + &account_number1, &amount_from_db + ); + + let amount = amount + .parse::() + .expect("Not able to parse string to usize"); + + if amount > amount_from_db { + eprintln!( + "You are trying to transfer that exceeds your current balance... aborting...\n" + ); + } else { + // Add money to account 2 + db.execute( + "UPDATE account SET balance = balance + ?1 WHERE account_number=?2", + (amount, account_number2), + )?; + + // Subtract money from account 1 + db.execute( + "UPDATE account SET balance = balance - ?1 WHERE account_number=?2", + (amount, account_number1), + )?; + + let query_string = format!( + "SELECT balance FROM account where account_number='{}';", + account_number1 + ); + + let amount_from_db: usize = db.query_row(&query_string, [], |row| row.get(0))?; + + println!( + "The account number `{}` now has a balance of `{}`.\n", + &account_number1, &amount_from_db + ); + }; + } else { + eprintln!("Wrong pin. Try again..."); + } + Ok(()) +} + +pub fn withdraw(amount: &str, pin: &str, account_number: &str) -> Result<()> { + let db = initialise_bankdb()?; + let query_string = format!( + "SELECT pin FROM account where account_number='{}';", + account_number + ); + + let pin_from_db: String = db.query_row(&query_string, [], |row| row.get(0))?; + + let correct_pin = { pin_from_db == pin }; + + if correct_pin { + let query_string = format!( + "SELECT balance FROM account where account_number='{}';", + account_number + ); + + let amount_from_db: usize = db.query_row(&query_string, [], |row| row.get(0))?; + + println!( + "The account number `{}` has a balance of `{}`.\n", + &account_number, &amount_from_db + ); + + let amount = amount + .parse::() + .expect("Not able to parse string to usize"); + + if amount > amount_from_db { + eprintln!( + "You are trying to withdraw that exceeds your current deposit... aborting...\n" + ); + } else { + db.execute( + "UPDATE account SET balance = balance - ?1 WHERE account_number=?2", + (amount, account_number), + )?; + + let query_string = format!( + "SELECT balance FROM account where account_number='{}';", + account_number + ); + + let amount_from_db: usize = db.query_row(&query_string, [], |row| row.get(0))?; + + println!( + "The account number `{}` now has a balance of `{}`.\n", + &account_number, &amount_from_db + ); + }; + } else { + eprintln!("Wrong pin. Try again..."); + } + Ok(()) +} + +pub fn delete_account(account_number: &str, pin: &str) -> Result<()> { + let db = initialise_bankdb()?; + let query_string = format!( + "SELECT pin FROM account where account_number='{}';", + &account_number + ); + + let pin_from_db: String = db.query_row(&query_string, [], |row| row.get(0))?; + let correct_pin = { pin_from_db == pin }; + + if correct_pin { + db.execute( + "DELETE FROM account WHERE account_number=?1", + (account_number,), + )?; + println!("DELETED ACCOUNT: {}", &account_number); + } else { + eprintln!("Wrong pin. Try again..."); + } + Ok(()) +} + +pub fn show_balance(account_number: &str) -> Result<()> { + let db = initialise_bankdb()?; + let query_string = format!( + "SELECT balance FROM account where account_number='{}';", + account_number + ); + + let amount_from_db: usize = db.query_row(&query_string, [], |row| row.get(0))?; + + println!( + "The account number `{}` now has a balance of `{}`.\n", + &account_number, &amount_from_db + ); + Ok(()) +} diff --git a/Domains/cli/MiniProjects/banking-system/src/lib.rs b/Domains/cli/MiniProjects/banking-system/src/lib.rs new file mode 100644 index 00000000..60dc900d --- /dev/null +++ b/Domains/cli/MiniProjects/banking-system/src/lib.rs @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: Unlicense + +pub mod cli; +pub mod database; +pub mod luhn; +pub mod menu; diff --git a/Domains/cli/MiniProjects/banking-system/src/luhn.rs b/Domains/cli/MiniProjects/banking-system/src/luhn.rs new file mode 100644 index 00000000..14117a5e --- /dev/null +++ b/Domains/cli/MiniProjects/banking-system/src/luhn.rs @@ -0,0 +1,132 @@ +// SPDX-License-Identifier: Unlicense + +use rand::prelude::*; +use rand::rng; +use std::fmt::Display; +use std::str::FromStr; + +/// The account number uses random number generation +/// for the payload. This payload is then calculated +/// to produce the check digit. +#[derive(Debug)] +pub struct AccountNumber { + /// Randomly generated number. Each digit is within 0..9 + pub(crate) payload: Vec, + /// Check digit using the Luhn formula + pub(crate) check_digit: u8, +} + +impl Default for AccountNumber { + fn default() -> Self { + Self::new(10) + } +} + +impl FromStr for AccountNumber { + type Err = std::io::ErrorKind; + + fn from_str(s: &str) -> Result { + if verify(s) { + let mut payload: Vec = s + .chars() + .map(|d| { + d.to_digit(10) + .expect("Not able to create u32 from character") as u8 + }) + .collect(); + let check_digit = payload.pop().expect("Cannot pop. Got an empty vector"); + Ok(Self { + payload, + check_digit, + }) + } else { + eprintln!("{s} is not a valid account number"); + Err(std::io::ErrorKind::InvalidData) + } + } +} + +impl Display for AccountNumber { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let mut account_number: String = self.payload.iter().map(|d| d.to_string()).collect(); + account_number.push_str(&self.check_digit.to_string()); + write!(f, "{}", account_number) + } +} + +impl AccountNumber { + pub fn new(length: usize) -> Self { + let mut payload: Vec = Vec::new(); + for _ in 1..=length - 1 { + let mut rng = rng(); + let zero_to_nine: u8 = rng.random_range(0..=9); + payload.push(zero_to_nine); + } + + let check_digit = get_check_digit(&payload); + + Self { + payload, + check_digit, + } + } + + pub fn check_digit(&self) -> u8 { + get_check_digit(&self.payload) + } + + pub fn human_readable(&self) -> String { + let mut payload: String = self.payload.iter().map(|d| d.to_string()).collect(); + payload.push_str(&self.check_digit.to_string()); + payload + } +} + +pub fn verify(account_number: &str) -> bool { + let account_number = account_number.to_string(); + let digits: Vec = account_number.trim().chars().collect(); + + let mut payload: Vec = digits + .iter() + .map(|digit| digit.to_digit(10).expect("Not a number") as u8) + .collect(); + + let check_digit = payload.pop().expect("This shouldn't be an empty iterator"); + + get_check_digit(&payload) == check_digit +} + +fn get_check_digit(payload: &[u8]) -> u8 { + let mut new_payload = payload.iter().copied(); + let mut luhn_sum = 0; + let mut index = 0; + + while let Some(item) = new_payload.next_back() { + let divisible_by_2 = { (index as f32 % 2_f32) == 0_f32 }; + if divisible_by_2 { + let mul = item * 2; + + let digits: Vec = mul + .to_string() + .chars() + .map(|d| d.to_digit(10).expect("Not a number character") as u8) + .collect(); + + let sum = { + let mut x = 0; + for digit in digits { + let y: u8 = digit; + x += y; + } + x + }; + + luhn_sum += sum; + } else { + luhn_sum += item; + }; + index += 1; + } + + 10 - (luhn_sum % 10) +} diff --git a/Domains/cli/MiniProjects/banking-system/src/main.rs b/Domains/cli/MiniProjects/banking-system/src/main.rs new file mode 100644 index 00000000..afb6d289 --- /dev/null +++ b/Domains/cli/MiniProjects/banking-system/src/main.rs @@ -0,0 +1,70 @@ +// SPDX-License-Identifier: Unlicense + +use banking_system::cli; +use banking_system::database; +use banking_system::luhn::AccountNumber; +use banking_system::menu; +use clap::Parser; +use rusqlite::Result; + +fn main() -> Result<()> { + let cli = cli::Opts::parse(); + + match cli.account { + cli::AccountOpts::Login { account, pin } => { + let db = database::initialise_bankdb()?; + let query_string = format!( + "SELECT pin FROM account where account_number='{}';", + account + ); + + let pin_from_db: Result = db.query_row(&query_string, [], |row| row.get(0)); + match pin_from_db { + Ok(p) => { + if p == pin { + menu::prompt(&account).expect("Something went wrong"); + } else { + eprintln!("Wrong pin... try again"); + }; + } + Err(_) => { + eprintln!("No such account"); + } + }; + } + cli::AccountOpts::Delete { account, pin } => { + database::delete_account(&account, &pin)?; + } + cli::AccountOpts::Create => { + let mut new_account = AccountNumber::default(); + + let db = database::initialise_bankdb()?; + loop { + let query_string = format!( + "SELECT 1 FROM account where account_number='{}';", + new_account + ); + + match db.query_row(&query_string, [], |row| row.get::(0)) { + Ok(_) => { + new_account = AccountNumber::default(); + } + Err(_) => break, + } + } + + let _ = database::create_account(&new_account, 0); + + let query_string = format!( + "SELECT pin FROM account where account_number='{}';", + &new_account + ); + let pin_from_db: String = db.query_row(&query_string, [], |row| row.get(0))?; + println!( + "YOUR NEW ACCOUNT: `{}`\nYOUR PIN: `{}`\n", + &new_account, &pin_from_db + ); + } + }; + Ok(()) +} diff --git a/Domains/cli/MiniProjects/banking-system/src/menu.rs b/Domains/cli/MiniProjects/banking-system/src/menu.rs new file mode 100644 index 00000000..74cb71a5 --- /dev/null +++ b/Domains/cli/MiniProjects/banking-system/src/menu.rs @@ -0,0 +1,96 @@ +// SPDX-License-Identifier: Unlicense + +use crate::database; +use std::io::BufRead; + +pub fn prompt(account_number: &str) -> std::io::Result<()> { + let prompt_text = "0) Show Current Balance +1) Deposit Money +2) Transfer Money +3) Withdraw Money +4) Delete Account +5) Exit"; + + loop { + println!("{}", &prompt_text); + let stdin = std::io::stdin(); + let mut query = String::new(); + let mut handle = stdin.lock(); + + handle.read_line(&mut query)?; + + let query = query.trim(); + + if query == "5" { + eprintln!("Exiting bank machine..."); + break; + } else if query == "4" { + println!("You are going to delete your account..."); + println!("Please input the pin:"); + let mut pin = String::new(); + handle.read_line(&mut pin)?; + let pin = pin.trim(); + + database::delete_account(account_number, pin) + .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?; + eprintln!("Account is not accessible. Exiting..."); + break; + } else if query == "3" { + println!("Please input the amount:"); + let mut amount = String::new(); + handle.read_line(&mut amount)?; + let amount = amount.trim(); + println!("The amount you wanted to withdraw: {}\n", &amount); + + println!("Please input the pin:"); + let mut pin = String::new(); + handle.read_line(&mut pin)?; + let pin = pin.trim(); + + database::withdraw(amount, pin, account_number) + .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?; + } else if query == "2" { + println!("Please input the amount:"); + let mut amount = String::new(); + handle.read_line(&mut amount)?; + let amount = amount.trim(); + println!("The amount you wanted to transfer: {}\n", &amount); + + println!("Please input the account number of the recipient:"); + let mut account_number2 = String::new(); + handle.read_line(&mut account_number2)?; + let account_number2 = account_number2.trim(); + println!( + "The account number you want to send is {} and the amount to transfer is {}\n", + &account_number2, &amount + ); + + println!("Please input your pin:"); + let mut pin = String::new(); + handle.read_line(&mut pin)?; + let pin = pin.trim(); + database::transfer(amount, pin, account_number, account_number2) + .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?; + } else if query == "1" { + println!("Please input the amount:"); + let mut amount = String::new(); + handle.read_line(&mut amount)?; + let amount = amount.trim(); + println!("The amount you wanted to deposit: {}\n", &amount); + + println!("Please input the pin:"); + let mut pin = String::new(); + handle.read_line(&mut pin)?; + let pin = pin.trim(); + + database::deposit(amount, pin, account_number) + .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?; + } else if query == "0" { + database::show_balance(account_number) + .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?; + } else { + eprintln!("Invalid choice. Please try again..."); + } + } + Ok(()) +} diff --git a/Domains/cli/MiniProjects/banking-system/tests/luhn_tests.rs b/Domains/cli/MiniProjects/banking-system/tests/luhn_tests.rs new file mode 100644 index 00000000..8496c897 --- /dev/null +++ b/Domains/cli/MiniProjects/banking-system/tests/luhn_tests.rs @@ -0,0 +1,61 @@ +use banking_system::luhn::AccountNumber; +use std::str::FromStr; + +#[test] +/// Valid account numbers in different lengths +fn valid_account_numbers_in_different_lengths() { + let accounts = [ + AccountNumber::from_str("35001576202"), + AccountNumber::from_str("8536276945"), + AccountNumber::from_str("20024"), + AccountNumber::from_str("26"), + ]; + + for account in accounts { + assert!(account.is_ok()); + } +} + +#[test] +fn valid_account_numbers_fixed_length() { + let valid_account_numbers = [ + "2334841596", + "5072686164", + "8330789085", + "2303133926", + "7730632457", + "1310866767", + "9083062142", + "8936042657", + "3188178648", + "1513312791", + "0204434294", + ]; + + for v in valid_account_numbers { + let account = AccountNumber::from_str(v); + assert!(account.is_ok()); + } +} + +#[test] +fn invalid_account_numbers_fixed_length() { + let invalid_account_numbers = [ + "2334841592", + "5072686163", + "8330789084", + "2303133925", + "7730632456", + "1310866766", + "9083062141", + "8936042656", + "3188178647", + "1513312790", + "0204434293", + ]; + + for v in invalid_account_numbers { + let account = AccountNumber::from_str(v); + assert!(account.is_err()); + } +}