On this page, a member can define a list of named credit cards to be used during the checkout process.

As with other customized pages in the Pioneer Cycling store, we used the editValue map property to collect the credit card information. The Add Card button invokes the handleCreateNewCreditCard() method, which creates a credit card in the creditCards map property of the user profile using the information the user enters. The card is identified by a nickname. If the customer does not provide a nickname, the system automatically creates a nickname using an abbreviation for the credit card type appended by the credit card number.

To look up the credit card type abbreviation, we used a ResourceBundle. The ResourceBundle is used for internationalization and to map key/value pairs in a file.

The file B2CUserResources.properties is a java.util.ResourceBundle used by Java files specific to Pioneer Cycling. A ResourceBundle basically consists of strings that should be used in the Java code and user-friendly strings that map to each of those strings. For the credit card abbreviations, we use the actual creditcard type name (MasterCard, Visa, American_Express, Discover) to look up the abbreviation. This snippet from B2CUserResources.properties shows how the types and abbreviations of credit cards are stored.

# CreditCard abbreviations
MasterCard=MC
Visa=VISA
American_Express=AMEX
Discover=DISC

For security reasons, credit card numbers should never be displayed fully on the browser. To display credit cards to the user on pages such as the My Saved Credit Cards page, we used a special credit card tag converter that masks all digits but the last 4 with an ‘X’ or another character chosen by the site developer. (For example, a credit card with the number 4111 1111 1111 1111 would be displayed as XXXX XXXX XXXX 1111.) The creditcard converter is invoked in JSP by specifying a creditcard value to the converter attribute of the valueof tag as shown here:

<dsp:valueof converter="creditcard" param="element.creditCardNumber"/>

We also used the converter to display the credit card nickname because, if it were generated automatically, it would also contain the credit card number. In this case, the groupingsize property of the creditcard converter is specified as zero so that the nickname is not divided into groups of four characters:

<dsp:valueof converter="creditcard" groupingsize="4"
             param="element.creditCardNumber"/>

These are highlights of the handleCreateNewCreditCard() method, stressing the most important sections:

//------------ Submit: CreateNewCreditCard ------------
/**
 * Creates a new credit card using the entries entered in the editValue map
 * by the credit_card.jsp page
 *
 * @param pRequest the servlet's request
 * @param pResponse the servlet's response
 * @exception ServletException if there was an error while executing the code
 * @exception IOException if there was an error with servlet io
 */
public boolean handleCreateNewCreditCard(DynamoHttpServletRequest pRequest,
  DynamoHttpServletResponse pResponse) throws ServletException, IOException
{
  Profile profile = getProfile();
  MutableRepository repository = (MutableRepository) profile.getRepository();
  Map creditCards = (Map) profile.getPropertyValue("creditCards");
  ResourceBundle bundle = ResourceUtils.getBundle(RESOURCE_BUNDLE,
    getLocale(pRequest));

  // Identify credit card properties we need from user
  String[] cardProperties = getCardProperties();

  // Get editValue map, containing the credit card properties
  HashMap newCard = (HashMap) getEditValue();

  boolean isMissingField = false;
  // Verify all required fields entered before creating new card
  for (int i = 0; i < cardProperties.length; i++)
    {
      if (newCard.get(cardProperties[i]) == null ||
        ((String) newCard.get(cardProperties[i])).length() == 0)
          {
            generateFormException(MSG_MISSING_CC_PROPERTY, cardProperties[i],
            pRequest);
            isMissingField = true;
          }
    }
  if (isMissingField) return true;

  // Verify that card number and expiry date are valid
  if (!validateCreditCard(newCard, bundle))
    {
      // form exceptions added by validateCreditCard method
      return true;
    }

  // Check that the nickname is not already used for a credit card
  String cardNickname = (String) newCard.get("creditCardNickname");
  if( creditCards.get(cardNickname) != null )
    {
      String rawErrorStr = bundle.getString(MSG_DUPLICATE_CC_NICKNAME);
      String formattedErrorStr =
        (new MessageFormat(rawErrorStr)).format(new String[] {cardNickname});
          addFormException(new DropletFormException(formattedErrorStr,
          new String(getAbsoluteName() + "editValue.creditCardNickname"),
          MSG_DUPLICATE_CC_NICKNAME));
      return true;
    }

  try
    {
      // Create a repository item to be filled with stuff.
      MutableRepositoryItem card = repository.createItem("credit-card");

      // Copy values from the newCreditCard object
      for (int i = 0; i < cardProperties.length; i++)
        card.setPropertyValue(cardProperties[i], newCard.get(cardProperties[i]));

      // Set billing address
      card.setPropertyValue("billingAddress",
        profile.getPropertyValue("billingAddress"));

      // Insert card into the db
      repository.addItem(card);

      // Insert the credit card into the creditCards map
      // Did the user provide us with a name?
      String key = (String) newCard.get("creditCardNickname");
      if (key == null || key.trim().length() == 0)
      {
        // If no name, generate unique key
        // Use the credit card type (convert any spaces to _ so as to find it in
        // the bundle
        StringBuffer buffer = new StringBuffer((String)
          card.getPropertyValue("creditCardType"));
        for (int i = 0; i < buffer.length(); i++)
          {
            if (buffer.charAt(i) == ' ')
              buffer.setCharAt(i, '_');
          }
      // Generate the key as CARD-ABBREV + CardNumber
      String abbrev = bundle.getString(buffer.toString());
      if (abbrev == null) abbrev = ""; key = abbrev + (String)
        card.getPropertyValue("creditCardNumber");
      }

      // Set the new credit card
      creditCards.put(key, card);

      // empty out the map
      newCard.clear();
  }
  catch (RepositoryException repositoryExc)
  {
    generateFormException(MSG_ERR_CREATING_CC, repositoryExc, pRequest);
    if (isLoggingError())
      logError(repositoryExc);

      return redirectIfPossible(getCreateCardErrorURL(), pRequest, pResponse);
  }

  return redirectIfPossible(getCreateCardSuccessURL(), pRequest, pResponse);

}

