Bug 1427939 - Use total and any additionalDisplayItems when a modifier matches the payment method. draft
authorSam Foster <sfoster@mozilla.com>
Fri, 09 Feb 2018 16:30:26 -0800
changeset 760728 fccc5cff803d55b83f79019ade4e9898e182c189
parent 760727 0031ceb8b50d0c27c2d151f53af2d52370b52092
push id100738
push userbmo:sfoster@mozilla.com
push dateWed, 28 Feb 2018 00:54:35 +0000
bugs1427939
milestone60.0a1
Bug 1427939 - Use total and any additionalDisplayItems when a modifier matches the payment method. * WIP: A paymentRequestHelpers library, with a method to get the right modifier. * Use selectedPaymentMethod from state store. * Ensure saved cards always present with a paymentMethod property. * Update and add tests for the modifier case for the main total in PaymentDialog * Update and add tests for handling modifiers and additionalDisplayItems in the OrderDetails component MozReview-Commit-ID: FmlovZjP0t1
toolkit/components/payments/jar.mn
toolkit/components/payments/res/containers/order-details.js
toolkit/components/payments/res/containers/payment-dialog.js
toolkit/components/payments/res/debugging.js
toolkit/components/payments/res/paymentRequest.xhtml
toolkit/components/payments/res/paymentRequestHelpers.js
toolkit/components/payments/test/browser/browser_total.js
toolkit/components/payments/test/mochitest/mochitest.ini
toolkit/components/payments/test/mochitest/test_order_details.html
toolkit/components/payments/test/mochitest/test_payment_dialog.html
--- a/toolkit/components/payments/jar.mn
+++ b/toolkit/components/payments/jar.mn
@@ -13,11 +13,12 @@ toolkit.jar:
     res/payments                                      (res/paymentRequest.*)
     res/payments/components/                          (res/components/*.css)
     res/payments/components/                          (res/components/*.js)
     res/payments/containers/                          (res/containers/*.js)
     res/payments/containers/                          (res/containers/*.css)
     res/payments/debugging.css                        (res/debugging.css)
     res/payments/debugging.html                       (res/debugging.html)
     res/payments/debugging.js                         (res/debugging.js)
+    res/payments/paymentRequestHelpers.js             (res/paymentRequestHelpers.js)
     res/payments/mixins/                              (res/mixins/*.js)
     res/payments/PaymentsStore.js                     (res/PaymentsStore.js)
     res/payments/vendor/                              (res/vendor/*)
--- a/toolkit/components/payments/res/containers/order-details.js
+++ b/toolkit/components/payments/res/containers/order-details.js
@@ -1,13 +1,13 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
-/* global PaymentStateSubscriberMixin, PaymentRequest */
+/* global PaymentStateSubscriberMixin, PaymentRequest, paymentRequestHelpers */
 
 "use strict";
 
 /**
  * <order-details></order-details>
  */
 
 class OrderDetails extends PaymentStateSubscriberMixin(HTMLElement) {
@@ -51,29 +51,48 @@ class OrderDetails extends PaymentStateS
       row.amountValue = item.amount.value;
       row.amountCurrency = item.amount.currency;
       fragment.appendChild(row);
     }
     listEl.appendChild(fragment);
     return listEl;
   }
 
+  _getTotalItem(state) {
+    let methodId = state.selectedPaymentCard;
+    if (methodId) {
+      let modifier = paymentRequestHelpers.getModifierForPaymentMethod(state.request, methodId);
+      if (modifier && modifier.hasOwnProperty("total")) {
+        return modifier.total;
+      }
+    }
+    return state.request.paymentDetails.totalItem;
+  }
+
+  _getAdditionalDisplayItems(state) {
+    let methodId = state.selectedPaymentCard;
+    let modifier = paymentRequestHelpers.getModifierForPaymentMethod(state.request, methodId);
+    if (modifier && modifier.additionalDisplayItems) {
+      return modifier.additionalDisplayItems;
+    }
+    return [];
+  }
+
   render(state) {
-    let request = state.request;
-    let totalItem = request.paymentDetails.totalItem;
+    let totalItem = this._getTotalItem(state);
 
     OrderDetails._emptyList(this.mainItemsList);
     OrderDetails._emptyList(this.footerItemsList);
 
-    let mainItems = OrderDetails._getMainListItems(request);
+    let mainItems = OrderDetails._getMainListItems(state);
     if (mainItems.length) {
       OrderDetails._populateList(this.mainItemsList, mainItems);
     }
 
-    let footerItems = OrderDetails._getFooterListItems(request);
+    let footerItems = OrderDetails._getFooterListItems(state);
     if (footerItems.length) {
       OrderDetails._populateList(this.footerItemsList, footerItems);
     }
 
     this.totalAmountElem.value = totalItem.amount.value;
     this.totalAmountElem.currency = totalItem.amount.currency;
   }
 
