Hands-On Project: The WebStore |
The application that we will develop in this project is a simple shopping cart system called WebStore. Among the many goals of this project, we will try to pull together many of the elements that have been explored in this Tutorial. The most significant new feature in this Session is that we will build an application whereas in the previous Sessions we created a series of applets.
The WebStore project uses a single program file. The program is controlled by a single case statement which is keyed against the name of a submit button. Each of the forms that comprise the WebStore application submits its information to the same program file. The Name of each form's submit button is a unique string. The program tests this name in order to control the flow of the application.
The various response pages and data management procedures for this application are collected together into a single custom control file named WebStore.cc. Much like we've done many times in this Tutorial, this library is subclassed from the WebClassEx library. But, rather than a single streamBody() method, the WebStore library contains a number of procedures and response pages.
We begin this Session with an overview of the interface. We will see how the application is intended to run. Following that we will explore the various procedures used in this application.
The web pages used in this project will be simple so that we can focus on the dBASE part of the application. We use HTML tables and leave a visible border so that you can see how elements are organized on the pages. There are many features and functions that we have left out of this application. After completing the project, you may wish to add functionality to the application.
We begin by presenting an overview of the user interface.
The Catalog page. The first page of this application is the catalog of items that are for sale at the WebStore. Our customer is provided the option of selecting the quantity of an item and adding that item to their shopping cart. A customer can add only one item at a time. Each catalog item on this page is contained within its own separate form. This is why there's a "Buy" button for each item. When the button is clicked, only information for that item is sent to the server.
The Shopping Cart Page. Each time our customer adds an item to their shopping cart, the Shopping Cart page is rendered. This page is dynamic and must include each item in the customer's cart. The customer has a number of choices on this page: 1) Continue Shopping will take the customer back to the inventory page; 2) Delete will delete the item in the same row as the button; 3) Modify will update the quantity of the item; and 4) Check Out will begin finalizing the order.
The Checkout Page. On the Check Out page we need to obtain customer information for billing and shipping purposes. The page contains two HTML forms. If the customer's information is already in our database, they need only enter their customer number. This application is a simulation and given the database that we are working with, the customer number is the "CustomerID" field in the Customer.dbf table. In a real application customers are generally given the option of choosing a username and a password. But in order to keep our program simple, we are using the customer table's primary key. You can enter any number from 1 - 50 for testing the page.
When an existing customer enters a Customer ID and submits the form, we will lookup the existing information and steam it in the response page. The second option on the Check Out page is for a new customer to add their information. When such a customer submits the information, we will add the information to our database and stream it in the response page. Both procedures generate the same response page.
The Invoice Page. The invoice page displays the Customer Information, the Shopping Cart items, and it provides a form for entering Payment Information. This is the last step for the customer. When the form is submitted, the purchase is finalized and our program will post the order in the database.
The Thank You page. The Thank You page confirms the customer's order was successful posted.
Set-up
In the Document Root of your web server you will find a folder named "WebStore". This folder was created when you installed the Tutorial and it is where the application's executable will be stored. There is also a images folder that was installed with the Tutorial. WebStore will use the WebTutorial database, and therefore, the database alias is already created.
The Shopping Cart Table
Items that a customer add to their shopping cart are stored in a dbf file called Session. All customers share the same Session file. This table includes the itemID, its description, the quantity ordered, and a sessionID (see below). The information in Session.dbf is temporary. Ideally a customers can add, remove and otherwise modify items in this file. When the customer finalizes their order, we will append a new row in the Orders table and copy the itemIDs and quantities from the Session table to the LineItems table.
After an order has been posted (or if a customer abandons a shopping cart) the data in the Session table is no longer needed, and can be deleted. In a full featured shopping cart application, the Session table could grow rather large until the dBASE table is packed. This can be a problem because, in order to pack the table, exclusive use is necessary. If your site is such that gaining exclusive use of this table is problematic, I suggest that you use a Paradox table for your Session file. Paradox tables do no support soft deletes, so packing the table will not be necessary.
The sessionID
An important feature of this application is the shopping cart ID, which we are calling the "sessionID". When a customer adds their first item to the shopping cart, the program generates a sessionID. This ID is included with each record added to the Session file, and, most importantly, it must be passed as a hidden field in every HTML page. This way we'll be able to find each customer's shopping cart items. Passing the sessionID from page to page is easy to do so long as we remember to do it.
Many shopping cart applications use cookies to keep track of the shopping cart ID. Although I didn't use cookies in the project, we saw in Session 7 that cookies are not difficult to use, and, if you wish, you may use a cookie to store the sessionID rather than passing a hidden field.
Controlling Application Flow
The WebStore application includes a number of primary procedures. We can include each procedure in a separate program file and the web forms could call the respective executable. For a number reasons, however, I find it simpler to create one executable and let the program branch with a Case statement. The Case statement must test for which action to take, and to call a procedure that matches the action. In order to control the application, all the submit buttons in the web forms are assigned a unique name. For example, the HTML for two buttons the might be used to delete and modify items in the shopping cart would look like this:
<INPUT TYPE="submit" name='DeleteItem' value='Delete this item'> <INPUT TYPE="submit" name='ModifyItem' value='Update this item'>
When the user clicks the button titled "Update this item," the CGI name/value pair sent to your CGI application is DeleteItem=Delete+this+item. The program file must evaluate this name/value pair in order to control the application flow. Notice that we are going to test the name part of the name/value pair. The value part is ignored. The main reason for using the name is that value is used by the HTML form as the caption on the submit button.
To test whether a name is contained in the oCGI, we can use the isKey() method.
if oCGI.isKey("ModifyItem") endif
The primary requirement of this technique is that each button in the application is assigned a unique name. Also remember that associative array keys are case sensitive, so "modifyitem" will not match "ModifyItem". The following is the WebStore program file that we will use for our application.
//////////////////////////////////////////// Try // Error trap entire CGI applet. ////// Create new instance of the Web Class //Set proc to WebClassEx.cc additive set proc to WebStore.cc additive oCGI = new WebStore() ////// Connect to the Web Server (StdIn/StdOut) oCGI.Connect() do case case oCGI.isKey('AddItem') oCGI.AddItem() case oCGI.isKey('CheckOut') oCGI.CheckOut() case oCGI.isKey('GetCustomer') oCGI.GetCustomer() case oCGI.isKey('AddCustomer') oCGI.AddCustomer() case oCGI.isKey('ModifyItem') oCGI.ModifyItem() case oCGI.isKey('DeleteItem') oCGI.DeleteItem() case oCGI.isKey('PostOrder') oCGI.PostOrder() otherwise oCGI.streamHeader() oCGI.streamInventory() oCGI.streamFooter() endcase catch (exception e) oCGI.errorPage(e) endtry /// cleanup oCGI = null ////// all done Quit
The response pages and other procedures called from the above program will be stored in a custom class library called WebStore.cc. Although stored in a single file, each procedure is developed much it would be if a single program file were used.
The procedure begins with code to that will complete the task at hand. Then a response page is streamed. Use a HTML editor to create the response pages. You should try to make the HTML pages as complete as possible, because after the HTML is converted to a PRG, you will not be able to visually edit the page. Feel free to brighten up or otherwise modify the appearance of the HTML pages. When the HTML page is satisfactory, use HTMLtoPRG.wfm to convert the file into dBL code.
The following is an outline of the WebStore library. The WebTutorial database is opened in the class constructor, because all but one of our procedures need to access the data.
class WebStore of CGISessionEx from "WebClassEx.cc" // Open the database this.db = new DATABASE() this.db.databaseName = "WEBTutorial" this.db.active = true function AddItem function ModifyItem function DeleteItem function GetCustomer function AddCustomer function PostOrder function streamInventory function streamCart function streamInvoice function streamCheckOut endclass
The remainder of this Session will discuss each of the functions used in the WebStore library.
StreamInventory
The purpose of the inventory program is to render a page that lists information about the items offered for purchase at the WebStore.
The data used on this page is taken from Inventory.dbf. This includes the item's description and the quantity in stock. The images used in our application are not stored in the table. Rather, they are stored in a folder on the server, and their path is stored in the table. Notice that the inventory table contains a field called "Image". That field is the URL to the image file (you should modify the path if your server is setup different than mine). When the inventory response page is rendered, the path is inserted into an HTML Image tag and the browser requests the image from the server.
Each item on the Catalog page is contained within it's own HTML form, and each of the forms call the same procedure in the program. The difference is the information passed to the program. Each submit button on the Catalog page sends only information related to that item. Also notice that the ItemID is a hidden field in the HTML form.
The main HTML table on Catalog page contains rows of repeating information, which lends itself to a "do" loop. You can design the response page with a two column by one row table. The HTML form is placed in the first column and the image is placed in the second. Then, with the "do" loop, repeat the row for each record in the Inventory table.
The final issue related to the streamInventory procedure is the sessionID. The first time the procedure is called, there will not be a sessionID (so the value of the hidden field is empty). Our program will then create a sessionID, add it to the Session file, and stream it in "Shopping Cart" response page. When a customer chooses to "Continue Shopping," the sessionID will be included in the oCGI array and, it can be inserted into the inventory page. This way all subsequent items selected by the shopper will be associated with the same shopping cart.
The following code is used for the streamInventory method.
function streamInventory local f q = new QUERY() q.database = this.db q.sql = "select * from inventory.dbf" q.active = true f = q.rowset.fields with (this.fOut) puts([<BODY BGCOLOR="#FFFFFF"> ]) puts([<P> </P> <P> </P> <P> ]) puts([<CENTER><TABLE BORDER=1 BGCOLOR="#FFFFFF" WIDTH=500> ]) //////////////////////////////////////////// /// /// Loop for streaming inventory items /// Each item is placed in an HTML table row /// Each row is a complete HTML form /// //////////////////////////////////////////// do puts([<TR>]) puts([<FORM ACTION="WebStore.exe" METHOD=POST>]) puts([<TD WIDTH=250>]) if this.isKey('sessionID') cSessionID = [VALUE="]+ this['sessionID'] + ["] else cSessionID = "" endif puts([<INPUT TYPE=hidden NAME=sessionID ]+ cSessionID + [>]) cValue = this.GetFieldValue( q , "ItemID", false ) puts([<INPUT TYPE=hidden NAME=ItemID VALUE="] + cValue + [">]) puts([<TABLE BORDER=1 width='100%'> ]) puts([<TR>]) puts([<TD><P>Descrtiption </TD>]) puts([<TD>]) cValue = this.GetFieldValue( q, "Description", false ) puts(cValue ) puts([<INPUT TYPE=hidden NAME=Description VALUE="] + cValue + [">]) puts([</TD></TR>]) puts([<TR><TD><P>Number Available</TD><TD>]) cValue = this.GetFieldValue( q, "Qty", false ) puts( cValue ) puts([</TD></TR> ]) puts([<TR><TD><P>Quantity to buy</TD><TD>]) puts([<P><INPUT TYPE=text NAME=Qty VALUE="1" SIZE=4> ]) puts([</TD></TR> ]) puts([ ]) puts([<TR><TD><P>Buy Now</TD><TD>]) puts([<P><INPUT TYPE=submit NAME=AddItem VALUE="Submit"> ]) puts([</TD></TR></TABLE> ]) puts([<TD VALIGN=top WIDTH=250> ]) if not empty(f["Image"].value) cValue = [<IMG SRC="] + trim(f["Image"].value) + [" ALIGN=top>] else cValue = "Image not avaliable" endif puts([ <P><CENTER>]+ cValue +[</CENTER> ]) puts([</FORM> ]) until not q.rowset.next() puts([</TD></TR></TABLE></CENTER>]) endwith q.active = false return true
AddItem
Each of the "Buy" buttons on the Catalog page is named "AddItem". If the case statement in the program file finds this name, it will call the AddItem() method in the WebStore class.
The AddItem procedure must create a query object that selects records from the Session.dbf table and append the field values passed from the catalog page. You can use the loadFieldsFromArray() method to append the data.
This procedure must also create a sessionID if one does not exist.
The following code is used for the AddItem procedure.
function AddItem // open the Tempory session file this.q = new QUERY() this.q.database = this.db this.q.sql = 'select * from Session.dbf' this.q.active = true // Check for sessionID if this.isKey('sessionID') and len(trim(this['sessionID'])) > 0 /// A SessionID already exists cSessionFile = this['sessionID'] else /// create new sessionID cSessionID = "CGI"+ left(ltrim(str(random(-1)*100000000)),7) if this.q.rowset.findkey( cSessionID ) do cSessionID = "CGI"+ left(ltrim(str(random(-1)*100000000)),7) until not this.q.rowset.findkey( cSessionID ) endif this['sessionID'] = cSessionID endif /// cSessionID = this['sessionID'] /// For testing /// We must have a sessionID at this point. if not this.isKey( 'sessionID' ) this.sorrypage("Programing error") endif /// Add Item to session file // itemID, Description, Qty , sessionID this.loadFieldsFromArray(this.q.rowset.fields, true) /// Stream response page /// shopcart.htm this.StreamHeader() this.streamCart() this.StreamFooter() ///this.q.active = false return
This code checks to see whether "sessionID" is a key name in the oCGI array. When sessionID is in the array we simply store it in a memory variable and move on. However, when sessionID does not exist, we generate a random number, verify that the generated number is not already in the Session table (if it is we generate a different random number), and finally we create a new name/value pair in the oCGI array for the sessionID.
Therefore when we exit this if block, we are assured that sessionID is an element in the array. It was either passed from the inventory page or we generated it. When the program calls loadFieldsFromArray to append the record to Session, the sessionID is added to the record. And we can use this to find all the items in Session that are associated with this sessionID.
The response page for this procedure is the Shopping Cart page. It must list each item in the Session file for the current sessionID. This is done by setting a filter in the streamCart() method (setRange(oCGI['sessionID']) will also work), and then streaming the page. The HTML table is rendered in a "do" loop, with one row for each item in the shopping cart.
function streamCart local f this.q.rowset.filter = [sessionID = '] + this['sessionID'] + ['] this.q.rowset.first() f = this.q.rowset.fields q = this.q /// Get sessionID if this.isKey('sessionID') cSessionID = [VALUE="]+ this['sessionID'] + ["] else cSessionID = "ERROR" endif with (this.fOut) puts('<BODY BGCOLOR="#FFFFFF">') puts('<P> </P>') puts('') puts('<CENTER><TABLE BORDER=1 width="80%">') puts('<TR>') puts('<TD><P><B>Item</B></TD>') puts('<TD><P><B>Description</B></TD>') puts('<TD><P><B>Quantity</B></TD>') puts('<TD><P><B>Price</B></TD>') puts('<TD><P><B> </B></TD></TR>') /// Loop for streaming Cart Items /// One table row per record in session.dbf do puts('<TR>') cValue = this.GetFieldValue( q, "ItemID", false ) puts('<TD><P>'+ cValue + '</P></TD>') cValue = this.GetFieldValue( q, "Description", false ) puts( '<TD><P>' + cValue + '</P></TD>') cValue = this.GetFieldValue( q, "Qty", false ) ///////////////////// /// Put Quantity in a Text field /// so that the user can change /// the quantity to order and /// update the shopping cart. //////////////////////////////////////// puts('<FORM METHOD=POST ACTION="WebStore.exe">') puts('<INPUT TYPE="HIDDEN" NAME="ItemNo" VALUE="'+ ltrim(str(f['itemNo'].value)) +'">') puts('<INPUT TYPE="HIDDEN" NAME="sessionID" '+ cSessionID + '>') puts([<TD><INPUT TYPE="submit" name='ModifyItem' value='Modify'>]) puts('<INPUT TYPE="TEXT" NAME="Qty" VALUE="'+ ltrim(cValue) + '">') nPrice = (f["Qty"].value ) * 6.5 puts('<TD>') puts('<P> $' + nPrice+ '</P>') puts('</TD>') puts('</FORM>') //////////////////////////// // Each delete button is a self contained form. // it calls deleteItem() and passes the sessionID // and the itemID. //////////////////////////// puts('<FORM METHOD=POST ACTION="WebStore.exe">') puts('<INPUT TYPE="HIDDEN" NAME="ItemNo" VALUE="'+ ltrim(str(f['ItemNo'].value)) +'">') puts('<INPUT TYPE="HIDDEN" NAME="sessionID" '+ cSessionID + '>') puts('<TD>') puts([<INPUT TYPE="submit" name='DeleteItem' value='Delete'>]) puts('</TD>') puts('</FORM>') puts('</TR>') until not this.q.rowset.next() puts('<TR><TD>') //////////////////////////////////////// /// /// HTML form with submit button and /// hidden field with sessionID /// //////////////////////////////////////// puts([<FORM ACTION="WebStore.exe" METHOD=POST>]) puts('<P><INPUT TYPE="submit" NAME="Submit" VALUE="Continue Shopping"></P>') puts([<INPUT TYPE=hidden NAME=sessionID ]+ cSessionID + [>]) puts('</FORM>') puts('</TD>') //////////////////////////////////////// /// /// Second HTML form with submit button and /// hidden field with sessionID /// //////////////////////////////////////// puts('<TD>') puts([<FORM ACTION="WebStore.exe" METHOD=POST>]) puts('<P><INPUT TYPE="submit" NAME="CheckOut" VALUE="Check Out"></P>') puts([<INPUT TYPE=hidden NAME=sessionID ]+ cSessionID + [>]) puts('</FORM>') puts('</TD>') puts('<TD> </TD>') puts('<TD> </TD>') puts('</TR>') puts('</TABLE></CENTER>') //puts('</BODY>') endwith return true
There are four procedures that can be called from this page. The Continue Shopping button will call the streamInventory() method and take the user back to the Catalog Page. There are also two edit buttons and the Check Out button. We will consider the two edit buttons next.
Editing Items
The shopper is given the opportunity to delete an item in the shopping cart or to modify the quantity to purchase of an item. Again the button names are used control which procedure is called in the program file. The name of the Edit button is "EditItem" and the name of the Modify button is "ModifyItem".
The ModifyItem procedure is a standard data editing routine. The HTML form contains an ItemNo, which is the key field for the Session table. This field value is used to locate the record to be edited. When found, the row is updated and the Shopping Cart page is streamed back to the shopper.
function ModifyItem // open the Tempory session file this.q = new QUERY() this.q.database = this.db this.q.sql = 'select * from Session.dbf' this.q.active = true ////////////////// this.q.rowset.indexName = "ItemNo" /////////////// if not this.isKey( 'sessionID' ) this.sorrypage("Programing error") endif if this.isKey('ItemNo') cItemNo = this['ItemNo'] if this.q.rowset.findKey(val(cItemNo)) this.loadFieldsFromArray(this.q.rowset.fields, false) /// Stream response page this.StreamHeader() this.StreamCart() this.StreamFooter() endif else this.sorryPage("No ItemNo") endif return
The DeleteItem procedure is similar to the ModifyItem procedure. The ItemNo is used to locate the record to be deleted. When found, the row is deleted and the Shopping Cart page is again streamed back to the shopper.
function DeleteItem // open the Tempory session file this.q = new QUERY() this.q.database = this.db this.q.sql = 'select * from Session.dbf' this.q.active = true ////////////////// this.q.rowset.indexName = "ItemNo" /////////////// if not this.isKey( 'sessionID' ) this.sorrypage("Programing error") endif if this.isKey('ItemNo') cItemNo = this['ItemNo'] if this.q.rowset.findKey(val(cItemNo)) this.q.rowset.delete() /// Stream response page this.StreamHeader() this.StreamCart() this.StreamFooter() endif else this.sorryPage("No ItemNo") endif return
CheckOut
The final procedure that can be run from the Shopping Cart page is called by the "Check Out" button. The name of this button is "CheckOut," and calls the CheckOut() method from the program file.
This button is used after the shopper has finished adding items to their shopping cart. Its purpose is to get customer information from the user. The procedure does not connect to the database. Its only function is to pass the sessionID and render a response page.
The response page contains two forms. The first form is used to submit a customer ID in the case where the customer information is already on file. This form must also contain a hidden field with the sessionID. When submitted, this form calls the GetCustomer procedure.
The second form is used to submit name and address information for a new customer. It also must contain the sessionID in a hidden field. On submission, this form calls the AddCustomer procedure.
function streamCheckOut with (this.fOut) puts('<BODY BGCOLOR="#FFFFFF">') puts('<Center><P>Enter your Customer ID here. If you are a new Customer please ') puts('complete the information in Part II below. ') puts('') puts('<P>PART I: For Existing Customers.') puts('<FORM ACTION="WebStore.exe" METHOD=POST>') puts('<P><TABLE BORDER=1>') puts('<TR>') puts('<TD>') puts('<INPUT TYPE="hidden" NAME=sessionID VALUE="'+this['sessionID']+'">') puts('<P>Enter Your Customer ID</TD>') puts('<TD><P> </P>') puts('</TD></TR>') puts('<TR>') puts('<TD><P><INPUT TYPE=text NAME=CUSTNO VALUE="" SIZE=30></TD>') puts('<TD><P><INPUT TYPE=submit NAME=GetCustomer VALUE="Submit">') puts('</TD></TR></TABLE>') puts('</FORM>') puts('') puts('<!-- Second form for adding new Customer -->') puts('<P>PART II: For new Customers. ') puts('<P><FORM ACTION="WebStore.exe" METHOD=POST>') puts('<P><TABLE BORDER=1>') puts('<TR>') puts('<TD WIDTH=133>') puts('<INPUT TYPE="hidden" NAME=sessionID VALUE="'+this['sessionID']+'">') puts('<P>First Name</TD>') puts('<TD><P><INPUT TYPE=text NAME="FirstName" VALUE="" SIZE=30></TD>') puts('</TR>') puts('<TR>') puts('<TD WIDTH=133><P>Last Name</TD>') puts('<TD><P><INPUT TYPE=text NAME="LastName" VALUE="" SIZE=30></TD>') puts('</TR>') puts('<TR>') puts('<TD WIDTH=133><P>Address </TD>') puts('<TD><P><INPUT TYPE=text NAME=Street1 VALUE="" SIZE=30></TD>') puts('</TR>') puts('<TR>') puts('<TD WIDTH=133><P>Address</TD>') puts('<TD><P><INPUT TYPE=text NAME=Stree2 VALUE="" SIZE=30></TD>') puts('</TR>') puts('<TR>') puts('<TD WIDTH=133><P>City</TD>') puts('<TD><P><INPUT TYPE=text NAME=City VALUE="" SIZE=30>') puts('</TD>') puts('</TR>') puts('<TR>') puts('<TD WIDTH=133><P>State</TD>') puts('<TD><P><INPUT TYPE=text NAME="StateID VALUE="" SIZE=2>') puts('</TD>') puts('</TR>') puts('<TR>') puts('<TD WIDTH=133><P>Postal Code</TD>') puts('<TD><P><INPUT TYPE=text NAME=Postal VALUE="" SIZE=30>') puts('</TD>') puts('</TR>') puts('') puts('<TR>') puts('<TD WIDTH=133><P> </TD>') puts('<TD><P><INPUT TYPE="submit" NAME=AddCustomer VALUE="Submit"><INPUT TYPE="reset" NAME=reset VALUE="Reset"></TD>') puts('</TR>') puts('</TABLE>') puts('') puts('</FORM></Center>') // puts('</BODY>') endwith return true
GetCustomer and AddCustomer
We will consider GetCustomer and AddCustomer procedures together because they both share the same response page.
The purpose of the getCustomer procedure is to verify that the Customer ID exists in our Customer table and, if it does, we will include the value in the response page. The purpose of the addCustomer proceduer is to append new-customer information to the Customer table and passes the new Customer ID to the response page. Both procedures generate the Invoice page as a response page.
function GetCustomer this.qCustomer = new QUERY() this.qCustomer.database = this.db this.qCustomer.sql = "select * from customer.dbf" this.qCustomer.active = true this.qCustomer.rowset.indexName := "CustomerID" cCustomerID = str(this.qCustomer.rowset.fields['customerID'].value) if this.qCustomer.rowset.findkey( val( cCustomerID ) ) /// Stream response page this.StreamHeader() this.StreamInvoice() this.StreamFooter() else this.sorryPage("Customer code not found") endif return function AddCustomer this.qCustomer = new QUERY() this.qCustomer.database = this.db this.qCustomer.sql = "select * from customer.dbf" this.qCustomer.active = true this.loadFieldsFromArray(this.qCustomer.rowset.fields, true) /// Stream response page this.StreamHeader() this.StreamInvoice() this.StreamFooter() return
StreamInvoice
The steamInvoice procedure performs three main tasks. The output of each of these tasks is place in a separate row of a HTML table. First, we print customer information from the Customer table. When the streamInvoice() method is called, the GetCustomer or AddCustomer procedures have opened the Customer table and moved the row point the correct record. Therefore, the streamInvoice() method can print directly from that row.
Second, we print the shopping cart information. The procedure lists each of the items in the shopping cart. To stream this, we must open the Session table and set a filter (or setRange()) for the current sessionID.
Third, we need an HTML form in which the user can enter credit card information. In addition to data input fields, the form must contain a hidden field for the sessionID and another one for the CustomerID. The submit button on this form calls the PostOrder procedure.
function streamInvoice local f, c, qCart, nTotal, cValue f = this.qCustomer.rowset.fields qCart = new query() qCart.database = this.db qCart.sql = 'select * from session.dbf' qCart.active = true this.sessionID = this['sessionID'] qCart.rowset.filter = [sessionID = '] + this.sessionID + ['] c = qCart.rowset.fields qCust = this.qCustomer with (this.fOut) puts('<BODY BGCOLOR="#FFFFFF">') puts('<P><FORM ACTION="WebStore.exe" METHOD=POST>') puts('<Center><TABLE BORDER=1 bgcolor="#C0C0C0">') puts('<TR>') puts('<TD>') puts('<BR><Center><TABLE BORDER=1 bgcolor="#FFFFFF"> ') puts('<TR><TD colspan=2>') puts('<P><B>Customer Information</B>') puts('</TD></TR>') puts('<TR><TD WIDTH=110>') puts('<P>Name</P>') puts('</TD>') puts('<TD>') puts('<P>') cStr = this.GetFieldValue(qCust, "FirstName", false ) cStr += " " + this.GetFieldValue(qCust, "LastName", false ) puts( cStr + '</P></TD>') puts('</TR>') puts('<TR>') puts('<TD WIDTH=110>') puts('<P>Address</P>') puts('</TD>') puts('<TD>') puts('<P>') cStr = this.GetFieldValue(qCust, "Street1", false ) cStr += "<BR>" + this.GetFieldValue(qCust, "City", false ) cStr += ", " + this.GetFieldValue(qCust, "StateID", false ) cStr += " " + this.GetFieldValue(qCust, "Postal", false ) puts( cStr + '</P></TD>') puts('</TR>') puts('</TABLE></center><BR>') puts('</P>') puts('</TD>') puts('</TR>') puts('<TR>') puts('<TD>') ///////////////////////////////////////////////////////// /// /// Begin streaming the shopping cart table /// ///////////////////////////////////////////////////////// puts('<BR><Center><TABLE BORDER=1 bgcolor="#FFFFFF">') puts('<TR>') puts('<TD><P><B>Item</B></TD>') puts('<TD><P><B>Description</B></TD>') puts('<TD><P><B>Quantity</B></TD>') puts('<TD><P><B>Price</B>') puts('</TD></TR>') // Calc invoice total nTotal = 0 /// Loop for invoice items do puts('<TR>') puts('<TD><P> ') cValue = this.GetFieldValue(qCart, "ItemID", false ) puts("" + cValue + '</P></TD>') puts('<TD><P>') cValue = this.GetFieldValue(qCart, "Description", false ) puts( cValue + '</P></TD>') puts(' <TD><P>') cValue = this.GetFieldValue(qCart, "Qty", false ) puts( "" + cValue + '</P></TD>') nPrice = (c["Qty"].value ) * 6.5 puts('<TD>') puts('<P> $' + nPrice+ '</P>') puts('</TD>') puts('</TR>') nTotal += nPrice until not qCart.rowset.next() puts('<TR><TD colspan=4 align=Right>' + nTotal ) puts('</TD></TR>') puts('</TABLE></Center><BR>') //// END SHOPPING CART ////// puts('</TD>') puts('</TR>') puts('<TR>') puts('<TD>') ///////////////////////////////////////////////// /// /// Begin streaming payment info table /// ///////////////////////////////////////////////// puts('<BR><TABLE BORDER=1 bgcolor="#FFFFFF">') puts('<TR>') puts('<TD WIDTH=160 colspan=3>') puts('<P><B>Payment Infromation</B></P>') puts('<INPUT TYPE=hidden NAME="CustomerID" VALUE=') puts(ltrim(str(qCust.rowset.fields['CustomerID'].value)) + '>') puts('<INPUT TYPE=hidden NAME="sessionID" VALUE='+this['sessionID']+'>') puts('</TD></TR>') puts('<TR>') puts('<TD WIDTH=160>') puts('<P><INPUT TYPE=radio NAME="PayType" VALUE=VI>Visa') puts('<P><INPUT TYPE=radio NAME="PayType" VALUE=MC>Master Card') puts('<P><INPUT TYPE=radio NAME="PayType" VALUE=DN>Discover/Nova</P>') puts('</TD>') puts('<TD VALIGN=top WIDTH=94>') puts('<P ALIGN=right>Card Number') puts('<P ALIGN=right>Exp. Date</TD>') puts('<TD VALIGN=top>') puts('<P><INPUT TYPE=text NAME="CardNumber" VALUE="" SIZE=30></P>') puts('<P><INPUT TYPE=text NAME="CardExp" VALUE="" SIZE=30></P>') puts('</TD></TR></TABLE><BR>') puts('</TD></TR>') puts('<TR><TD>') puts('') puts('<CENTER><INPUT TYPE=submit NAME=PostOrder VALUE="Buy Now"></CENTER>') puts('</TD></TR></TABLE><BR>') puts('</FORM></Center>') // puts('</BODY>') endwith return true
PostOrder
The final procedure used in our WebStore application must finalize the order by posting the a new order in the Invoice table and copying the items in the Session table to the LineItem table. The program first opens a query for the Invoice table, and then appends the credit card information and the CustomerID. Also remember to add the current date so that an order date is appended to the table.
After this record is saved, we can store the new InvoiceID in a memory variable, open queries for the Session and LineItem tables and then, copy the shopping cart items from the session table to the lineitem table.
function PostOrder q = new QUERY() q.database = this.db q.sql = "select * from invoice.dbf" q.active = true /// Append new order to the Invoice table this.loadFieldsFromArray(q.rowset.fields, true) // Store Invoice ID to memory variable for // adding to the lineitem table nInvoiceID = q.rowset.fields['invoiceID'].value cSessionID = this["sessionID"] /// Open Session.dbf /// and set filter s = new query() s.database = this.db s.sql= "select * from session.dbf" s.active = true s.rowset.filter = [sessionID = '] + cSessionID + ['] /// Open Lineitem.dbf l = new QUERY() l.database = this.db l.sql = "select * from lineitem.dbf" l.active = true /// Append each item in Session.dbf (the Cart) /// to the lineitem table /// also append the Invoice ID s.rowset.first() do l.rowset.beginAppend() l.rowset.fields['InvoiceID'].value = nInvoiceID l.rowset.fields['ItemID'].value = s.rowset.fields['ItemID'].value l.rowset.fields['Qty'].value = s.rowset.fields['Qty'].value l.rowset.save() until not s.rowset.next() /// Stream response page /// Customer_information.htm this.StreamHeader() this.StreamBody('<Center><H2>Thank you for shopping with us</H2></center>') this.StreamFooter() return true
We send a simple "Thank You" message to the browser after the order is posted. However, it is common for shopping cart applications to provide a printable invoice of the order. If you would like to create such a page, add another method to the WebStore class and stream the desired details.