8
\$\begingroup\$

I have completed my bank ATM application in C# class library project. For simplicity, the data of this version will be stored in the List instead of a database. The basic feature are

  • Login
  • Check balance
  • Place cash deposit
  • Withdraw cash
  • Make third party transfer
  • View transactions

Entities

public class UserBankAccount
{
    public long Id { get; set; }
    private long cardNumber;
    public long CardNumber { get; set; }    
    public long CardPin { get; set; }
    public string FullName { get; set; }
    public long AccountNumber { get; set; }
    public decimal AccountBalance { get; set; }
    public int TotalLogin { get; set; }
    public bool IsLocked { get; set; }
}

public class Transaction
{
    public int TransactionId { get; set; }
    public DateTime TransactionDate { get; set; }
    public TransactionType TransactionType { get; set; }
    public string Description { get; set; }
    public decimal TransactionAmount { get; set; }
}

Enum

internal enum SecureMenu
{
    [Description("Check balance")]
    CheckBalance = 1,
    [Description("Place Deposit")]
    PlaceDeposit = 2,
    [Description("Make Withdrawal")]
    MakeWithdrawal = 3,
    [Description("Third Party Transfer")]
    ThirdPartyTransfer = 4,
    [Description("Transaction")]
    ViewTransaction = 5,
    [Description("Logout")]
    Logout = 6
 }

public enum TransactionType
{
    Deposit,
    Withdrawal,
    ThirdPartyTransfer
}

Interface

public interface IATMApp
{
    void CheckCardNoPassword();
}

public interface ITransaction
{
    void InsertTransaction(Transaction transaction);
    void ViewTransaction();
}

public interface IUserBankAccount
{
    void CheckBalance();
    void PlaceDeposit();
    void MakeWithdrawal();
}

StaticClass

internal class AtmScreen
{
    internal const string cur = "RM ";
    internal static void WelcomeATM()
    {
        Console.Title = "Meybank ATM System.";
        Console.WriteLine("Welcome to Meybank ATM.\n");
        Console.WriteLine("Please insert your ATM card.");
        PrintEnterMessage();
    }
    internal static void WelcomeCustomer()
    {
        Utility.PrintUserInputLabel("Welcome back, ");
    }
    internal static void PrintLockAccount()
    {
        Console.Clear();
        Utility.PrintMessage("Your account is locked. Please go to " +
            "the nearest branch to unlocked your account. Thank you.", true);

        PrintEnterMessage();
        Environment.Exit(1);
    }
    internal static void LoginProgress()
    {
        Console.Write("\nChecking card number and card pin.");
        Utility.printDotAnimation();
        Console.Clear();
    }
    internal static void LogoutProgress()
    {
        Console.WriteLine("Thank you for using Meybank ATM system.");
        Utility.printDotAnimation();
        Console.Clear();
    }
    internal static void DisplaySecureMenu()
    {
        Console.Clear();
        Console.WriteLine(" ---------------------------");
        Console.WriteLine("| Meybank ATM Secure Menu    |");
        Console.WriteLine("|                            |");
        Console.WriteLine("| 1. Balance Enquiry         |");
        Console.WriteLine("| 2. Cash Deposit            |");
        Console.WriteLine("| 3. Withdrawal              |");
        Console.WriteLine("| 4. Third Party Transfer    |");
        Console.WriteLine("| 5. Transactions            |");
        Console.WriteLine("| 6. Logout                  |");
        Console.WriteLine("|                            |");
        Console.WriteLine(" ---------------------------");

        // The menu selection is tied to Enum:SecureMenu.
    }
    internal static void PrintCheckBalanceScreen()
    {
        Console.Write("Account balance amount: ");
    }
    internal static void PrintMakeWithdrawalScreen()
    {
        Console.Write("Enter amount: ");
    }

    // This is the only non-static method.
    // Reason is this method needs to return an object.
    // ToDo: Find other way to solve this design issue.
    internal VMThirdPartyTransfer ThirdPartyTransferForm()
    {
        var vMThirdPartyTransfer = new VMThirdPartyTransfer();

        vMThirdPartyTransfer.RecipientBankAccountNumber = Validator.GetValidIntInputAmt("recipient's account number");

        vMThirdPartyTransfer.TransferAmount = Validator.GetValidDecimalInputAmt("amount");

        vMThirdPartyTransfer.RecipientBankAccountName = Utility.GetRawInput("recipient's account name");
        // no validation here yet.

        return vMThirdPartyTransfer;
    }
    internal static void PrintEnterMessage()
    {
        Console.WriteLine("\nPress enter to continue.");
        Console.ReadKey();
    }
}

