diff --git a/imports/plugins/core/orders/client/components/invoice.js b/imports/plugins/core/orders/client/components/invoice.js index 160ff087cb7..c834941f998 100644 --- a/imports/plugins/core/orders/client/components/invoice.js +++ b/imports/plugins/core/orders/client/components/invoice.js @@ -1,36 +1,76 @@ import React, { Component } from "react"; import PropTypes from "prop-types"; +import moment from "moment"; import { formatPriceString } from "/client/api"; -import { Translation } from "/imports/plugins/core/ui/client/components"; -import DiscountList from "/imports/plugins/core/discounts/client/components/list"; - +import { Components, registerComponent } from "@reactioncommerce/reaction-components"; +import LineItems from "./lineItems"; +import InvoiceActions from "./invoiceActions"; + +/** + * @summary React component for displaying the `invoice` section on the orders sideview + * @param {Object} props - React PropTypes + * @property {Object} invoice - An object representing an invoice + * @property {Object} order - An object representing an order + * @property {Bool} discounts - A boolean indicating whether discounts are enabled + * @property {Array} refunds - An array/list of refunds + * @property {Bool} paymentCaptured - A boolean indicating whether payment has been captured + * @property {Bool} canMakeAdjustments - A boolean indicating whether adjustments could be made on total payment + * @property {Bool} hasRefundingEnabled - A boolean indicating whether payment supports refunds + * @property {Bool} isFetching - A boolean indicating whether refund list is being loaded + * @return {Node} React node containing component for displaying the `invoice` section on the orders sideview + */ class Invoice extends Component { static propTypes = { canMakeAdjustments: PropTypes.bool, - collection: PropTypes.string, - dateFormat: PropTypes.func, discounts: PropTypes.bool, - handleClick: PropTypes.func, hasRefundingEnabled: PropTypes.bool, invoice: PropTypes.object, isFetching: PropTypes.bool, - isOpen: PropTypes.bool, - orderId: PropTypes.string, + order: PropTypes.object, paymentCaptured: PropTypes.bool, refunds: PropTypes.array } - renderDiscountForm() { - const { isOpen, orderId, collection } = this.props; + state = { + isOpen: false + } + + /** + * @summary Formats dates + * @param {Number} context - the date to be formatted + * @param {String} block - the preferred format + * @returns {String} formatted date + */ + formatDate(context, block) { + const dateFormat = block || "MMM DD, YYYY hh:mm:ss A"; + return moment(context).format(dateFormat); + } + /** + * @summary Handle clicking the add discount link + * @param {Event} event - the event that fired + * @returns {null} null + */ + handleClick = (event) => { + event.preventDefault(); + this.setState({ + isOpen: true + }); + } + + /** + * @summary Displays the discount form + * @returns {null} null + */ + renderDiscountForm() { return (
- {isOpen && + {this.state.isOpen &&

-
@@ -40,9 +80,12 @@ class Invoice extends Component { ); } + /** + * @summary Displays the refund information after the order payment breakdown on the invoice + * @returns {null} null + */ renderRefundsInfo() { - const { hasRefundingEnabled, isFetching, refunds, dateFormat } = this.props; - + const { hasRefundingEnabled, isFetching, refunds } = this.props; return (
{(hasRefundingEnabled && isFetching) && @@ -56,7 +99,7 @@ class Invoice extends Component { {refunds && refunds.map((refund) => (
- Refunded on: {dateFormat(refund.created, "MM/D/YYYY")} + Refunded on: {this.formatDate(refund.created, "MM/D/YYYY")}
{formatPriceString(refund.amount)}
))} @@ -64,23 +107,28 @@ class Invoice extends Component { ); } + /** + * @summary Displays the total payment form + * @returns {null} null + */ renderTotal() { - const { invoice } = this.props; - return (

TOTAL
- {formatPriceString(invoice.total)} + {formatPriceString(this.props.invoice.total)}
); } + /** + * @summary Displays either refunds info or the total payment form + * @returns {null} null + */ renderConditionalDisplay() { const { canMakeAdjustments, paymentCaptured } = this.props; - return (
{canMakeAdjustments ? @@ -99,8 +147,12 @@ class Invoice extends Component { ); } - render() { - const { invoice, discounts, handleClick } = this.props; + /** + * @summary Displays the invoice form with broken down payment info + * @returns {null} null + */ + renderInvoice() { + const { invoice, discounts } = this.props; return (
@@ -112,21 +164,21 @@ class Invoice extends Component {
- +
{formatPriceString(invoice.subtotal)}
- +
{formatPriceString(invoice.shipping)}
- +
{formatPriceString(invoice.taxes)}
@@ -135,10 +187,10 @@ class Invoice extends Component { {discounts &&
{this.renderDiscountForm()} @@ -148,6 +200,33 @@ class Invoice extends Component {
); } + + render() { + return ( + + + + + + +
+ {this.renderInvoice()} +
+ + +
+
+
+ ); + } } +registerComponent("Invoice", Invoice); + export default Invoice; diff --git a/imports/plugins/core/orders/client/components/invoiceActions.js b/imports/plugins/core/orders/client/components/invoiceActions.js index 1b135a7e3dc..2a6122a76f0 100644 --- a/imports/plugins/core/orders/client/components/invoiceActions.js +++ b/imports/plugins/core/orders/client/components/invoiceActions.js @@ -1,14 +1,48 @@ import React, { Component } from "react"; import PropTypes from "prop-types"; import { formatPriceString } from "/client/api"; -import { IconButton, NumericInput, Translation } from "/imports/plugins/core/ui/client/components"; +import { Components, registerComponent } from "@reactioncommerce/reaction-components"; +/** + * @summary React component for displaying the actionable data on the invoice section on the orders sideview + * @param {Object} props - React PropTypes + * @property {Object} invoice - An object representing an invoice + * @property {Array} refunds - An array/list of refunds + * @property {Function} handleApprove - A function for approving payments + * @property {Function} handleCapturePayment - A function for capturing payments + * @property {Function} handleRefund - A function for refunding payments + * @property {Object} currency - A object represting current shop currency details + * @property {String} printOrder - A string representing the route/path for printed order + * @property {Number} adjustedTotal - The calculated adjusted total after refunds/discounts + * @property {Bool} paymentCaptured - A boolean indicating whether payment has been captured + * @property {Bool} hasRefundingEnabled - A boolean indicating whether payment supports refunds + * @property {Bool} paymentApproved - A boolean indicating whether payment has been approved + * @property {Bool} paymentPendingApproval - A boolean indicating whether payment is yet to be approved + * @property {Bool} showAfterPaymentCaptured - A boolean indicating that status of the order is completed + * @property {Bool} isCapturing - A boolean indicating whether payment is being captured + * @property {Bool} isRefunding - A boolean indicating whether payment is being refunded + * @return {Node} React node containing component for displaying the `invoice` section on the orders sideview + */ class InvoiceActions extends Component { static propTypes = { adjustedTotal: PropTypes.number, - handleActionViewBack: PropTypes.func, + currency: PropTypes.object, + handleApprove: PropTypes.func, + handleCapturePayment: PropTypes.func, + handleRefund: PropTypes.func, + hasRefundingEnabled: PropTypes.bool, invoice: PropTypes.object, - isAdjusted: PropTypes.func + isCapturing: PropTypes.bool, + isRefunding: PropTypes.bool, + paymentApproved: PropTypes.bool, + paymentCaptured: PropTypes.bool, + paymentPendingApproval: PropTypes.bool, + printOrder: PropTypes.string, + showAfterPaymentCaptured: PropTypes.bool + } + + state = { + value: 0 } renderCapturedTotal() { @@ -17,7 +51,7 @@ class InvoiceActions extends Component { return (
- +
@@ -33,7 +67,7 @@ class InvoiceActions extends Component { return (
- +
@@ -42,48 +76,165 @@ class InvoiceActions extends Component {
); } + renderRefundForm() { const { adjustedTotal } = this.props; return ( -
- - - +
+ {this.props.hasRefundingEnabled && +
+
+
+ { + this.setState({ + value: data.numberValue + }); + }} + /> +
-
- - - - - - -> - -
+
+ + { + this.props.handleRefund(event, this.state.value); + this.setState({ + value: 0 + }); + }} + > + {this.props.isRefunding ? + Refunding : + Apply Refund + } + +
+ } + + {this.props.showAfterPaymentCaptured && + + Cancel Order + + } + + + Print Invoice +
); } - render() { - const { isAdjusted } = this.props; + renderApproval() { + if (this.props.paymentPendingApproval) { + return ( +
+
+ +
+
+ ); + } + + if (this.props.paymentApproved) { + return ( +
+ + Print + + + +
+ ); + } + } + render() { return ( -
- {this.renderCapturedTotal()} - {isAdjusted() && this.renderAdjustedTotal()} - {/* {this.renderRefundForm()} */} -
+
+ +
+ {this.renderApproval()} + {this.props.paymentCaptured && +
+ {this.renderCapturedTotal()} + {this.props.invoice.total !== this.props.adjustedTotal && this.renderAdjustedTotal()} + {this.renderRefundForm()} +
+ } +
+
+ ); } } +registerComponent("InvoiceActions", InvoiceActions); + export default InvoiceActions; diff --git a/imports/plugins/core/orders/client/components/lineItems.js b/imports/plugins/core/orders/client/components/lineItems.js index 94529f7a364..7621b97453b 100644 --- a/imports/plugins/core/orders/client/components/lineItems.js +++ b/imports/plugins/core/orders/client/components/lineItems.js @@ -1,39 +1,77 @@ import React, { Component } from "react"; import PropTypes from "prop-types"; +import { isEmpty } from "lodash"; +import classnames from "classnames"; +import { Meteor } from "meteor/meteor"; +import { Roles } from "meteor/alanning:roles"; +import { Reaction } from "/client/api"; import { formatPriceString } from "/client/api"; -import { Translation } from "/imports/plugins/core/ui/client/components"; +import { Components, registerComponent } from "@reactioncommerce/reaction-components"; +/** + * @summary React component for displaying the actionable data on the invoice section on the orders sideview + * @param {Object} props - React PropTypes + * @property {Object} order - An object represnting an order + * @property {Object} uniqueItems - An object representing a line item + * @property {Array} editedItems - An array/list of line items that have been edited/modified + * @property {Array} selectedItems - An array of all the line items that have been selected + * @property {Function} displayMedia - A function to display line items images + * @property {Function} clearRefunds - A function to clear edited/selected items + * @property {Function} getRefundedItemsInfo - A function that returns an object containing refunded items info + * @property {Function} getSelectedItemsInfo - A function that returns an object containing selected items info + * @property {Function} handleInputChange - A function to handle numeric input change + * @property {Function} handleItemSelect - A function to handle selecting an item via chekbox + * @property {Function} handlePopOverOpen - A function to handle the popover open and close + * @property {Function} handleRefundItems - A function to handle items return + * @property {Function} handleSelectAllItems - A function to handle selecting of all items + * @property {Bool} selectAllItems - A boolean indicating whether all items have been selected + * @property {Bool} isRefunding - A boolean indicating whether payment is being refunded + * @property {Bool} popOverIsOpen - A boolean indicating whether popover is open + * @return {Node} React node containing component for displaying the `invoice` section on the orders sideview + */ class LineItems extends Component { static propTypes = { + clearRefunds: PropTypes.func, displayMedia: PropTypes.func, - handleClick: PropTypes.func, - isExpanded: PropTypes.func, - onClose: PropTypes.func, + editedItems: PropTypes.array, + getRefundedItemsInfo: PropTypes.func, + getSelectedItemsInfo: PropTypes.func, + handleInputChange: PropTypes.func, + handleItemSelect: PropTypes.func, + handlePopOverOpen: PropTypes.func, + handleRefundItems: PropTypes.func, + handleSelectAllItems: PropTypes.func, + isRefunding: PropTypes.bool, + order: PropTypes.object, + popOverIsOpen: PropTypes.bool, + selectAllItems: PropTypes.bool, + selectedItems: PropTypes.array, uniqueItems: PropTypes.array } - calculateTotal(price, shipping, taxes) { - return formatPriceString(price + shipping + taxes); - } + displayMedia(uniqueItem) { + const { displayMedia } = this.props; - renderLineItem(uniqueItem, quantity) { - const { handleClick, displayMedia } = this.props; + if (displayMedia(uniqueItem)) { + return ( + + ); + } + return ( + + ); + } + renderLineItem(uniqueItem) { return ( -
+
handleClick(uniqueItem._id)} - style={{ height: 70 }} + className="order-item form-group order-summary-form-group" > - -
- { !displayMedia(uniqueItem) ? - : - - } +
+
+ {this.displayMedia(uniqueItem)} +
@@ -43,7 +81,7 @@ class LineItems extends Component {
- {quantity || uniqueItem.quantity} +
{uniqueItem.quantity}
@@ -57,94 +95,248 @@ class LineItems extends Component { ); } - renderLineItemInvoice(uniqueItem) { + renderPopOverLineItem(uniqueItem) { + const className = classnames({ + "order-items": true, + "invoice-item": true, + "selected": this.props.selectedItems.includes(uniqueItem._id) + }); + return ( -
-
- -
- {formatPriceString(uniqueItem.variants.price)} +
+
+
+ this.props.handleItemSelect(uniqueItem)} + checked={this.props.selectedItems.includes(uniqueItem._id)} + > + {this.displayMedia(uniqueItem)} +
-
-
- -
- {formatPriceString(uniqueItem.shipping.rate)} +
+
+ {uniqueItem.title}
{uniqueItem.variants.title} +
-
-
- Item tax -
- {uniqueItem.taxDetail ? formatPriceString(uniqueItem.taxDetail.tax / uniqueItem.quantity) : formatPriceString(0)} +
+ {!this.props.selectedItems.includes(uniqueItem._id) && uniqueItem.quantity > 0 ? + this.props.handleInputChange(event, value, uniqueItem)} + maxValue={uniqueItem.quantity} + /> : +
0
+ }
-
-
- Tax code -
- {uniqueItem.taxDetail ? uniqueItem.taxDetail.taxCode : uniqueItem.variants.taxCode} +
+
+ {formatPriceString(uniqueItem.variants.price)} +
+
+
+ ); + } -
- TOTAL -
- {uniqueItem.taxDetail ? - - {this.calculateTotal(uniqueItem.variants.price, uniqueItem.shipping.rate, uniqueItem.taxDetail.tax)} - : - - {this.calculateTotal(uniqueItem.variants.price, uniqueItem.shipping.rate, 0)} - - } + renderLineItemInvoice(uniqueItem) { + return ( +
+ {this.props.order.taxes && +
+ + + +
+ + {uniqueItem.taxDetail ? uniqueItem.taxDetail.taxCode : uniqueItem.variants.taxCode} + +
+
+ + {uniqueItem.taxDetail ? + formatPriceString(uniqueItem.taxDetail.tax) : + formatPriceString(uniqueItem.tax) + } + +
+ } +
+ + + + {formatPriceString(uniqueItem.variants.price * uniqueItem.quantity)}
-
); } - render() { - const { uniqueItems, isExpanded, onClose } = this.props; + renderLineItemRefund() { + const { editedItems } = this.props; return (
- {uniqueItems.map((uniqueItem) => { - if (!isExpanded(uniqueItem._id)) { - return ( -
{ this.renderLineItem(uniqueItem) }
- ); - } - - return ( -
-
- -
- +
+
+
+ +
+
+ +
+
+ +
+
+
+ {editedItems.map((item, index) => ( +
+
+ {item.title}
+
+ {item.refundedQuantity} +
+
+ {formatPriceString(item.refundedTotal)} +
+
+ ) + )} +
+
+ +
+
+ + {this.props.getRefundedItemsInfo().quantity} + +
+
+ + {formatPriceString(this.props.getRefundedItemsInfo().total)} + +
+
+
+
+
+ ); + } -

- - {[...Array(uniqueItem.quantity)].map((v, i) => -
- { this.renderLineItem(uniqueItem, 1) } - { this.renderLineItemInvoice(uniqueItem) } -
- )} + renderPopOver() { + return ( + + {this.popOverContent()} + + ); + } -
+ popOverContent() { + return ( +
+
+ this.props.handleSelectAllItems(this.props.uniqueItems)} + /> +
+ +
+
+
+ {this.props.uniqueItems.map((uniqueItem, index) => ( +
+ {this.renderPopOverLineItem(uniqueItem)} + {this.renderLineItemInvoice(uniqueItem)}
+ ))} +
+
+ {!isEmpty(this.props.editedItems) && this.renderLineItemRefund()} +
+
+
+ +
+
+ + {this.props.isRefunding ? Refunding : + Refund Items + } + +
+
+
+ ); + } + + render() { + const { uniqueItems } = this.props; + return ( +
+ {uniqueItems.map((uniqueItem) => { + return ( +
{this.renderLineItem(uniqueItem)}
); })} + + { + Roles.userIsInRole(Meteor.userId(), ["orders", "dashboard/orders"], Reaction.getShopId()) && + this.renderPopOver() + }
); } } +registerComponent("LineItems", LineItems); + export default LineItems; diff --git a/imports/plugins/core/orders/client/components/orderSummary.js b/imports/plugins/core/orders/client/components/orderSummary.js index ee8b301182f..6eee3a77b50 100644 --- a/imports/plugins/core/orders/client/components/orderSummary.js +++ b/imports/plugins/core/orders/client/components/orderSummary.js @@ -1,7 +1,7 @@ import React, { Component } from "react"; import PropTypes from "prop-types"; import moment from "moment"; -import { Badge, ClickToCopy } from "/imports/plugins/core/ui/client/components"; +import { Badge, ClickToCopy } from "@reactioncommerce/reaction-ui"; class OrderSummary extends Component { static propTypes = { diff --git a/imports/plugins/core/orders/client/components/orderTable.js b/imports/plugins/core/orders/client/components/orderTable.js index b96fb0fd0eb..f12ae263137 100644 --- a/imports/plugins/core/orders/client/components/orderTable.js +++ b/imports/plugins/core/orders/client/components/orderTable.js @@ -3,8 +3,9 @@ import PropTypes from "prop-types"; import Avatar from "react-avatar"; import moment from "moment"; import classnames from "classnames/dedupe"; -import { Badge, ClickToCopy, Icon, Translation, Checkbox, Loading, SortableTable } from "@reactioncommerce/reaction-ui"; +import { Reaction } from "/client/api"; import { Orders } from "/lib/collections"; +import { Badge, ClickToCopy, Icon, Translation, Checkbox, Loading, SortableTable } from "@reactioncommerce/reaction-ui"; import OrderTableColumn from "./orderTableColumn"; import OrderBulkActionsBar from "./orderBulkActionsBar"; import { formatPriceString } from "/client/api"; @@ -52,6 +53,13 @@ class OrderTable extends Component { toggleShippingFlowList: PropTypes.func } + // helper function to get appropriate billing info + getBillingInfo(order) { + return order.billing.find( + billing => billing.shopId === Reaction.getShopId() + ) || {}; + } + /** * Fullfilment Badge * @param {Object} order object containing info for order and coreOrderWorkflow @@ -110,7 +118,7 @@ class OrderTable extends Component { - Total: {formatPriceString(order.billing[0].invoice.total)} + Total: {formatPriceString(this.getBillingInfo(order).invoice.total)}
diff --git a/imports/plugins/core/orders/client/components/productImage.js b/imports/plugins/core/orders/client/components/productImage.js index 1eaccbc304a..f007b479579 100644 --- a/imports/plugins/core/orders/client/components/productImage.js +++ b/imports/plugins/core/orders/client/components/productImage.js @@ -1,6 +1,6 @@ import React, { Component } from "react"; import PropTypes from "prop-types"; -import { Badge } from "/imports/plugins/core/ui/client/components"; +import { Badge } from "@reactioncommerce/reaction-ui"; class ProductImage extends Component { diff --git a/imports/plugins/core/orders/client/containers/invoiceActionsContainer.js b/imports/plugins/core/orders/client/containers/invoiceActionsContainer.js deleted file mode 100644 index 9015fdcfa43..00000000000 --- a/imports/plugins/core/orders/client/containers/invoiceActionsContainer.js +++ /dev/null @@ -1,50 +0,0 @@ -import React, { Component } from "react"; -import PropTypes from "prop-types"; -import { composeWithTracker } from "@reactioncommerce/reaction-components"; -import InvoiceActions from "../components/invoiceActions"; - -class InvoiceActionsContainer extends Component { - static propTypes = { - adjustedTotal: PropTypes.number, - invoice: PropTypes.object, - paymentCaptured: PropTypes.bool - } - - constructor(props) { - super(props); - this.isAdjusted = this.isAdjusted.bind(this); - } - - isAdjusted = () => { - const { adjustedTotal, invoice } = this.props; - - if (invoice.total === adjustedTotal) { - return false; - } - return true; - } - - render() { - const { paymentCaptured, adjustedTotal, invoice } = this.props; - return ( -
- -
- ); - } -} - -const composer = (props, onData) => { - onData(null, { - paymentCaptured: props.paymentCaptured, - adjustedTotal: props.adjustedTotal, - invoice: props.invoice - }); -}; - -export default composeWithTracker(composer)(InvoiceActionsContainer); diff --git a/imports/plugins/core/orders/client/containers/invoiceContainer.js b/imports/plugins/core/orders/client/containers/invoiceContainer.js index db18ff5ce7a..0c8ac34266d 100644 --- a/imports/plugins/core/orders/client/containers/invoiceContainer.js +++ b/imports/plugins/core/orders/client/containers/invoiceContainer.js @@ -1,83 +1,691 @@ import React, { Component } from "react"; import PropTypes from "prop-types"; -import moment from "moment"; -import { composeWithTracker } from "@reactioncommerce/reaction-components"; -import { TranslationProvider } from "/imports/plugins/core/ui/client/providers"; +import accounting from "accounting-js"; +import _ from "lodash"; +import { Meteor } from "meteor/meteor"; +import { i18next, Logger, Reaction, formatPriceString } from "/client/api"; +import { Media, Packages } from "/lib/collections"; +import { composeWithTracker, registerComponent } from "@reactioncommerce/reaction-components"; import Invoice from "../components/invoice.js"; class InvoiceContainer extends Component { static propTypes = { - canMakeAdjustments: PropTypes.bool, - collection: PropTypes.string, - discounts: PropTypes.bool, - hasRefundingEnabled: PropTypes.bool, - invoice: PropTypes.object, + currency: PropTypes.object, isFetching: PropTypes.bool, - orderId: PropTypes.string, - paymentCaptured: PropTypes.bool, - refunds: PropTypes.array + order: PropTypes.object, + refunds: PropTypes.array, + uniqueItems: PropTypes.array } constructor(props) { super(props); this.state = { - isOpen: false + currency: props.currency, + refunds: props.refunds, + order: props.order, + isUpdating: false, + isCapturing: false, + isRefunding: false, + popOverIsOpen: false, + selectAllItems: false, + selectedItems: [], + editedItems: [], + value: undefined }; - this.handleClick = this.handleClick.bind(this); - this.dateFormat = this.dateFormat.bind(this); } - dateFormat = (context, block) => { - const f = block || "MMM DD, YYYY hh:mm:ss A"; - return moment(context).format(f); + componentWillReceiveProps(nextProps) { + if (nextProps !== this.props) { + this.setState({ + order: nextProps.order, + currency: nextProps.currency, + refunds: nextProps.refunds + }); + } } - handleClick = (event) => { + handlePopOverOpen = (event) => { event.preventDefault(); this.setState({ - isOpen: true + popOverIsOpen: true }); } - render() { - const { - canMakeAdjustments, paymentCaptured, - discounts, invoice, orderId, refunds, - isFetching, collection, hasRefundingEnabled - } = this.props; + handleClearRefunds = () => { + this.setState({ + editedItems: [], + selectedItems: [], + selectAllItems: false, + popOverIsOpen: false + }); + }; + + handleItemSelect = (lineItem) => { + let { selectedItems, editedItems } = this.state; + + // if item is not in the selectedItems array + if (!selectedItems.includes(lineItem._id)) { + // include it in the array + selectedItems.push(lineItem._id); + + // Add every quantity in the row to be refunded + const isEdited = editedItems.find(item => { + return item.id === lineItem._id; + }); + + const adjustedQuantity = lineItem.quantity - this.state.value; + + if (isEdited) { + editedItems = editedItems.filter(item => item.id !== lineItem._id); + isEdited.refundedTotal = lineItem.variants.price * adjustedQuantity; + isEdited.refundedQuantity = adjustedQuantity; + editedItems.push(isEdited); + } else { + editedItems.push({ + id: lineItem._id, + title: lineItem.title, + refundedTotal: lineItem.variants.price * lineItem.quantity, + refundedQuantity: lineItem.quantity + }); + } + + this.setState({ + editedItems, + selectedItems, + isUpdating: true, + selectAllItems: false + }); + } else { + // remove item from selected items + selectedItems = selectedItems.filter((id) => { + if (id !== lineItem._id) { + return id; + } + }); + // remove item from edited quantities + editedItems = editedItems.filter(item => item.id !== lineItem._id); + + this.setState({ + editedItems, + selectedItems, + isUpdating: true, + selectAllItems: false + }); + } + } + + handleSelectAllItems = (uniqueItems) => { + if (this.state.selectAllItems) { + // if all items are selected, clear the selectedItems array + // and set selectAllItems to false + this.setState({ + selectedItems: [], + editedItems: [], + selectAllItems: false + }); + } else { + // if there are no selected items, or if there are some items that have been + // selected but not all of them, loop through the items array and return a + // new array with item ids only, then set the selectedItems array with the itemIds + const updateEditedItems = []; + + const itemIds = uniqueItems.map((item) => { + // on select all refunded quantity should be all existing items + updateEditedItems.push({ + id: item._id, + title: item.title, + refundedTotal: item.variants.price * item.quantity, + refundedQuantity: item.quantity + }); + return item._id; + }); + this.setState({ + editedItems: updateEditedItems, + selectedItems: itemIds, + selectAllItems: true, + isUpdating: true + }); + } + } + + handleInputChange = (event, value, lineItem) => { + let { editedItems } = this.state; + + const isEdited = editedItems.find(item => { + return item.id === lineItem._id; + }); + + const refundedQuantity = lineItem.quantity - value; + + if (isEdited) { + editedItems = editedItems.filter(item => item.id !== lineItem._id); + isEdited.refundedTotal = lineItem.variants.price * refundedQuantity; + isEdited.refundedQuantity = refundedQuantity; + if (refundedQuantity !== 0) { + editedItems.push(isEdited); + } + } else { + editedItems.push({ + id: lineItem._id, + title: lineItem.title, + refundedTotal: lineItem.variants.price * refundedQuantity, + refundedQuantity + }); + } + this.setState({ + editedItems, + value + }); + } + + /** + * Media - find media based on a product/variant + * @param {Object} item object containing a product and variant id + * @return {Object|false} An object contianing the media or false + */ + handleDisplayMedia = (item) => { + const variantId = item.variants._id; + const productId = item.productId; + + const variantImage = Media.findOne({ + "metadata.variantId": variantId, + "metadata.productId": productId + }); + + if (variantImage) { + return variantImage; + } + + const defaultImage = Media.findOne({ + "metadata.productId": productId, + "metadata.priority": 0 + }); + + if (defaultImage) { + return defaultImage; + } + return false; + } + + getRefundedItemsInfo = () => { + const { editedItems } = this.state; + return { + quantity: editedItems.reduce((acc, item) => acc + item.refundedQuantity, 0), + total: editedItems.reduce((acc, item) => acc + item.refundedTotal, 0), + items: editedItems + }; + } + + hasRefundingEnabled() { + const order = this.state.order; + const paymentMethodId = getBillingInfo(order).paymentMethod.paymentPackageId; + const paymentMethod = Packages.findOne({ _id: paymentMethodId }); + const paymentMethodName = paymentMethod.name; + const isRefundable = paymentMethod.settings[paymentMethodName].support.includes("Refund"); + return isRefundable; + } + + handleApprove = (event) => { + event.preventDefault(); + + const order = this.state.order; + approvePayment(order); + } + + handleCapturePayment = (event) => { + event.preventDefault(); + + this.setState({ + isCapturing: true + }); + + const order = this.state.order; + capturePayments(order); + } + + handleCancelPayment = (event) => { + event.preventDefault(); + const order = this.state.order; + const invoiceTotal = getBillingInfo(order).invoice.total; + const currencySymbol = this.state.currency.symbol; + + Meteor.subscribe("Packages", Reaction.getShopId()); + const packageId = getBillingInfo(order).paymentMethod.paymentPackageId; + const settingsKey = getBillingInfo(order).paymentMethod.paymentSettingsKey; + // check if payment provider supports de-authorize + const checkSupportedMethods = Packages.findOne({ + _id: packageId, + shopId: Reaction.getShopId() + }).settings[settingsKey].support; + + const orderStatus = getBillingInfo(order).paymentMethod.status; + const orderMode = getBillingInfo(order).paymentMethod.mode; + + let alertText; + if (_.includes(checkSupportedMethods, "de-authorize") || + (orderStatus === "completed" && orderMode === "capture")) { + alertText = i18next.t("order.applyRefundDuringCancelOrder", { currencySymbol, invoiceTotal }); + } + + Alerts.alert({ + title: i18next.t("order.cancelOrder"), + text: alertText, + type: "warning", + showCancelButton: true, + showCloseButton: true, + confirmButtonColor: "#98afbc", + cancelButtonColor: "#98afbc", + confirmButtonText: i18next.t("order.cancelOrderNoRestock"), + cancelButtonText: i18next.t("order.cancelOrderThenRestock") + }, (isConfirm, cancel)=> { + let returnToStock; + if (isConfirm) { + returnToStock = false; + return Meteor.call("orders/cancelOrder", order, returnToStock); + } + if (cancel === "cancel") { + returnToStock = true; + return Meteor.call("orders/cancelOrder", order, returnToStock); + } + }); + } + + handleRefund = (event, value) => { + event.preventDefault(); + + const currencySymbol = this.state.currency.symbol; + const order = this.state.order; + const paymentMethod = orderCreditMethod(order).paymentMethod; + const orderTotal = paymentMethod.amount; + const discounts = paymentMethod.discounts; + const refund = value; + const refunds = this.state.refunds; + const refundTotal = refunds.reduce((acc, item) => acc + parseFloat(item.amount), 0); + + let adjustedTotal; + + // TODO extract Stripe specific fullfilment payment handling out of core. + // Stripe counts discounts as refunds, so we need to re-add the discount to not "double discount" in the adjustedTotal + if (paymentMethod.processor === "Stripe") { + adjustedTotal = accounting.toFixed(orderTotal + discounts - refundTotal, 2); + } else { + adjustedTotal = accounting.toFixed(orderTotal - refundTotal, 2); + } + + if (refund > adjustedTotal) { + Alerts.inline("Refund(s) total cannot be greater than adjusted total", "error", { + placement: "coreOrderRefund", + i18nKey: "order.invalidRefund", + autoHide: 10000 + }); + } else { + Alerts.alert({ + title: i18next.t("order.applyRefundToThisOrder", { refund: refund, currencySymbol: currencySymbol }), + showCancelButton: true, + confirmButtonText: i18next.t("order.applyRefund") + }, (isConfirm) => { + if (isConfirm) { + this.setState({ + isRefunding: true + }); + Meteor.call("orders/refunds/create", order._id, paymentMethod, refund, (error, result) => { + if (error) { + Alerts.alert(error.reason); + } + if (result) { + Alerts.toast(i18next.t("mail.alerts.emailSent"), "success"); + } + this.setState({ + isRefunding: false + }); + }); + } + }); + } + } + + handleRefundItems = () => { + const paymentMethod = orderCreditMethod(this.state.order).paymentMethod; + const orderMode = paymentMethod.mode; + const order = this.state.order; + // Check if payment is yet to be captured approve and capture first before return + if (orderMode === "authorize") { + Alerts.alert({ + title: i18next.t("order.refundItemsTitle"), + type: "warning", + text: i18next.t("order.refundItemsApproveAlert", { + refundItemsQuantity: this.getRefundedItemsInfo().quantity, + totalAmount: formatPriceString(getBillingInfo(order).invoice.total) + }), + showCancelButton: true, + confirmButtonText: i18next.t("order.approveInvoice") + }, (isConfirm) => { + if (isConfirm) { + approvePayment(order); + this.alertToCapture(order); + } + }); + } else { + this.alertToRefund(order); + } + } + + alertToCapture = (order) => { + Alerts.alert({ + title: i18next.t("order.refundItemsTitle"), + text: i18next.t("order.refundItemsCaptureAlert", { + refundItemsQuantity: this.getRefundedItemsInfo().quantity, + totalAmount: formatPriceString(getBillingInfo(order).invoice.total) + }), + type: "warning", + showCancelButton: true, + confirmButtonText: i18next.t("order.capturePayment") + }, (isConfirm) => { + if (isConfirm) { + capturePayments(order); + this.alertToRefund(order); + } + }); + } + + alertToRefund = (order) => { + const paymentMethod = orderCreditMethod(order).paymentMethod; + const orderMode = paymentMethod.mode; + const refundInfo = this.getRefundedItemsInfo(); + + Alerts.alert({ + title: i18next.t("order.refundItemsTitle"), + text: i18next.t("order.refundItemsAlert", { + refundItemsQuantity: refundInfo.quantity, + refundItemsTotal: formatPriceString(refundInfo.total) + }), + showCancelButton: true, + confirmButtonText: i18next.t("order.refundAmount") + }, (isConfirm) => { + if (isConfirm) { + this.setState({ + isRefunding: true + }); + + // Set warning if order is not yet captured + if (orderMode !== "capture") { + Alerts.alert({ + text: i18next.t("order.refundItemsWait"), + type: "warning" + }); + this.setState({ + isRefunding: false + }); + return; + } + + Meteor.call("orders/refunds/refundItems", this.state.order._id, paymentMethod, refundInfo, (error, result) => { + if (result.refund === false) { + Alerts.alert(result.error.reason || result.error.error); + } + if (result.refund === true) { + Alerts.toast(i18next.t("mail.alerts.emailSent"), "success"); + + Alerts.alert({ + text: i18next.t("order.refundItemsSuccess"), + type: "success", + allowOutsideClick: false + }); + } + + this.setState({ + isRefunding: false, + popOverIsOpen: false, + editedItems: [], + selectedItems: [] + }); + }); + } + }); + } + + render() { return ( - - - + ); } } +/** + * @method getBillingInfo + * @summary helper method to get appropriate billing info + * @param {Object} order - object representing an order + * @return {Object} object representing the order billing info + */ +function getBillingInfo(order) { + return order.billing.find( + billing => billing.shopId === Reaction.getShopId() + ) || {}; +} + + +/** + * @method orderCreditMethod + * @summary helper method to return the order payment object + * @param {Object} order - object representing an order + * @return {Object} object representing entire payment method + */ +function orderCreditMethod(order) { + const billingInfo = getBillingInfo(order); + + if (billingInfo.paymentMethod.method === "credit") { + return billingInfo; + } +} + +/** + * @method approvePayment + * @summary helper method to approve payment + * @param {Object} order - object representing an order + * @return {null} null + */ +function approvePayment(order) { + const paymentMethod = orderCreditMethod(order); + const orderTotal = accounting.toFixed( + paymentMethod.invoice.subtotal + + paymentMethod.invoice.shipping + + paymentMethod.invoice.taxes + , 2); + + const discount = order.discount; + // TODO: review Discount cannot be greater than original total price + // logic is probably not valid any more. Discounts aren't valid below 0 order. + if (discount > orderTotal) { + Alerts.inline("Discount cannot be greater than original total price", "error", { + placement: "coreOrderShippingInvoice", + i18nKey: "order.invalidDiscount", + autoHide: 10000 + }); + } else if (orderTotal === accounting.toFixed(discount, 2)) { + Alerts.alert({ + title: i18next.t("order.fullDiscountWarning"), + showCancelButton: true, + confirmButtonText: i18next.t("order.applyDiscount") + }, (isConfirm) => { + if (isConfirm) { + Meteor.call("orders/approvePayment", order, (error) => { + if (error) { + Logger.warn(error); + } + }); + } + }); + } else { + Meteor.call("orders/approvePayment", order, (error) => { + if (error) { + Logger.warn(error); + if (error.error === "orders/approvePayment.discount-amount") { + Alerts.inline("Discount cannot be greater than original total price", "error", { + placement: "coreOrderShippingInvoice", + i18nKey: "order.invalidDiscount", + autoHide: 10000 + }); + } + } + }); + } +} + +/** + * @method capturePayments + * @summary helper method to capture payments + * @param {Object} order - object representing an order + * @return {null} null + */ +function capturePayments(order) { + Meteor.call("orders/capturePayments", order._id); + if (order.workflow.status === "new") { + Meteor.call("workflow/pushOrderWorkflow", "coreOrderWorkflow", "processing", order); + + Reaction.Router.setQueryParams({ + filter: "processing", + _id: order._id + }); + } +} + const composer = (props, onData) => { + const order = props.order; + const refunds = props.refunds; + + const shopBilling = getBillingInfo(order); + + const paymentMethod = orderCreditMethod(order).paymentMethod; + const orderStatus = orderCreditMethod(order).paymentMethod.status; + const orderDiscounts = orderCreditMethod(order).invoice.discounts; + + const paymentApproved = orderStatus === "approved"; + const showAfterPaymentCaptured = orderStatus === "completed"; + const paymentCaptured = _.includes(["completed", "refunded", "partialRefund"], orderStatus); + const paymentPendingApproval = _.includes(["created", "adjustments", "error"], orderStatus); + + // get whether adjustments can be made + const canMakeAdjustments = !_.includes(["approved", "completed", "refunded", "partialRefund"], orderStatus); + + // get adjusted Total + let adjustedTotal; + const refundTotal = refunds.reduce((acc, item) => acc + parseFloat(item.amount), 0); + + if (paymentMethod.processor === "Stripe") { + adjustedTotal = Math.abs(paymentMethod.amount + orderDiscounts - refundTotal); + } + adjustedTotal = Math.abs(paymentMethod.amount - refundTotal); + + // get invoice + const invoice = Object.assign({}, shopBilling.invoice, { + totalItems: _.sumBy(order.items, (o) => o.quantity) + }); + + // get discounts + const enabledPaymentsArr = []; + const apps = Reaction.Apps({ + provides: "paymentMethod", + enabled: true + }); + for (const app of apps) { + if (app.enabled === true) enabledPaymentsArr.push(app); + } + let discounts = false; + + for (const enabled of enabledPaymentsArr) { + if (enabled.packageName === "discount-codes") { + discounts = true; + break; + } + } + + // get unique lineItems + const shipment = props.currentData.fulfillment; + + // returns order items with shipping detail + const returnItems = order.items.map((item) => { + const shipping = shipment.shipmentMethod; + item.shipping = shipping; + return item; + }); + + let uniqueItems; + + // if avalara tax has been enabled it adds a "taxDetail" field for every item + if (order.taxes !== undefined) { + const taxes = order.taxes.slice(0, -1); + + uniqueItems = returnItems.map((item) => { + if (taxes.length !== 0) { + const taxDetail = taxes.find((tax) => { + return tax.lineNumber === item._id; + }); + item.taxDetail = taxDetail; + } + }); + } else { + uniqueItems = returnItems; + } + + // print order + const printOrder = Reaction.Router.pathFor("dashboard/pdf/orders", { + hash: { + id: props.order._id, + shipment: props.currentData.fulfillment._id + } + }); + onData(null, { - canMakeAdjustments: props.canMakeAdjustments, - paymentCaptured: props.paymentCaptured, - discounts: props.discounts, - invoice: props.invoice, - orderId: props.orderId, - refunds: props.refunds, + uniqueItems, + invoice, + discounts, + adjustedTotal, + paymentCaptured, + paymentPendingApproval, + paymentApproved, + canMakeAdjustments, + showAfterPaymentCaptured, + printOrder, + + currentData: props.currentData, + currency: props.currency, isFetching: props.isFetching, - collection: props.collection + order: props.order, + refunds: props.refunds }); }; +registerComponent("InvoiceContainer", InvoiceContainer, composeWithTracker(composer)); + export default composeWithTracker(composer)(InvoiceContainer); diff --git a/imports/plugins/core/orders/client/containers/lineItemsContainer.js b/imports/plugins/core/orders/client/containers/lineItemsContainer.js deleted file mode 100644 index 3bb57567b96..00000000000 --- a/imports/plugins/core/orders/client/containers/lineItemsContainer.js +++ /dev/null @@ -1,103 +0,0 @@ -import React, { Component } from "react"; -import PropTypes from "prop-types"; -import { composeWithTracker } from "@reactioncommerce/reaction-components"; -import { Meteor } from "meteor/meteor"; -import { Media } from "/lib/collections"; -import { TranslationProvider } from "/imports/plugins/core/ui/client/providers"; -import LineItems from "../components/lineItems.js"; - -class LineItemsContainer extends Component { - static propTypes = { - invoice: PropTypes.object, - uniqueItems: PropTypes.array - } - - constructor(props) { - super(props); - this.state = { - isClosed: false - }; - - this.handleClick = this.handleClick.bind(this); - this.isExpanded = this.isExpanded.bind(this); - this.handleClose = this.handleClose.bind(this); - this.handleDisplayMedia = this.handleDisplayMedia.bind(this); - } - - isExpanded = (itemId) => { - if (this.state[`item_${itemId}`]) { - return true; - } - return false; - } - - handleClose = (itemId) => { - this.setState({ - [`item_${itemId}`]: false - }); - } - - handleClick = (itemId) => { - this.setState({ - [`item_${itemId}`]: true - }); - } - - /** - * Media - find media based on a product/variant - * @param {Object} item object containing a product and variant id - * @return {Object|false} An object contianing the media or false - */ - handleDisplayMedia = (item) => { - const variantId = item.variants._id; - const productId = item.productId; - - const variantImage = Media.findOne({ - "metadata.variantId": variantId, - "metadata.productId": productId - }); - - if (variantImage) { - return variantImage; - } - - const defaultImage = Media.findOne({ - "metadata.productId": productId, - "metadata.priority": 0 - }); - - if (defaultImage) { - return defaultImage; - } - return false; - } - - render() { - const { invoice, uniqueItems } = this.props; - return ( - - - - ); - } -} - -const composer = (props, onData) => { - const subscription = Meteor.subscribe("Media"); - if (subscription.ready()) { - onData(null, { - uniqueItems: props.items, - invoice: props.invoice - }); - } -}; - -export default composeWithTracker(composer)(LineItemsContainer); diff --git a/imports/plugins/core/orders/client/containers/orderSummaryContainer.js b/imports/plugins/core/orders/client/containers/orderSummaryContainer.js index 601c7cbcce2..9faf381ff3f 100644 --- a/imports/plugins/core/orders/client/containers/orderSummaryContainer.js +++ b/imports/plugins/core/orders/client/containers/orderSummaryContainer.js @@ -2,8 +2,8 @@ import React, { Component } from "react"; import PropTypes from "prop-types"; import moment from "moment"; import _ from "lodash"; -import { composeWithTracker } from "@reactioncommerce/reaction-components"; import { Meteor } from "meteor/meteor"; +import { composeWithTracker } from "@reactioncommerce/reaction-components"; import { Orders } from "/lib/collections"; import { Card, CardHeader, CardBody, CardGroup } from "/imports/plugins/core/ui/client/components"; import { i18next } from "/client/api"; diff --git a/imports/plugins/core/orders/client/templates/workflow/shippingInvoice.html b/imports/plugins/core/orders/client/templates/workflow/shippingInvoice.html index 95f816eb225..f432ed1ae0a 100644 --- a/imports/plugins/core/orders/client/templates/workflow/shippingInvoice.html +++ b/imports/plugins/core/orders/client/templates/workflow/shippingInvoice.html @@ -1,120 +1,11 @@ diff --git a/imports/plugins/core/orders/client/templates/workflow/shippingInvoice.js b/imports/plugins/core/orders/client/templates/workflow/shippingInvoice.js index 980cbfd5ce6..9546762f42b 100644 --- a/imports/plugins/core/orders/client/templates/workflow/shippingInvoice.js +++ b/imports/plugins/core/orders/client/templates/workflow/shippingInvoice.js @@ -5,15 +5,9 @@ import $ from "jquery"; import { Template } from "meteor/templating"; import { ReactiveVar } from "meteor/reactive-var"; import { ReactiveDict } from "meteor/reactive-dict"; -import { i18next, Logger, formatNumber, Reaction } from "/client/api"; -import { NumericInput } from "/imports/plugins/core/ui/client/components"; +import { i18next, Logger, Reaction } from "/client/api"; import { Orders, Shops, Packages } from "/lib/collections"; -import { ButtonSelect } from "../../../../ui/client/components/button"; -import DiscountList from "/imports/plugins/core/discounts/client/components/list"; import InvoiceContainer from "../../containers/invoiceContainer.js"; -import InvoiceActionsContainer from "../../containers/invoiceActionsContainer.js"; -import LineItemsContainer from "../../containers/lineItemsContainer.js"; -import TotalActionsContainer from "../../containers/totalActionsContainer.js"; // helper to return the order payment object @@ -56,6 +50,28 @@ Template.coreOrderShippingInvoice.onCreated(function () { }); Template.coreOrderShippingInvoice.helpers({ + currentData() { + const currentData = Template.currentData(); + return currentData; + }, + order() { + const instance = Template.instance(); + const order = instance.state.get("order"); + return order; + }, + currency() { + const instance = Template.instance(); + const currency = instance.state.get("currency"); + return currency; + }, + refunds() { + const refunds = Template.instance().refunds.get(); + if (Array.isArray(refunds)) { + return refunds.reverse(); + } + + return refunds; + }, isCapturing() { const instance = Template.instance(); if (instance.state.get("isCapturing")) { @@ -80,50 +96,9 @@ Template.coreOrderShippingInvoice.helpers({ } return false; }, - DiscountList() { - return DiscountList; - }, + InvoiceContainer() { return InvoiceContainer; - }, - InvoiceActionsContainer() { - return InvoiceActionsContainer; - }, - buttonSelectComponent() { - return { - component: ButtonSelect, - buttons: [ - { - name: "Approve", - i18nKeyLabel: "order.approveInvoice", - active: true, - status: "success", - eventAction: "approveInvoice", - bgColor: "bg-success", - buttonType: "submit" - }, { - name: "Cancel", - i18nKeyLabel: "order.cancelInvoice", - active: false, - status: "danger", - eventAction: "cancelOrder", - bgColor: "bg-danger", - buttonType: "button" - } - ] - }; - }, - LineItemsContainer() { - return LineItemsContainer; - }, - TotalActionsContainer() { - return TotalActionsContainer; - }, - orderId() { - const instance = Template.instance(); - const state = instance.state; - const order = state.get("order"); - return order._id; } }); @@ -191,148 +166,12 @@ Template.coreOrderShippingInvoice.events({ } }); }, - /** - * Submit form - * @param {Event} event - Event object - * @param {Template} instance - Blaze Template - * @return {void} - */ - "submit form[name=capture]": (event, instance) => { - event.preventDefault(); - const state = instance.state; - const order = state.get("order"); - - const paymentMethod = orderCreditMethod(order); - const orderTotal = accounting.toFixed( - paymentMethod.invoice.subtotal - + paymentMethod.invoice.shipping - + paymentMethod.invoice.taxes - , 2); - - const discount = state.get("field-discount") || order.discount; - // TODO: review Discount cannot be greater than original total price - // logic is probably not valid any more. Discounts aren't valid below 0 order. - if (discount > orderTotal) { - Alerts.inline("Discount cannot be greater than original total price", "error", { - placement: "coreOrderShippingInvoice", - i18nKey: "order.invalidDiscount", - autoHide: 10000 - }); - } else if (orderTotal === accounting.toFixed(discount, 2)) { - Alerts.alert({ - title: i18next.t("order.fullDiscountWarning"), - showCancelButton: true, - confirmButtonText: i18next.t("order.applyDiscount") - }, (isConfirm) => { - if (isConfirm) { - Meteor.call("orders/approvePayment", order, (error) => { - if (error) { - Logger.warn(error); - } - }); - } - }); - } else { - Meteor.call("orders/approvePayment", order, (error) => { - if (error) { - Logger.warn(error); - if (error.error === "orders/approvePayment.discount-amount") { - Alerts.inline("Discount cannot be greater than original total price", "error", { - placement: "coreOrderShippingInvoice", - i18nKey: "order.invalidDiscount", - autoHide: 10000 - }); - } - } - }); - } - }, - - /** - * Submit form - * @param {Event} event - Event object - * @param {Template} instance - Blaze Template - * @return {void} - */ - "click [data-event-action=applyRefund]": (event, instance) => { - event.preventDefault(); - - const { state } = Template.instance(); - const currencySymbol = state.get("currency").symbol; - const order = instance.state.get("order"); - const paymentMethod = orderCreditMethod(order).paymentMethod; - const orderTotal = paymentMethod.amount; - const discounts = paymentMethod.discounts; - const refund = state.get("field-refund") || 0; - const refunds = Template.instance().refunds.get(); - let refundTotal = 0; - _.each(refunds, function (item) { - refundTotal += parseFloat(item.amount); - }); - - let adjustedTotal; - - // TODO extract Stripe specific fullfilment payment handling out of core. - // Stripe counts discounts as refunds, so we need to re-add the discount to not "double discount" in the adjustedTotal - if (paymentMethod.processor === "Stripe") { - adjustedTotal = accounting.toFixed(orderTotal + discounts - refundTotal, 2); - } else { - adjustedTotal = accounting.toFixed(orderTotal - refundTotal, 2); - } - - if (refund > adjustedTotal) { - Alerts.inline("Refund(s) total cannot be greater than adjusted total", "error", { - placement: "coreOrderRefund", - i18nKey: "order.invalidRefund", - autoHide: 10000 - }); - } else { - Alerts.alert({ - title: i18next.t("order.applyRefundToThisOrder", { refund: refund, currencySymbol: currencySymbol }), - showCancelButton: true, - confirmButtonText: i18next.t("order.applyRefund") - }, (isConfirm) => { - if (isConfirm) { - state.set("isRefunding", true); - Meteor.call("orders/refunds/create", order._id, paymentMethod, refund, (error, result) => { - if (error) { - Alerts.alert(error.reason); - } - if (result) { - Alerts.toast(i18next.t("mail.alerts.emailSent"), "success"); - } - $("#btn-refund-payment").text(i18next.t("order.applyRefund")); - state.set("field-refund", 0); - state.set("isRefunding", false); - }); - } - }); - } - }, "click [data-event-action=makeAdjustments]": (event, instance) => { event.preventDefault(); Meteor.call("orders/makeAdjustmentsToInvoice", instance.state.get("order")); }, - "click [data-event-action=capturePayment]": (event, instance) => { - event.preventDefault(); - - instance.state.set("isCapturing", true); - - const order = instance.state.get("order"); - Meteor.call("orders/capturePayments", order._id); - - if (order.workflow.status === "new") { - Meteor.call("workflow/pushOrderWorkflow", "coreOrderWorkflow", "processing", order); - - Reaction.Router.setQueryParams({ - filter: "processing", - _id: order._id - }); - } - }, - "change input[name=refund_amount], keyup input[name=refund_amount]": (event, instance) => { instance.refundAmount.set(accounting.unformat(event.target.value)); } @@ -343,81 +182,10 @@ Template.coreOrderShippingInvoice.events({ * coreOrderShippingInvoice helpers */ Template.coreOrderShippingInvoice.helpers({ - NumericInput() { - return NumericInput; - }, - - numericInputProps(fieldName, value = 0, enabled = true) { - const { state } = Template.instance(); - const order = state.get("order"); - const paymentMethod = orderCreditMethod(order); - const status = paymentMethod.status; - const isApprovedAmount = (status === "approved" || status === "completed"); - - return { - component: NumericInput, - numericType: "currency", - value: value, - disabled: !enabled, - isEditing: !isApprovedAmount, // Dont allow editing if its approved - format: state.get("currency"), - classNames: { - input: { amount: true }, - text: { - "text-success": status === "completed" - } - }, - onChange(event, data) { - state.set(`field-${fieldName}`, data.numberValue); - } - }; - }, - - refundInputProps() { - const { state } = Template.instance(); - const order = state.get("order"); - const paymentMethod = orderCreditMethod(order).paymentMethod; - const refunds = Template.instance().refunds.get(); - - let refundTotal = 0; - _.each(refunds, function (item) { - refundTotal += parseFloat(item.amount); - }); - const adjustedTotal = paymentMethod.amount - refundTotal; - - return { - component: NumericInput, - numericType: "currency", - value: state.get("field-refund") || 0, - maxValue: adjustedTotal, - format: state.get("currency"), - classNames: { - input: { amount: true } - }, - onChange(event, data) { - state.set("field-refund", data.numberValue); - } - }; - }, - refundAmount() { return Template.instance().refundAmount; }, - invoice() { - const instance = Template.instance(); - const order = instance.state.get("order"); - - const invoice = Object.assign({}, order.billing[0].invoice, { - totalItems: _.sumBy(order.items, (o) => o.quantity) - }); - return invoice; - }, - - money(amount) { - return formatNumber(amount); - }, - disabled() { const instance = Template.instance(); const order = instance.state.get("order"); @@ -430,186 +198,11 @@ Template.coreOrderShippingInvoice.helpers({ return ""; }, - paymentPendingApproval() { - const instance = Template.instance(); - const order = instance.state.get("order"); - const status = orderCreditMethod(order).paymentMethod.status; - - return status === "created" || status === "adjustments" || status === "error"; - }, - - canMakeAdjustments() { - const instance = Template.instance(); - const order = instance.state.get("order"); - const status = orderCreditMethod(order).paymentMethod.status; - - if (status === "approved" || status === "completed" || status === "refunded") { - return false; - } - return true; - }, - - showAfterPaymentCaptured() { - const instance = Template.instance(); - const order = instance.state.get("order"); - const orderStatus = orderCreditMethod(order).paymentMethod.status; - return orderStatus === "completed"; - }, - - paymentApproved() { - const instance = Template.instance(); - const order = instance.state.get("order"); - - return order.billing[0].paymentMethod.status === "approved"; - }, - - paymentCaptured() { - const instance = Template.instance(); - const order = instance.state.get("order"); - const orderStatus = orderCreditMethod(order).paymentMethod.status; - const orderMode = orderCreditMethod(order).paymentMethod.mode; - return orderStatus === "completed" || (orderStatus === "refunded" && orderMode === "capture"); - }, - - refundTransactions() { - const instance = Template.instance(); - const order = instance.state.get("order"); - const transactions = orderCreditMethod(order).paymentMethod.transactions; - - return _.filter(transactions, (transaction) => { - return transaction.type === "refund"; - }); - }, - - refunds() { - const refunds = Template.instance().refunds.get(); - if (_.isArray(refunds)) { - return refunds.reverse(); - } - - return refunds; - }, - - /** - * Get the total after all refunds - * @return {Number} the amount after all refunds - */ - adjustedTotal() { - const instance = Template.instance(); - const order = instance.state.get("order"); - const paymentMethod = orderCreditMethod(order).paymentMethod; - const discounts = orderCreditMethod(order).invoice.discounts; - const refunds = Template.instance().refunds.get(); - let refundTotal = 0; - - _.each(refunds, function (item) { - refundTotal += parseFloat(item.amount); - }); - - if (paymentMethod.processor === "Stripe") { - return Math.abs(paymentMethod.amount + discounts - refundTotal); - } - return Math.abs(paymentMethod.amount - refundTotal); - }, - capturedDisabled() { const isLoading = Template.instance().state.get("isCapturing"); if (isLoading) { return "disabled"; } return null; - }, - - refundSubmitDisabled() { - const amount = Template.instance().state.get("field-refund") || 0; - const isLoading = Template.instance().state.get("isRefunding"); - if (amount === 0 || isLoading) { - return "disabled"; - } - - return null; - }, - - /** - * Order - * @summary find a single order using the order id spplied with the template - * data context - * @return {Object} A single order - */ - order() { - const instance = Template.instance(); - const order = instance.state.get("order"); - - return order; - }, - - shipment() { - const instance = Template.instance(); - const order = instance.state.get("order"); - const currentData = Template.currentData(); - const shipment = _.filter(order.shipping, { _id: currentData.fulfillment._id })[0]; - - return shipment; - }, - - discounts() { - const enabledPaymentsArr = []; - const apps = Reaction.Apps({ - provides: "paymentMethod", - enabled: true - }); - for (const app of apps) { - if (app.enabled === true) enabledPaymentsArr.push(app); - } - let discount = false; - - for (const enabled of enabledPaymentsArr) { - if (enabled.packageName === "discount-codes") { - discount = true; - break; - } - } - return discount; - }, - - items() { - const instance = Template.instance(); - const order = instance.state.get("order"); - const currentData = Template.currentData(); - const shipment = currentData.fulfillment; - - // returns order items with shipping detail - const returnItems = _.map(order.items, (item) => { - const shipping = shipment.shipmentMethod; - return _.extend(item, { shipping }); - }); - - let items; - - - // if avalara tax has been enabled it adds a "taxDetail" field for every item - if (order.taxes !== undefined) { - const taxes = order.taxes.slice(0, -1); - - items = _.map(returnItems, (item) => { - const taxDetail = _.find(taxes, { - lineNumber: item._id - }); - return _.extend(item, { taxDetail }); - }); - } else { - items = returnItems; - } - return items; - }, - - hasRefundingEnabled() { - const instance = Template.instance(); - const order = instance.state.get("order"); - const paymentMethodId = order.billing[0].paymentMethod.paymentPackageId; - const paymentMethod = Packages.findOne({ _id: paymentMethodId }); - const paymentMethodName = paymentMethod.name; - const isRefundable = paymentMethod.settings[paymentMethodName].support.includes("Refund"); - return isRefundable; } }); diff --git a/imports/plugins/core/orders/server/i18n/en.json b/imports/plugins/core/orders/server/i18n/en.json index 4e7db00aa98..3c986703ed1 100644 --- a/imports/plugins/core/orders/server/i18n/en.json +++ b/imports/plugins/core/orders/server/i18n/en.json @@ -28,7 +28,11 @@ "cardTitle": "Invoice", "adjustedTotal": "Adjusted Total", "capturedTotal": "Captured Total", - "refund": "Refund" + "refund": "Refund", + "refundLabel": "For Refund", + "refundItem": "Items", + "refundItemAmount": "Total", + "refundTotal": "Refund Total" }, "fulfillment": "Fulfillment", "orderDetails": "Order Details", diff --git a/imports/plugins/core/ui/client/components/button/buttonSelect.js b/imports/plugins/core/ui/client/components/button/buttonSelect.js index ad8e221f603..a7707eeb361 100644 --- a/imports/plugins/core/ui/client/components/button/buttonSelect.js +++ b/imports/plugins/core/ui/client/components/button/buttonSelect.js @@ -51,7 +51,7 @@ class ButtonSelect extends Component { bezelStyle="solid" label={defaultButton.name} i18nKeyLabel={defaultButton.i18nKeyLabel} - buttonType={defaultButton.buttonType} + type={defaultButton.buttonType} /> ); @@ -101,7 +101,7 @@ class ButtonSelect extends Component { bezelStyle="solid" label={button.name} i18nKeyLabel={button.i18nKeyLabel} - buttonType={button.buttonType} + type={button.buttonType} /> ); diff --git a/imports/plugins/core/ui/client/components/checkbox/checkbox.js b/imports/plugins/core/ui/client/components/checkbox/checkbox.js index 6b891aabfca..bbc905197aa 100644 --- a/imports/plugins/core/ui/client/components/checkbox/checkbox.js +++ b/imports/plugins/core/ui/client/components/checkbox/checkbox.js @@ -10,6 +10,14 @@ class Checkbox extends Component { } } + renderLabel() { + const { label, i18nKeyLabel } = this.props; + if (label || i18nKeyLabel) { + return (); + } + return null; + } + render() { return (