Support stored credit cards

When you create a Generic Payment gateway integration for credit card payments, you can enable the integration to allow logged-in shoppers to store card data in their profiles, and then later access the stored cards when they place orders.

Commerce does not store the complete credit card data. Instead, when a shopper stores a credit card, the payment processor associated with the gateway sends back a token that is stored with the shopper’s profile. When the shopper places orders in the future, he or she is given a list of saved cards and can select the one to use. The token associated with the selected card is then sent to the gateway, which retrieves the card data and sends the authorization request to the payment processor.

Tokens are stored on a per-gateway basis. This has important implications in an environment running multiple sites, because different sites may use different gateway extensions. For example, if you have two sites that require different gateway configurations for credit cards, you must create a separate gateway extension for each site. In this case, a card stored on one site will not automatically be stored for the other, because the gateways require separate tokens. (The shopper can store the same card on each site separately, though.) But if both sites use the same gateway configuration, they can share the same gateway extension. In this case, a card stored on one site will also be stored for the other.

Save and use stored credit cards

This section describes the workflow supported for storing and using saved credit cards. Note that you must implement this logic on your storefront; the widgets included with Commerce cannot handle stored cards by default.

Store a credit card when placing an order

The following is the logic you implement for storing a credit card when a shopper places an order:

  1. When a shopper pays for an order with a credit card that has not been previously saved by the current payment gateway, the checkout page provides an option for storing the card. The shopper selects the option to indicate that the card should be saved.
  2. When the shopper submits the order, the storefront invokes the createOrder or updateOrder endpoint of the Store API. The endpoint sends the order information to the Commerce server, along with information about the credit card, which includes a flag indicating the credit card should be saved.
  3. When the server receives the order submission, it invokes the Generic Payment webhook, which posts an authorization request to the gateway. The webhook request contains the credit card information that was sent to the server by the endpoint, including the flag indicating the credit card should be saved.
  4. The gateway saves the card information and generates a token to associate with the card. It sends the authorization request to the payment provider.
  5. The provider sends a response back to the gateway, which it passes on to the Commerce server along with the token.
  6. The Commerce server stores the token and passes the authorization response on to the storefront.

Store credit cards without placing an order

Commerce provides an Update Profile store API endpoint that lets you add and store customer credit cards as part of a customer Billing Profile without actually placing an order.

The name of the endpoint is addCreditCard. The URI for the endpoint is POST /ccstore/v1/current/creditCards/.

The endpoint can be used to invoke Add Card requests multiple times to let you add more than one card to a profile. Each new card is then stored against the profile. The inputs of this endpoint are:

  • cardType
  • nameOnCard
  • cardNumber
  • expiryMonth
  • expiryYear

This endpoint triggers the Generic Payment webhook for a Tokenize operation on the payment system. The payment system is expected to return a tokenized value of the card which is then saved against the profile. The endpoint then returns back a stored card ID.

Note: The ability to add credit cards directly to a shopper profile requires configuring the Generic Payment webhook and enabling 3D-Secure support by specifying the card3ds processor in the gateway extension's config.json file. Keep in mind that enabling 3D-Secure support does not mean 3D-Secure is used for all credit card transactions; it is used only for transactions that require it. See Incorporate 3D-Secure support for more information.

Use a stored card

The following describes the logic you implement to enable shoppers to use stored credit cards for order payments:

  1. When the shopper accesses the checkout page, the storefront calls the listCreditCards endpoint of the Store API. The Commerce server sends a response that includes information about the shopper’s stored credit cards. The storefront displays the cards and provides controls for selecting a card.
  2. The shopper selects the card to use. Depending on how the payment gateway is configured, the shopper may need to then provide the CVV.
  3. The shopper submits the order. The storefront invokes the createOrder or updateOrder endpoint of the Store API, which sends the order information to the Commerce server.
  4. When the server receives the order submission, it retrieves the token associated with the credit card. It invokes the Generic Payment webhook, which posts an authorization request to the gateway, along with the token.
  5. The gateway retrieves the card data and sends the authorization request to the payment provider. The provider sends a response back to the gateway, which it passes on to the Commerce server.
  6. The Commerce server passes the authorization response on to the storefront.
