DoneJS StealJS jQuery ++ FuncUnit DocumentJS
3.0.0
2.3.27

 

  • Github
  • Twitter
  • Chat
  • Forum
  • Guides
    • introduction
      • Mission
      • Technical Highlights
      • Who uses CanJS?
    • experiment
      • Chat Guide
      • TodoMVC Guide
      • ATM Guide
      • Setting up CanJS
    • commitment
      • API Guide
      • Examples
      • Roadmap
      • Migrating to 3.0
    • contribute
      • Bug Report
      • Code
      • Documentation
      • Evangelism
      • Feature Suggestion
      • Releases
  • Core
  • Ecosystem
  • Infrastructure
  • Legacy
  • Bitovi
    • Bitovi.com
    • Blog
    • Consulting
    • Training
    • Open Source
  • Chat
  • Forum
  • Star
  • Follow @canjs
  • CanJS
  • /
  • Guides
  • /
  • ATM Guide
  • / On this page
    • ATM Guide

      page

      This guide walks through building and testing an ATM application with CanJS's Core libraries. It teaches how to do test driven development (TDD) and manage complex state. It takes about 2 hours to complete.

      • source

      Overview

      Checkout the final app:

      JS Bin on jsbin.com

      Notice it has tests at the bottom of the Output tab.

      Watch the following video to see it in action:

      TODO: VIDEO OF IT IN ACTION

      Setup

      The easiest way to get started is to clone the following JSBin by clicking the JS Bin button on the top left:

      JS Bin on jsbin.com

      The JSBin is designed to run both the application and its tests in the OUTPUT tab. To set this up, the HTML tab:

      • Loads QUnit for its testing library. It also includes the <div id="qunit"></div> element where QUnit's test results will be written to.

      • Loads can.all.js, which is a script that includes all of CanJS core under a single global can namespace.

        Generally speaking, you should not use the global can script and instead should import things directly with a module loader like StealJS, WebPack or Browserify. Read Setting up CanJS on how to setup CanJS in a real app.

      • Includes the content for a app-template can-stache template. This template provides the title for the ATM app, and uses the <atm-machine> custom can-component element that will eventually provide the ATM functionality.

      The JS tab is split into two sections:

      • CODE - The ATM's models, view-models and component code will go here.
      • TESTS - The ATM's tests will go here.

      Normally, your application's code and test will be in separate files and loaded by different html pages. But we combine them here to fit within JSBin's limitations.

      The CODE section is rendering the app-template with:

      document.body.insertBefore(
          can.stache.from("app-template")({}),
          document.body.firstChild
      );
      

      The TESTS section is labeling which module will be tested:

      QUnit.module("ATM system", {});
      

      Mock out switching between pages

      In this section, we will mock out which pages will be shown as the state of the ATM changes.

      Update the HTML tab to:

      • Switch between different pages of the application as the ATM view-model's state property changes with {{#switch expression}}.
      <!DOCTYPE html>
      <html>
      <head>
      <meta name="description" content="CanJS 3.0 - ATM Guide - Setup">
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width">
        <title>JS Bin</title>
      
      </head>
      <body>
      
      <script type='text/stache' id='atm-template'>
      <div class="screen">
          <div class="screen-content">
              <div class="screen-glass">
      
      {{#switch state}}
          {{#case "readingCard"}}
              <h2>Reading Card</h2>
          {{/case}}
      
          {{#case "readingPin"}}
              <h2>Reading Pin</h2>
          {{/case}}
      
          {{#case "choosingTransaction"}}
              <h2>Choose Transaction</h2>
          {{/case}}
      
          {{#case  "pickingAccount"}}
              <h2>Pick Account</h2>
          {{/case}}
      
          {{#case "depositInfo"}}
              <h2>Deposit</h2>
          {{/case}}
      
          {{#case "withdrawalInfo"}}
              <h2>Withdraw</h2>
          {{/case}}
      
          {{#case "successfulTransaction"}}
              <h2>Transaction Successful!</h2>
          {{/case}}
      
          {{#case  "printingReceipt"}}
              <h2>Printing Receipt</h2>
          {{/case}}
      
          {{#default}}
              <h2>Error</h2>
              <p>Invalid state - {{state}}</p>
          {{/default}}
      
      {{/switch}}
      
              </div>
          </div>
      </div>
      </script>
      
      <script type='text/stache' id='app-template'>
      <div class="title">
          <h1>canATM</h1>
      </div>
      <atm-machine/>
      </script>
      
      <script src="https://code.jquery.com/jquery-2.2.4.js"></script>
      <script src="https://unpkg.com/can/dist/global/can.all.js"></script>
      
      <div id="qunit"></div>
      <link rel="stylesheet" href="http://code.jquery.com/qunit/qunit-1.12.0.css">
      <script src="http://code.jquery.com/qunit/qunit-1.12.0.js"></script>
      
      </body>
      </html>
      
      

      Update the JavaScript tab to:

      • Create the ATM view-model with a state property initialized to readingCard with can-define/map/map.
      • Create an <atm-machine> custom element with can-component.
      // ========================================
      // CODE
      // ========================================
      
      var ATM = can.DefineMap.extend({
          state: {type: "string", value: "readingCard"}
      });
      
      can.Component.extend({
          tag: "atm-machine",
          view: can.stache.from("atm-template"),
          ViewModel: ATM,
      });
      
      document.body.insertBefore(
          can.stache.from("app-template")({}),
          document.body.firstChild
      );
      
      // ========================================
      // TESTS
      // ========================================
      
      QUnit.module("ATM system", {});
      
      

      When complete, you should see the "Reading Card" title.

      This step outlines the page transitions we're going to make the state property transition between:

      • readingCard
      • readingPin
      • choosingTransaction
      • pickingAccount
      • depositInfo
      • withdrawalInfo
      • successfulTransaction
      • printingReceipt

      Each of those states are present in the following state diagram.

      We'll build out these pages once we build the Card and Transaction sub-models that will make building the ATM view model easier.

      Card tests

      In this section, we will:

      • Design an API for an ATM Card
      • Write out tests for the card.

      An ATM Card will take a card number and pin. It will start out as having a state of "unverified". It will have a verify method that will change the state to "verifying" and if the response is successful, state will change to "verified".

      Update the JS tab to:

      • Make the fake data request delay 1ms by setting delay to 1 before every test and restoring it to 2s after every test runs.
      • Write a test that creates a valid card, calls .verify(), and asserts the state is "verified".
      • Write a test that creates a invalid card, calls .verify(), and asserts the state is "invalid".
      // ========================================
      // CODE
      // ========================================
      
      var ATM = can.DefineMap.extend({
          state: {type: "string", value: "readingCard"}
      });
      
      can.Component.extend({
          tag: "atm-machine",
          view: can.stache.from("atm-template"),
          ViewModel: ATM,
      });
      
      document.body.insertBefore(
          can.stache.from("app-template")({}),
          document.body.firstChild
      );
      
      // ========================================
      // TESTS
      // ========================================
      
      QUnit.module("ATM system", {
          setup: function() {
              can.fixture.delay = 1;
          },
          teardown: function() {
              can.fixture.delay = 2000;
          }
      });
      
      QUnit.asyncTest("Valid Card", function() {
      
          var c = new Card({
              number: "01234567890",
              pin: 1234
          });
      
          QUnit.equal(c.state, "unverified");
      
          c.verify();
      
          QUnit.equal(c.state, "verifying", "card is verifying");
      
          c.on("state", function(ev, newVal) {
      
              QUnit.equal(newVal, "verified", "card is verified");
      
              QUnit.start();
          });
      });
      
      QUnit.asyncTest("Invalid Card", function() {
      
          var c = new Card({});
      
          QUnit.equal(c.state, "unverified");
      
          c.verify();
      
          QUnit.equal(c.state, "verifying", "card is verifying");
      
          c.on("state", function(ev, newVal) {
      
              QUnit.equal(newVal, "invalid", "card is invalid");
      
              QUnit.start();
          });
      });
      
      

      When complete you should have a breaking test. Now lets make it pass.

      Card model

      In this section, we will:

      • Implement the Card model so that all tests pass.

      Update the JavaScript tab to:

      • Simulate the /verifyCard with can-fixture. It return a successful response if the request body has a number and pin and a 400 if not.
      • Use can-define/map/map to define the Card model, including:
        • a number and pin property.
        • a state property initialized to unverified that is not part of the card's serialized data.
        • a verify method that posts the card's data to /verifyCard and updates the state accordingly.
      // ========================================
      // CODE
      // ========================================
      
      can.fixture({
          "/verifyCard": function(request, response) {
              if (!request.data || !request.data.number || !request.data.pin) {
                  response(400, {});
              } else {
                  return {};
              }
          }
      });
      can.fixture.delay = 1000;
      
      var Card = can.DefineMap.extend({
          number: "string",
          pin: "string",
          state: {
              value: "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 ATM = can.DefineMap.extend({
          state: {type: "string", value: "readingCard"}
      });
      
      can.Component.extend({
          tag: "atm-machine",
          view: can.stache.from("atm-template"),
          ViewModel: ATM,
      });
      
      document.body.insertBefore(
          can.stache.from("app-template")({}),
          document.body.firstChild
      );
      
      // ========================================
      // TESTS
      // ========================================
      
      QUnit.module("ATM system", {
          setup: function() {
              can.fixture.delay = 1;
          },
          teardown: function() {
              can.fixture.delay = 2000;
          }
      });
      
      QUnit.asyncTest("Valid Card", function() {
      
          var c = new Card({
              number: "01234567890",
              pin: 1234
          });
      
          QUnit.equal(c.state, "unverified");
      
          c.verify();
      
          QUnit.equal(c.state, "verifying", "card is verifying");
      
          c.on("state", function(ev, newVal) {
      
              QUnit.equal(newVal, "verified", "card is verified");
      
              QUnit.start();
          });
      });
      
      QUnit.asyncTest("Invalid Card", function() {
      
          var c = new Card({});
      
          QUnit.equal(c.state, "unverified");
      
          c.verify();
      
          QUnit.equal(c.state, "verifying", "card is verifying");
      
          c.on("state", function(ev, newVal) {
      
              QUnit.equal(newVal, "invalid", "card is invalid");
      
              QUnit.start();
          });
      });
      
      

      When complete, all tests should pass.

      In this step, you implemented a Card model that encapsulates the behavior of its own state.

      Deposit test

      In this section, we will:

      • Design an API retrieving Accounts.
      • Design an API for a Deposit type.
      • Write out tests for the Deposit type.

      An Account will have an id, name, and balance. We'll use can-connect to add a getList method that retrieves an account given a card.

      A Deposit will take a card, an amount, and an account. Deposits will start out having a state of "invalid". When the deposit has a card, amount and account, the state will change to "ready". Once the deposit is ready, the .execute() method will change the state to "executing" and then to "executed" once the transaction completes.

      Update the JS tab to:

      • Create a deposit with an amount and a card.
      • Check that the state is "invalid" because there is no account.
      • Use Account.getList to get the accounts for the card and:
        • set the deposit.accounts to the first account.
        • remember the starting balance.
      • Use on to listen for state changes. When state is:
        • "ready", .execute() the transaction.
        • "executed", verify the new account balance.
      // ========================================
      // CODE
      // ========================================
      
      can.fixture({
          "/verifyCard": function(request, response) {
              if (!request.data || !request.data.number || !request.data.pin) {
                  response(400, {});
              } else {
                  return {};
              }
          }
      });
      can.fixture.delay = 1000;
      
      var Card = can.DefineMap.extend({
          number: "string",
          pin: "string",
          state: {
              value: "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 ATM = can.DefineMap.extend({
          state: {type: "string", value: "readingCard"}
      });
      
      can.Component.extend({
          tag: "atm-machine",
          view: can.stache.from("atm-template"),
          ViewModel: ATM,
      });
      
      document.body.insertBefore(
          can.stache.from("app-template")({}),
          document.body.firstChild
      );
      
      // ========================================
      // TESTS
      // ========================================
      
      QUnit.module("ATM system", {
          setup: function() {
              can.fixture.delay = 1;
          },
          teardown: function() {
              can.fixture.delay = 2000;
          }
      });
      
      QUnit.asyncTest("Valid Card", function() {
      
          var c = new Card({
              number: "01234567890",
              pin: 1234
          });
      
          QUnit.equal(c.state, "unverified");
      
          c.verify();
      
          QUnit.equal(c.state, "verifying", "card is verifying");
      
          c.on("state", function(ev, newVal) {
      
              QUnit.equal(newVal, "verified", "card is verified");
      
              QUnit.start();
          });
      });
      
      QUnit.asyncTest("Invalid Card", function() {
      
          var c = new Card({});
      
          QUnit.equal(c.state, "unverified");
      
          c.verify();
      
          QUnit.equal(c.state, "verifying", "card is verifying");
      
          c.on("state", function(ev, newVal) {
      
              QUnit.equal(newVal, "invalid", "card is invalid");
      
              QUnit.start();
          });
      });
      
      QUnit.asyncTest("Deposit", 6, function() {
      
          var card = new Card({
              number: "0123456789",
              pin: "1122"
          });
      
          var deposit = new Deposit({
              amount: 100,
              card: card
          });
      
          equal(deposit.state, "invalid");
      
          var startingBalance;
      
          Account.getList(card.serialize()).then(function(accounts) {
              QUnit.ok(true, "got accounts");
              deposit.account = accounts[0];
              startingBalance = accounts[0].balance;
          });
      
          deposit.on("state", function(ev, newVal) {
              if (newVal === "ready") {
      
                  QUnit.ok(true, "deposit is ready");
                  deposit.execute();
      
              } else if (newVal === "executing") {
      
                  QUnit.ok(true, "executing a deposit");
      
              } else if (newVal === "executed") {
      
                  QUnit.ok(true, "executed a deposit");
                  equal(deposit.account.balance, 100 + startingBalance);
                  start();
      
              }
          });
      });
      
      

      When complete, the Deposit test should run, but error because Deposit is not defined.

      Optional: Challenge yourself by writing the Withdrawal test on your own. How is it different than the Deposit test?

      Transaction, Deposit, and Withdrawal models

      In this section, we will:

      • Implement the Account model.
      • Implement a base Transaction model and extend it into a Deposit and Withdrawal model.
      • Get the Deposit test to pass.

      Update the JavaScript tab to:

      • Simulate /accounts to return Account data with can-fixture.
      • Simulate /deposit to always return a successful result.
      • Simulate /withdrawal to always return a successful result.
      • Define the Account model to:
        • have an id property
        • have a balance property
        • have a name property
      • Define an Account.List type with can-define/list/list
      • Connect Account and Account.List types to the restful "/accounts" endpoint using can-connect/can/base-map/base-map.
      • Define the Transaction model to:
        • have an account and card property.
        • have an executing and executed property that track if the transaction is executing or has executed.
        • have a rejected property that stores the error given for a failed transaction.
        • have an abstract ready property that Deposit and Withdrawal will implement to return true when the transaction is in a state able to be executed.
        • have a state property that reads other stateful properties and returns a string representation of the state.
        • have an abstract executeStart method that Deposit and Withdrawal will implement to execute the transaction and return a Promise the resolves when the transaction completes.
        • have an abstract executeEnd method that Deposit and Withdrawal will implement to update the transactions values (typically the account balance) if the transaction completed successfully.
        • have an execute method that calls .executeStart() and executeEnd() and keeps the stateful properties updated correctly.
      • Define the Deposit model to:
        • have an amount property.
        • implement ready to return true when the amount is greater than 0 and there's an account and card.
        • implement executeStart to POST the deposit information to /deposit
        • implement executeEnd to update the account balance.
      • Define the Withdrawal model to behave in the same way as Deposit except that it POSTs the withdrawal information to /withdrawal.
      // ========================================
      // CODE
      // ========================================
      
      can.fixture({
          "/verifyCard": function(request, response) {
              if (!request.data || !request.data.number || !request.data.pin) {
                  response(400, {});
              } else {
                  return {};
              }
          },
          "/accounts": function() {
              return {
                  data: [{
                      balance: 100,
                      id: 1,
                      name: "checking"
                  }, {
                      balance: 10000,
                      id: 2,
                      name: "savings"
                  }]
              };
          },
          "/deposit": function() {
              return {};
          },
          "/withdrawal": function() {
              return {};
          }
      });
      can.fixture.delay = 1000;
      
      var Card = can.DefineMap.extend({
          number: "string",
          pin: "string",
          state: {
              value: "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,
          executing: {
              type: "boolean",
              value: false
          },
          executed: {
              type: "boolean",
              value: false
          },
          rejected: "any",
          get ready(){
              throw new Error("Transaction::ready must be provided by extended type");
          },
          get state() {
              if (this.rejected) {
                  return "rejected";
              }
              if (this.executed) {
                  return "executed";
              }
              if (this.executing) {
                  return "executing";
              }
              // make sure there's an amount, account, and card
              if (this.ready) {
                  return "ready";
              }
              return "invalid";
          },
          executeStart: function(){
              throw new Error("Transaction::executeStart must be provided by extended type");
          },
          executeEnd: function(){
              throw new Error("Transaction::executeEnd must be provided by extended type");
          },
          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();
                  }, function(reason){
                      self.set({
                          executing: false,
                          executed: true,
                          rejected: reason
                      });
                  });
              }
          }
      });
      
      var Deposit = Transaction.extend({
          amount: "number",
          get ready() {
              return this.amount > 0 &&
                  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",
          get ready() {
              return this.amount > 0 &&
                  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({
          state: {type: "string", value: "readingCard"}
      });
      
      can.Component.extend({
          tag: "atm-machine",
          view: can.stache.from("atm-template"),
          ViewModel: ATM,
      });
      
      document.body.insertBefore(
          can.stache.from("app-template")({}),
          document.body.firstChild
      );
      
      // ========================================
      // TESTS
      // ========================================
      
      QUnit.module("ATM system", {
          setup: function() {
              can.fixture.delay = 1;
          },
          teardown: function() {
              can.fixture.delay = 2000;
          }
      });
      
      QUnit.asyncTest("Valid Card", function() {
      
          var c = new Card({
              number: "01234567890",
              pin: 1234
          });
      
          QUnit.equal(c.state, "unverified");
      
          c.verify();
      
          QUnit.equal(c.state, "verifying", "card is verifying");
      
          c.on("state", function(ev, newVal) {
      
              QUnit.equal(newVal, "verified", "card is verified");
      
              QUnit.start();
          });
      });
      
      QUnit.asyncTest("Invalid Card", function() {
      
          var c = new Card({});
      
          QUnit.equal(c.state, "unverified");
      
          c.verify();
      
          QUnit.equal(c.state, "verifying", "card is verifying");
      
          c.on("state", function(ev, newVal) {
      
              QUnit.equal(newVal, "invalid", "card is invalid");
      
              QUnit.start();
          });
      });
      
      QUnit.asyncTest("Deposit", 6, function() {
      
          var card = new Card({
              number: "0123456789",
              pin: "1122"
          });
      
          var deposit = new Deposit({
              amount: 100,
              card: card
          });
      
          equal(deposit.state, "invalid");
      
          var startingBalance;
      
          Account.getList(card.serialize()).then(function(accounts) {
              QUnit.ok(true, "got accounts");
              deposit.account = accounts[0];
              startingBalance = accounts[0].balance;
          });
      
          deposit.on("state", function(ev, newVal) {
              if (newVal === "ready") {
      
                  QUnit.ok(true, "deposit is ready");
                  deposit.execute();
      
              } else if (newVal === "executing") {
      
                  QUnit.ok(true, "executing a deposit");
      
              } else if (newVal === "executed") {
      
                  QUnit.ok(true, "executed a deposit");
                  equal(deposit.account.balance, 100 + startingBalance);
                  start();
      
              }
          });
      });
      
      

      When complete, the Deposit tests will pass.

      Reading Card page and test

      In this section, we will:

      • Allow the user to enter a card number and go to the Reading Pin page.
      • Add tests to ATM Basics test.

      Update the HTML tab to:

      • Allow a user to call cardNumber with the <input>'s value.
      <!DOCTYPE html>
      <html>
      <head>
      <meta name="description" content="CanJS 3.0 - ATM Guide - Setup">
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width">
        <title>JS Bin</title>
      
      </head>
      <body>
      
      <script type='text/stache' id='atm-template'>
      <div class="screen">
          <div class="screen-content">
              <div class="screen-glass">
      
      {{#switch state}}
          {{#case "readingCard"}}
              <h2>Reading Card</h2>
              <p>Welcome to canATM where there are <strong>never</strong>
                fees!</p>
              </p>
              <p>
                  Enter Card Number:
                  <input name="card" ($enter)="cardNumber(%element.value)"/>
              </p>
          {{/case}}
      
          {{#case "readingPin"}}
              <h2>Reading Pin</h2>
          {{/case}}
      
          {{#case "choosingTransaction"}}
              <h2>Choose Transaction</h2>
          {{/case}}
      
          {{#case  "pickingAccount"}}
              <h2>Pick Account</h2>
          {{/case}}
      
          {{#case "depositInfo"}}
              <h2>Deposit</h2>
          {{/case}}
      
          {{#case "withdrawalInfo"}}
              <h2>Withdraw</h2>
          {{/case}}
      
          {{#case "successfulTransaction"}}
              <h2>Transaction Successful!</h2>
          {{/case}}
      
          {{#case  "printingReceipt"}}
              <h2>Printing Receipt</h2>
          {{/case}}
      
          {{#default}}
              <h2>Error</h2>
              <p>Invalid state - {{state}}</p>
          {{/default}}
      
      {{/switch}}
      
              </div>
          </div>
      </div>
      </script>
      
      <script type='text/stache' id='app-template'>
      <div class="title">
          <h1>canATM</h1>
      </div>
      <atm-machine/>
      </script>
      
      <script src="https://code.jquery.com/jquery-2.2.4.js"></script>
      <script src="https://unpkg.com/can/dist/global/can.all.js"></script>
      
      <div id="qunit"></div>
      <link rel="stylesheet" href="http://code.jquery.com/qunit/qunit-1.12.0.css">
      <script src="http://code.jquery.com/qunit/qunit-1.12.0.js"></script>
      
      </body>
      </html>
      
      

      Update the JavaScript tab to:

      • Declare a card property.
      • Derive a state property that changes to "readingPin" when card is defined.
      • Add a cardNumber that creates a card with the number provided.
      // ========================================
      // CODE
      // ========================================
      
      can.fixture({
          "/verifyCard": function(request, response) {
              if (!request.data || !request.data.number || !request.data.pin) {
                  response(400, {});
              } else {
                  return {};
              }
          },
          "/accounts": function() {
              return {
                  data: [{
                      balance: 100,
                      id: 1,
                      name: "checking"
                  }, {
                      balance: 10000,
                      id: 2,
                      name: "savings"
                  }]
              };
          },
          "/deposit": function() {
              return {};
          },
          "/withdrawal": function() {
              return {};
          }
      });
      can.fixture.delay = 1000;
      
      var Card = can.DefineMap.extend({
          number: "string",
          pin: "string",
          state: {
              value: "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,
          executing: {
              type: "boolean",
              value: false
          },
          executed: {
              type: "boolean",
              value: false
          },
          rejected: "any",
          get ready(){
              throw new Error("Transaction::ready must be provided by extended type");
          },
          get state() {
              if (this.rejected) {
                  return "rejected";
              }
              if (this.executed) {
                  return "executed";
              }
              if (this.executing) {
                  return "executing";
              }
              // make sure there's an amount, account, and card
              if (this.ready) {
                  return "ready";
              }
              return "invalid";
          },
          executeStart: function(){
              throw new Error("Transaction::executeStart must be provided by extended type");
          },
          executeEnd: function(){
              throw new Error("Transaction::executeEnd must be provided by extended type");
          },
          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();
                  }, function(reason){
                      self.set({
                          executing: false,
                          executed: true,
                          rejected: reason
                      });
                  });
              }
          }
      });
      
      var Deposit = Transaction.extend({
          amount: "number",
          get ready() {
              return this.amount > 0 &&
                  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",
          get ready() {
              return this.amount > 0 &&
                  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,
      
          // derived properties
          get state(){
              if(this.card) {
                  return "readingPin";
              }
              return "readingCard";
          },
          // methods
          cardNumber: function(number) {
              this.card = new Card({
                  number: number
              });
          }
      });
      
      can.Component.extend({
          tag: "atm-machine",
          view: can.stache.from("atm-template"),
          ViewModel: ATM,
      });
      
      document.body.insertBefore(
          can.stache.from("app-template")({}),
          document.body.firstChild
      );
      
      // ========================================
      // TESTS
      // ========================================
      
      QUnit.module("ATM system", {
          setup: function() {
              can.fixture.delay = 1;
          },
          teardown: function() {
              can.fixture.delay = 2000;
          }
      });
      
      QUnit.asyncTest("Valid Card", function() {
      
          var c = new Card({
              number: "01234567890",
              pin: 1234
          });
      
          QUnit.equal(c.state, "unverified");
      
          c.verify();
      
          QUnit.equal(c.state, "verifying", "card is verifying");
      
          c.on("state", function(ev, newVal) {
      
              QUnit.equal(newVal, "verified", "card is verified");
      
              QUnit.start();
          });
      });
      
      QUnit.asyncTest("Invalid Card", function() {
      
          var c = new Card({});
      
          QUnit.equal(c.state, "unverified");
      
          c.verify();
      
          QUnit.equal(c.state, "verifying", "card is verifying");
      
          c.on("state", function(ev, newVal) {
      
              QUnit.equal(newVal, "invalid", "card is invalid");
      
              QUnit.start();
          });
      });
      
      QUnit.asyncTest("Deposit", 6, function() {
      
          var card = new Card({
              number: "0123456789",
              pin: "1122"
          });
      
          var deposit = new Deposit({
              amount: 100,
              card: card
          });
      
          equal(deposit.state, "invalid");
      
          var startingBalance;
      
          Account.getList(card.serialize()).then(function(accounts) {
              QUnit.ok(true, "got accounts");
              deposit.account = accounts[0];
              startingBalance = accounts[0].balance;
          });
      
          deposit.on("state", function(ev, newVal) {
              if (newVal === "ready") {
      
                  QUnit.ok(true, "deposit is ready");
                  deposit.execute();
      
              } else if (newVal === "executing") {
      
                  QUnit.ok(true, "executing a deposit");
      
              } else if (newVal === "executed") {
      
                  QUnit.ok(true, "executed a deposit");
                  equal(deposit.account.balance, 100 + startingBalance);
                  start();
      
              }
          });
      });
      
      QUnit.asyncTest("ATM basics", function() {
      
          var atm = new ATM();
      
          equal(atm.state, "readingCard", "starts at reading card state");
      
          atm.cardNumber("01233456789");
      
          equal(atm.state, "readingPin", "moves to reading card state");
      
          QUnit.start();
      });
      
      

      When complete, you should be able to enter a card number and see the Reading Pin page.

      Reading Pin page and test

      In this section, we will:

      • Allow the user to enter a pin number and go to the Choosing Transaction page.
      • Add tests to ATM Basics test.

      Update the HTML tab to:

      • Call pinNumber with the <input>'s value.
      • Disable the <input> while the pin is being verified.
      • Show a loading icon while the pin is being verified.
      <!DOCTYPE html>
      <html>
      <head>
      <meta name="description" content="CanJS 3.0 - ATM Guide - Setup">
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width">
        <title>JS Bin</title>
      
      </head>
      <body>
      
      <script type='text/stache' id='atm-template'>
      <div class="screen">
          <div class="screen-content">
              <div class="screen-glass">
      
      {{#switch state}}
          {{#case "readingCard"}}
              <h2>Reading Card</h2>
              <p>Welcome to canATM where there are <strong>never</strong>
                fees!</p>
              </p>
              <p>
                  Enter Card Number:
                  <input name="card" ($enter)="cardNumber(%element.value)"/>
              </p>
          {{/case}}
      
          {{#case "readingPin"}}
              <h2>Reading Pin</h2>
              <p>
                  Enter Pin Number:
                  <input name="pin" type="password"
                      autofocus
                      {{#is card.state "verifying"}}DISABLED{{/is}}
                      ($enter)="pinNumber(%element.value)"/>
      
                  {{#is card.state "verifying"}}
                      <div class='warn'>
                          <p>
                          <img src="http://v3.canjs.com/docs/images/loader.gif"/>
                          verifying
                          </p>
                      </div>
                  {{/is}}
              </p>
              <a href="javascript://" ($click)="exit()">exit</a>
          {{/case}}
      
          {{#case "choosingTransaction"}}
              <h2>Choose Transaction</h2>
          {{/case}}
      
          {{#case  "pickingAccount"}}
              <h2>Pick Account</h2>
          {{/case}}
      
          {{#case "depositInfo"}}
              <h2>Deposit</h2>
          {{/case}}
      
          {{#case "withdrawalInfo"}}
              <h2>Withdraw</h2>
          {{/case}}
      
          {{#case "successfulTransaction"}}
              <h2>Transaction Successful!</h2>
          {{/case}}
      
          {{#case  "printingReceipt"}}
              <h2>Printing Receipt</h2>
          {{/case}}
      
          {{#default}}
              <h2>Error</h2>
              <p>Invalid state - {{state}}</p>
          {{/default}}
      
      {{/switch}}
      
              </div>
          </div>
      </div>
      </script>
      
      <script type='text/stache' id='app-template'>
      <div class="title">
          <h1>canATM</h1>
      </div>
      <atm-machine/>
      </script>
      
      <script src="https://code.jquery.com/jquery-2.2.4.js"></script>
      <script src="https://unpkg.com/can/dist/global/can.all.js"></script>
      
      <div id="qunit"></div>
      <link rel="stylesheet" href="http://code.jquery.com/qunit/qunit-1.12.0.css">
      <script src="http://code.jquery.com/qunit/qunit-1.12.0.js"></script>
      
      </body>
      </html>
      
      

      Update the ATM view model in the CODE section of the JavaScript tab to:

      • Define an accountsPromise property that will contain a list of accounts for the card.
      • Define a transactions property that will contain a list of transactions for this session.
      • Update state to be in the "choosingTransaction" state when the card is verified.
      • Define a pinNumber method that updates the card's pin, calls .verify(), and then initializes the accountsPromise and transactions property.

      Update the TESTS section of the JavaScript tab to:

      • Test calling pinNumber moves the state to "choosingTransaction".
      // ========================================
      // CODE
      // ========================================
      
      can.fixture({
          "/verifyCard": function(request, response) {
              if (!request.data || !request.data.number || !request.data.pin) {
                  response(400, {});
              } else {
                  return {};
              }
          },
          "/accounts": function() {
              return {
                  data: [{
                      balance: 100,
                      id: 1,
                      name: "checking"
                  }, {
                      balance: 10000,
                      id: 2,
                      name: "savings"
                  }]
              };
          },
          "/deposit": function() {
              return {};
          },
          "/withdrawal": function() {
              return {};
          }
      });
      can.fixture.delay = 1000;
      
      var Card = can.DefineMap.extend({
          number: "string",
          pin: "string",
          state: {
              value: "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,
          executing: {
              type: "boolean",
              value: false
          },
          executed: {
              type: "boolean",
              value: false
          },
          rejected: "any",
          get ready(){
              throw new Error("Transaction::ready must be provided by extended type");
          },
          get state() {
              if (this.rejected) {
                  return "rejected";
              }
              if (this.executed) {
                  return "executed";
              }
              if (this.executing) {
                  return "executing";
              }
              // make sure there's an amount, account, and card
              if (this.ready) {
                  return "ready";
              }
              return "invalid";
          },
          executeStart: function(){
              throw new Error("Transaction::executeStart must be provided by extended type");
          },
          executeEnd: function(){
              throw new Error("Transaction::executeEnd must be provided by extended type");
          },
          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();
                  }, function(reason){
                      self.set({
                          executing: false,
                          executed: true,
                          rejected: reason
                      });
                  });
              }
          }
      });
      
      var Deposit = Transaction.extend({
          amount: "number",
          get ready() {
              return this.amount > 0 &&
                  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",
          get ready() {
              return this.amount > 0 &&
                  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: "any",
          transactions: can.DefineList,
      
          // derived properties
          get state(){
              if(this.card) {
                  if (this.card.state === "verified") {
                      return "choosingTransaction";
                  }
                  return "readingPin";
              }
              return "readingCard";
          },
      
          // methods
          cardNumber: function(number) {
              this.card = new Card({
                  number: number
              });
          },
          pinNumber: function(pin) {
              var card = this.card;
      
              card.pin = pin;
              this.transactions = new can.DefineList();
              this.accountsPromise = card.verify().then(function(card) {
      
                  return Account.getList(card.serialize());
              });
          },
          exit: function(){
              this.set({
                  card: null,
                  accountsPromise: null,
                  transactions: null
              });
          }
      });
      
      can.Component.extend({
          tag: "atm-machine",
          view: can.stache.from("atm-template"),
          ViewModel: ATM,
      });
      
      document.body.insertBefore(
          can.stache.from("app-template")({}),
          document.body.firstChild
      );
      
      // ========================================
      // TESTS
      // ========================================
      
      QUnit.module("ATM system", {
          setup: function() {
              can.fixture.delay = 1;
          },
          teardown: function() {
              can.fixture.delay = 2000;
          }
      });
      
      QUnit.asyncTest("Valid Card", function() {
      
          var c = new Card({
              number: "01234567890",
              pin: 1234
          });
      
          QUnit.equal(c.state, "unverified");
      
          c.verify();
      
          QUnit.equal(c.state, "verifying", "card is verifying");
      
          c.on("state", function(ev, newVal) {
      
              QUnit.equal(newVal, "verified", "card is verified");
      
              QUnit.start();
          });
      });
      
      QUnit.asyncTest("Invalid Card", function() {
      
          var c = new Card({});
      
          QUnit.equal(c.state, "unverified");
      
          c.verify();
      
          QUnit.equal(c.state, "verifying", "card is verifying");
      
          c.on("state", function(ev, newVal) {
      
              QUnit.equal(newVal, "invalid", "card is invalid");
      
              QUnit.start();
          });
      });
      
      QUnit.asyncTest("Deposit", 6, function() {
      
          var card = new Card({
              number: "0123456789",
              pin: "1122"
          });
      
          var deposit = new Deposit({
              amount: 100,
              card: card
          });
      
          equal(deposit.state, "invalid");
      
          var startingBalance;
      
          Account.getList(card.serialize()).then(function(accounts) {
              QUnit.ok(true, "got accounts");
              deposit.account = accounts[0];
              startingBalance = accounts[0].balance;
          });
      
          deposit.on("state", function(ev, newVal) {
              if (newVal === "ready") {
      
                  QUnit.ok(true, "deposit is ready");
                  deposit.execute();
      
              } else if (newVal === "executing") {
      
                  QUnit.ok(true, "executing a deposit");
      
              } else if (newVal === "executed") {
      
                  QUnit.ok(true, "executed a deposit");
                  equal(deposit.account.balance, 100 + startingBalance);
                  start();
      
              }
          });
      });
      
      QUnit.asyncTest("ATM basics", function() {
      
          var atm = new ATM();
      
          equal(atm.state, "readingCard", "starts at reading card state");
      
          atm.cardNumber("01233456789");
      
          equal(atm.state, "readingPin", "moves to reading card state");
      
          atm.pinNumber("1234");
      
          ok(atm.state, "readingPin", "remain in the reading pin state until verifyied");
      
          atm.on("state", function(ev, newVal) {
      
              if (newVal === "choosingTransaction") {
                  QUnit.ok(true, "in choosingTransaction");
                  QUnit.start();
              }
          });
      });
      
      

      When complete, you should be able to enter a card and pin number and see the Choosing Transaction page.

      Choosing Transaction page and test

      In this section, we will:

      • Allow the user to pick a transaction type and go to the Picking Account page.
      • Add tests to ATM Basics test.

      Update the HTML tab to:

      • Have buttons for choosing a deposit, withdrawal, or print a receipt and exit.
      <!DOCTYPE html>
      <html>
      <head>
      <meta name="description" content="CanJS 3.0 - ATM Guide - Setup">
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width">
        <title>JS Bin</title>
      
      </head>
      <body>
      
      <script type='text/stache' id='atm-template'>
      <div class="screen">
          <div class="screen-content">
              <div class="screen-glass">
      
      {{#switch state}}
          {{#case "readingCard"}}
              <h2>Reading Card</h2>
              <p>Welcome to canATM where there are <strong>never</strong>
                fees!</p>
              </p>
              <p>
                  Enter Card Number:
                  <input name="card" ($enter)="cardNumber(%element.value)"/>
              </p>
          {{/case}}
      
          {{#case "readingPin"}}
              <h2>Reading Pin</h2>
              <p>
                  Enter Pin Number:
                  <input name="pin" type="password"
                      autofocus
                      {{#is card.state "verifying"}}DISABLED{{/is}}
                      ($enter)="pinNumber(%element.value)"/>
      
                  {{#is card.state "verifying"}}
                      <div class='warn'>
                          <p>
                          <img src="http://v3.canjs.com/docs/images/loader.gif"/>
                          verifying
                          </p>
                      </div>
                  {{/is}}
              </p>
              <a href="javascript://" ($click)="exit()">exit</a>
          {{/case}}
      
          {{#case "choosingTransaction"}}
              <h2>Choose Transaction</h2>
              <p>What would you like to do?</p>
              <nav>
                  <ul>
                      <li ($click)="chooseDeposit()">Deposit</li>
                      <li ($click)="chooseWithdraw()">Withdraw</li>
                      <li ($click)="printReceiptAndExit()">Exit</li>
                  </ul>
              </nav>
          {{/case}}
      
          {{#case  "pickingAccount"}}
              <h2>Pick Account</h2>
          {{/case}}
      
          {{#case "depositInfo"}}
              <h2>Deposit</h2>
          {{/case}}
      
          {{#case "withdrawalInfo"}}
              <h2>Withdraw</h2>
          {{/case}}
      
          {{#case "successfulTransaction"}}
              <h2>Transaction Successful!</h2>
          {{/case}}
      
          {{#case  "printingReceipt"}}
              <h2>Printing Receipt</h2>
          {{/case}}
      
          {{#default}}
              <h2>Error</h2>
              <p>Invalid state - {{state}}</p>
          {{/default}}
      
      {{/switch}}
      
              </div>
          </div>
      </div>
      </script>
      
      <script type='text/stache' id='app-template'>
      <div class="title">
          <h1>canATM</h1>
      </div>
      <atm-machine/>
      </script>
      
      <script src="https://code.jquery.com/jquery-2.2.4.js"></script>
      <script src="https://unpkg.com/can/dist/global/can.all.js"></script>
      
      <div id="qunit"></div>
      <link rel="stylesheet" href="http://code.jquery.com/qunit/qunit-1.12.0.css">
      <script src="http://code.jquery.com/qunit/qunit-1.12.0.js"></script>
      
      </body>
      </html>
      
      

      Update the ATM view model in the CODE section of the JavaScript tab to:

      • Define a currentTransaction property that when set, adds the previous currentTransaction to the list of transactions.
      • Update the state property to "pickingAccount" when there is a currentTransaction.
      • Update the exit method to clear the currentTransaction property.
      • Define chooseDeposit that creates a Deposit and sets it as the currentTransaction.
      • Define chooseWithdraw that creates a Withdraw and sets it as the currentTransaction.

      Update the TESTS section of the JavaScript tab to:

      • Call .chooseDeposit() and verify the state moves to "pickingAccount".
      // ========================================
      // CODE
      // ========================================
      
      can.fixture({
          "/verifyCard": function(request, response) {
              if (!request.data || !request.data.number || !request.data.pin) {
                  response(400, {});
              } else {
                  return {};
              }
          },
          "/accounts": function() {
              return {
                  data: [{
                      balance: 100,
                      id: 1,
                      name: "checking"
                  }, {
                      balance: 10000,
                      id: 2,
                      name: "savings"
                  }]
              };
          },
          "/deposit": function() {
              return {};
          },
          "/withdrawal": function() {
              return {};
          }
      });
      can.fixture.delay = 1000;
      
      var Card = can.DefineMap.extend({
          number: "string",
          pin: "string",
          state: {
              value: "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,
          executing: {
              type: "boolean",
              value: false
          },
          executed: {
              type: "boolean",
              value: false
          },
          rejected: "any",
          get ready(){
              throw new Error("Transaction::ready must be provided by extended type");
          },
          get state() {
              if (this.rejected) {
                  return "rejected";
              }
              if (this.executed) {
                  return "executed";
              }
              if (this.executing) {
                  return "executing";
              }
              // make sure there's an amount, account, and card
              if (this.ready) {
                  return "ready";
              }
              return "invalid";
          },
          executeStart: function(){
              throw new Error("Transaction::executeStart must be provided by extended type");
          },
          executeEnd: function(){
              throw new Error("Transaction::executeEnd must be provided by extended type");
          },
          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();
                  }, function(reason){
                      self.set({
                          executing: false,
                          executed: true,
                          rejected: reason
                      });
                  });
              }
          }
      });
      
      var Deposit = Transaction.extend({
          amount: "number",
          get ready() {
              return this.amount > 0 &&
                  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",
          get ready() {
              return this.amount > 0 &&
                  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: "any",
          transactions: can.DefineList,
          currentTransaction: {
              set: function(newTransaction) {
                  var currentTransaction = this.currentTransaction;
                  if (this.transactions && currentTransaction &&
                      currentTransaction.state === "executed") {
      
                      this.transactions.push(currentTransaction);
                  }
                  return newTransaction;
              }
          },
      
          // derived properties
          get state(){
      
              if (this.currentTransaction) {
                  return "pickingAccount";
              }
      
              if(this.card) {
                  if (this.card.state === "verified") {
                      return "choosingTransaction";
                  }
                  return "readingPin";
              }
              return "readingCard";
          },
      
          // methods
          cardNumber: function(number) {
              this.card = new Card({
                  number: number
              });
          },
          pinNumber: function(pin) {
              var card = this.card;
      
              card.pin = pin;
              this.transactions = new can.DefineList();
              this.accountsPromise = card.verify().then(function(card) {
      
                  return Account.getList(card.serialize());
              });
          },
          exit: function(){
              this.set({
                  card: null,
                  accountsPromise: null,
                  transactions: null,
                  currentTransaction: null
              });
          },
          chooseDeposit: function() {
              this.currentTransaction = new Deposit({
                  card: this.card
              });
          },
          chooseWithdraw: function() {
              this.currentTransaction = new Withdrawal({
                  card: this.card
              });
          }
      });
      
      can.Component.extend({
          tag: "atm-machine",
          view: can.stache.from("atm-template"),
          ViewModel: ATM,
      });
      
      document.body.insertBefore(
          can.stache.from("app-template")({}),
          document.body.firstChild
      );
      
      // ========================================
      // TESTS
      // ========================================
      
      QUnit.module("ATM system", {
          setup: function() {
              can.fixture.delay = 1;
          },
          teardown: function() {
              can.fixture.delay = 2000;
          }
      });
      
      QUnit.asyncTest("Valid Card", function() {
      
          var c = new Card({
              number: "01234567890",
              pin: 1234
          });
      
          QUnit.equal(c.state, "unverified");
      
          c.verify();
      
          QUnit.equal(c.state, "verifying", "card is verifying");
      
          c.on("state", function(ev, newVal) {
      
              QUnit.equal(newVal, "verified", "card is verified");
      
              QUnit.start();
          });
      });
      
      QUnit.asyncTest("Invalid Card", function() {
      
          var c = new Card({});
      
          QUnit.equal(c.state, "unverified");
      
          c.verify();
      
          QUnit.equal(c.state, "verifying", "card is verifying");
      
          c.on("state", function(ev, newVal) {
      
              QUnit.equal(newVal, "invalid", "card is invalid");
      
              QUnit.start();
          });
      });
      
      QUnit.asyncTest("Deposit", 6, function() {
      
          var card = new Card({
              number: "0123456789",
              pin: "1122"
          });
      
          var deposit = new Deposit({
              amount: 100,
              card: card
          });
      
          equal(deposit.state, "invalid");
      
          var startingBalance;
      
          Account.getList(card.serialize()).then(function(accounts) {
              QUnit.ok(true, "got accounts");
              deposit.account = accounts[0];
              startingBalance = accounts[0].balance;
          });
      
          deposit.on("state", function(ev, newVal) {
              if (newVal === "ready") {
      
                  QUnit.ok(true, "deposit is ready");
                  deposit.execute();
      
              } else if (newVal === "executing") {
      
                  QUnit.ok(true, "executing a deposit");
      
              } else if (newVal === "executed") {
      
                  QUnit.ok(true, "executed a deposit");
                  equal(deposit.account.balance, 100 + startingBalance);
                  start();
      
              }
          });
      });
      
      QUnit.asyncTest("ATM basics", function() {
      
          var atm = new ATM();
      
          equal(atm.state, "readingCard", "starts at reading card state");
      
          atm.cardNumber("01233456789");
      
          equal(atm.state, "readingPin", "moves to reading card state");
      
          atm.pinNumber("1234");
      
          ok(atm.state, "readingPin", "remain in the reading pin state until verifyied");
      
          atm.on("state", function(ev, newVal) {
      
              if (newVal === "choosingTransaction") {
      
                  QUnit.ok(true, "in choosingTransaction");
                  atm.chooseDeposit();
      
              } else if (newVal === "pickingAccount") {
      
                  QUnit.ok(true, "in picking account state");
                  QUnit.start();
      
              }
          });
      });
      
      

      We will define printReceiptAndExit later!

      Picking Account page and test

      In this section, we will:

      • Allow the user to pick an account and go to either the Deposit Info or Withdrawal Info page.
      • Add tests to ATM Basics test.

      Update the HTML tab to:

      • Write out a "Loading Accounts ..." message while the accounts are loading.
      • Write out the accounts when loaded.
      • Call chooseAccount() when an account is clicked.
      <!DOCTYPE html>
      <html>
      <head>
      <meta name="description" content="CanJS 3.0 - ATM Guide - Setup">
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width">
        <title>JS Bin</title>
      
      </head>
      <body>
      
      <script type='text/stache' id='atm-template'>
      <div class="screen">
          <div class="screen-content">
              <div class="screen-glass">
      
      {{#switch state}}
          {{#case "readingCard"}}
              <h2>Reading Card</h2>
              <p>Welcome to canATM where there are <strong>never</strong>
                fees!</p>
              </p>
              <p>
                  Enter Card Number:
                  <input name="card" ($enter)="cardNumber(%element.value)"/>
              </p>
          {{/case}}
      
          {{#case "readingPin"}}
              <h2>Reading Pin</h2>
              <p>
                  Enter Pin Number:
                  <input name="pin" type="password"
                      autofocus
                      {{#is card.state "verifying"}}DISABLED{{/is}}
                      ($enter)="pinNumber(%element.value)"/>
      
                  {{#is card.state "verifying"}}
                      <div class='warn'>
                          <p>
                          <img src="http://v3.canjs.com/docs/images/loader.gif"/>
                          verifying
                          </p>
                      </div>
                  {{/is}}
              </p>
              <a href="javascript://" ($click)="exit()">exit</a>
          {{/case}}
      
          {{#case "choosingTransaction"}}
              <h2>Choose Transaction</h2>
              <p>What would you like to do?</p>
              <nav>
                  <ul>
                      <li ($click)="chooseDeposit()">Deposit</li>
                      <li ($click)="chooseWithdraw()">Withdraw</li>
                      <li ($click)="printReceiptAndExit()">Exit</li>
                  </ul>
              </nav>
          {{/case}}
      
          {{#case  "pickingAccount"}}
              <h2>Pick Account</h2>
              <p>Please pick your account:</p>
              {{#if accountsPromise.isPending}}
                  <div class='warn'>
                      <p>
                          <img src="http://v3.canjs.com/docs/images/loader.gif"/>
                          Loading Accounts ...
                      </p>
                  </div>
              {{else}}
                  <ul>
                      {{#each accountsPromise.value}}
                          <li ($click)="chooseAccount(.)">{{name}} - ${{balance}}</li>
                      {{/each}}
                  </ul>
              {{/if}}
          {{/case}}
      
          {{#case "depositInfo"}}
              <h2>Deposit</h2>
          {{/case}}
      
          {{#case "withdrawalInfo"}}
              <h2>Withdraw</h2>
          {{/case}}
      
          {{#case "successfulTransaction"}}
              <h2>Transaction Successful!</h2>
          {{/case}}
      
          {{#case  "printingReceipt"}}
              <h2>Printing Receipt</h2>
          {{/case}}
      
          {{#default}}
              <h2>Error</h2>
              <p>Invalid state - {{state}}</p>
          {{/default}}
      
      {{/switch}}
      
              </div>
          </div>
      </div>
      </script>
      
      <script type='text/stache' id='app-template'>
      <div class="title">
          <h1>canATM</h1>
      </div>
      <atm-machine/>
      </script>
      
      <script src="https://code.jquery.com/jquery-2.2.4.js"></script>
      <script src="https://unpkg.com/can/dist/global/can.all.js"></script>
      
      <div id="qunit"></div>
      <link rel="stylesheet" href="http://code.jquery.com/qunit/qunit-1.12.0.css">
      <script src="http://code.jquery.com/qunit/qunit-1.12.0.js"></script>
      
      </body>
      </html>
      
      

      Update the ATM view model in the CODE section of the JavaScript tab to:

      • Change state to check if the currentTransaction has an account and update the value to "depositInfo" or "withdrawalInfo" depending on the type of the currentTransaction.
      • Add a chooseAccount method that sets the currentTransaction's account.

      Update the TESTS section of the JavaScript tab to:

      • Call .chooseAccount() with the first account loaded.
      • Verify the state changes to "depositInfo".
      // ========================================
      // CODE
      // ========================================
      
      can.fixture({
          "/verifyCard": function(request, response) {
              if (!request.data || !request.data.number || !request.data.pin) {
                  response(400, {});
              } else {
                  return {};
              }
          },
          "/accounts": function() {
              return {
                  data: [{
                      balance: 100,
                      id: 1,
                      name: "checking"
                  }, {
                      balance: 10000,
                      id: 2,
                      name: "savings"
                  }]
              };
          },
          "/deposit": function() {
              return {};
          },
          "/withdrawal": function() {
              return {};
          }
      });
      can.fixture.delay = 1000;
      
      var Card = can.DefineMap.extend({
          number: "string",
          pin: "string",
          state: {
              value: "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,
          executing: {
              type: "boolean",
              value: false
          },
          executed: {
              type: "boolean",
              value: false
          },
          rejected: "any",
          get ready(){
              throw new Error("Transaction::ready must be provided by extended type");
          },
          get state() {
              if (this.rejected) {
                  return "rejected";
              }
              if (this.executed) {
                  return "executed";
              }
              if (this.executing) {
                  return "executing";
              }
              // make sure there's an amount, account, and card
              if (this.ready) {
                  return "ready";
              }
              return "invalid";
          },
          executeStart: function(){
              throw new Error("Transaction::executeStart must be provided by extended type");
          },
          executeEnd: function(){
              throw new Error("Transaction::executeEnd must be provided by extended type");
          },
          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();
                  }, function(reason){
                      self.set({
                          executing: false,
                          executed: true,
                          rejected: reason
                      });
                  });
              }
          }
      });
      
      var Deposit = Transaction.extend({
          amount: "number",
          get ready() {
              return this.amount > 0 &&
                  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",
          get ready() {
              return this.amount > 0 &&
                  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: "any",
          transactions: can.DefineList,
          currentTransaction: {
              set: function(newTransaction) {
                  var currentTransaction = this.currentTransaction;
                  if (this.transactions && currentTransaction &&
                      currentTransaction.state === "executed") {
      
                      this.transactions.push(currentTransaction);
                  }
                  return newTransaction;
              }
          },
      
          // derived properties
          get state(){
      
              if (this.currentTransaction) {
                  if (this.currentTransaction.account) {
                      if (this.currentTransaction instanceof Deposit) {
                          return "depositInfo";
                      } else {
                          return "withdrawalInfo";
                      }
                  }
      
                  return "pickingAccount";
              }
      
              if(this.card) {
                  if (this.card.state === "verified") {
                      return "choosingTransaction";
                  }
                  return "readingPin";
              }
              return "readingCard";
          },
      
          // methods
          cardNumber: function(number) {
              this.card = new Card({
                  number: number
              });
          },
          pinNumber: function(pin) {
              var card = this.card;
      
              card.pin = pin;
              this.transactions = new can.DefineList();
              this.accountsPromise = card.verify().then(function(card) {
      
                  return Account.getList(card.serialize());
              });
          },
          exit: function(){
              this.set({
                  card: null,
                  accountsPromise: null,
                  transactions: null,
                  currentTransaction: null
              });
          },
          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;
          }
      });
      
      can.Component.extend({
          tag: "atm-machine",
          view: can.stache.from("atm-template"),
          ViewModel: ATM,
      });
      
      document.body.insertBefore(
          can.stache.from("app-template")({}),
          document.body.firstChild
      );
      
      // ========================================
      // TESTS
      // ========================================
      
      QUnit.module("ATM system", {
          setup: function() {
              can.fixture.delay = 1;
          },
          teardown: function() {
              can.fixture.delay = 2000;
          }
      });
      
      QUnit.asyncTest("Valid Card", function() {
      
          var c = new Card({
              number: "01234567890",
              pin: 1234
          });
      
          QUnit.equal(c.state, "unverified");
      
          c.verify();
      
          QUnit.equal(c.state, "verifying", "card is verifying");
      
          c.on("state", function(ev, newVal) {
      
              QUnit.equal(newVal, "verified", "card is verified");
      
              QUnit.start();
          });
      });
      
      QUnit.asyncTest("Invalid Card", function() {
      
          var c = new Card({});
      
          QUnit.equal(c.state, "unverified");
      
          c.verify();
      
          QUnit.equal(c.state, "verifying", "card is verifying");
      
          c.on("state", function(ev, newVal) {
      
              QUnit.equal(newVal, "invalid", "card is invalid");
      
              QUnit.start();
          });
      });
      
      QUnit.asyncTest("Deposit", 6, function() {
      
          var card = new Card({
              number: "0123456789",
              pin: "1122"
          });
      
          var deposit = new Deposit({
              amount: 100,
              card: card
          });
      
          equal(deposit.state, "invalid");
      
          var startingBalance;
      
          Account.getList(card.serialize()).then(function(accounts) {
              QUnit.ok(true, "got accounts");
              deposit.account = accounts[0];
              startingBalance = accounts[0].balance;
          });
      
          deposit.on("state", function(ev, newVal) {
              if (newVal === "ready") {
      
                  QUnit.ok(true, "deposit is ready");
                  deposit.execute();
      
              } else if (newVal === "executing") {
      
                  QUnit.ok(true, "executing a deposit");
      
              } else if (newVal === "executed") {
      
                  QUnit.ok(true, "executed a deposit");
                  equal(deposit.account.balance, 100 + startingBalance);
                  start();
      
              }
          });
      });
      
      QUnit.asyncTest("ATM basics", function() {
      
          var atm = new ATM();
      
          equal(atm.state, "readingCard", "starts at reading card state");
      
          atm.cardNumber("01233456789");
      
          equal(atm.state, "readingPin", "moves to reading card state");
      
          atm.pinNumber("1234");
      
          ok(atm.state, "readingPin", "remain in the reading pin state until verifyied");
      
          atm.on("state", function(ev, newVal) {
      
              if (newVal === "choosingTransaction") {
      
                  QUnit.ok(true, "in choosingTransaction");
                  atm.chooseDeposit();
      
              } else if (newVal === "pickingAccount") {
      
                  QUnit.ok(true, "in picking account state");
                  atm.accountsPromise.then(function(accounts){
                      atm.chooseAccount(accounts[0]);
                  });
      
              } else if (newVal === "depositInfo") {
      
                  QUnit.ok(true, "in depositInfo state");
                  QUnit.start();
      
              }
          });
      });
      
      

      Deposit Info page and test

      In this section, we will:

      • Allow the user to enter the amount of a deposit and go to the Successful Transaction page.
      • Add tests to ATM Basics test.

      Update the HTML tab to:

      • Ask the user how much they would like to deposit into the account.
      • Update currentTransaction.amount with an <input>'s value.
      • If the transaction is executing, show a spinner.
      • If the transaction is not executed:
        • show a Deposit button that will be active only once the transaction has a value.
        • show a cancel button that will clear this transaction.
      <!DOCTYPE html>
      <html>
      <head>
      <meta name="description" content="CanJS 3.0 - ATM Guide - Setup">
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width">
        <title>JS Bin</title>
      
      </head>
      <body>
      
      <script type='text/stache' id='atm-template'>
      <div class="screen">
          <div class="screen-content">
              <div class="screen-glass">
      
      {{#switch state}}
          {{#case "readingCard"}}
              <h2>Reading Card</h2>
              <p>Welcome to canATM where there are <strong>never</strong>
                fees!</p>
              </p>
              <p>
                  Enter Card Number:
                  <input name="card" ($enter)="cardNumber(%element.value)"/>
              </p>
          {{/case}}
      
          {{#case "readingPin"}}
              <h2>Reading Pin</h2>
              <p>
                  Enter Pin Number:
                  <input name="pin" type="password"
                      autofocus
                      {{#is card.state "verifying"}}DISABLED{{/is}}
                      ($enter)="pinNumber(%element.value)"/>
      
                  {{#is card.state "verifying"}}
                      <div class='warn'>
                          <p>
                          <img src="http://v3.canjs.com/docs/images/loader.gif"/>
                          verifying
                          </p>
                      </div>
                  {{/is}}
              </p>
              <a href="javascript://" ($click)="exit()">exit</a>
          {{/case}}
      
          {{#case "choosingTransaction"}}
              <h2>Choose Transaction</h2>
              <p>What would you like to do?</p>
              <nav>
                  <ul>
                      <li ($click)="chooseDeposit()">Deposit</li>
                      <li ($click)="chooseWithdraw()">Withdraw</li>
                      <li ($click)="printReceiptAndExit()">Exit</li>
                  </ul>
              </nav>
          {{/case}}
      
          {{#case  "pickingAccount"}}
              <h2>Pick Account</h2>
              <p>Please pick your account:</p>
              {{#if accountsPromise.isPending}}
                  <div class='warn'>
                      <p>
                          <img src="http://v3.canjs.com/docs/images/loader.gif"/>
                          Loading Accounts ...
                      </p>
                  </div>
              {{else}}
                  <ul>
                      {{#each accountsPromise.value}}
                          <li ($click)="chooseAccount(.)">{{name}} - ${{balance}}</li>
                      {{/each}}
                  </ul>
              {{/if}}
          {{/case}}
      
          {{#case "depositInfo"}}
              <h2>Deposit</h2>
              <p>
                  How much would you like to deposit
                  into {{currentTransaction.account.name}}
                  (${{currentTransaction.account.balance}})?
      
                  <input name="deposit" {($value)}="currentTransaction.amount"/>
              </p>
      
              {{#eq currentTransaction.state "executing"}}
                  <div class='warn'>
                      <p>
                      <img src="http://v3.canjs.com/docs/images/loader.gif"/>
                      executing
                      </p>
                  </div>
              {{else}}
                  <button ($click)="currentTransaction.execute()"
                      {{^eq currentTransaction.state "ready"}}DISABLED{{/eq}}>
                      Deposit
                  </button>
                  <a href="javascript://" ($click)="removeTransaction()">cancel</a>
              {{/eq}}
          {{/case}}
      
          {{#case "withdrawalInfo"}}
              <h2>Withdraw</h2>
          {{/case}}
      
          {{#case "successfulTransaction"}}
              <h2>Transaction Successful!</h2>
          {{/case}}
      
          {{#case  "printingReceipt"}}
              <h2>Printing Receipt</h2>
          {{/case}}
      
          {{#default}}
              <h2>Error</h2>
              <p>Invalid state - {{state}}</p>
          {{/default}}
      
      {{/switch}}
      
              </div>
          </div>
      </div>
      </script>
      
      <script type='text/stache' id='app-template'>
      <div class="title">
          <h1>canATM</h1>
      </div>
      <atm-machine/>
      </script>
      
      <script src="https://code.jquery.com/jquery-2.2.4.js"></script>
      <script src="https://unpkg.com/can/dist/global/can.all.js"></script>
      
      <div id="qunit"></div>
      <link rel="stylesheet" href="http://code.jquery.com/qunit/qunit-1.12.0.css">
      <script src="http://code.jquery.com/qunit/qunit-1.12.0.js"></script>
      
      </body>
      </html>
      
      

      Update the ATM view model in the JavaScript tab to:

      • Change state to "successfulTransaction" if the currentTransaction was executed.
      • Add a removeTransaction method that removes the currentTransaction, which will revert state to "choosingTransaction".

      Update the ATM basics test in the JavaScript tab to:

      • Add an amount to the currentTransaction.
      • Make sure the currentTransaction is ready to be executed.
      • Execute the currentTransaction and make sure that the state stays as "depositInfo" until the transaction is successful.
      • Verify the state changed to "successfulTransaction".
      // ========================================
      // CODE
      // ========================================
      
      can.fixture({
          "/verifyCard": function(request, response) {
              if (!request.data || !request.data.number || !request.data.pin) {
                  response(400, {});
              } else {
                  return {};
              }
          },
          "/accounts": function() {
              return {
                  data: [{
                      balance: 100,
                      id: 1,
                      name: "checking"
                  }, {
                      balance: 10000,
                      id: 2,
                      name: "savings"
                  }]
              };
          },
          "/deposit": function() {
              return {};
          },
          "/withdrawal": function() {
              return {};
          }
      });
      can.fixture.delay = 1000;
      
      var Card = can.DefineMap.extend({
          number: "string",
          pin: "string",
          state: {
              value: "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,
          executing: {
              type: "boolean",
              value: false
          },
          executed: {
              type: "boolean",
              value: false
          },
          rejected: "any",
          get ready(){
              throw new Error("Transaction::ready must be provided by extended type");
          },
          get state() {
              if (this.rejected) {
                  return "rejected";
              }
              if (this.executed) {
                  return "executed";
              }
              if (this.executing) {
                  return "executing";
              }
              // make sure there's an amount, account, and card
              if (this.ready) {
                  return "ready";
              }
              return "invalid";
          },
          executeStart: function(){
              throw new Error("Transaction::executeStart must be provided by extended type");
          },
          executeEnd: function(){
              throw new Error("Transaction::executeEnd must be provided by extended type");
          },
          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();
                  }, function(reason){
                      self.set({
                          executing: false,
                          executed: true,
                          rejected: reason
                      });
                  });
              }
          }
      });
      
      var Deposit = Transaction.extend({
          amount: "number",
          get ready() {
              return this.amount > 0 &&
                  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",
          get ready() {
              return this.amount > 0 &&
                  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: "any",
          transactions: can.DefineList,
          currentTransaction: {
              set: function(newTransaction) {
                  var currentTransaction = this.currentTransaction;
                  if (this.transactions && currentTransaction &&
                      currentTransaction.state === "executed") {
      
                      this.transactions.push(currentTransaction);
                  }
                  return newTransaction;
              }
          },
      
          // derived properties
          get state(){
      
              if (this.currentTransaction) {
                  if (this.currentTransaction.state === "executed") {
                      return "successfulTransaction";
                  }
      
                  if (this.currentTransaction.account) {
                      if (this.currentTransaction instanceof Deposit) {
                          return "depositInfo";
                      } else {
                          return "withdrawalInfo";
                      }
                  }
      
                  return "pickingAccount";
              }
      
              if(this.card) {
                  if (this.card.state === "verified") {
                      return "choosingTransaction";
                  }
                  return "readingPin";
              }
              return "readingCard";
          },
      
          // methods
          cardNumber: function(number) {
              this.card = new Card({
                  number: number
              });
          },
          pinNumber: function(pin) {
              var card = this.card;
      
              card.pin = pin;
              this.transactions = new can.DefineList();
              this.accountsPromise = card.verify().then(function(card) {
      
                  return Account.getList(card.serialize());
              });
          },
          exit: function(){
              this.set({
                  card: null,
                  accountsPromise: null,
                  transactions: null,
                  currentTransaction: null
              });
          },
          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;
          }
      });
      
      can.Component.extend({
          tag: "atm-machine",
          view: can.stache.from("atm-template"),
          ViewModel: ATM,
      });
      
      document.body.insertBefore(
          can.stache.from("app-template")({}),
          document.body.firstChild
      );
      
      // ========================================
      // TESTS
      // ========================================
      
      QUnit.module("ATM system", {
          setup: function() {
              can.fixture.delay = 1;
          },
          teardown: function() {
              can.fixture.delay = 2000;
          }
      });
      
      QUnit.asyncTest("Valid Card", function() {
      
          var c = new Card({
              number: "01234567890",
              pin: 1234
          });
      
          QUnit.equal(c.state, "unverified");
      
          c.verify();
      
          QUnit.equal(c.state, "verifying", "card is verifying");
      
          c.on("state", function(ev, newVal) {
      
              QUnit.equal(newVal, "verified", "card is verified");
      
              QUnit.start();
          });
      });
      
      QUnit.asyncTest("Invalid Card", function() {
      
          var c = new Card({});
      
          QUnit.equal(c.state, "unverified");
      
          c.verify();
      
          QUnit.equal(c.state, "verifying", "card is verifying");
      
          c.on("state", function(ev, newVal) {
      
              QUnit.equal(newVal, "invalid", "card is invalid");
      
              QUnit.start();
          });
      });
      
      QUnit.asyncTest("Deposit", 6, function() {
      
          var card = new Card({
              number: "0123456789",
              pin: "1122"
          });
      
          var deposit = new Deposit({
              amount: 100,
              card: card
          });
      
          equal(deposit.state, "invalid");
      
          var startingBalance;
      
          Account.getList(card.serialize()).then(function(accounts) {
              QUnit.ok(true, "got accounts");
              deposit.account = accounts[0];
              startingBalance = accounts[0].balance;
          });
      
          deposit.on("state", function(ev, newVal) {
              if (newVal === "ready") {
      
                  QUnit.ok(true, "deposit is ready");
                  deposit.execute();
      
              } else if (newVal === "executing") {
      
                  QUnit.ok(true, "executing a deposit");
      
              } else if (newVal === "executed") {
      
                  QUnit.ok(true, "executed a deposit");
                  equal(deposit.account.balance, 100 + startingBalance);
                  start();
      
              }
          });
      });
      
      QUnit.asyncTest("ATM basics", function() {
      
          var atm = new ATM();
      
          equal(atm.state, "readingCard", "starts at reading card state");
      
          atm.cardNumber("01233456789");
      
          equal(atm.state, "readingPin", "moves to reading card state");
      
          atm.pinNumber("1234");
      
          ok(atm.state, "readingPin", "remain in the reading pin state until verifyied");
      
          atm.on("state", function(ev, newVal) {
      
              if (newVal === "choosingTransaction") {
      
                  QUnit.ok(true, "in choosingTransaction");
                  atm.chooseDeposit();
      
              } else if (newVal === "pickingAccount") {
      
                  QUnit.ok(true, "in picking account state");
                  atm.accountsPromise.then(function(accounts){
                      atm.chooseAccount(accounts[0]);
                  });
      
              } else if (newVal === "depositInfo") {
      
                  QUnit.ok(true, "in depositInfo state");
                  var currentTransaction = atm.currentTransaction;
                  currentTransaction.amount = 120;
                  QUnit.ok(currentTransaction.ready, "we are ready to execute");
                  currentTransaction.execute();
                  QUnit.equal(atm.state, "depositInfo", "in deposit state until successful");
      
              } else if (newVal === "successfulTransaction") {
      
                  QUnit.ok(true, "in successfulTransaction state");
                  QUnit.start();
      
              }
          });
      });
      
      

      When complete, you should be able to enter a deposit amount and see that the transaction was successful.

      Withdrawal Info page

      In this section, we will:

      • Allow the user to enter the amount of a withdrawal and go to the Successful Transaction page.

      Update the HTML tab to:

      • Add a Withdraw page that works very similar to the Deposit page.
      <!DOCTYPE html>
      <html>
      <head>
      <meta name="description" content="CanJS 3.0 - ATM Guide - Setup">
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width">
        <title>JS Bin</title>
      
      </head>
      <body>
      
      <script type='text/stache' id='atm-template'>
      <div class="screen">
          <div class="screen-content">
              <div class="screen-glass">
      
      {{#switch state}}
          {{#case "readingCard"}}
              <h2>Reading Card</h2>
              <p>Welcome to canATM where there are <strong>never</strong>
                fees!</p>
              </p>
              <p>
                  Enter Card Number:
                  <input name="card" ($enter)="cardNumber(%element.value)"/>
              </p>
          {{/case}}
      
          {{#case "readingPin"}}
              <h2>Reading Pin</h2>
              <p>
                  Enter Pin Number:
                  <input name="pin" type="password"
                      autofocus
                      {{#is card.state "verifying"}}DISABLED{{/is}}
                      ($enter)="pinNumber(%element.value)"/>
      
                  {{#is card.state "verifying"}}
                      <div class='warn'>
                          <p>
                          <img src="http://v3.canjs.com/docs/images/loader.gif"/>
                          verifying
                          </p>
                      </div>
                  {{/is}}
              </p>
              <a href="javascript://" ($click)="exit()">exit</a>
          {{/case}}
      
          {{#case "choosingTransaction"}}
              <h2>Choose Transaction</h2>
              <p>What would you like to do?</p>
              <nav>
                  <ul>
                      <li ($click)="chooseDeposit()">Deposit</li>
                      <li ($click)="chooseWithdraw()">Withdraw</li>
                      <li ($click)="printReceiptAndExit()">Exit</li>
                  </ul>
              </nav>
          {{/case}}
      
          {{#case  "pickingAccount"}}
              <h2>Pick Account</h2>
              <p>Please pick your account:</p>
              {{#if accountsPromise.isPending}}
                  <div class='warn'>
                      <p>
                          <img src="http://v3.canjs.com/docs/images/loader.gif"/>
                          Loading Accounts ...
                      </p>
                  </div>
              {{else}}
                  <ul>
                      {{#each accountsPromise.value}}
                          <li ($click)="chooseAccount(.)">{{name}} - ${{balance}}</li>
                      {{/each}}
                  </ul>
              {{/if}}
          {{/case}}
      
          {{#case "depositInfo"}}
              <h2>Deposit</h2>
              <p>
                  How much would you like to deposit
                  into {{currentTransaction.account.name}}
                  (${{currentTransaction.account.balance}})?
      
                  <input name="deposit" {($value)}="currentTransaction.amount"/>
              </p>
      
              {{#eq currentTransaction.state "executing"}}
                  <div class='warn'>
                      <p>
                      <img src="http://v3.canjs.com/docs/images/loader.gif"/>
                      executing
                      </p>
                  </div>
              {{else}}
                  <button ($click)="currentTransaction.execute()"
                      {{^eq currentTransaction.state "ready"}}DISABLED{{/eq}}>
                      Deposit
                  </button>
                  <a href="javascript://" ($click)="removeTransaction()">cancel</a>
              {{/eq}}
          {{/case}}
      
          {{#case "withdrawalInfo"}}
              <h2>Withdraw</h2>
              <p>
                  How much would you like to withdraw
                  from {{currentTransaction.account.name}}
                  (${{currentTransaction.account.balance}})?
      
                  <input name="withdrawl" {($value)}="currentTransaction.amount"/>
              </p>
              {{#eq currentTransaction.state "executing"}}
                  <div class='warn'>
                      <p>
                      <img src="http://v3.canjs.com/docs/images/loader.gif"/>
                      executing
                      </p>
                  </div>
              {{else}}
                  <button ($click)="currentTransaction.execute()"
                      {{^eq currentTransaction.state "ready"}}DISABLED{{/eq}}>
                      Withdraw
                  </button>
                  <a href="javascript://" ($click)="removeTransaction()">cancel</a>
              {{/eq}}
          {{/case}}
      
          {{#case "successfulTransaction"}}
              <h2>Transaction Successful!</h2>
          {{/case}}
      
          {{#case  "printingReceipt"}}
              <h2>Printing Receipt</h2>
          {{/case}}
      
          {{#default}}
              <h2>Error</h2>
              <p>Invalid state - {{state}}</p>
          {{/default}}
      
      {{/switch}}
      
              </div>
          </div>
      </div>
      </script>
      
      <script type='text/stache' id='app-template'>
      <div class="title">
          <h1>canATM</h1>
      </div>
      <atm-machine/>
      </script>
      
      <script src="https://code.jquery.com/jquery-2.2.4.js"></script>
      <script src="https://unpkg.com/can/dist/global/can.all.js"></script>
      
      <div id="qunit"></div>
      <link rel="stylesheet" href="http://code.jquery.com/qunit/qunit-1.12.0.css">
      <script src="http://code.jquery.com/qunit/qunit-1.12.0.js"></script>
      
      </body>
      </html>
      
      

      When complete, you should be able to enter a withdrawal amount and see that the transaction was successful.

      Optional: Challenge yourself by adding a test for the withdrawalInfo state of an atm instance. Consider the progression of states needed to make it to the withdrawalInfo state. How is it different from the ATM basics test we already have?

      Transaction Successful page

      In this section, we will:

      • Show the result of the transaction.

      Update the HTML tab to:

      • List out the account balance.
      • Add buttons to:
        • start another transaction, or
        • print a receipt and exit the ATM. (printReceiptAndExit will be implemented in the next section)
      <!DOCTYPE html>
      <html>
      <head>
      <meta name="description" content="CanJS 3.0 - ATM Guide - Setup">
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width">
        <title>JS Bin</title>
      
      </head>
      <body>
      
      <script type='text/stache' id='atm-template'>
      <div class="screen">
          <div class="screen-content">
              <div class="screen-glass">
      
      {{#switch state}}
          {{#case "readingCard"}}
              <h2>Reading Card</h2>
              <p>Welcome to canATM where there are <strong>never</strong>
                fees!</p>
              </p>
              <p>
                  Enter Card Number:
                  <input name="card" ($enter)="cardNumber(%element.value)"/>
              </p>
          {{/case}}
      
          {{#case "readingPin"}}
              <h2>Reading Pin</h2>
              <p>
                  Enter Pin Number:
                  <input name="pin" type="password"
                      autofocus
                      {{#is card.state "verifying"}}DISABLED{{/is}}
                      ($enter)="pinNumber(%element.value)"/>
      
                  {{#is card.state "verifying"}}
                      <div class='warn'>
                          <p>
                          <img src="http://v3.canjs.com/docs/images/loader.gif"/>
                          verifying
                          </p>
                      </div>
                  {{/is}}
              </p>
              <a href="javascript://" ($click)="exit()">exit</a>
          {{/case}}
      
          {{#case "choosingTransaction"}}
              <h2>Choose Transaction</h2>
              <p>What would you like to do?</p>
              <nav>
                  <ul>
                      <li ($click)="chooseDeposit()">Deposit</li>
                      <li ($click)="chooseWithdraw()">Withdraw</li>
                      <li ($click)="printReceiptAndExit()">Exit</li>
                  </ul>
              </nav>
          {{/case}}
      
          {{#case  "pickingAccount"}}
              <h2>Pick Account</h2>
              <p>Please pick your account:</p>
              {{#if accountsPromise.isPending}}
                  <div class='warn'>
                      <p>
                          <img src="http://v3.canjs.com/docs/images/loader.gif"/>
                          Loading Accounts ...
                      </p>
                  </div>
              {{else}}
                  <ul>
                      {{#each accountsPromise.value}}
                          <li ($click)="chooseAccount(.)">{{name}} - ${{balance}}</li>
                      {{/each}}
                  </ul>
              {{/if}}
          {{/case}}
      
          {{#case "depositInfo"}}
              <h2>Deposit</h2>
              <p>
                  How much would you like to deposit
                  into {{currentTransaction.account.name}}
                  (${{currentTransaction.account.balance}})?
      
                  <input name="deposit" {($value)}="currentTransaction.amount"/>
              </p>
      
              {{#eq currentTransaction.state "executing"}}
                  <div class='warn'>
                      <p>
                      <img src="http://v3.canjs.com/docs/images/loader.gif"/>
                      executing
                      </p>
                  </div>
              {{else}}
                  <button ($click)="currentTransaction.execute()"
                      {{^eq currentTransaction.state "ready"}}DISABLED{{/eq}}>
                      Deposit
                  </button>
                  <a href="javascript://" ($click)="removeTransaction()">cancel</a>
              {{/eq}}
          {{/case}}
      
          {{#case "withdrawalInfo"}}
              <h2>Withdraw</h2>
              <p>
                  How much would you like to withdraw
                  from {{currentTransaction.account.name}}
                  (${{currentTransaction.account.balance}})?
      
                  <input name="withdrawl" {($value)}="currentTransaction.amount"/>
              </p>
              {{#eq currentTransaction.state "executing"}}
                  <div class='warn'>
                      <p>
                      <img src="http://v3.canjs.com/docs/images/loader.gif"/>
                      executing
                      </p>
                  </div>
              {{else}}
                  <button ($click)="currentTransaction.execute()"
                      {{^eq currentTransaction.state "ready"}}DISABLED{{/eq}}>
                      Withdraw
                  </button>
                  <a href="javascript://" ($click)="removeTransaction()">cancel</a>
              {{/eq}}
          {{/case}}
      
          {{#case "successfulTransaction"}}
              <h2>Transaction Successful!</h2>
              <p>
                  {{currentTransaction.account.name}} has
                  ${{currentTransaction.account.balance}}.
              </p>
              <p>What would you like to do?</p>
              <nav>
                  <ul>
                      <li ($click)="removeTransaction()">Another transaction</li>
                      <li ($click)="printReceiptAndExit()">Exit</li>
                  </ul>
              </nav>
          {{/case}}
      
          {{#case  "printingReceipt"}}
              <h2>Printing Receipt</h2>
          {{/case}}
      
          {{#default}}
              <h2>Error</h2>
              <p>Invalid state - {{state}}</p>
          {{/default}}
      
      {{/switch}}
      
              </div>
          </div>
      </div>
      </script>
      
      <script type='text/stache' id='app-template'>
      <div class="title">
          <h1>canATM</h1>
      </div>
      <atm-machine/>
      </script>
      
      <script src="https://code.jquery.com/jquery-2.2.4.js"></script>
      <script src="https://unpkg.com/can/dist/global/can.all.js"></script>
      
      <div id="qunit"></div>
      <link rel="stylesheet" href="http://code.jquery.com/qunit/qunit-1.12.0.css">
      <script src="http://code.jquery.com/qunit/qunit-1.12.0.js"></script>
      
      </body>
      </html>
      
      

      When complete, you should be able to make a deposit or withdrawal, see the updated account balance and then start another transaction.

      Printing Recipe page and test

      In this section, we will:

      • Make it possible to see a receipt of all transactions and exit the ATM.

      Update the HTML tab to:

      • List out all the transactions the user has completed.
      • List out the final value of all accounts.
      <!DOCTYPE html>
      <html>
      <head>
      <meta name="description" content="CanJS 3.0 - ATM Guide - Setup">
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width">
        <title>JS Bin</title>
      
      </head>
      <body>
      
      <script type='text/stache' id='atm-template'>
      <div class="screen">
          <div class="screen-content">
              <div class="screen-glass">
      
      {{#switch state}}
          {{#case "readingCard"}}
              <h2>Reading Card</h2>
              <p>Welcome to canATM where there are <strong>never</strong>
                fees!</p>
              </p>
              <p>
                  Enter Card Number:
                  <input name="card" ($enter)="cardNumber(%element.value)"/>
              </p>
          {{/case}}
      
          {{#case "readingPin"}}
              <h2>Reading Pin</h2>
              <p>
                  Enter Pin Number:
                  <input name="pin" type="password"
                      autofocus
                      {{#is card.state "verifying"}}DISABLED{{/is}}
                      ($enter)="pinNumber(%element.value)"/>
      
                  {{#is card.state "verifying"}}
                      <div class='warn'>
                          <p>
                          <img src="http://v3.canjs.com/docs/images/loader.gif"/>
                          verifying
                          </p>
                      </div>
                  {{/is}}
              </p>
              <a href="javascript://" ($click)="exit()">exit</a>
          {{/case}}
      
          {{#case "choosingTransaction"}}
              <h2>Choose Transaction</h2>
              <p>What would you like to do?</p>
              <nav>
                  <ul>
                      <li ($click)="chooseDeposit()">Deposit</li>
                      <li ($click)="chooseWithdraw()">Withdraw</li>
                      <li ($click)="printReceiptAndExit()">Exit</li>
                  </ul>
              </nav>
          {{/case}}
      
          {{#case  "pickingAccount"}}
              <h2>Pick Account</h2>
              <p>Please pick your account:</p>
              {{#if accountsPromise.isPending}}
                  <div class='warn'>
                      <p>
                          <img src="http://v3.canjs.com/docs/images/loader.gif"/>
                          Loading Accounts ...
                      </p>
                  </div>
              {{else}}
                  <ul>
                      {{#each accountsPromise.value}}
                          <li ($click)="chooseAccount(.)">{{name}} - ${{balance}}</li>
                      {{/each}}
                  </ul>
              {{/if}}
          {{/case}}
      
          {{#case "depositInfo"}}
              <h2>Deposit</h2>
              <p>
                  How much would you like to deposit
                  into {{currentTransaction.account.name}}
                  (${{currentTransaction.account.balance}})?
      
                  <input name="deposit" {($value)}="currentTransaction.amount"/>
              </p>
      
              {{#eq currentTransaction.state "executing"}}
                  <div class='warn'>
                      <p>
                      <img src="http://v3.canjs.com/docs/images/loader.gif"/>
                      executing
                      </p>
                  </div>
              {{else}}
                  <button ($click)="currentTransaction.execute()"
                      {{^eq currentTransaction.state "ready"}}DISABLED{{/eq}}>
                      Deposit
                  </button>
                  <a href="javascript://" ($click)="removeTransaction()">cancel</a>
              {{/eq}}
          {{/case}}
      
          {{#case "withdrawalInfo"}}
              <h2>Withdraw</h2>
              <p>
                  How much would you like to withdraw
                  from {{currentTransaction.account.name}}
                  (${{currentTransaction.account.balance}})?
      
                  <input name="withdrawl" {($value)}="currentTransaction.amount"/>
              </p>
              {{#eq currentTransaction.state "executing"}}
                  <div class='warn'>
                      <p>
                      <img src="http://v3.canjs.com/docs/images/loader.gif"/>
                      executing
                      </p>
                  </div>
              {{else}}
                  <button ($click)="currentTransaction.execute()"
                      {{^eq currentTransaction.state "ready"}}DISABLED{{/eq}}>
                      Withdraw
                  </button>
                  <a href="javascript://" ($click)="removeTransaction()">cancel</a>
              {{/eq}}
          {{/case}}
      
          {{#case "successfulTransaction"}}
              <h2>Transaction Successful!</h2>
              <p>
                  {{currentTransaction.account.name}} has
                  ${{currentTransaction.account.balance}}.
              </p>
              <p>What would you like to do?</p>
              <nav>
                  <ul>
                      <li ($click)="removeTransaction()">Another transaction</li>
                      <li ($click)="printReceiptAndExit()">Exit</li>
                  </ul>
              </nav>
          {{/case}}
      
          {{#case  "printingReceipt"}}
              <h2>Printing Receipt</h2>
              <h3>Transactions</h3>
              <ul>
                  {{#if transactions.length}}
                      {{#each transactions}}
                          <li>{{actionName(this)}} ${{amount}} {{actionPrep(this)}} {{account.name}}</li>
                      {{/each}}
                  {{else}}
                      <li>None</li>
                  {{/if}}
              </ul>
              <h3>Accounts</h3>
              <ul>
                  {{#each accountsPromise.value}}
                      <li ($click)="chooseAccount(.)">{{name}} - ${{balance}}</li>
                  {{/each}}
              </ul>
              <div class='warn'>
                  <p>
                      <img src="http://v3.canjs.com/docs/images/loader.gif"/>
                      printing
                  </p>
              </div>
          {{/case}}
      
          {{#default}}
              <h2>Error</h2>
              <p>Invalid state - {{state}}</p>
          {{/default}}
      
      {{/switch}}
      
              </div>
          </div>
      </div>
      </script>
      
      <script type='text/stache' id='app-template'>
      <div class="title">
          <h1>canATM</h1>
      </div>
      <atm-machine/>
      </script>
      
      <script src="https://code.jquery.com/jquery-2.2.4.js"></script>
      <script src="https://unpkg.com/can/dist/global/can.all.js"></script>
      
      <div id="qunit"></div>
      <link rel="stylesheet" href="http://code.jquery.com/qunit/qunit-1.12.0.css">
      <script src="http://code.jquery.com/qunit/qunit-1.12.0.js"></script>
      
      </body>
      </html>
      
      

      Update the ATM view model in the JavaScript tab to:

      • Add a printingReceipt and receiptTime property.
      • Change the state to "printingReceipt" when printingReceipt is true.
      • Make .exit set printingReceipt to null.
      • Add a printReceiptAndExit method that:
        • clears the current transaction, which will add the currentTransaction to the list of transactions.
        • sets printingReceipt to true for printingReceipt time.

      Update the ATM basics test in the JavaScript tab to:

      • Shorten the default receiptTime so the tests move quickly.
      • Call printReceiptAndExit and make sure that the state changes to "printingReceipt" and then to "readingCard" and ensure that sensitive information is cleared from the atm.
      // ========================================
      // CODE
      // ========================================
      
      can.fixture({
          "/verifyCard": function(request, response) {
              if (!request.data || !request.data.number || !request.data.pin) {
                  response(400, {});
              } else {
                  return {};
              }
          },
          "/accounts": function() {
              return {
                  data: [{
                      balance: 100,
                      id: 1,
                      name: "checking"
                  }, {
                      balance: 10000,
                      id: 2,
                      name: "savings"
                  }]
              };
          },
          "/deposit": function() {
              return {};
          },
          "/withdrawal": function() {
              return {};
          }
      });
      can.fixture.delay = 1000;
      
      var Card = can.DefineMap.extend({
          number: "string",
          pin: "string",
          state: {
              value: "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,
          executing: {
              type: "boolean",
              value: false
          },
          executed: {
              type: "boolean",
              value: false
          },
          rejected: "any",
          get ready(){
              throw new Error("Transaction::ready must be provided by extended type");
          },
          get state() {
              if (this.rejected) {
                  return "rejected";
              }
              if (this.executed) {
                  return "executed";
              }
              if (this.executing) {
                  return "executing";
              }
              // make sure there's an amount, account, and card
              if (this.ready) {
                  return "ready";
              }
              return "invalid";
          },
          executeStart: function(){
              throw new Error("Transaction::executeStart must be provided by extended type");
          },
          executeEnd: function(){
              throw new Error("Transaction::executeEnd must be provided by extended type");
          },
          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();
                  }, function(reason){
                      self.set({
                          executing: false,
                          executed: true,
                          rejected: reason
                      });
                  });
              }
          }
      });
      
      var Deposit = Transaction.extend({
          amount: "number",
          get ready() {
              return this.amount > 0 &&
                  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",
          get ready() {
              return this.amount > 0 &&
                  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: "any",
          transactions: can.DefineList,
          currentTransaction: {
              set: function(newTransaction) {
                  var currentTransaction = this.currentTransaction;
                  if (this.transactions && currentTransaction &&
                      currentTransaction.state === "executed") {
      
                      this.transactions.push(currentTransaction);
                  }
                  return newTransaction;
              }
          },
          printingReceipt: "boolean",
          receiptTime: {
              value: 5000,
              type: "number"
          },
      
          // derived properties
          get state(){
              if (this.printingReceipt) {
                  return "printingReceipt";
              }
              if (this.currentTransaction) {
                  if (this.currentTransaction.state === "executed") {
                      return "successfulTransaction";
                  }
      
                  if (this.currentTransaction.account) {
                      if (this.currentTransaction instanceof Deposit) {
                          return "depositInfo";
                      } else {
                          return "withdrawalInfo";
                      }
                  }
      
                  return "pickingAccount";
              }
      
              if(this.card) {
                  if (this.card.state === "verified") {
                      return "choosingTransaction";
                  }
                  return "readingPin";
              }
              return "readingCard";
          },
      
          // methods
          cardNumber: function(number) {
              this.card = new Card({
                  number: number
              });
          },
          pinNumber: function(pin) {
              var card = this.card;
      
              card.pin = pin;
              this.transactions = new can.DefineList();
              this.accountsPromise = card.verify().then(function(card) {
      
                  return Account.getList(card.serialize());
              });
          },
          exit: function(){
              this.set({
                  card: null,
                  accountsPromise: null,
                  transactions: null,
                  currentTransaction: null,
                  printingReceipt: null
              });
          },
          printReceiptAndExit: function() {
              this.currentTransaction = null;
              this.printingReceipt = true;
              var self = this;
              setTimeout(function() {
                  self.exit();
              }, this.receiptTime);
          },
          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;
          }
      });
      
      can.Component.extend({
          tag: "atm-machine",
          view: can.stache.from("atm-template"),
          ViewModel: ATM,
      });
      
      document.body.insertBefore(
          can.stache.from("app-template")({}),
          document.body.firstChild
      );
      
      // ========================================
      // TESTS
      // ========================================
      
      QUnit.module("ATM system", {
          setup: function() {
              can.fixture.delay = 1;
          },
          teardown: function() {
              can.fixture.delay = 2000;
          }
      });
      
      QUnit.asyncTest("Valid Card", function() {
      
          var c = new Card({
              number: "01234567890",
              pin: 1234
          });
      
          QUnit.equal(c.state, "unverified");
      
          c.verify();
      
          QUnit.equal(c.state, "verifying", "card is verifying");
      
          c.on("state", function(ev, newVal) {
      
              QUnit.equal(newVal, "verified", "card is verified");
      
              QUnit.start();
          });
      });
      
      QUnit.asyncTest("Invalid Card", function() {
      
          var c = new Card({});
      
          QUnit.equal(c.state, "unverified");
      
          c.verify();
      
          QUnit.equal(c.state, "verifying", "card is verifying");
      
          c.on("state", function(ev, newVal) {
      
              QUnit.equal(newVal, "invalid", "card is invalid");
      
              QUnit.start();
          });
      });
      
      QUnit.asyncTest("Deposit", 6, function() {
      
          var card = new Card({
              number: "0123456789",
              pin: "1122"
          });
      
          var deposit = new Deposit({
              amount: 100,
              card: card
          });
      
          equal(deposit.state, "invalid");
      
          var startingBalance;
      
          Account.getList(card.serialize()).then(function(accounts) {
              QUnit.ok(true, "got accounts");
              deposit.account = accounts[0];
              startingBalance = accounts[0].balance;
          });
      
          deposit.on("state", function(ev, newVal) {
              if (newVal === "ready") {
      
                  QUnit.ok(true, "deposit is ready");
                  deposit.execute();
      
              } else if (newVal === "executing") {
      
                  QUnit.ok(true, "executing a deposit");
      
              } else if (newVal === "executed") {
      
                  QUnit.ok(true, "executed a deposit");
                  equal(deposit.account.balance, 100 + startingBalance);
                  start();
      
              }
          });
      });
      
      QUnit.asyncTest("ATM basics", function() {
      
          var atm = new ATM();
      
          equal(atm.state, "readingCard", "starts at reading card state");
      
          atm.cardNumber("01233456789");
      
          equal(atm.state, "readingPin", "moves to reading card state");
      
          atm.pinNumber("1234");
      
          ok(atm.state, "readingPin", "remain in the reading pin state until verifyied");
      
          atm.on("state", function(ev, newVal) {
      
              if (newVal === "choosingTransaction") {
      
                  QUnit.ok(true, "in choosingTransaction");
                  atm.chooseDeposit();
      
              } else if (newVal === "pickingAccount") {
      
                  QUnit.ok(true, "in picking account state");
                  atm.accountsPromise.then(function(accounts){
                      atm.chooseAccount(accounts[0]);
                  });
      
              } else if (newVal === "depositInfo") {
      
                  QUnit.ok(true, "in depositInfo state");
                  var currentTransaction = atm.currentTransaction;
                  currentTransaction.amount = 120;
                  QUnit.ok(currentTransaction.ready, "we are ready to execute");
                  currentTransaction.execute();
                  QUnit.equal(atm.state, "depositInfo", "in deposit state until successful");
      
              } else if (newVal === "successfulTransaction") {
      
                  QUnit.ok(true, "in successfulTransaction state");
                  atm.receiptTime = 100;
                  atm.printReceiptAndExit();
      
              } else if (newVal === "printingReceipt") {
      
                  QUnit.ok(true, "in printingReceipt state");
      
              } else if (newVal === "readingCard") {
      
                  QUnit.ok(true, "in readingCard state");
                  QUnit.ok(!atm.card, "card is removed");
                  QUnit.ok(!atm.transactions, "transactions removed");
                  QUnit.start();
      
              }
          });
      });
      
      

      When complete, you have a working ATM! Cha-ching!

      CanJS is part of DoneJS. Created and maintained by the core DoneJS team and Bitovi. Currently 3.0.0.