A Cart-Based Sales Application

only for RuBoard - do not distribute or recompile

The doc_shop.pl application that was developed earlier in the chapter handled a shopping situation simple enough to allow us to avoid dealing with issues such as managing a shopping cart or creating a mechanism for presenting a large catalog in smaller portions. In this section, we ll deal with another shopping scenario that requires those capabilities. The application is called pet_shop.pl, because it provides access to a catalog of items that relate to keeping your pets happy, healthy, well-groomed, and so forth. When you first invoke pet_shop.pl, it presents a page showing a list of product categories and a set of links across the top that provide access to the various functions of the application (see Figure 10.2):

Figure 10.2. The pet_shop.pl main page.
graphics/10fig02.gif
  • Browse presents a category-based catalog browsing page (the same page that you see initially). Selecting a category name takes you to a page that lists the items in that category. Each item includes an Add Item link for adding the item to your order. The product catalog is too large to display in a single page; the browsing interface is one way the application allows you to view the catalog in sections.

  • Search takes you to a form you can use to look up items by keyword. This is another interface to the product catalog.

  • View Cart shows the current contents of your order. If you want to delete items, you can do so from this page.

  • Check Out begins the order-submission process by presenting a page that solicits a shipping address and billing information. The page also displays the cart so that you can delete items if you want.

  • Cancel terminates the shopping session. The order expires if you don t complete it within a reasonable amount of time, but this operation explicitly forces the order to be jettisoned immediately.

Catalog Presentation Issues

A general goal of online catalog presentation is to make it easy for customers to find product offerings so they can select and purchase them. If you offer only a few products, you can list them all on a single page (as we did for doc_shop.pl earlier in the chapter). That won t work for pet_shop.pl, because the catalog is larger and therefore not easily usable in single-page form. This has two important implications:

  • We need a mechanism for presenting the catalog in sections. One option is to present a browseable interface that divides the catalog into categories, subcategories, and so forth. Another is to provide a search interface so that users can specify directly what they re looking for. pet_shop.pl gives customers the choice of looking through the catalog either way.

  • Because the catalog is presented in sections, customers typically view products across multiple pages. That means we must implement a shopping cart so that items selected from different pages can be remembered.

A browsing interface can be implemented by treating the product catalog as a tree. At the top level, you present an index page containing links for general categories. These take the customer to more specific index pages, finally leading to leaf pages that list product descriptions. Two general principles govern the index navigation and item display functions of such an interface:

  • Don t make the user go through too many index page levels to get to leaf pages. The fewer steps it takes to reach an item description, the better.

  • Don t overwhelm the user by presenting too many items on a single leaf page. The fewer items there are, the easier a page is to understand.

Unfortunately, these principles conflict. If you subdivide your catalog into smaller sections to make leaf pages easier to understand, you ll often end up using more levels in the browse tree. Conversely, reducing the number of index levels in the tree generally is achieved at the cost of increasing the number of items per leaf page. With your own product catalogs, you ll likely need to do some experimenting to find a good balance between these opposing principles. For the catalog used by pet_shop.pl,we ll implement a simple two-level browse tree with a top-level index page that lists general product categories, and a leaf page for each category that lists items in that category.

Our other interface to the pet-care catalog uses a search mechanism. This is based on a form with a text field for entering keywords and radio buttons for selecting all words or any word matches. One problem here is that with a large enough catalog, it s possible for a search to match many, many items. What should we do in that case? Two options are to use a LIMIT clause to constrain the size of the result set, or to present the result over multiple pages. A multiple-page display is probably better because the customer can get at all the relevant information. In the interests of space and simplicity, pet_shop.pl uses the LIMIT clause method. However, to give the customer somewhat more control, the search form includes a field for controlling the maximum number of records to show. By default, this will be set to a small value (10), but the user can set it higher if desired. (For information on presenting multiple-page search results, see Chapter 7.)

Setting Up the Tables