@@ -84,28 +103,38 @@ class OrderDetails extends PaymentStateS
    *
    * @param {object} item - Data representing a PaymentItem
    * @returns {boolean}
    */
   static isFooterItem(item) {
     return item.type == "tax";
   }
 
-  static _getMainListItems(request) {
+  static _getMainListItems(state) {
+    let request = state.request;
     let items = request.paymentDetails.displayItems;
     if (Array.isArray(items) && items.length) {
       let predicate = item => !OrderDetails.isFooterItem(item);
       return request.paymentDetails.displayItems.filter(predicate);
     }
     return [];
   }
 
-  static _getFooterListItems(request) {
+  static _getFooterListItems(state) {
+    let request = state.request;
     let items = request.paymentDetails.displayItems;
+    let footerItems = [];
+    let methodId = state.selectedPaymentCard;
+    if (methodId) {
+      let modifier = paymentRequestHelpers.getModifierForPaymentMethod(state.request, methodId);
+      if (modifier && Array.isArray(modifier.additionalDisplayItems)) {
+        footerItems.push(...modifier.additionalDisplayItems);
+      }
+    }
     if (Array.isArray(items) && items.length) {
       let predicate = OrderDetails.isFooterItem;
-      return request.paymentDetails.displayItems.filter(predicate);
+      footerItems.push(...request.paymentDetails.displayItems.filter(predicate));
     }
-    return [];
+    return footerItems;
   }
 }
 
 customElements.define("order-details", OrderDetails);
--- a/toolkit/components/payments/res/containers/payment-dialog.js
+++ b/toolkit/components/payments/res/containers/payment-dialog.js
@@ -1,13 +1,13 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
-/* global PaymentStateSubscriberMixin, paymentRequest */
+/* global PaymentStateSubscriberMixin, paymentRequest, paymentRequestHelpers */
 
 "use strict";
 
 /**
  * <payment-dialog></payment-dialog>
  */
 
 class PaymentDialog extends PaymentStateSubscriberMixin(HTMLElement) {
@@ -177,22 +177,33 @@ class PaymentDialog extends PaymentState
         this.changeShippingOption(state.selectedShippingOption);
       }
     }
 
     this._cachedState.selectedShippingAddress = state.selectedShippingAddress;
     this._cachedState.selectedShippingOption = state.selectedShippingOption;
   }
 