public static class Utility
{
    private static CultureInfo culture = new CultureInfo("ms-MY");

    public static string GetRawInput(string message)
    {
        Console.Write($"Enter {message}: ");
        return Console.ReadLine();
    }

    public static string GetHiddenConsoleInput()
    {
        StringBuilder input = new StringBuilder();
        while (true)
        {
            var key = Console.ReadKey(true);
            if (key.Key == ConsoleKey.Enter) break;
            if (key.Key == ConsoleKey.Backspace && input.Length > 0) input.Remove(input.Length - 1, 1);
            else if (key.Key != ConsoleKey.Backspace) input.Append(key.KeyChar);
        }
        return input.ToString();
    }

    #region UIOutput - UX and output format
    public static void printDotAnimation(int timer = 10)
    {
        for (var x = 0; x < timer; x++)
        {
            Console.Write(".");
            Thread.Sleep(100);
        }
        Console.WriteLine();
    }

    public static string FormatAmount(decimal amt)
    {
        return String.Format(culture, "{0:C2}", amt);
    }

    public static void PrintConsoleWriteLine(string msg, bool ConsoleWriteLine = true)
    {
        if (ConsoleWriteLine)
            Console.WriteLine(msg);
        else
            Console.Write(msg);

        AtmScreen.PrintEnterMessage();
    }

    public static void PrintUserInputLabel(string msg, bool ConsoleWriteLine = false)
    {
        if (ConsoleWriteLine)
            Console.WriteLine(msg);
        else
            Console.Write(msg);

    }

    public static void PrintMessage(string msg, bool success)
    {
        if (success)
            Console.ForegroundColor = ConsoleColor.Yellow;
        else
            Console.ForegroundColor = ConsoleColor.Red;

        Console.WriteLine(msg);
        Console.ResetColor();
        AtmScreen.PrintEnterMessage();
    }
    #endregion
}

ViewModel

public class VMThirdPartyTransfer
{
    public decimal TransferAmount { get; set; }
    public long RecipientBankAccountNumber { get; set; }

    public string RecipientBankAccountName { get; set; }
}

Main Class

public class AtmApp : IATMApp, ITransaction, IUserBankAccount
{
    // This class in charge of main application where by Initialization and Execute 
    // method will be the only methods to be called when client code run this application.

    // This list is used in replace of database in this version.
    private List<UserBankAccount> _accountList;
    private UserBankAccount selectedAccount;
    private const decimal minimum_kept_amt = 20;
    private List<Transaction> _listOfTransactions;

    public void Initialization()
    {
        _accountList = new List<UserBankAccount>
        {
            new UserBankAccount() { Id=1, FullName = "Peter Parker", AccountNumber=333111, CardNumber = 123123, CardPin = 111111, AccountBalance = 2000.00m, IsLocked = false },
            new UserBankAccount() { Id=2, FullName = "Bruce Bane", AccountNumber=111222, CardNumber = 456456, CardPin = 222222, AccountBalance = 1500.30m, IsLocked = true },
            new UserBankAccount() { Id=3, FullName = "Clark Kent", AccountNumber=888555, CardNumber = 789789, CardPin = 333333, AccountBalance = 2900.12m, IsLocked = false }
        };
    }

    public void Execute()
    {
        AtmScreen.WelcomeATM();

        CheckCardNoPassword();
        AtmScreen.WelcomeCustomer();
        Utility.PrintConsoleWriteLine(selectedAccount.FullName, false);
        _listOfTransactions = new List<Transaction>();

        while (true)
        {
            AtmScreen.DisplaySecureMenu();
            ProcessMenuOption();
        }
    }

