As expected, you need to make several modifications to BalloonShop to enable the tax and shipping schemes outlined previously. You have two more database tables to add, Tax and Shipping, as well as modifications to make to the Orders table. You’ll need to add new stored procedures and make some modifications to existing ones. Some of the business tier classes need modifications to account for these changes, and the presentation tier must include a method for users to select a shipping method (the taxing scheme is selected automatically).
So, without further ado, let’s get started.
Database Modifications
In this section, you’ll add the new tables and modify the Orders table and stored procedures.
The Tax Table
The Tax table simply provides a number of tax options that are available, each of which has a name and a percentage tax rate. Table 13-2 shows the table structure that you’ll need to add.
These columns are not nullable. Figure 13-2 shows the data to add to this table.
Figure 13-2. Data for the Tax table
The Shipping Table
The Shipping table is also very simple. It provides a number of shipping options, each of which has a name, a cost, and an associated shipping region. Table 13-3 shows the table structure that you’ll need to add.
Table 13-2. The Tax Table
Column Name Column Type Description
TaxID int The ID of the tax option. This column is the primary key and should be configured as an identity so that it will be auto-numbered.
TaxType varchar(100) A text description of the tax option.
TaxPercentage float The percentage tax rate for this option.
C H A P T E R 1 3 ■ A D V A N C E D C U S T O M E R O R D E R S 503
These columns are not nullable. Figure 13-3 shows the data to add to this table.
Figure 13-3. Data for the Shipping table
Orders Table Modifications
The modifications to the Orders table are to associate an order with one entry each from the Tax and Shipping tables, as shown in Table 13-4.
CommerceLibOrderGetInfo Modifications
The existing CommerceLibOrderGetInfo stored procedure now needs to include the tax and shipping data for an order. The new stored procedure is as follows:
Table 13-3. The Shipping Table
Column Name Column Type Description
ShippingID int The ID of the shipping option. This column is the primary key and identity.
ShippingType varchar(100) A text description of the shipping option.
ShippingCost money The cost (to the customer) of the shipping option.
ShippingRegionID int The ID of the shipping region that this option applies to.
Table 13-4. Orders Table Modifications
Column Name Column Type Description
TaxID int The ID of the tax option to use for the order ShippingID int The ID of the shipping option to use for the order
8213592a117456a340854d18cee57603
ALTER PROCEDURE CommerceLibOrderGetInfo (@OrderID int)
AS
SELECT OrderID, DateCreated, DateShipped, Comments, Status, CustomerID, AuthCode, Reference,
Orders.ShippingID, ShippingType, ShippingCost, Orders.TaxID, TaxType, TaxPercentage FROM Orders
LEFT OUTER JOIN Tax ON Tax.TaxID = Orders.TaxID
LEFT OUTER JOIN Shipping ON Shipping.ShippingID = Orders.ShippingID WHERE OrderID = @OrderID
Here there are two joins to the Tax and Shipping tables, both of which are LEFT OUTER joins so that data will be retrieved from the Orders table regardless of a value of TaxID and ShippingID (to enable backward compatibility among other issues).
CreateCustomerOrder Modifications
You also need to modify CreateCustomerOrder so that a tax and a shipping option are added when an order is added. The modifications are as follows:
ALTER PROCEDURE CreateCustomerOrder (@CartID char(36),
@CustomerID uniqueidentifier, @ShippingID int,
@TaxID int) AS
/* Insert a new record into Orders */
DECLARE @OrderID int
INSERT INTO Orders (CustomerID, ShippingID, TaxID) VALUES (@CustomerID, @ShippingID, @TaxID)
/* Save the new Order ID */
SET @OrderID = @@IDENTITY
/* Add the order details to OrderDetail */
INSERT INTO OrderDetail
(OrderID, ProductID, ProductName, Quantity, UnitCost)
C H A P T E R 1 3 ■ A D V A N C E D C U S T O M E R O R D E R S 505
SELECT
@OrderID, Product.ProductID, Product.Name, ShoppingCart.Quantity, Product.Price FROM Product JOIN ShoppingCart
ON Product.ProductID = ShoppingCart.ProductID WHERE ShoppingCart.CartID = @CartID
/* Clear the shopping cart */
DELETE FROM ShoppingCart WHERE CartID = @CartID /* Return the Order ID */
SELECT @OrderID
The two new parameters to deal with are @ShippingID and @TaxID.
The CommerceLibShippingGetInfo Stored Procedure
You need to add a new stored procedure so that a list of shipping options associated with a shipping region can be obtained. The CommerceLibShippingGetInfo stored procedure achieves this:
CREATE PROCEDURE CommerceLibShippingGetInfo (@ShippingRegionID int)
AS
SELECT ShippingID, ShippingType, ShippingCost FROM Shipping
WHERE ShippingRegionID = @ShippingRegionID
Business Layer Modifications
To work with the new database tables and stored procedures, you need to make several changes to CommerceLibAccess.cs. You need to add two structs to represent tax and shipping options, TaxInfo and ShippingInfo. You also need to give access to shipping info based on shipping regions and modify CommerceLibOrderInfo to use the tax and shipping structs. You must modify CreateCommerceLibOrder in ShoppingCartAccess to configure tax and shipping for new orders as well.
The TaxInfo and ShippingInfo Structs
These structs use very simple code, which you can add to the top of CommerceLibAccess.cs:
/// <summary>
/// Wraps tax data /// </summary>
public struct TaxInfo {
public int TaxID;
public string TaxType;
public double TaxPercentage;
}
/// <summary>
/// Wraps shipping data /// </summary>
public struct ShippingInfo {
public int ShippingID;
public string ShippingType;
public double ShippingCost;
public int ShippingRegionId;
}
There’s not much to comment on here. The fields in the struct simply match up to the columns in the associated tables.
The GetShippingInfo Method
This method obtains a List<ShippingInfo> object containing shipping information for a shipping region. If it’s not there already, this code requires a reference to the System.Collections.Generic namespace in the file. Add this method to the CommerceLibAccess class:
public static List<ShippingInfo> GetShippingInfo(
int shippingRegionId) {
// get a configured DbCommand object
DbCommand comm = GenericDataAccess.CreateCommand();
// set the stored procedure name
comm.CommandText = "CommerceLibShippingGetInfo";
// create a new parameter
DbParameter param = comm.CreateParameter();
param.ParameterName = "@ShippingRegionId";
param.Value = shippingRegionId;
param.DbType = DbType.Int32;
comm.Parameters.Add(param);
// obtain the results
DataTable table = GenericDataAccess.ExecuteSelectCommand(comm);
List<ShippingInfo> result = new List<ShippingInfo>();
foreach (DataRow row in table.Rows) {
ShippingInfo rowData = new ShippingInfo();
rowData.ShippingID = int.Parse(row["ShippingId"].ToString());
rowData.ShippingType = row["ShippingType"].ToString();
rowData.ShippingCost =
double.Parse(row["ShippingCost"].ToString());
rowData.ShippingRegionId = shippingRegionId;
result.Add(rowData);
}
return result;
}
C H A P T E R 1 3 ■ A D V A N C E D C U S T O M E R O R D E R S 507
Here the ID of a shipping region is accepted as a parameter and used to access the CommerceLibShippingGetInfo stored procedure added earlier. The collection is assembled from row data.
CreateCommerceLibOrder Modifications
This method, in ShoppingCartAccess.cs, needs modifying as follows (again, a reference to System.Collections.Generic may be necessary):
public static string CreateCommerceLibOrder(int shippingId, int taxId)
{
// get a configured DbCommand object
DbCommand comm = GenericDataAccess.CreateCommand();
// set the stored procedure name
comm.CommandText = "CreateCustomerOrder";
// create parameters
DbParameter param = comm.CreateParameter();
param.ParameterName = "@CartID";
param.Value = shoppingCartId;
param.DbType = DbType.String;
param.Size = 36;
comm.Parameters.Add(param);
// create a new parameter param = comm.CreateParameter();
param.ParameterName = "@CustomerId";
param.Value =
Membership.GetUser(
HttpContext.Current.User.Identity.Name) .ProviderUserKey;
param.DbType = DbType.Guid;
param.Size = 16;
comm.Parameters.Add(param);
// create a new parameter param = comm.CreateParameter();
param.ParameterName = "@ShippingId";
param.Value = shippingId;
param.DbType = DbType.Int32;
comm.Parameters.Add(param);
// create a new parameter param = comm.CreateParameter();
param.ParameterName = "@TaxId";
param.Value = taxId;
param.DbType = DbType.Int32;
comm.Parameters.Add(param);
// return the result table
return GenericDataAccess.ExecuteScalar(comm);
}
Here two more parameters have been added to match up with the revised stored procedure CreateCustomerOrder.
CommerceLibOrderInfo Modifications
This class requires several modifications. First, you need to add two new fields for tax and shipping info:
public class CommerceLibOrderInfo {
...
public ShippingInfo Shipping;
public TaxInfo Tax;
Next, the constructor needs to be modified to extract this new data from the row returned by the CommerceLibOrderGetInfo stored procedure:
public CommerceLibOrderInfo(DataRow orderRow) {
...
CreditCard = new SecureCard(CustomerProfile.CreditCard);
OrderDetails =
CommerceLibAccess.GetOrderDetails(
orderRow["OrderID"].ToString());
// Get Shipping Data
if (orderRow["ShippingID"] != DBNull.Value && orderRow["ShippingType"] != DBNull.Value && orderRow["ShippingCost"] != DBNull.Value) {
Shipping.ShippingID =
Int32.Parse(orderRow["ShippingID"].ToString());
Shipping.ShippingType = orderRow["ShippingType"].ToString();
Shipping.ShippingCost =
double.Parse(orderRow["ShippingCost"].ToString());
} else {
Shipping.ShippingID = -1;
}
// Get Tax Data
if (orderRow["TaxID"] != DBNull.Value && orderRow["TaxType"] != DBNull.Value && orderRow["TaxPercentage"] != DBNull.Value)
C H A P T E R 1 3 ■ A D V A N C E D C U S T O M E R O R D E R S 509
{
Tax.TaxID = Int32.Parse(orderRow["TaxID"].ToString());
Tax.TaxType = orderRow["TaxType"].ToString();
Tax.TaxPercentage =
double.Parse(orderRow["TaxPercentage"].ToString());
} else {
Tax.TaxID = -1;
}
// set info properties Refresh();
}
Note here that checks are made for null values for tax and shipping information. If data isn’t found for tax information, TaxID will be set to -1. Similarly, no shipping data will result in ShippingID being -1. If all is well, these situations shouldn’t occur, but just in case they do (especially if you end up modifying the tax and shipping schemes), this will prevent an error from occurring.
Finally, the Refresh method needs to include tax and shipping costs in its calculation of total cost and in its creation of the OrderAsString field:
public void Refresh() {
// calculate total cost and set data StringBuilder sb = new StringBuilder();
TotalCost = 0.0;
foreach (CommerceLibOrderDetailInfo item in OrderDetails) {
sb.AppendLine(item.ItemAsString);
TotalCost += item.Subtotal;
}
// Add shipping cost
if (Shipping.ShippingID != -1) {
sb.AppendLine("Shipping: " + Shipping.ShippingType);
TotalCost += Shipping.ShippingCost;
}
// Add tax
if (Tax.TaxID != -1 && Tax.TaxPercentage != 0.0) {
double taxAmount = Math.Round(TotalCost * Tax.TaxPercentage, MidpointRounding.AwayFromZero) / 100.0;
sb.AppendLine("Tax: " + Tax.TaxType + ", $"
+ taxAmount.ToString());
TotalCost += taxAmount;
}
sb.AppendLine();
sb.Append("Total order cost: $");
sb.Append(TotalCost.ToString());
OrderAsString = sb.ToString();
...
}
The calculation of the tax amount involves some mathematical functionality from the System.Math class, but otherwise it’s all simple stuff.
Presentation Layer Modifications
Finally we come to the presentation layer. In fact, due to the changes we’ve made, the only changes to make here are to the checkout page.
Checkout.aspx Modifications
The .aspx page simply needs a means of selecting a shipping type prior to placing an order.
This can be achieved using a drop-down list:
<asp:Label ID="InfoLabel" runat="server" CssClass="InfoText" />
<br />
<br />
<span class="InfoText">Shipping type:
<asp:DropDownList ID="shippingSelection" runat="server" /></span>
<br />
<br />
<asp:Button ID="placeOrderButton" runat="server"
CssClass="ButtonText" Text="Place order"
OnClick="placeOrderButton_Click" />
</asp:Content>
Now you need to populate this list and/or hide it in the code behind.
Checkout.aspx.cs Modifications
The code behind for this page already checks to see whether an order can be placed in PopulateControls, based on whether a valid address and credit card have been entered. You can use this information to set the visibility of the new list control (shippingSelection) and populate the shipping option list accordingly. The code to modify is as follows:
8213592a117456a340854d18cee57603
C H A P T E R 1 3 ■ A D V A N C E D C U S T O M E R O R D E R S 511
private void PopulateControls() {
...
placeOrderButton.Visible = addressOK && cardOK;
shippingSelection.Visible = addressOK && cardOK;
// Populate shipping selection if (addressOK && cardOK) {
int shippingRegionId = int.Parse(Profile.ShippingRegion);
List<ShippingInfo> shippingInfoData =
CommerceLibAccess.GetShippingInfo(shippingRegionId);
foreach (ShippingInfo shippingInfo in shippingInfoData) {
shippingSelection.Items.Add(
new ListItem(shippingInfo.ShippingType, shippingInfo.ShippingID.ToString()));
}
shippingSelection.SelectedIndex = 0;
} }
This code uses the CommerceLibAccess.GetShippingInfo method added earlier, and creates ListItem controls dynamically for adding to the drop-down list. Note also that a valid selection in the list is ensured by setting the initially selected item in the drop-down list to the item with an index of 0, that is, the first entry in the list.
Next, you need to modify the placeOrderButton_Click event handler to create an order with tax and shipping option references. For the shipping option, you use the selected item in the drop-down list; for the tax option, you make an arbitrary selection based on the shipping region of the customer and the items you added earlier to the Tax table.
protected void placeOrderButton_Click(object sender, EventArgs e)
{
// Store the total amount because the cart // is emptied when creating the order
decimal amount = ShoppingCartAccess.GetTotalAmount();
// Get shipping ID or default to 0 int shippingId = 0;
try {
shippingId = int.Parse(shippingSelection.SelectedValue);
} catch { }
// Get tax ID or default to "No tax"
string shippingRegion =
(HttpContext.Current.Profile as ProfileCommon).ShippingRegion;
int taxId;
switch (shippingRegion) {
case "2":
taxId = 1;
break;
default:
taxId = 2;
break;
}
// Create the order and store the order ID string orderId =
ShoppingCartAccess.CreateCommerceLibOrder(shippingId, taxId);
// Redirect to the conformation page Response.Redirect("OrderPlaced.aspx");
}
Note that this is one of the most crucial pieces of code in this chapter. Here you’ll most likely make any modifications to the tax and shipping systems if you decide to add your own system, because choices are made on this page. The database and business layer changes are far more generic—although that’s not to say that modifications wouldn’t be necessary.