+  _getTotalItem(state) {
+    let methodId = state.selectedPaymentCard;
+    if (methodId) {
+      let modifier = paymentRequestHelpers.getModifierForPaymentMethod(state.request, methodId);
+      if (modifier && modifier.hasOwnProperty("total")) {
+        return modifier.total;
+      }
+    }
+    return state.request.paymentDetails.totalItem;
+  }
+
   render(state) {
     let request = state.request;
     let paymentDetails = request.paymentDetails;
     this._hostNameEl.textContent = request.topLevelPrincipal.URI.displayHost;
 
-    let totalItem = paymentDetails.totalItem;
+    let totalItem = this._getTotalItem(state);
     let totalAmountEl = this.querySelector("#total > currency-amount");
     totalAmountEl.value = totalItem.amount.value;
     totalAmountEl.currency = totalItem.amount.currency;
 
     this._orderDetailsOverlay.hidden = !state.orderDetailsShowing;
     this._errorText.textContent = paymentDetails.error;
     let paymentOptions = request.paymentOptions;
     for (let element of this._shippingRelatedEls) {
--- a/toolkit/components/payments/res/debugging.js
+++ b/toolkit/components/payments/res/debugging.js
@@ -6,17 +6,21 @@ const paymentDialog = window.parent.docu
 // The requestStore should be manipulated for most changes but autofill storage changes
 // happen through setStateFromParent which includes some consistency checks.
 const requestStore = paymentDialog.requestStore;
 
 let REQUEST_1 = {
   tabId: 9,
   topLevelPrincipal: {URI: {displayHost: "tschaeff.github.io"}},
   requestId: "3797081f-a96b-c34b-a58b-1083c6e66e25",
-  paymentMethods: [],
+  paymentMethods: [
+    {
+      supportedMethods: "basic-card",
+    },
+  ],
   paymentDetails: {
     id: "",
     totalItem: {label: "Demo total", amount: {currency: "EUR", value: "1.00"}, pending: false},
     displayItems: [
       {
         label: "Square",
         amount: {
           currency: "USD",
@@ -107,17 +111,41 @@ let REQUEST_2 = {
         label: "Slow",
         amount: {
           currency: "USD",
           value: 10,
         },
         selected: false,
       },
     ],
-    modifiers: null,
+    modifiers: [
+      {
+        supportedMethods: "basic-card",
+        total: {
+          label: "Total",
+          amount: {
+            currency: "CAD",
+            value: "28.75",
+          },
+          pending: false,
+        },
+        additionalDisplayItems: [
+          {
+            label: "Credit card fee",
+            amount: {
+              currency: "CAD",
+              value: "1.50",
+            },
+          },
+        ],
+        data: {
+          supportedTypes: "credit",
+        },
+      },
+    ],
     error: "",
   },
   paymentOptions: {
     requestPayerName: false,
     requestPayerEmail: false,
     requestPayerPhone: false,
     requestShipping: true,
     shippingType: "shipping",
--- a/toolkit/components/payments/res/paymentRequest.xhtml
+++ b/toolkit/components/payments/res/paymentRequest.xhtml
@@ -29,16 +29,17 @@
   <link rel="stylesheet" href="containers/order-details.css"/>
 
   <script src="vendor/custom-elements.min.js"></script>
 
   <script src="PaymentsStore.js"></script>
 
   <script src="mixins/ObservedPropertiesMixin.js"></script>
   <script src="mixins/PaymentStateSubscriberMixin.js"></script>
+  <script src="paymentRequestHelpers.js"></script>
 
   <script src="components/currency-amount.js"></script>
   <script src="containers/order-details.js"></script>
   <script src="components/payment-details-item.js"></script>
   <script src="components/rich-select.js"></script>
   <script src="components/rich-option.js"></script>
   <script src="components/address-option.js"></script>
   <script src="components/shipping-option.js"></script>
new file mode 100644
--- /dev/null
+++ b/toolkit/components/payments/res/paymentRequestHelpers.js
@@ -0,0 +1,29 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * Define common helpers for working with payment requests
+ */
+
+/* exported paymentRequestHelpers */
+
+
+var paymentRequestHelpers = {
+  getModifierForPaymentMethod(request, methodId) {
+    if (methodId !== "basic-card") {
+      throw new Error(`${methodId} is not a supported payment method`);
+    }
+    let modifiers = request.paymentDetails.modifiers;
+    if (!modifiers || !modifiers.length) {
+      return null;
+    }
+    let modifier = modifiers.find(m => {
+      // take the first matching modifier
+      // TODO: match on supportedTypes and supportedNetworks
+      return (m.supportedMethods == "basic-card");
+    });
+    return modifier || null;
+  },
+};
+
--- a/toolkit/components/payments/test/browser/browser_total.js
+++ b/toolkit/components/payments/test/browser/browser_total.js
@@ -8,8 +8,22 @@ add_task(async function test_total() {
        "Check total currency amount");
   };
   const args = {
     methodData: [PTU.MethodData.basicCard],
     details: PTU.Details.total60USD,
   };
   await spawnInDialogForMerchantTask(PTU.ContentTasks.createRequest, testTask, args);
 });
+
+add_task(async function test_modified_total() {
+  const testTask = ({methodData, details}) => {
+    // We expect the *first* payment method to be selected when applying modifiers
+    is(content.document.querySelector("#total > currency-amount").textContent,
+       "$3.50",
+       "Check modified total currency amount");
+  };
+  const args = {
+    methodData: [PTU.MethodData.bobPay, PTU.MethodData.basicCard],
+    details: PTU.Details.bobPayPaymentModifier,
+  };
+  await spawnInDialogForMerchantTask(PTU.ContentTasks.createRequest, testTask, args);
+});
--- a/toolkit/components/payments/test/mochitest/mochitest.ini
+++ b/toolkit/components/payments/test/mochitest/mochitest.ini
@@ -1,15 +1,16 @@
 [DEFAULT]
 prefs =
    dom.webcomponents.customelements.enabled=false
 support-files =
    ../../../../../testing/modules/sinon-2.3.2.js
    ../../res/paymentRequest.css
    ../../res/paymentRequest.xhtml
+   ../../res/paymentRequestHelpers.js
    ../../res/PaymentsStore.js
    ../../res/components/currency-amount.js
    ../../res/components/address-option.js
    ../../res/components/address-option.css
    ../../res/components/basic-card-option.js
    ../../res/components/basic-card-option.css
    ../../res/components/payment-details-item.js
    ../../res/components/rich-option.js
--- a/toolkit/components/payments/test/mochitest/test_order_details.html
+++ b/toolkit/components/payments/test/mochitest/test_order_details.html
@@ -7,16 +7,17 @@
   <meta charset="utf-8">
   <title>Test the order-details component</title>
   <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
   <script type="application/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
   <script src="payments_common.js"></script>
   <script src="custom-elements.min.js"></script>
   <script src="PaymentsStore.js"></script>
   <script src="PaymentStateSubscriberMixin.js"></script>
+  <script src="paymentRequestHelpers.js"></script>
   <script src="order-details.js"></script>
   <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
 
   <template id="order-details-template">
     <ul class="main-list"></ul>
     <ul class="footer-items-list"></ul>
 
     <div class="details-total">
@@ -41,16 +42,17 @@
 /* import-globals-from ../../res/mixins/PaymentStateSubscriberMixin.js */
 /* import-globals-from ../../res/containers/order-details.js */
 
 let orderDetails = document.querySelector("order-details");
 let emptyState = requestStore.getState();
 
 function setup() {
   let initialState = deepClone(emptyState);
+  initialState.selectedPaymentCard = "basic-card";
   requestStore.setState(initialState);
 }
 
 function deepClone(obj) {
   return JSON.parse(JSON.stringify(obj));
 }
 
 add_task(async function isFooterItem() {
@@ -69,32 +71,36 @@ add_task(async function test_initial_sta
   setup();
   is(orderDetails.mainItemsList.childElementCount, 0, "main items list is initially empty");
   is(orderDetails.footerItemsList.childElementCount, 0, "footer items list is initially empty");
   is(orderDetails.totalAmountElem.value, 0, "total amount is 0");
 });
 
 add_task(async function test_list_population() {
   setup();
-  let paymentDetails = requestStore.getState().request.paymentDetails;
+  let state = requestStore.getState();
+  let request = state.request;
+  let paymentDetails = request.paymentDetails;
   paymentDetails.displayItems = [
     {
       label: "One",
       amount: { currency: "USD", value: "5" },
     },
     {
       label: "Two",
       amount: { currency: "USD", value: "6" },
     },
     {
       label: "Three",
       amount: { currency: "USD", value: "7" },
     },
   ];
-  requestStore.setState({ paymentDetails });
+  Object.assign(request, { paymentDetails });
+  requestStore.setState(state);
+
   await asyncElementRendered();
   is(orderDetails.mainItemsList.childElementCount, 3, "main items list has correct # children");
   is(orderDetails.footerItemsList.childElementCount, 0, "footer items list has 0 children");
 
   paymentDetails.displayItems = [
     {
       label: "Levy",
       type: "tax",
@@ -104,29 +110,89 @@ add_task(async function test_list_popula
       label: "Item",
       amount: { currency: "USD", value: "6" },
     },
     {
       label: "Thing",
       amount: { currency: "USD", value: "7" },
     },
   ];
-  requestStore.setState({ paymentDetails });
+  Object.assign(request, { paymentDetails });
+  requestStore.setState({ request });
   await asyncElementRendered();
 
   is(orderDetails.mainItemsList.childElementCount, 2, "main list has correct # children");
   is(orderDetails.footerItemsList.childElementCount, 1, "footer list has correct # children");
 });
 
+add_task(async function test_additionalDisplayItems() {
+  setup();
+  let paymentMethods = ["basic-card"];
+  let state = requestStore.getState();
+  let request = state.request;
+  let paymentDetails = request.paymentDetails;
+
+  paymentDetails.modifiers = [{
+    additionalDisplayItems: [
+      {
+        label: "Card fee",
+        amount: { currency: "USD", value: "1.50" },
+      },
+    ],
+    supportedMethods: "basic-card",
+    total: {
+      label: "Total due",
+      amount: { currency: "USD", value: "3.50" },
+    },
+  }];
+
+  Object.assign(request, { paymentMethods, paymentDetails });
+  requestStore.setState(state);
+  await asyncElementRendered();
+
+  is(orderDetails.mainItemsList.childElementCount, 0,
+     "main list added 0 children from additionalDisplayItems");
+  is(orderDetails.footerItemsList.childElementCount, 1,
+     "footer list added children from additionalDisplayItems");
+});
+
+
 add_task(async function test_total() {
-  let paymentDetails = requestStore.getState().request.paymentDetails;
+  setup();
+  let request = requestStore.getState().request;
+  let paymentDetails = request.paymentDetails;
   paymentDetails.totalItem = { label: "foo", amount: { currency: "JPY", value: 5 }};
-  requestStore.setState({ paymentDetails });
+
+  Object.assign(request, { paymentDetails });
+  requestStore.setState({ request });
   await asyncElementRendered();
 
   is(orderDetails.totalAmountElem.value, 5, "total amount gets updated");
   is(orderDetails.totalAmountElem.currency, "JPY", "total currency gets updated");
 });
 
+add_task(async function test_modified_total() {
+  setup();
+  let paymentMethods = ["basic-card"];
+  let state = requestStore.getState();
+  let request = state.request;
+  let paymentDetails = request.paymentDetails;
+  paymentDetails.totalItem = { label: "foo", amount: { currency: "JPY", value: 5 }};
+  paymentDetails.modifiers = [{
+    supportedMethods: "basic-card",
+    total: {
+      label: "Total due",
+      amount: { currency: "USD", value: 3.5 },
+    },
+  }];
+  Object.assign(request, { paymentMethods, paymentDetails });
+  info("state: ", JSON.stringify(state, null, 2));
+  requestStore.setState(state);
+  await asyncElementRendered();
+
+  is(orderDetails.totalAmountElem.value, 3.5, "total amount uses modifier total");
+  is(orderDetails.totalAmountElem.currency, "USD", "total currency uses modifier currency");
+});
+
 </script>
 
 </body>
 </html>
--- a/toolkit/components/payments/test/mochitest/test_payment_dialog.html
+++ b/toolkit/components/payments/test/mochitest/test_payment_dialog.html
@@ -9,16 +9,17 @@ Test the payment-dialog custom element
   <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
   <script type="application/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
   <script src="sinon-2.3.2.js"></script>
   <script src="payments_common.js"></script>
   <script src="custom-elements.min.js"></script>
   <script src="PaymentsStore.js"></script>
   <script src="ObservedPropertiesMixin.js"></script>
   <script src="PaymentStateSubscriberMixin.js"></script>
+  <script src="paymentRequestHelpers.js"></script>
   <script src="payment-dialog.js"></script>
   <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
   <link rel="stylesheet" type="text/css" href="paymentRequest.css"/>
 </head>
 <body>
   <p id="display">
     <iframe id="templateFrame" src="paymentRequest.xhtml" width="0" height="0"></iframe>
   </p>