The payload of the authorization request includes several properties for tracking information about transactions involving a stored credit card:
  • originOfOrder -- A top-level property that indicates the source of the order. Valid values: default, scheduledOrder, contactCenter, punchout, purchaseOrder, bulk.
  • storedCardUsed -- A boolean property of the cardDetails object that is set to true for transactions involving a stored credit card.
  • additionalSavedCardProperties -- An object containing custom properties that are sent by the gateway in the webhook response when a card is stored. Each subsequent time the saved card is used, these properties are updated with values received in the associated webhook response.

The following example shows a portion of an authorization request that includes these properties:

{
...
"originOfOrder": "scheduledOrder",
"paymentId": "pg40429",
"cardDetails": {
  "expirationYear": "2029",
  "storedCardUsed": true,
  "number": "411111xxxxxx1111",
  "tokenExpiryDate": "2023-05-14 06:45:35.0",
  "expirationMonth": "11",
  "additionalSavedCardProperties":
   { 
     "prop1": "val1",
     "prop2": "val2", 
     "prop3": "val3" 
   }
,
"type": "visa",
"maskedCardNumber": "xxxxxxxxxxxx1111",
"token": "Token-1557816335786"
...
}

Manage cards

The createOrder endpoint’s payments array includes properties for specifying that the card used for the order should be saved, as well as additional properties for optionally making it the default card for the shopper and for specifying a nickname for the card (for example, WorkAMEX). In addition, the checkout page can call the listCreditCards endpoint to display a list of the cards stored for the current site, so a shopper will be able to select the card to use when placing a future order.

You can also use the listCreditCards endpoint to display the shopper’s stored cards on the Your Account page. In addition, there are several other endpoints that you can use to enable shoppers to update card details and delete cards.

Note that by default the listCreditCards endpoint returns only the shopper’s cards that are active and apply to the current gateway and current site. This ensures that the cards displayed on the checkout page are all valid for the order being placed. The endpoint also supports query parameters that can be used to return all of a shopper’s stored cards, regardless of site, gateway, or active status. These parameters should be used on the Your Account page, as shown in Create a saved credit card widget.

Configure the payment gateway integration to support stored cards

To configure a payment gateway integration that supports storing credit cards, the config.json file in the gateway extension can include these settings:

  • isCVVRequiredForSavedCards – A boolean indicating whether the shopper must supply the CVV when using a stored card. Default is true.
  • enabledForScheduledOrders – A boolean indicating whether the gateway supports using stored cards as payment for scheduled orders. Default is true.
  • isCVVRequiredForScheduledOrders – A boolean indicating whether the shopper must supply the CVV for each instance of a scheduled order. Default is false, which allows the instances to be processed without the CVV. See Use stored credit cards for scheduled orders for more information.

Note: Because the scheduler runs on the store server, if you have an Oracle Commerce Agent Console configuration, you should configure the gateway so that both the storefront and the agent instances use the same value for the isCVVRequiredForScheduledOrders flag. This prevents the settings used in the storefront from overwriting the scheduled order settings used in the agent environment.

Use stored credit cards for scheduled orders

A shopper can use a stored credit card to pay for scheduled orders. This provides an alternative to sending an invoice to the shopper after each order is placed. Note that the credit card must have already been stored before creating the scheduled order. The ID of the selected credit card is retained on the scheduled order, and is used to retrieve the token that is sent when processing an instance of the order.

Submission of the orders depends on whether the shopper is required to take further action, such as supplying a CVV or 3D-Secure login credentials. If no shopper intervention is required, order instances are submitted automatically in the background, and the shopper is sent an email indicating the order has been submitted. To avoid the need for shopper intervention, you can set the payment gateway’s isCVVRequiredForScheduledOrders to false. This setting allows scheduled orders to be processed without the CVV.

If the shopper is required to take further action, then when an instance of the order is created, it is placed in the PENDING_PAYMENT state and an email is sent to the shopper about the action required. Similarly, if a card-related problem occurs (for example, the card has been deleted or has expired), the order instance moves to the PENDING_PAYMENT state and an email is sent to the shopper.

After receiving an email indicating further action is required, the shopper can access the order and do any of the following:

  • Enter any required card information (for example, update the expiration date) and submit the order.
  • Change the payment method and submit the order. Note that the payment method change applies only to this order instance; future instances of the order still use the credit card associated with the order.
  • Cancel the order instance.

Note that for a scheduled order, the value of the orderId property in the Generic Payment webhook authorization request is the ID for the individual order instance, not for the order template. Also, the value of the originOfOrder property in the request is set to scheduledOrder.

Use stored credit cards for orders requiring approval

An account-based commerce shopper can use a stored credit card to pay for an order that requires approval. The card must have already been stored before creating the order. The shopper’s credit card information cannot be seen by delegated administrators or approvers.

Once an order has been created and approved, submission of the order depends on whether the shopper is required to take further action, such as supplying a CVV or 3D-Secure login credentials. If no shopper intervention is required, the order is submitted upon approval, and the shopper is sent an email indicating the order has been submitted.

If the shopper is required to take further action, then once the order is approved, it is placed in the PENDING_PAYMENT state and an email is sent to the shopper about the action required. Similarly, if a card-related problem occurs (for example, the card has been deleted or has expired), the order instance moves to the PENDING_PAYMENT state and an email is sent to the shopper.

After receiving an email indicating further action is required, the shopper can access the order and do any of the following:

  • Enter any required card information (for example, update the expiration date) and submit the order.
  • Change the payment method and submit the order.
  • Cancel the order instance.

If an order is rejected by an approver, there is no effect on the stored credit card. It remains available for use with other orders.

Customize your storefront to support stored cards

To add support for stored credit cards to your storefront, you must customize some of the widgets. This section describes the fields in the credit card view model that enable access to stored cards, and provides guidance for creating a new widget for managing stored cards on the Your Account page, as well as for customizing the Split Payments widget on the checkout page to enable saving and retrieving stored cards.

View model support for stored cards

The credit card view model includes several fields for working with stored credit cards:

  • nickname -- Stores a nickname supplied by the shopper to identify the card.
  • isSavedCard – A boolean used to indicate whether a card has been stored.
  • saveCard – A boolean used when the shopper enters a new card for an order, indicating whether the card should be saved.
  • setAsDefault – A boolean that indicates whether the card is the default card.
  • isCVVRequiredForSavedCards – A boolean whose value is set from the gateway property of the same name. If true, the shopper must supply the CVV when using a stored card.

Modify the Split Payments widget

To support storing credit cards, modify the display template of the Split Payments widget to add checkboxes for setting the saveCard and setAsDefault properties of the credit card view model. In addition, you will need to make changes to the widget’s JavaScript to add the ability to select a previously stored card to pay for an order.

The following example shows a function which calls the listCreditCards endpoint to retrieve stored cards. It takes the results from this REST call, and for each credit card returned it calls the view model’s populateData() function to create a credit card object. Each credit card object is stored as an entry in an observableArray named allCreditCards:

getCreditCardsForProfile: function() {
   var widget = this;
   var inputData = {};
   var url = "listCreditCards";
   var maskedNumberRegex = /\d(?=\d{4})/g;
   var maskedSymbol = "*";
   ccRestClient.request(url, inputData,
       function(data){
       data.creditCards=data.items;
         for (var i = 0; i < data.creditCards.length; i++) {
           var creditCard = widget.paymentsContainer().createPaymentGroup(
             CCConstants.CARD_PAYMENT_TYPE);
           creditCard.populateData(data.creditCards[i]);
           creditCard.isSavedCard(true);
           widget.allCreditCards.push(creditCard);
         }
         if (data.creditCards.length > 0) {
           widget.resetSelectedSavedCardId();
           widget.allCreditCards()[0].amount.subscribe(function(newVal) {
             console.log(newVal);
           });
     }
       },
       function(data) {
         console.log("Error while retrieving the credit cards");
    });
}

The widget includes fields named selectedSavedCardId (to hold the ID of the selected card) and orderDefaultSavedCardId (to hold the ID of the default card). The onLoad() function contains code that sets the initial values of these fields:

onLoad: function(widget) {
   widget.resetSelectedSavedCardId = function() {
      widget.selectedSavedCardId(null);
      for (var i = 0; i < widget.allCreditCards().length; i++) {
        widget.allCreditCards()[i].resetCardCvv();
        widget.allCreditCards()[i].cardCVV.isModified(false);
        if (widget.orderDefaultSavedCardId ==
          widget.allCreditCards()[i].savedCardId()) {
          widget.selectedSavedCardId(widget.orderDefaultSavedCardId);
          break;
        }
      }
      if (widget.selectedSavedCardId() == null) {
        for (var i = 0; i < widget.allCreditCards().length; i++) {
          if (widget.allCreditCards()[i].isDefault() == true) {
            widget.selectedSavedCardId(widget.allCreditCards()[i].savedCardId());
            break;
          }
        }
      }
      if (widget.selectedSavedCardId() == null &&
        widget.allCreditCards().length>0) {
        widget.selectedSavedCardId(widget.allCreditCards()[0].savedCardId());
      }
    }

    widget.addCardToPaymentViewModel = function() {
      for (var i = 0; i < widget.allCreditCards().length; i++) {
        if (widget.selectedSavedCardId() ==
          widget.allCreditCards()[i].savedCardId()) {
          var newCard = widget.paymentsContainer().createPaymentGroup(
          CCConstants.CARD_PAYMENT_TYPE)
          newCard.populateData(ko.mapping.toJS(widget.allCreditCards()[i]));
          newCard.cardCVV(widget.allCreditCards()[i].cardCVV());
          newCard.isSavedCard(true);
          if (widget.allCreditCards()[i].cardCVV() === undefined) {
                widget.allCreditCards()[i].cardCVV.isModified(true);
          }
          widget.paymentViewModel(newCard);
        }
      }
    };
...
}

The widget’s beforeAppear() function clears the values of these fields and calls the getCreditCardsForProfile() function to repopulate the fields with the current data:

beforeAppear: function (page) {
   var widget = this;
   widget.orderDefaultSavedCardId = null;
   widget.allCreditCards.removeAll();
   widget.getCreditCardsForProfile();
...
}

Create a saved credit card widget

In addition to modifying the Split Payments widget as described above, you will need to create a new widget for displaying and modifying saved credit cards on the Your Account page.

This widget includes a function called getCreditCardsForProfile() that is called by the beforeAppear() function. This is similar to the getCreditCardsForProfile() function in the updated Split Payments widget, except that when it calls the listCreditCards endpoint, it uses the allCards=true, allGateways=true, and allSites=true query parameters so that all of the shopper’s saved credit cards are displayed:

getCreditCardsForProfile: function() {
  var widget = this;
  var inputData = {"allCards":true, "allGateways":true, "allSites":true};
  var url = "listCreditCards";
  var maskedNumberRegex = /\d(?=\d{4})/g;
  var maskedSymbol = "*";
  CCRestClient.request(url, inputData,
      function(data){
      data.creditCards=data.items;
        for (var i = 0; i < data.creditCards.length; i++) {
          var creditCard = new CreditCard();
          creditCard.populateData(data.creditCards[i]);
          creditCard.isSavedCard(true);
          widget.allCreditCards.push(creditCard);
        }
      },
      function(data) {
        console.log("Error while retrieving the credit cards");
      });
}

The widget should also include a function that calls the updateCreditCard endpoint to modify card nicknames and to change which card is the default, and a function that calls the removeCreditCard endpoint to delete a credit card.