);
}
- 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 @@
-
-
-
-
-
Invoice
-
-
+
+ {{> React component=InvoiceContainer
+ isFetching=isFetching
+ order=order
+ currency=currency
+ refunds=refunds
+ currentData=currentData
+ }}
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 (