pet_shop.pl uses several tables. The pet_catalog table holds the product catalog:

 CREATE TABLE pet_catalog  (     item_id     BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,      description VARCHAR(60) NOT NULL,   # item description      category    VARCHAR(40) NOT NULL,   # item category      price       NUMERIC(10,2) NOT NULL  # unit price  ) 

item_id is the unique item identifier, description says what the item is, category indicates the general category the item falls into, and price lists the item unit cost.

Two tables are used to store orders received from customers. pet_order records general order and customer information, and pet_item records individual items in each order:

 CREATE TABLE pet_order  (     order_id    BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,      order_date  DATE NOT NULL,          # date of order      order_amt   DECIMAL(10,2) NOT NULL, # total cost      auth_id     VARCHAR(10) NOT NULL,   # transaction authorization      cust_name   VARCHAR(40) NOT NULL,   # customer name      cust_email  VARCHAR(40) NOT NULL,   # customer email address      street      VARCHAR(40),            # shipping address      city        VARCHAR(40),      state       CHAR(2),      zip         VARCHAR(10)  ) TYPE = BDB  CREATE TABLE pet_item  (     order_id    BIGINT UNSIGNED NOT NULL,   # order number      item_id     BIGINT UNSIGNED NOT NULL,   # item number      qty         INT UNSIGNED NOT NULL,      # item quantity      price       NUMERIC(10,2) NOT NULL,     # unit price      UNIQUE (order_id, item_id)  ) TYPE = BDB 

The doc_shop.pl application also used two tables for recording orders, but the tables used by pet_shop.pl contain somewhat different information. For one thing, pet-care items can t be delivered electronically, so we need the customer s shipping address to tell where to send each order. Also, it s possible that customers might want to order items in quantities greater than one, so the pet_item table contains a qty column.

Just as with the order-recording tables used for doc_shop.pl, you should remove TYPE = BDB from the CREATETABLE statements if your version of MySQL does not include transaction support.

The final two tables used by pet_shop.pl are lookup tables. One, pet_category, contains all the possible categories into which items can be classified:

 CREATE TABLE pet_category  (     category    VARCHAR(40) NOT NULL  ) 

This table is the source for the category names presented in the browsing interface to the product catalog. (It relates to the catalog in that the category column value stored in each pet_catalog record should match some value in the pet_category table.) The other lookup table, us_state, contains state abbreviations and names. It will be used to generate a pop-up menu for the state field in the shipping address part of the billing form:

 CREATE TABLE us_state  (     abbrev  CHAR(2),        # state abbreviation      name    CHAR(30)        # state name  ) 

The webdb distribution contains data files you can use to populate the pet_catalog, pet_category, and us_state tables. To load these files into the database, move into the ecom directory of the distribution. Then, after you ve created the tables, issue the following commands from the mysql program:

 % mysql webdb  mysql> LOAD DATA LOCAL INFILE 'pet_catalog.txt' INTO TABLE pet__catalog;  mysql> LOAD DATA LOCAL INFILE 'pet_category.txt' INTO TABLE pet__category;  mysql> LOAD DATA LOCAL INFILE 'us_state.txt' INTO TABLE us__state; 

Alternatively, use the mysqlimport command-line utility:

 % mysqlimport --local webdb pet_catalog.txt  % mysqlimport --local webdb pet_category.txt  % mysqlimport --local webdb us_state.txt 

Tracking the Customer s Selections

Before discussing how the general application logic works, it s worth having a look at some of the support routines in particular, the mechanism for keeping track of what the user wants to buy. pet_shop.pl uses a shopping cart to remember items as the user selects them. The cart is based on server-side session-based storage so that we don t have to send all the information back and forth with each page.[7] The only thing we ll

[7] pet_shop.pl requires that you have session support set up for using the WebDB::Sessions module described in Chapter 8, Session Management.

send to the client is a session ID. As discussed in Chapter 8, we could store this in a hidden field, in the URL, or in a cookie. pet_shop.pl uses a cookie, which means that even if a customer visits another area of the site (or even some other site altogether) and then returns to the pet shopping area, the application will remember the order.

The shopping cart implementation is relatively simple. A cart is maintained as a hash that consists of elements indexed by item ID values. Each element contains information about an item such as the price, description, and the quantity the customer wants. We track the cart using a reference to the hash, so initializing a new cart amounts to nothing more than this:

 $cart_ref = {}; 

To perform manipulations on cart contents, we ll use two routines, add_item() and delete_item(). The add_item() function increments the item quantity if the item is already in the cart. Otherwise, it looks up the item in the pet_catalog table and uses the information it finds there to create a new cart entry. Because add_item() might need to perform database lookups, it takes a database handle argument in addition to the cart reference and item ID:

 sub add_item  { my ($dbh, $cart_ref, $item_id) = @_;      # If the item isn't already in the cart, look it up from the database      # and store it in the cart as a new entry with a quantity of zero.      if (!exists ($cart_ref->{$item_id}))      {         my $sth = $dbh->prepare ("SELECT * FROM pet_catalog                                      WHERE item_id = ?");          $sth->execute ($item_id);          my $item_ref = $sth->fetchrow_hashref ();          $sth->finish ();          return if !defined ($item_ref); # this shouldn't happen...          $cart_ref->{$item_id} = {}; # create new entry, indexed by item ID          $cart_ref->{$item_id}->{description} = $item_ref->{description};          $cart_ref->{$item_id}->{price} = $item_ref->{price};          $cart_ref->{$item_id}->{qty} = 0;      }      ++$cart_ref->{$item_id}->{qty}; # increment item quantity  } 

delete_item() removes the item from the cart:

 sub delete_item  { my ($cart_ref, $item_id) = @_;      delete ($cart_ref->{$item_id});  } 

To convert the contents of the cart to HTML for display to the user, we call format_cart_html(). The arguments are a reference to the cart itself and a flag indicating whether to include Delete links in the display that enable the customer to remove items from the cart. format_cart_html() is used while the customer is shopping (in which case the second argument is true), and also for presenting the confirmation page at the end of the shopping session. (In which case, the argument is false, because at that point the order has been stored for processing and cannot be changed).

 sub format_cart_html  { my ($cart_ref, $show_links) = @_;  my $total_price = 0;  my @row;      if (!keys (%{$cart_ref}))      {         return (p ("Shopping cart is empty."));      }      push (@row, Tr (                 th (""),                  th ("Item"),                  th ("Quantity"),                  th ("Description"),                  th ("Unit Price"),                  th ("Price")              ));      foreach my $item_id (sort (keys (%{$cart_ref})))      {         my $item_ref = $cart_ref->{$item_id};          my $total_item_price = $item_ref->{qty} * $item_ref->{price};          $total_price += $total_item_price;          # generate a link allowing the item to be deleted from the cart          my $url = sprintf ("%s?choice=delete;item_id=%s",                              url (), escape ($item_id));          push (@row, Tr (                     ($show_links                          ? td (a ({-href => $url}, "Delete"))                          : td ("")),                      td (escapeHTML ($item_id)),                      td (escapeHTML ($item_ref->{qty})),                      td (escapeHTML ($item_ref->{description})),                      td ({-align => "right"},                          escapeHTML (sprintf ("%.2f", $item_ref->{price}))),                      td ({-align => "right"},                          escapeHTML (sprintf ("%.2f", $total_item_price)))                  ));      }      push (@row, Tr (                 td ({-colspan => "3"}, "'),                  td ({-colspan => "2"}, "Total"),                  td ({-align => "right"},                      escapeHTML (sprintf ("%.2f", $total_price)))              ));      return (table (@row));  } 

A similar function format_cart_text() does the same thing, but produces plain text. (It s needed for sending email to the customer after the order has been submitted.) This routine is very similar to format_cart_html(), so I won t show it here.

We ll also need to retrieve the cart from the session record when pet_shop.pl begins and store it back into the session when the script ends.These are trivial operations:

 $cart_ref = $sess_ref->attr ("cart");   # retrieve cart  $sess_ref->attr ("cart", $cart_ref);    # store cart 

The cart helps us remember a customer s selections, but we don t necessarily want to remember them forever perhaps the customer leaves the PseudEcom site without completing the order and never comes back! To cause the cart to expire, we ll assign an appropriate expiration date to both the session in which the cart is stored and the cookie with which the session is associated. That way the browser will expire the cookie on the client side, and we can set up one of the session expiration scripts described in Chapter 8 to remove dead shopping sessions periodically on the server side.

As a concession to the customer who might shop somewhat slowly or who is interrupted by some other activity, we won t constrain the total shopping session time to one hour. Instead, pet_shop.pl uses a rolling horizon expiration technique that is, each time the customer invokes the application, the session expiration date is reset to an hour from the current time. Thus, as long as the customer continues to shop (even if slowly), the session remains active.

Presenting the Shopping Session

The initial part of pet_shop.pl sets up the variables needed for tracking the user s actions. First, it checks whether a session is already in progress. If not, it creates one. Then it sets the lifetime of the session to an hour and creates a cookie (also with a lifetime of an hour) to send to the client. It also extracts the current contents of the shopping cart from the session. (For a new session, there won t be any cart, in which case the script initializes a new one.) The preamble of pet_shop.pl that sets up these tracking variables looks like this:

 my $cookie;         # cookie to hold session ID  my $sess_id;        # session ID  my $sess_ref;       # reference to session record  my $cart_ref;       # reference to shopping cart  # Look for the cookie containing the session ID and read the  # corresponding session record. (Use open_with_expiration() so that  # if the session exists but is too old, it'll be expired automatically.)  if (defined ($sess_id = cookie ("pet_shop")))  {     $sess_ref = WebDB::Session->open_with_expiration (undef, $sess_id);  }  # If no session was found (or it was too old), create a new one.  if (!defined ($sess_ref))  {     defined ($sess_ref = WebDB::Session->open (undef, undef))          or fatal_error ("Cannot create session to track shopping activity");  }  # Set the session and the cookie to have a lifetime of 60 minutes.  # (Setting this each invocation implements rolling-horizon expiration.)  $sess_ref->expires ($sess_ref->now () + 3600);  # 3600 seconds = 60 minutes  $cookie = cookie (-name => "pet_shop",                      -value => $sess_ref->session_id (),                      -path => url (-absolute => 1),                      -expires => "+1h");         # expire in one hour  # Extract the shopping cart from the session; if there wasn't one yet,  # initialize it.  if (!defined ($cart_ref = $sess_ref->attr ("cart")))  {     # cart is a reference to a hash of item records indexed by item ID      $cart_ref = {};  } 

At this point the cookie, session, and shopping cart are set up and ready to go, and pet_shop.pl connects to the database and prepares for page generation by setting up some initial page content:

 my $dbh = WebDB::connect ();  my $page = h3 ("The PseudEcom Pet-Care Shop");  my $std_links = get_std_links ();   # standard page links  my $redisplay_previous = 0; 

These statements create the page title and a set of standard page links that let the customer invoke the various operations the application is capable of performing (browsing, searching, displaying the cart, and so forth).[8] These links normally go across the top of the page, but we don t add them to the page just yet, because some actions the user may take terminate the shopping session and the links no longer will apply. (If the customer cancels the order or submits the order, for example, the shopping session terminates and there s no need for the links. The dispatch code for such actions unde-fines $std_links to prevent them from being displayed.)

[8] get_std_links() isn t shown here; it s simply the concatenation of several hyperlink-generating statements.

The $redisplay_previous variable is used to tell the application when it needs to redisplay the previous page (the one the user was just looking at) rather than a new one. This variable is set true for operations that add or delete items from the shopping cart. For example, if the customer submitted a search and is viewing a result page showing the matching items, clicking an Add Item link will add the corresponding item, and then cause pet_shop.pl to rerun the search to show the same results page again. This way the customer doesn t have to click the Back button to return to the result page, or enter the search parameters again manually.

Next, pet_shop.pl determines what action to perform, based on the value of the choice parameter. I ll show the dispatch logic that implements these actions, and then discuss the cases in more detail. Because the dispatch code is rather long and ungainly, you probably should just skim through this listing initially and then refer back to it as you read the discussion that follows:

 # Dispatch to proper action based on user selection  my $choice = lc (param ("choice")); # get choice, lowercased  if ($choice eq "")                  # initial invocation  {     # default page is the category browse list      $page .= get_category_list ($dbh);  }  elsif ($choice eq "browse")         # show browse page  {     if (!defined (param ("cat")))      {         # show category browse list          $page .= get_category_list ($dbh);      }      else      {         # show items-in-category browse list          $page .= get_category_items ($dbh, param ("cat"));      }  }  elsif ($choice eq "search")         # display search form/perform search 
 {     $page .= search_catalog ($dbh);  }  elsif ($choice eq "view")          # display shopping cart  {     $page .= format_cart_html ($cart_ref);  }  elsif ($choice eq "checkout")      # customer is ready to check out  {     $page .= display_billing_form ($dbh, $cart_ref);  }  elsif ($choice eq "submit order")  # customer submitted the order  {     my ($status, $str) = close_order ($dbh, $cart_ref);      if ($status)      {         # the order was stored properly: tell the browser to destroy          # the cookie; destroy the session          $cookie = cookie (-name => "pet_shop",                              -value => $sess_ref->session_id (),                              -path => url (-absolute => 1),                              -expires => "-1h");          $sess_ref->delete ();          undef $sess_ref;          # this is the final page of the session; no standard links needed          undef $std_links;      }      # if no error occurred, $str contains the confirmation page;      # otherwise, it contains the billing form to redisplay      $page .= $str;  }  elsif ($choice eq "cancel")         # customer wants to quit  {     # tell the browser to destroy the cookie; destroy the session      $cookie = cookie (-name => "pet_shop",                          -value => $sess_ref->session_id (),                          -path => url (-absolute => 1),                          -expires => "-1h");      $sess_ref->delete ();      undef $sess_ref;      # this is the final page of the session; no standard links needed      undef $std_links;      $page .= p ("Your shopping session has been terminated.")              . p (a ({-href => url ()}, "Start over"));  }  elsif ($choice eq "add")            # add item to shopping cart  {     add_item ($dbh, $cart_ref, param ("item_id"));      $redisplay_previous = 1;    # display previous page  }  elsif ($choice eq "delete")         # delete item from shopping cart  {     delete_item ($cart_ref, param ("item_id"));      $redisplay_previous = 1;    # display previous page  }  else  {     $page .= p (escapeHTML ("Logic error, unknown choice: $choice"));  } 
Presenting the Catalog Interfaces

The first few cases of the dispatch code present the browsing and search interfaces to the product catalog. The top-level browsing page is generated by get_category_list(). It contains a list of category names, each of which is linked to a leaf page that displays items in the corresponding category:

 sub get_category_list  { my $dbh = shift;  my ($cat_name_ref, @link, $page);      # get the product category names      $cat_name_ref = WebDB::get_lookup_values (                         $dbh,                          "SELECT category FROM pet_category ORDER BY category");      # Generate links to pages for each category      foreach my $cat_name (@{$cat_name_ref})      {         push (@link, get_browse_link ($cat_name, $cat_name));      }      if (@link)      {         $page .= p ("Please select a product category:")                  . ul (li (\@link));      }      else      {         $page .= p ("Hmm ... someone forgot to load the category table!");      }      return ($page);  } 

The get_lookup_values() routine used here was described in Chapter 6, Automating the Form-Handling Process. It returns a reference to the array of values returned from the lookup table named in the query. Each value is passed to get_browse_link() to generate a link to a leaf page. If the category name is Toys, for instance, get_browse_link() generates a hyperlink that looks like this:

 <a href="pet_shop.pl?choice=browse;cat=Toys">Toys</a> 

If the customer selects one of these links, the dispatch code calls get_category_items() to look up items in the category and produce a table showing information about each item:

 sub get_category_items  { my ($dbh, $cat_name) = @_;  my ($sth, $page);      $sth = $dbh->prepare ("SELECT * FROM pet_catalog                              WHERE category = ? ORDER BY description");      $sth->execute ($cat_name);      $page = get_product_table ($sth);      if ($page)      {         $page = p ("Items in product category:\n" .. escapeHTML ($cat_name))                  . $page;      }      else      {         # if the category is empty, say so and show the category list again          $page .= p ("There are no items in this product category:\n"                      . escapeHTML ($cat_name));          $page .= get_category_list ($dbh);      }      return ($page);  } 

get_category_items() calls get_product_table() to do most of its work. (This latter function will be described shortly.) If it turns out there aren t any items in the category, we display the category list again immediately so the customer doesn t have to click the Browse link to get to it.

When the customer wants to specify a search or submits a search, pet_shop.pl invokes search_catalog(). This function presents a new search form and, if a search was just submitted, runs a catalog query and displays the results:

 sub search_catalog  { my $dbh = shift;  my $sth;  my @condition;      # conditions for WHERE clause  my @placeholder;    # values for placeholders  my ($where, $limit);  my $page;      # Put the search form at the top of the page. Use GET method so      # that search parameters appear on the URL when the user submits the      # form (otherwise redirecting back to this page won't work when user      # selects an "Add Item" link)).      $page .= start_form (-action => url (), -method => "GET")          . table (             Tr (                 td ("Keywords:"),                  td (textfield (-name => "keywords", -size => "60"))              ),              Tr (                 td (""),                  td (radio_group (-name => "match_type",                                      -values => [ "all", "any" ],                                      labels => {                                         "all" => "Match all words",                                          "any" => "Match any word"                                      }))              ),              Tr (                 td ("Max. items to show:"),                  td (popup_menu (-name => "limit",                                  -values => [ "10", "25", "50", "75", "100" ]))              )          )          . br ()          . submit (-name => "choice", -value => "Search")          . end_form ();      # Get keywords to look for, bust up into individual words,      # construct LIKE tests for each of them, and join with the proper      # boolean according to the match_type parameter.      my $val = WebDB::trim (param ("keywords"));      # if no keywords were entered, just display the form      return ($page) if $val eq "";      $page .= p (escapeHTML ("Search keywords: $val"));      my @word = split (/\s+/, $val);      $val = lc (param ("match_type"));      my $bool_op = "AND"; # determine boolean connective (default = AND)      $bool_op = "AND" if $$val eq "all";      $bool_op = "OR" if $$val eq "any";      # create one test per word; join tests with $bool_op;      # enclose entire string in parentheses      push (@condition,              "("              . join (" $bool_op ", ("description LIKE ?") x @word)              . ")");      # convert each word xxx to %xxx% before adding to @placeholder      push (@placeholder, map { "%$_%" } @word);      # construct WHERE clause listing the keyword conditions      $where = "WHERE " .. join (" AND ", @condition) if @condition;      $where = "" unless $where;      # determine maximum number of items to show;      # default is 10 if $limit looks suspicious      $limit = WebDB::trim (param ("limit"));      $limit = 10 if $limit !~ /^\d+$/ || $limit > 100;      $sth = $dbh->prepare ("SELECT * FROM pet_catalog                              $where                              ORDER BY category, description                              LIMIT $limit");      $sth->execute (@placeholder);      my $table = get_product_table ($sth);      $page .= ($table ? $table : p ("No items were found."));      return ($page);  } 

search_catalog() generates the search form using the GET method rather than POST. This is done so that the form parameters will be included in the URL when the user submits a search. Then when the search result is displayed, if the user selects Add Item for one of the items, we can examine the HTTP_REFERER environment variable to find out how to redisplay the proper result page by running the search again. If we generate the form using POST, the referring URL won t include the parameters, and we d have to add them ourselves. (You can see this for yourself by changing the script to use POST rather than GET.)

Much of the code for constructing the keyword search query is similar to that used by the res_search2.pl script developed in Chapter 7. See the section Searching for Keywords in Text in that chapter for further discussion.

Like get_category_items(), search_catalog() prepares and executes a query and then calls get_product_table() to do the work of producing the item table. The latter routine takes the statement handle, fetches the records, and formats them. Each item in the table includes a link that enables the customer to add the item to the shopping cart:

 sub get_product_table  { my $sth = shift;  my @row;      while (my $ref = $sth->fetchrow_hashref ())      {         # generate a link allowing the item to be added to the cart          my $url = sprintf ("%s?choice=add;item_id=%s",                              url (), escape ($ref->{item_id}));          push (@row, Tr (                     td (a ({-href => $url}, "Add Item")),                      td (escapeHTML ($ref->{item_id})),                      td (""), # spacer                      td (escapeHTML ($ref->{description})),                      td ({-align => "right"},                          escapeHTML (sprintf ("%.2f", $ref->{price})))                  ));      }      $sth->finish ();      return undef unless @row;       # no items?      unshift (@row, Tr (             # put header row at beginning                  th (""),                  th ("Item No."),                  th (""), # spacer                  th ("Description"),                  th ("Price")              ));      return (table (@row));  } 
Displaying the Shopping Cart

The View Cart link takes the customer to a page that lists the shopping cart. The display is generated by format_cart_html(), a routine that was shown earlier. Each entry in the cart display contains a Delete link for removing the item from the cart.

Checking Out

The final stage in creating an order is reached when the customer has finished shopping and selects Check Out from the top of the page. pet_shop.pl sets up a secure connection and proceeds to the checkout page. This page displays the contents of the shopping cart for the customer to review, a form for collecting shipping and billing information, and a Submit Order button. When the customer fills in the form and selects Submit Order, pet_shop.pl stores the order and terminates the session.

As it happens, most of the machinery in pet_shop.pl for gathering billing information, storing the order, and presenting the confirmation page to the customer is quite similar to the code that performs these actions in doc_shop.pl. Consequently, I m not going to discuss this process for pet_shop.pl at all. You can check the source for the two scripts to see the nature of the differences, such as they are.

Canceling the Order

The Cancel link terminates the shopping session. It s implemented by destroying the session; the effect is to delete the shopping cart entirely. Also, to cause the browser to forget the session ID value, we tell it to delete the cookie containing that identifier.

Modifying the Shopping Cart

The dispatch logic contains two cases for adding items to and deleting items from the shopping cart. These actions are handled by add_cart() or delete_cart(), two routines that already have been shown. The notable thing about these cases is that they both enable $redisplay_previous to tell pet_shop.pl to display the page the user just came from, instead of generating a new page.

Finishing Up

After executing the dispatch code, pet_shop.pl saves the contents of the shopping cart as necessary, disconnects from the database, and displays the page constructed during dispatch execution:

 # If session hasn't been destroyed, store the shopping  # cart back into it and close it.  my $cart_count = 0;  if (defined ($sess_ref))  {     # count the number of items in the cart for use      # below as a bit of feedback for the user      foreach my $item_id (sort (keys (%{$cart_ref})))      {         my $item_ref = $cart_ref->{$item_id};          $cart_count += $item_ref->{qty};      }      $sess_ref->attr ("cart", $cart_ref);      $sess_ref->close ();  }  $dbh->disconnect ();  # Add/Delete operations set $redisplay_previous to cause  # the page the user just came from to be redisplayed. referer()  # contains the value of the HTTP_REFERER environment variable and  # redirect() generates Status: and Location: redirection headers.  if ($redisplay_previous)  {     print redirect (referer ());      exit (0)  }  # Add the standard links if they haven't been suppressed;  # add the cart item count as well  if (defined ($std_links))  {     $page = p ($std_links . " ((items in cart: $cart_count)")              . $page;  }  print header (-cookie => $cookie),          start_html (-title => "PseudEcom Corporation", -bgcolor => "white"),          add_boilerplate ($page),          end_html (); 

Suggested Modifications

Present a note this application requires cookies to be enabled in your browser when a customer first invokes pet_shop.pl. (You can detect this condition by the absence of the session ID cookie.) If your visitor has cookies turned on, the message will appear only once per shopping session. If cookies are turned off, the customer s browser will never return a cookie to the application and pet_shop.pl will present the message every time it s invoked. But that s what you want; the customer gets a constant reminder that the application requires a condition that isn t being met.

The pet_shop.pl application allows only one item to be added to the cart at a time. Modify the pages that display Add Item links to include quantity fields so that the customer can specify quantities explicitly.

Modify the search form to add a pop-up menu that lists product categories so that customers can limit searches to a single category. (Remember to include an All or Any item so that users can still search all categories.)

The search form contains a limit field, enabling the customer to specify a limit on the number of hits displayed in search result pages. This value is carried from page to page as long as the customer moves between pages that display the search form, but is lost otherwise. Modify pet_shop.pl to remember the limit in the session record so that it can be preserved uniformly across all pages presented by the application.

pet_shop.pl doesn t include any image-display capabilities, but it s not a bad idea to present pictures of your products if you have them; people often like to see what they re buying. Add the capability of displaying text and images on the same page, based on the techniques discussed in Chapter 5 and Chapter 7.

If a customer searches for an item that is out of stock, provide an option for requesting an email notification when the item is back in stock. (To do this, you d need to add some machinery for maintaining current inventory stock levels, of course.)

If you select an Add Item link on a page that displays items from the catalog, the link changes to the visited-link color in your browser when the page is redisplayed. However, if you then delete the item and revisit a page that displays the item, the Add Item link continues to display in the visited-link color, even though the item no longer is in the cart. This might be a source of confusion to customers. Change the application to present the links for adding and deleting items in such a way that it doesn t matter whether these links have been followed.

only for RuBoard - do not distribute or recompile


MySQL and Perl for the Web
MySQL and Perl for the Web
ISBN: 0735710546
EAN: 2147483647
Year: 2005
Pages: 77
Authors: Paul DuBois

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net