    public void CheckCardNoPassword()
    {
        bool isLoginPassed = false;

        while (isLoginPassed == false)
        {
            var inputAccount = new UserBankAccount();

            // Actual ATM system will accept and validate physical ATM card.
            // Card validation includes read card number and check bank account status
            // and other security checking.

            inputAccount.CardNumber = Validator.GetValidIntInputAmt("ATM Card Number");

            Utility.PrintUserInputLabel("Enter 6 Digit PIN: ");
            inputAccount.CardPin = Convert.ToInt32(Utility.GetHiddenConsoleInput());
            // for brevity, length 6 is not validated and data type.

            AtmScreen.LoginProgress();

            foreach (UserBankAccount account in _accountList)
            {
                selectedAccount = account;
                if (inputAccount.CardNumber.Equals(account.CardNumber))
                {
                    selectedAccount.TotalLogin++;

                    if (inputAccount.CardPin.Equals(account.CardPin))
                    {
                        selectedAccount = account;
                        if (selectedAccount.IsLocked)
                        {
                            // This is when database is used and when the app is restarted.
                            // Even user login with the correct card number and pin,
                            // If IsLocked status is locked, user still will be still blocked.
                            AtmScreen.PrintLockAccount();
                        }
                        else
                        {
                            selectedAccount.TotalLogin = 0;
                            isLoginPassed = true;
                            break;
                        }
                    }
                }
            }

            if (isLoginPassed == false)
            {
                Utility.PrintMessage("Invalid card number or PIN.", false);

                // Lock the account if user fail to login more than 3 times.
                selectedAccount.IsLocked = selectedAccount.TotalLogin == 3;
                if (selectedAccount.IsLocked)
                    AtmScreen.PrintLockAccount();
            }

            Console.Clear();
        }
    }

    private void ProcessMenuOption()
    {
        switch (Validator.GetValidIntInputAmt("your option"))
        {
            case (int)SecureMenu.CheckBalance:
                CheckBalance();
                break;
            case (int)SecureMenu.PlaceDeposit:
                PlaceDeposit();
                break;
            case (int)SecureMenu.MakeWithdrawal:
                MakeWithdrawal();
                break;
            case (int)SecureMenu.ThirdPartyTransfer:
                var screen = new AtmScreen();
                var vMThirdPartyTransfer = screen.ThirdPartyTransferForm();
                PerformThirdPartyTransfer(vMThirdPartyTransfer);
                break;
            case (int)SecureMenu.ViewTransaction:
                ViewTransaction();
                break;

            case (int)SecureMenu.Logout:
                AtmScreen.LogoutProgress();
                Utility.PrintConsoleWriteLine("You have succesfully logout. Please collect your ATM card.");
                ClearSession();
                Execute();
                break;
            default:
                Utility.PrintMessage("Invalid Option Entered.", false);

                break;
        }
    }

    public void CheckBalance()
    {
        AtmScreen.PrintCheckBalanceScreen();
        Utility.PrintConsoleWriteLine(Utility.FormatAmount(selectedAccount.AccountBalance), false);
    }

    public void PlaceDeposit()
    {
        // Note: Actual ATM system will just let you
        // place bank notes into physical ATM machine.

        Utility.PrintConsoleWriteLine("\nNote: Actual ATM system will just\nlet you " +
        "place bank notes into physical ATM machine. \n");

        var transaction_amt = Validator.GetValidDecimalInputAmt("amount");

        Utility.PrintUserInputLabel("\nCheck and counting bank notes.");
        Utility.printDotAnimation();
        Console.SetCursorPosition(0, Console.CursorTop-3);
        Console.WriteLine("");

        if (transaction_amt <= 0)
        {
            Utility.PrintMessage("Amount needs to be more than zero. Try again.", false);
            return;
        }

        if (transaction_amt % 10 != 0)
        {
            Utility.PrintMessage($"Key in the deposit amount only with multiply of 10. Try again.", false);
            return;
        }

        if (PreviewBankNotesCount(transaction_amt) == false)
        {
            Utility.PrintMessage($"You have cancelled your action.", false);
            return;
        }


        // Bind transaction_amt to Transaction object
        // Add transaction record - Start
        var transaction = new Transaction()
        {
            TransactionDate = DateTime.Now,
            TransactionType = TransactionType.Deposit,
            TransactionAmount = transaction_amt
        };
        InsertTransaction(transaction);
        // Add transaction record - End

        // Another method to update account balance.
        selectedAccount.AccountBalance = selectedAccount.AccountBalance + transaction_amt;

        Utility.PrintMessage($"You have successfully deposited {Utility.FormatAmount(transaction_amt)}. " +
            "Please collect the bank slip. ", true);
    }

