bitovi/canjs

View on GitHub
docs/can-guides/experiment/atm/atm.js

Summary

Maintainability
F
3 days
Test Coverage
"use strict";
can.fixture({
    "/verifyCard": function(request, response) {
        if (!request.data || !request.data.number || !request.data.pin) {
            response(400, {});
        } else {
            return {};
        }
    },
    "/deposit": function() {
        return {};
    },
    "/Withdrawal": function() {
        return {};
    },
    "/accounts": function() {
        return {
            data: [{
                balance: 100,
                id: 1,
                name: "checking"
            }, {
                balance: 10000,
                id: 2,
                name: "savings"
            }]
        };
    }
});

can.fixture.delay = 1000;

var Card = can.DefineMap.extend({
    number: "number",
    pin: "number",
    state: {
        default: "unverified",
        serialize: false
    },
    verify: function() {

        this.state = "verifying";

        var self = this;
        return can.ajax({
            type: "POST",
            url: "/verifyCard",
            data: this.serialize()
        }).then(
            function() {
                self.state = "verified";
                return self;
            },
            function() {
                self.state = "invalid";
                return self;
            });

    }
});

var Account = can.DefineMap.extend("Account", {
    id: "number",
    balance: "number",
    name: "string"
});
Account.List = can.DefineList.extend("AccountList", {
    "#": Account
});

can.connect.baseMap({
    url: "/accounts",
    Map: Account,
    List: Account.List,
    name: "accounts"
});

var Transaction = can.DefineMap.extend({
    account: Account,
    card: Card,
    executed: {
        type: "boolean",
        default: false
    },
    executing: {
        type: "boolean",
        default: false
    },
    get state() {
        if (this.executed) {
            return "executed";
        }
        if (this.executing) {
            return "executing";
        }
        // make sure there’s an amount, account, and card
        if (this.isReady()) {
            return "ready";
        }
        return "invalid";
    },
    execute: function() {
        if (this.state === "ready") {

            this.executing = true;

            var def = this.executeStart(),
                self = this;

            def.then(function() {
                can.batch.start();
                self.set({
                    executing: false,
                    executed: true
                });
                self.executeEnd();
                can.batch.stop();
            });
        }
    }
});

var Deposit = Transaction.extend({
    amount: "number",
    isReady: function() {
        return typeof this.amount === "number" &&
            this.account &&
            this.card;
    },
    executeStart: function() {
        return can.ajax({
            type: "POST",
            url: "/deposit",
            data: {
                card: this.card.serialize(),
                accountId: this.account.id,
                amount: this.amount
            }
        });
    },
    executeEnd: function(data) {
        this.account.balance = this.account.balance + this.amount;
    }
});

var Withdrawal = Transaction.extend({
    amount: "number",
    isReady: function() {
        return typeof this.amount === "number" &&
            this.account &&
            this.card;
    },
    executeStart: function() {
        return can.ajax({
            type: "POST",
            url: "/Withdrawal",
            data: {
                card: this.card.serialize(),
                accountId: this.account.id,
                amount: this.amount
            }
        });
    },
    executeEnd: function(data) {
        this.account.balance = this.account.balance - this.amount;
    }
});

var ATM = can.DefineMap.extend({
    // stateful properties
    card: Card,
    accountsPromise: "*",
    transactions: can.DefineList,
    printingReceipt: "boolean",
    receiptTime: {
        default: 5000,
        type: "number"
    },
    currentTransaction: {
        set: function(newTransaction) {
            // if current was executed, move it to transactions array
            var currentTransaction = this.currentTransaction;
            if (this.transactions && currentTransaction &&
                currentTransaction.state === "executed") {

                this.transactions.push(currentTransaction);
            }
            return newTransaction;
        }
    },

    // Derived properties
    get isVerifyingPin() {
        return this.card && (this.card.state === "verifying");
    },
    get isTransactionReady() {
        var currentTransaction = this.currentTransaction;
        if (currentTransaction) {
            return currentTransaction.state === "ready";
        }
    },
    get isTransactionExecuting() {
        return this.currentTransaction && (this.currentTransaction.state === "executing");
    },
    get state() {
        // if printingReceipt
        // if a currentTransaction
        //    and the transaction is executed -> successfulTransaction
        //    if an account
        //        is it a deposit or withdraw
        //    else
        //        pick account or wait for accounts
        // if a card
        //    is card verified -> choosingTransaction
        //    if pin -> verifyingPin
        //    else -> readingPin
        // readingCard
        if (this.printingReceipt) {
            return "printingReceipt";
        }
        var currentTransaction = this.currentTransaction;
        if (currentTransaction) {
            if (currentTransaction.state === "executed") {
                return "successfulTransaction";
            }

            if (currentTransaction.account) {
                if (currentTransaction instanceof Deposit) {
                    return "depositInfo";
                } else {
                    return "withdrawalInfo";
                }
            }

            if (this.accountsPromise) {
                return "pickingAccount";
            }
        }

        if (this.card) {
            if (this.card.state === "verified") {
                return "choosingTransaction";
            }
            return "readingPin";
        }

        return "readingCard";
    },
    cardNumber: function(number) {
        this.card = new Card({
            number: number
        });
    },
    pinNumber: function(pin) {
        var card = this.card;
        var atm = this;

        card.pin = pin;
        card.verify().then(function(card) {
            atm.accountsPromise = Account.getList(card.serialize());
            atm.transactions = new can.DefineList();
        });
    },

    // Methods
    chooseDeposit: function() {
        this.currentTransaction = new Deposit({
            card: this.card
        });
    },
    chooseWithdraw: function() {
        this.currentTransaction = new Withdrawal({
            card: this.card
        });
    },

    chooseAccount: function(account) {
        this.currentTransaction.account = account;
    },
    removeTransaction: function() {
        this.currentTransaction = null;
    },
    printReceiptAndExit: function() {
        this.removeTransaction();
        this.printingReceipt = true;
        var self = this;
        setTimeout(function() {
            self.exit();
        }, this.receiptTime);
    },
    exit: function() {
        can.batch.start();
        this.currentTransaction = this.card = this.transactions = null;
        this.printingReceipt = false;
        can.batch.stop();
    }
});

can.Component.extend({
    tag: "atm-machine",
    view: can.stache.from("atm-template"),
    ViewModel: ATM,
    helpers: {
        actionName: function(transaction) {
            return transaction instanceof Deposit ? "deposited" : "withdrew";
        },
        actionPrep: function(transaction) {
            return transaction instanceof Deposit ? "into" : "from";
        }
    }

});

document.body.appendChild(
    can.stache.from("app-template")({})
);