Note that handleCreateNewCreditCard() calls the validateCreditCard() method, which does two things. First, it checks that the credit number is composed solely of digits and spaces. Second, it checks that the card has not expired.

validateCreditCard()

//------ Utility method: validate credit card number & expiration date ------
/**
 * Validate the credit card number entered and the expiration date
 * (must be later than today).
 *
 * @param card A hashmap containing the user-entered credit card data
 * @param bundle A ResourceBundle providing the error message text
 * @return true if the credit card is valid
 **/
protected boolean validateCreditCard(HashMap card, ResourceBundle bundle) {

  try {
   // 1. Check that the credit card number is composed solely of digits and spaces
    String cardNumber = (String) card.get("creditCardNumber");
    if (cardNumber == null) {
        throw new B2CProfileException(
           MSG_INVALID_CC,
           "The card number is a required field for a credit card entry",
           "creditCardNumber");
    }

    for (int i = 0 ; i < cardNumber.length(); i++) {
      char c = cardNumber.charAt(i);
      if ((c < '0' || c > '9') && c != ' ') {
        throw new B2CProfileException(
           MSG_INVALID_CC,
           "The card number must consist only of digits and spaces",
           "creditCardNumber");
      }
    }

    // 2. Get expiration month & year
    int cardExpirationYear = 0;
    int cardExpirationMonth = 0;

    java.util.Calendar now = java.util.Calendar.getInstance();
    int year = now.get(Calendar.YEAR);
    // convert month from 0-11 to 1-12
    int month = now.get(Calendar.MONTH) + 1;
    // Convert year and month to integer values
    try {
      cardExpirationYear = Integer.parseInt((String) card.get("expirationYear"));
    }
    catch (NumberFormatException exc) {
       throw new B2CProfileException(MSG_INVALID_CC,
                                     "The year of expiration must be specified",
                                     "expirationYear");
    }
    try {
     cardExpirationMonth = Integer.parseInt((String) card.get("expirationMonth"));
    }
    catch (NumberFormatException exc) {
       throw new B2CProfileException(MSG_INVALID_CC,
                                     "The month of expiration must be specified",
                                     "expirationMonth");
    }

    // 3. Check that the card has not expired
    if (cardExpirationYear < year) {
      throw new B2CProfileException(MSG_INVALID_CC,
         "The card you entered is past its expiration date", "expirationYear");
    }
    if (cardExpirationYear == year) {
      if (cardExpirationMonth < month) {
        throw new B2CProfileException(MSG_INVALID_CC,
           "The card you entered is past its expiration date", "expirationMonth");
      }
    }
    return true;
  }
  catch (B2CProfileException exc) {
    String rawErrorStr = bundle.getString(exc.getCode());
    String formattedErrorStr = exc.getMessage();
      (new MessageFormat(rawErrorStr)).format(new String[] {exc.getMessage()});
    addFormException(new DropletFormException(formattedErrorStr,
                                              new String(getAbsoluteName() +
                                                         "editValue." +
                                                          exc.getDescription()),
                                                          MSG_INVALID_CC));
    return false;
  }
}

You could easily override this method for more rigorous validation. You could check that certain credit card types have a specific number of digits; for example, Visa cards have sixteen digits. In addition, all credit cards are validated during the checkout process by the commerce pipeline. See the section on Additional Order Capture Information in the Order Processing chapter for more information.

 
loading table of contents...