    public void MakeWithdrawal()
    {
        Console.WriteLine("\nNote: For GUI or actual ATM system, user can ");
        Console.Write("choose some default withdrawal amount or custom amount. \n\n");

        var transaction_amt = Validator.GetValidDecimalInputAmt("amount");

        // Input data validation - Start
        if (transaction_amt <= 0)
        {
            Utility.PrintMessage("Amount needs to be more than zero. Try again.", false);
            return;
        }

        if (transaction_amt % 10 != 0)
        {
            Utility.PrintMessage($"Key in the deposit amount only with multiply of 10. Try again.", false);
            return;
        }
        // Input data validation - End


        // Business rules validation - Start
        if (transaction_amt > selectedAccount.AccountBalance)
        {
            Utility.PrintMessage($"Withdrawal failed. You do not have enough fund to withdraw {Utility.FormatAmount(transaction_amt)}", false);
            return;
        }

        if ((selectedAccount.AccountBalance - transaction_amt) < minimum_kept_amt)
        {
            Utility.PrintMessage($"Withdrawal failed. Your account needs to have minimum {Utility.FormatAmount(minimum_kept_amt)}", false);
            return;
        }
        // Business rules validation - End


        // Bind transaction_amt to Transaction object
        // Add transaction record - Start
        var transaction = new Transaction()
        {
            TransactionDate = DateTime.Now,
            TransactionType = TransactionType.Withdrawal,
            TransactionAmount = Math.Abs(transaction_amt)
        };
        InsertTransaction(transaction);
        // Add transaction record - End

        // Another method to update account balance.
        selectedAccount.AccountBalance = selectedAccount.AccountBalance - transaction_amt;

        Utility.PrintMessage("Please collect your money. You have successfully withdraw " +
            $"{Utility.FormatAmount(transaction_amt)}. Please collect your bank slip.", true);

    }

    public void PerformThirdPartyTransfer(VMThirdPartyTransfer vMThirdPartyTransfer)
    {
        if (vMThirdPartyTransfer.TransferAmount <= 0)
        {
            Utility.PrintMessage("Amount needs to be more than zero. Try again.", false);
            return;
        }

        // Check giver's account balance - Start
        if (vMThirdPartyTransfer.TransferAmount > selectedAccount.AccountBalance)
        {
            Utility.PrintMessage($"Withdrawal failed. You do not have enough " +
                "fund to withdraw {Utility.FormatAmount(vMThirdPartyTransfer.TransferAmount)}", false);
            return;
        }

        if (selectedAccount.AccountBalance - vMThirdPartyTransfer.TransferAmount < minimum_kept_amt)
        {
            Utility.PrintMessage($"Withdrawal failed. Your account needs to have " +
                "minimum {Utility.FormatAmount(minimum_kept_amt)}", false);
            return;
        }
        // Check giver's account balance - End

        // Check if receiver's bank account number is valid.
        var selectedBankAccountReceiver = (from b in _accountList
                                           where b.AccountNumber == vMThirdPartyTransfer.RecipientBankAccountNumber
                                           select b).FirstOrDefault();

        if (selectedBankAccountReceiver == null)
        {
            Utility.PrintMessage($"Third party transfer failed. Receiver bank account number is invalid.", false);
            return;
        }

        if (selectedBankAccountReceiver.FullName != vMThirdPartyTransfer.RecipientBankAccountName)
        {
            Utility.PrintMessage($"Third party transfer failed. Recipient's account name does not match.", false);
            return;
        }

        // Bind transaction_amt to Transaction object
        // Add transaction record (Giver) - Start
        Transaction transaction = new Transaction()
        {
            TransactionDate = DateTime.Now,
            TransactionType = TransactionType.ThirdPartyTransfer,
            TransactionAmount = Math.Abs(vMThirdPartyTransfer.TransferAmount),
            Description = $"Transfered to {selectedBankAccountReceiver.AccountNumber} ({selectedBankAccountReceiver.FullName})"
        };
        _listOfTransactions.Add(transaction);
        // Add transaction record (Giver) - End

        // Update balance amount (Giver)
        selectedAccount.AccountBalance = selectedAccount.AccountBalance - vMThirdPartyTransfer.TransferAmount;

        // Add transaction record (Receiver) - Start
        transaction = new Transaction()
        {
            TransactionDate = DateTime.Now,
            TransactionType = TransactionType.ThirdPartyTransfer,
            TransactionAmount = vMThirdPartyTransfer.TransferAmount,
            Description = $"Transfered from {selectedAccount.AccountNumber} ({selectedAccount.FullName})"
        };
        _listOfTransactions.Add(transaction);
        // Add transaction record (Receiver) - End

        // Update balance amount (Receiver)
        selectedBankAccountReceiver.AccountBalance = selectedBankAccountReceiver.AccountBalance + vMThirdPartyTransfer.TransferAmount;

        Utility.PrintMessage($"You have successfully transferred out " + 
            " {Utility.FormatAmount(vMThirdPartyTransfer.TransferAmount)} to {vMThirdPartyTransfer.RecipientBankAccountName}", true);
    }



    public void ViewTransaction()
    {

        if (_listOfTransactions.Count <= 0)
            Utility.PrintMessage($"There is no transaction yet.", true);
        else
        {
            var table = new ConsoleTable("Transaction Date", "Type", "Descriptions", "Amount " + AtmScreen.cur);

            foreach (var tran in _listOfTransactions)
            {
                table.AddRow(tran.TransactionDate, tran.TransactionType, tran.Description, tran.TransactionAmount);
            }
            table.Options.EnableCount = false;
            table.Write();
            Utility.PrintMessage($"You have performed {_listOfTransactions.Count} transactions.", true);
        }
    }

    public void InsertTransaction(Transaction transaction)
    {
        _listOfTransactions.Add(transaction);
    }

    private void ClearSession()
    {
        // No session is used in this version.
    }

}

Any comment on the my coding style and design pattern, for example SOLID principles? My concern is the ThirdPartyTransferForm() method in AtmScreen class. How should I split the UI nicely in this class? Another major issue I notice is that because ATMApp class implement the interface directly, if client code want to run this ATMApp application, those interface methods will be 'open' as well which is not supposed to be.

This video shows a sample run, based on an earlier version of the program.

\$\endgroup\$
  • 2
    \$\begingroup\$ There is even a video! How cool is that ;-) \$\endgroup\$ – t3chb0t Jul 6 at 18:07
  • \$\begingroup\$ I will upload my source code to github soon. \$\endgroup\$ – Steve Ngai Jul 7 at 0:54
1
\$\begingroup\$

Class Library

Console-based ATM client as a class library

I wouldn't call your application a class library. Class libraries provide a set of reusable classes and interfaces. Your interfaces are black boxes that have no usability purpose other than being a trigger for some action that requires user interactive console interop.

For instance,

public interface IUserBankAccount
{
    void CheckBalance();
    void PlaceDeposit();
    void MakeWithdrawal();
}

In a class library, this would be something like:

public interface IUserBankAccount
{
    Balance CheckBalance();
    Transaction PlaceDeposit(DepositRequest deposit);
    Transaction MakeWithdrawal(WithdrawalRequest withdrawal);
}

Try to refactor your code into several layers (domain, application).


Remarks

  • Try to adhere to "Seperation of Concerns": don't mix domain flow with user interactive flow. You could even split domain from application flow.
  • Provide a member with value 0 for any enum. C# specifies the member with value 0 as default value.
  • Create interfaces based on usability attributes, such as proper arguments and return values, rather than providing void operations that take 0 arguments.
  • Try to avoid static classes except for some specific situations (good post about this)
\$\endgroup\$
  • \$\begingroup\$ Thanks a lot for the code review. About static class , are you referring to Utility and validation class? And any post or articles on soc of domain and application flow? \$\endgroup\$ – Steve Ngai Jul 7 at 14:13
  • 1
    \$\begingroup\$ In this situation, I consider the domain layer the bank account, transactions, and deposit, withdraw operations. The application layer would use the domain layer and provide a presentation layer on top that handles user input/output with the console. \$\endgroup\$ – dfhwze Jul 7 at 14:15
  • \$\begingroup\$ Okay, I think I got it. I should create multiple projects for each layer like domain driven design architecture right? I thought of adding that when I add database. The application layer is the web api layer right.? \$\endgroup\$ – Steve Ngai Jul 7 at 14:26
  • 1
    \$\begingroup\$ The application layer could be a service layer (like web api), or could be a console app (like your situation) or any other presentation layer (wpf, asp.net, commandline tool, wcf, winforms, ..) \$\endgroup\$ – dfhwze Jul 7 at 14:44

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.